"use client"; import { useState } from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; const UNLIMITED = -1; const FIELDS = [ { key: "maxRecipes", label: "Max Recipes" }, { key: "aiCallsPerMonth", label: "AI Calls / Month" }, { key: "storageMb", label: "Storage (MB)" }, { key: "maxPublicRecipes", label: "Max Public Recipes" }, ] as const; const STRIPE_FIELDS = [ { key: "stripeProductId", label: "Stripe Product ID", placeholder: "prod_..." }, { key: "stripePriceIdMonthly", label: "Monthly Price ID", placeholder: "price_..." }, { key: "stripePriceIdYearly", label: "Yearly Price ID", placeholder: "price_... (optional)" }, ] as const; type TierDefinition = { tier: string; maxRecipes: number; aiCallsPerMonth: number; storageMb: number; maxPublicRecipes: number; stripeProductId: string | null; stripePriceIdMonthly: string | null; stripePriceIdYearly: string | null; }; export function TierLimitsForm({ tierDefinition }: { tierDefinition: TierDefinition }) { const [values, setValues] = useState>({ maxRecipes: tierDefinition.maxRecipes, aiCallsPerMonth: tierDefinition.aiCallsPerMonth, storageMb: tierDefinition.storageMb, maxPublicRecipes: tierDefinition.maxPublicRecipes, }); const [stripeValues, setStripeValues] = useState>({ stripeProductId: tierDefinition.stripeProductId ?? "", stripePriceIdMonthly: tierDefinition.stripePriceIdMonthly ?? "", stripePriceIdYearly: tierDefinition.stripePriceIdYearly ?? "", }); // Remembers the last finite value per field so toggling "Unlimited" off restores it. const [lastFinite, setLastFinite] = useState>({ maxRecipes: tierDefinition.maxRecipes === UNLIMITED ? 0 : tierDefinition.maxRecipes, aiCallsPerMonth: tierDefinition.aiCallsPerMonth === UNLIMITED ? 0 : tierDefinition.aiCallsPerMonth, storageMb: tierDefinition.storageMb === UNLIMITED ? 0 : tierDefinition.storageMb, maxPublicRecipes: tierDefinition.maxPublicRecipes === UNLIMITED ? 0 : tierDefinition.maxPublicRecipes, }); const [saving, setSaving] = useState(false); async function handleSave() { setSaving(true); try { const res = await fetch(`/api/v1/admin/tiers/${tierDefinition.tier}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...values, ...stripeValues }), }); if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error((data as { error?: string }).error ?? "Save failed"); } toast.success(`${tierDefinition.tier} tier limits saved`); } catch (err) { toast.error(err instanceof Error ? err.message : "Failed to save"); } finally { setSaving(false); } } return (

{tierDefinition.tier}

{FIELDS.map(({ key, label }) => { const isUnlimited = values[key] === UNLIMITED; return (
{ if (checked) { setLastFinite((prev) => ({ ...prev, [key]: values[key] ?? 0 })); setValues((prev) => ({ ...prev, [key]: UNLIMITED })); } else { setValues((prev) => ({ ...prev, [key]: lastFinite[key] ?? 0 })); } }} />
setValues((prev) => ({ ...prev, [key]: Number(e.target.value) })) } />
); })}
{tierDefinition.tier !== "free" && (

Stripe (maps a checkout Price back to this tier)

{STRIPE_FIELDS.map(({ key, label, placeholder }) => (
setStripeValues((prev) => ({ ...prev, [key]: e.target.value }))} className="font-mono text-sm" />
))}
)}
); }