import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import { api } from "../api/client"; import { CheckIcon } from "./icons/Icons"; type Plan = { id: string; name: string; tagline: string; price_monthly_usd: number; price_annual_monthly_usd?: number; price_annual_usd?: number; annual_savings_pct?: number; features: string[]; }; type Cycle = "monthly" | "annual"; const ANNUAL_DISCOUNT = 0.25; // Mirrors backend app/services/plans.py so the page renders before (or without) the API. const FALLBACK: Plan[] = [ { id: "free", name: "Free", tagline: "For trying Notestack on your archive", price_monthly_usd: 0, features: [ "Up to 5 posts indexed, from any source", "Grounded research chat with citations", "1 audio overview, any length", "1 video", "2 Launch Kits", "2 infographics", "2 reports a month", ], }, { id: "writer", name: "Writer", tagline: "For writers publishing every week", price_monthly_usd: 24.99, features: [ "Up to 100 posts indexed, from any source", "10x more audio overviews than Free (60 min a month)", "10 videos a month", "20 Launch Kits a month", "30 reports a month", "30 infographics a month", "Voice cloning with consent", ], }, { id: "studio", name: "Studio", tagline: "For publications and power users", price_monthly_usd: 48.99, features: [ "Up to 250 posts indexed, from any source", "40x more audio overviews than Free (240 min a month)", "20 videos a month", "50 Launch Kits a month", "Unlimited reports", "Unlimited infographics", "Voice cloning with consent", ], }, ]; export function money(n: number): string { return Number.isInteger(n) ? `$${n}` : `$${n.toFixed(2)}`; } /** Nearest .99 price, mirroring plans.to_99 on the backend. */ function to99(amount: number): number { return amount <= 0 ? 0 : Math.max(0.99, Math.round(amount + 0.01) - 0.01); } export function annualMonthly(plan: Plan): number { return plan.price_annual_monthly_usd ?? to99(plan.price_monthly_usd * (1 - ANNUAL_DISCOUNT)); } function savingsPct(plan: Plan): number { if (!plan.price_monthly_usd) return 0; return plan.annual_savings_pct ?? Math.round((1 - annualMonthly(plan) / plan.price_monthly_usd) * 100); } export function annualTotal(plan: Plan): number { return plan.price_annual_usd ?? Math.round(annualMonthly(plan) * 12 * 100) / 100; } export default function PricingTiers() { const [plans, setPlans] = useState(FALLBACK); const [billingEnabled, setBillingEnabled] = useState(false); const [cycle, setCycle] = useState("annual"); const bestSaving = Math.max(0, ...plans.map(savingsPct)); useEffect(() => { api<{ billing_enabled: boolean; plans: Plan[] }>("/api/billing/plans") .then((r) => { if (Array.isArray(r.plans) && r.plans.length > 0) setPlans(r.plans); setBillingEnabled(r.billing_enabled); }) .catch(() => { /* keep fallback */ }); }, []); return (
{plans.map((plan) => { const featured = plan.id === "writer"; const free = plan.price_monthly_usd === 0; const annual = cycle === "annual" && !free; return (
{featured && Most popular}

{plan.name}

{plan.tagline}

{free ? ( Free ) : ( <> {annual && {money(plan.price_monthly_usd)}} {money(annual ? annualMonthly(plan) : plan.price_monthly_usd)} /month )}

{free ? "Free forever" : annual ? `${money(annualTotal(plan))} billed yearly` : "Billed monthly"}

    {plan.features.map((f) => (
  • {f}
  • ))}
{billingEnabled && !free ? `Choose ${plan.name}` : "Start free"}
); })}
); }