Files
podcastdistributiona/components/admin/plan-editor.tsx
T

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>
);
}