diff --git a/apps/web/app/api/v1/admin/tiers/[tier]/route.ts b/apps/web/app/api/v1/admin/tiers/[tier]/route.ts index 8df0472..0b24034 100644 --- a/apps/web/app/api/v1/admin/tiers/[tier]/route.ts +++ b/apps/web/app/api/v1/admin/tiers/[tier]/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { requireAdmin } from "@/lib/api-auth"; import { db, tierDefinitions, auditLogs, eq } from "@epicure/db"; import { randomUUID } from "crypto"; +import { UNLIMITED } from "@/lib/tiers"; interface RouteContext { params: Promise<{ tier: string }>; @@ -25,7 +26,7 @@ export async function PATCH(req: NextRequest, { params }: RouteContext) { for (const field of NUMERIC_FIELDS) { const value = body[field]; if (value === undefined) continue; - if (!Number.isInteger(value) || value < 0) { + if (!Number.isInteger(value) || (value < 0 && value !== UNLIMITED)) { return NextResponse.json({ error: `Invalid value for ${field}` }, { status: 400 }); } updateData[field] = value; diff --git a/apps/web/components/admin/tier-limits-form.tsx b/apps/web/components/admin/tier-limits-form.tsx index 8db53ab..3b7291e 100644 --- a/apps/web/components/admin/tier-limits-form.tsx +++ b/apps/web/components/admin/tier-limits-form.tsx @@ -5,6 +5,9 @@ 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" }, @@ -28,6 +31,13 @@ export function TierLimitsForm({ tierDefinition }: { tierDefinition: TierDefinit storageMb: tierDefinition.storageMb, maxPublicRecipes: tierDefinition.maxPublicRecipes, }); + // 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() { @@ -55,20 +65,45 @@ export function TierLimitsForm({ tierDefinition }: { tierDefinition: TierDefinit

{tierDefinition.tier}

- {FIELDS.map(({ key, label }) => ( -
- - - setValues((prev) => ({ ...prev, [key]: Number(e.target.value) })) - } - /> -
- ))} + {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) })) + } + /> +
+ ); + })}