Add sponsor price endpoint and fetch pricing from backend on frontend
Made-with: Cursor
This commit is contained in:
@@ -9,6 +9,11 @@ const router = Router();
|
|||||||
|
|
||||||
const SNAP_DAYS = [1, 3, 7, 14, 30, 60, 90, 180, 365];
|
const SNAP_DAYS = [1, 3, 7, 14, 30, 60, 90, 180, 365];
|
||||||
|
|
||||||
|
/** Public endpoint: returns base sponsor price per day for display. */
|
||||||
|
router.get("/price", (_req: Request, res: Response) => {
|
||||||
|
res.json({ baseSponsorPricePerDay: config.baseSponsorPricePerDay });
|
||||||
|
});
|
||||||
|
|
||||||
function snapDays(days: number): number {
|
function snapDays(days: number): number {
|
||||||
let best = SNAP_DAYS[0];
|
let best = SNAP_DAYS[0];
|
||||||
for (const d of SNAP_DAYS) {
|
for (const d of SNAP_DAYS) {
|
||||||
|
|||||||
@@ -375,6 +375,14 @@ export interface SponsorCreateResult {
|
|||||||
status: string;
|
status: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SponsorPrice {
|
||||||
|
baseSponsorPricePerDay: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSponsorPrice(): Promise<SponsorPrice> {
|
||||||
|
return request<SponsorPrice>("/sponsor/price");
|
||||||
|
}
|
||||||
|
|
||||||
export async function getSponsorHomepage(): Promise<SponsorHomepageItem[]> {
|
export async function getSponsorHomepage(): Promise<SponsorHomepageItem[]> {
|
||||||
return request<SponsorHomepageItem[]>("/sponsor/homepage");
|
return request<SponsorHomepageItem[]>("/sponsor/homepage");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ import { useState, useCallback, useMemo } from "react";
|
|||||||
import { postSponsorCreate, type SponsorCreateResult } from "../api";
|
import { postSponsorCreate, type SponsorCreateResult } from "../api";
|
||||||
import { SponsorTimeSlider } from "./SponsorTimeSlider";
|
import { SponsorTimeSlider } from "./SponsorTimeSlider";
|
||||||
|
|
||||||
const BASE_PRICE = 200;
|
function calculatePrice(basePricePerDay: number, days: number): number {
|
||||||
|
let price = basePricePerDay * days;
|
||||||
function calculatePrice(days: number): number {
|
|
||||||
let price = BASE_PRICE * days;
|
|
||||||
if (days >= 180) price *= 0.7;
|
if (days >= 180) price *= 0.7;
|
||||||
else if (days >= 90) price *= 0.8;
|
else if (days >= 90) price *= 0.8;
|
||||||
else if (days >= 30) price *= 0.9;
|
else if (days >= 30) price *= 0.9;
|
||||||
@@ -13,11 +11,12 @@ function calculatePrice(days: number): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface SponsorFormProps {
|
interface SponsorFormProps {
|
||||||
|
basePricePerDay: number;
|
||||||
onSuccess?: (result: SponsorCreateResult) => void;
|
onSuccess?: (result: SponsorCreateResult) => void;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SponsorForm({ onSuccess, onCancel }: SponsorFormProps) {
|
export function SponsorForm({ basePricePerDay, onSuccess, onCancel }: SponsorFormProps) {
|
||||||
const [title, setTitle] = useState("");
|
const [title, setTitle] = useState("");
|
||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
const [linkUrl, setLinkUrl] = useState("");
|
const [linkUrl, setLinkUrl] = useState("");
|
||||||
@@ -26,7 +25,7 @@ export function SponsorForm({ onSuccess, onCancel }: SponsorFormProps) {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const priceSats = useMemo(() => calculatePrice(durationDays), [durationDays]);
|
const priceSats = useMemo(() => calculatePrice(basePricePerDay, durationDays), [basePricePerDay, durationDays]);
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
async (e: React.FormEvent) => {
|
async (e: React.FormEvent) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
import { getSponsorList, getToken, postSponsorExtend } from "../api";
|
import { getSponsorList, getSponsorPrice, getToken, postSponsorExtend } from "../api";
|
||||||
import { SponsorCard } from "../components/SponsorCard";
|
import { SponsorCard } from "../components/SponsorCard";
|
||||||
import { SponsorForm } from "../components/SponsorForm";
|
import { SponsorForm } from "../components/SponsorForm";
|
||||||
import { SponsorInvoiceModal } from "../components/SponsorInvoiceModal";
|
import { SponsorInvoiceModal } from "../components/SponsorInvoiceModal";
|
||||||
@@ -27,6 +27,7 @@ export function SponsorsPage() {
|
|||||||
const [extendDuration, setExtendDuration] = useState(30);
|
const [extendDuration, setExtendDuration] = useState(30);
|
||||||
const [extendLoading, setExtendLoading] = useState(false);
|
const [extendLoading, setExtendLoading] = useState(false);
|
||||||
const [extendError, setExtendError] = useState<string | null>(null);
|
const [extendError, setExtendError] = useState<string | null>(null);
|
||||||
|
const [basePricePerDay, setBasePricePerDay] = useState<number | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = "Sponsors — Sats Faucet";
|
document.title = "Sponsors — Sats Faucet";
|
||||||
@@ -41,6 +42,12 @@ export function SponsorsPage() {
|
|||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getSponsorPrice()
|
||||||
|
.then((p) => setBasePricePerDay(p.baseSponsorPricePerDay))
|
||||||
|
.catch(() => setBasePricePerDay(200)); // fallback if fetch fails
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -127,7 +134,9 @@ export function SponsorsPage() {
|
|||||||
|
|
||||||
<div className="sponsors-pricing">
|
<div className="sponsors-pricing">
|
||||||
<h3>Pricing</h3>
|
<h3>Pricing</h3>
|
||||||
<p>Base: 200 sats/day. Discounts: 30+ days 10% off, 90+ days 20% off, 180+ days 30% off.</p>
|
<p>
|
||||||
|
Base: {basePricePerDay != null ? basePricePerDay.toLocaleString() : "…"} sats/day. Discounts: 30+ days 10% off, 90+ days 20% off, 180+ days 30% off.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="sponsors-cta-wrap">
|
<div className="sponsors-cta-wrap">
|
||||||
@@ -158,10 +167,15 @@ export function SponsorsPage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Modal open={formOpen} onClose={() => setFormOpen(false)} title="Create Sponsor" variant="sponsor">
|
<Modal open={formOpen} onClose={() => setFormOpen(false)} title="Create Sponsor" variant="sponsor">
|
||||||
<SponsorForm
|
{basePricePerDay != null ? (
|
||||||
onSuccess={handleCreateSuccess}
|
<SponsorForm
|
||||||
onCancel={() => setFormOpen(false)}
|
basePricePerDay={basePricePerDay}
|
||||||
/>
|
onSuccess={handleCreateSuccess}
|
||||||
|
onCancel={() => setFormOpen(false)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="sponsors-loading">Loading pricing…</div>
|
||||||
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<SponsorInvoiceModal
|
<SponsorInvoiceModal
|
||||||
@@ -190,7 +204,12 @@ export function SponsorsPage() {
|
|||||||
<div className="sponsor-form-price">
|
<div className="sponsor-form-price">
|
||||||
<span className="sponsor-form-price-label">Total:</span>
|
<span className="sponsor-form-price-label">Total:</span>
|
||||||
<strong className="sponsor-form-price-value">
|
<strong className="sponsor-form-price-value">
|
||||||
{Math.round(200 * extendDuration * (extendDuration >= 180 ? 0.7 : extendDuration >= 90 ? 0.8 : extendDuration >= 30 ? 0.9 : 1)).toLocaleString()} sats
|
{basePricePerDay != null
|
||||||
|
? Math.round(
|
||||||
|
basePricePerDay * extendDuration * (extendDuration >= 180 ? 0.7 : extendDuration >= 90 ? 0.8 : extendDuration >= 30 ? 0.9 : 1)
|
||||||
|
).toLocaleString()
|
||||||
|
: "…"}{" "}
|
||||||
|
sats
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
{extendError && <p className="sponsor-form-error" role="alert">{extendError}</p>}
|
{extendError && <p className="sponsor-form-error" role="alert">{extendError}</p>}
|
||||||
|
|||||||
Reference in New Issue
Block a user