159 lines
5.3 KiB
TypeScript
159 lines
5.3 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { Save, Loader2, RotateCcw } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { updatePlanAction } from "@/app/(admin)/admin/actions";
|
|
|
|
export interface PlanLimitsValue {
|
|
script: number;
|
|
audio: number;
|
|
art: number;
|
|
repurpose: number;
|
|
seats: number;
|
|
maxEpisodeMinutes: number;
|
|
}
|
|
|
|
export interface EditablePlan {
|
|
key: string;
|
|
name: string;
|
|
priceMonthly: number; // cents
|
|
priceYearly: number; // cents
|
|
limits: PlanLimitsValue;
|
|
}
|
|
|
|
const LIMIT_FIELDS: { key: keyof PlanLimitsValue; label: string }[] = [
|
|
{ key: "script", label: "Scripts / mo" },
|
|
{ key: "audio", label: "Audio / mo" },
|
|
{ key: "art", label: "Cover art / mo" },
|
|
{ key: "repurpose", label: "Repurpose / mo" },
|
|
{ key: "seats", label: "Seats" },
|
|
{ key: "maxEpisodeMinutes", label: "Max minutes" },
|
|
];
|
|
|
|
export function PlanEditor({ plan }: { plan: EditablePlan }) {
|
|
const router = useRouter();
|
|
const [priceMonthly, setPriceMonthly] = useState(plan.priceMonthly);
|
|
const [priceYearly, setPriceYearly] = useState(plan.priceYearly);
|
|
const [limits, setLimits] = useState<PlanLimitsValue>(plan.limits);
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
const dirty =
|
|
priceMonthly !== plan.priceMonthly ||
|
|
priceYearly !== plan.priceYearly ||
|
|
LIMIT_FIELDS.some((f) => limits[f.key] !== plan.limits[f.key]);
|
|
|
|
function reset() {
|
|
setPriceMonthly(plan.priceMonthly);
|
|
setPriceYearly(plan.priceYearly);
|
|
setLimits(plan.limits);
|
|
}
|
|
|
|
async function save() {
|
|
setSaving(true);
|
|
try {
|
|
const res = await updatePlanAction(plan.key, { priceMonthly, priceYearly, limits });
|
|
if (res.ok) {
|
|
toast.success(`${plan.name} updated`);
|
|
router.refresh();
|
|
} else {
|
|
toast.error(res.error ?? "Failed");
|
|
}
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Card className="transition-shadow hover:shadow-md">
|
|
<CardHeader className="flex-row items-center justify-between space-y-0">
|
|
<CardTitle className="flex items-center gap-2">
|
|
{plan.name}
|
|
<Badge variant="brand" className="capitalize">
|
|
{plan.key}
|
|
</Badge>
|
|
</CardTitle>
|
|
{dirty && (
|
|
<span className="text-xs font-semibold text-warning">Unsaved changes</span>
|
|
)}
|
|
</CardHeader>
|
|
<CardContent className="space-y-5">
|
|
{/* Prices — stored in cents; entered in dollars. */}
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor={`${plan.key}-monthly`}>Monthly price ($)</Label>
|
|
<Input
|
|
id={`${plan.key}-monthly`}
|
|
type="number"
|
|
min={0}
|
|
step="0.01"
|
|
value={(priceMonthly / 100).toString()}
|
|
onChange={(e) =>
|
|
setPriceMonthly(Math.max(0, Math.round((Number(e.target.value) || 0) * 100)))
|
|
}
|
|
className="h-11"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1.5">
|
|
<Label htmlFor={`${plan.key}-yearly`}>Yearly price ($)</Label>
|
|
<Input
|
|
id={`${plan.key}-yearly`}
|
|
type="number"
|
|
min={0}
|
|
step="0.01"
|
|
value={(priceYearly / 100).toString()}
|
|
onChange={(e) =>
|
|
setPriceYearly(Math.max(0, Math.round((Number(e.target.value) || 0) * 100)))
|
|
}
|
|
className="h-11"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Limits — use -1 for unlimited. */}
|
|
<div className="space-y-2">
|
|
<p className="text-sm font-semibold">Limits</p>
|
|
<p className="text-xs text-muted-foreground">Use -1 for unlimited.</p>
|
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
{LIMIT_FIELDS.map((f) => (
|
|
<div key={f.key} className="space-y-1.5">
|
|
<Label htmlFor={`${plan.key}-${f.key}`} className="text-xs font-medium">
|
|
{f.label}
|
|
</Label>
|
|
<Input
|
|
id={`${plan.key}-${f.key}`}
|
|
type="number"
|
|
value={limits[f.key].toString()}
|
|
onChange={(e) =>
|
|
setLimits((prev) => ({
|
|
...prev,
|
|
[f.key]: Math.round(Number(e.target.value) || 0),
|
|
}))
|
|
}
|
|
className="h-11"
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-end gap-2 pt-1">
|
|
<Button variant="ghost" size="sm" onClick={reset} disabled={!dirty || saving}>
|
|
<RotateCcw className="h-4 w-4" /> Reset
|
|
</Button>
|
|
<Button size="sm" onClick={save} disabled={!dirty || saving}>
|
|
{saving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Save className="h-4 w-4" />}
|
|
Save
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|