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];
|
||||
|
||||
/** 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 {
|
||||
let best = SNAP_DAYS[0];
|
||||
for (const d of SNAP_DAYS) {
|
||||
|
||||
@@ -375,6 +375,14 @@ export interface SponsorCreateResult {
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface SponsorPrice {
|
||||
baseSponsorPricePerDay: number;
|
||||
}
|
||||
|
||||
export async function getSponsorPrice(): Promise<SponsorPrice> {
|
||||
return request<SponsorPrice>("/sponsor/price");
|
||||
}
|
||||
|
||||
export async function getSponsorHomepage(): Promise<SponsorHomepageItem[]> {
|
||||
return request<SponsorHomepageItem[]>("/sponsor/homepage");
|
||||
}
|
||||
|
||||
@@ -2,10 +2,8 @@ import { useState, useCallback, useMemo } from "react";
|
||||
import { postSponsorCreate, type SponsorCreateResult } from "../api";
|
||||
import { SponsorTimeSlider } from "./SponsorTimeSlider";
|
||||
|
||||
const BASE_PRICE = 200;
|
||||
|
||||
function calculatePrice(days: number): number {
|
||||
let price = BASE_PRICE * days;
|
||||
function calculatePrice(basePricePerDay: number, days: number): number {
|
||||
let price = basePricePerDay * days;
|
||||
if (days >= 180) price *= 0.7;
|
||||
else if (days >= 90) price *= 0.8;
|
||||
else if (days >= 30) price *= 0.9;
|
||||
@@ -13,11 +11,12 @@ function calculatePrice(days: number): number {
|
||||
}
|
||||
|
||||
interface SponsorFormProps {
|
||||
basePricePerDay: number;
|
||||
onSuccess?: (result: SponsorCreateResult) => void;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
export function SponsorForm({ onSuccess, onCancel }: SponsorFormProps) {
|
||||
export function SponsorForm({ basePricePerDay, onSuccess, onCancel }: SponsorFormProps) {
|
||||
const [title, setTitle] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [linkUrl, setLinkUrl] = useState("");
|
||||
@@ -26,7 +25,7 @@ export function SponsorForm({ onSuccess, onCancel }: SponsorFormProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const priceSats = useMemo(() => calculatePrice(durationDays), [durationDays]);
|
||||
const priceSats = useMemo(() => calculatePrice(basePricePerDay, durationDays), [basePricePerDay, durationDays]);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
async (e: React.FormEvent) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from "react";
|
||||
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 { SponsorForm } from "../components/SponsorForm";
|
||||
import { SponsorInvoiceModal } from "../components/SponsorInvoiceModal";
|
||||
@@ -27,6 +27,7 @@ export function SponsorsPage() {
|
||||
const [extendDuration, setExtendDuration] = useState(30);
|
||||
const [extendLoading, setExtendLoading] = useState(false);
|
||||
const [extendError, setExtendError] = useState<string | null>(null);
|
||||
const [basePricePerDay, setBasePricePerDay] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = "Sponsors — Sats Faucet";
|
||||
@@ -41,6 +42,12 @@ export function SponsorsPage() {
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getSponsorPrice()
|
||||
.then((p) => setBasePricePerDay(p.baseSponsorPricePerDay))
|
||||
.catch(() => setBasePricePerDay(200)); // fallback if fetch fails
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
@@ -127,7 +134,9 @@ export function SponsorsPage() {
|
||||
|
||||
<div className="sponsors-pricing">
|
||||
<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 className="sponsors-cta-wrap">
|
||||
@@ -158,10 +167,15 @@ export function SponsorsPage() {
|
||||
</section>
|
||||
|
||||
<Modal open={formOpen} onClose={() => setFormOpen(false)} title="Create Sponsor" variant="sponsor">
|
||||
<SponsorForm
|
||||
onSuccess={handleCreateSuccess}
|
||||
onCancel={() => setFormOpen(false)}
|
||||
/>
|
||||
{basePricePerDay != null ? (
|
||||
<SponsorForm
|
||||
basePricePerDay={basePricePerDay}
|
||||
onSuccess={handleCreateSuccess}
|
||||
onCancel={() => setFormOpen(false)}
|
||||
/>
|
||||
) : (
|
||||
<div className="sponsors-loading">Loading pricing…</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<SponsorInvoiceModal
|
||||
@@ -190,7 +204,12 @@ export function SponsorsPage() {
|
||||
<div className="sponsor-form-price">
|
||||
<span className="sponsor-form-price-label">Total:</span>
|
||||
<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>
|
||||
</div>
|
||||
{extendError && <p className="sponsor-form-error" role="alert">{extendError}</p>}
|
||||
|
||||
Reference in New Issue
Block a user