generated from fahricansecer/boilerplate-fe
Some checks failed
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Has been cancelled
185 lines
7.5 KiB
TypeScript
185 lines
7.5 KiB
TypeScript
"use client";
|
||
|
||
import { useState } from "react";
|
||
import { motion } from "framer-motion";
|
||
import { LayoutGrid, Save, Loader2, CheckCircle2, RefreshCw } from "lucide-react";
|
||
import { useAdminPlans, useAdminUpdatePlan } from "@/hooks/use-api";
|
||
|
||
const fadeUp = {
|
||
hidden: { opacity: 0, y: 14 },
|
||
show: { opacity: 1, y: 0, transition: { duration: 0.4 } },
|
||
};
|
||
|
||
type PlanField = {
|
||
key: string;
|
||
label: string;
|
||
type: "number" | "text" | "toggle";
|
||
suffix?: string;
|
||
};
|
||
|
||
const PLAN_FIELDS: PlanField[] = [
|
||
{ key: "displayName", label: "Görünen Ad", type: "text" },
|
||
{ key: "monthlyPrice", label: "Aylık Fiyat", type: "number", suffix: "$¢ (cent)" },
|
||
{ key: "yearlyPrice", label: "Yıllık Fiyat", type: "number", suffix: "$¢ (cent)" },
|
||
{ key: "monthlyCredits", label: "Aylık Kredi", type: "number" },
|
||
{ key: "maxDuration", label: "Max Süre", type: "number", suffix: "sn" },
|
||
{ key: "maxResolution", label: "Max Çözünürlük", type: "text" },
|
||
{ key: "maxProjects", label: "Max Proje", type: "number" },
|
||
{ key: "isActive", label: "Aktif", type: "toggle" },
|
||
];
|
||
|
||
export default function AdminPlansPage() {
|
||
const { data, isLoading, refetch } = useAdminPlans();
|
||
const updatePlan = useAdminUpdatePlan();
|
||
const [savedIds, setSavedIds] = useState<Set<string>>(new Set());
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
const [edits, setEdits] = useState<Record<string, any>>({});
|
||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
const rawPlans = (data as any)?.data ?? data;
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
const plans = (Array.isArray(rawPlans) ? rawPlans : []) as any[];
|
||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
function getValue(plan: any, key: string) {
|
||
return edits[plan.id]?.[key] !== undefined ? edits[plan.id][key] : plan[key];
|
||
}
|
||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
function setEdit(planId: string, key: string, value: any) {
|
||
setEdits((prev) => ({ ...prev, [planId]: { ...prev[planId], [key]: value } }));
|
||
}
|
||
|
||
function savePlan(planId: string) {
|
||
if (!edits[planId]) return;
|
||
updatePlan.mutate(
|
||
{ id: planId, data: edits[planId] },
|
||
{
|
||
onSuccess: () => {
|
||
setSavedIds((prev) => new Set([...prev, planId]));
|
||
setTimeout(() => setSavedIds((prev) => { const n = new Set(prev); n.delete(planId); return n; }), 2000);
|
||
setEdits((prev) => { const n = { ...prev }; delete n[planId]; return n; });
|
||
refetch();
|
||
},
|
||
}
|
||
);
|
||
}
|
||
|
||
const PLAN_COLORS: Record<string, string> = {
|
||
free: "from-gray-500/10 to-gray-600/5 border-gray-500/20",
|
||
pro: "from-violet-500/10 to-violet-600/5 border-violet-500/20",
|
||
business: "from-amber-500/10 to-amber-600/5 border-amber-500/20",
|
||
};
|
||
|
||
return (
|
||
<motion.div
|
||
initial="hidden"
|
||
animate="show"
|
||
variants={{ show: { transition: { staggerChildren: 0.07 } } }}
|
||
className="space-y-6 max-w-7xl mx-auto"
|
||
>
|
||
<motion.div variants={fadeUp} className="flex items-center justify-between">
|
||
<div>
|
||
<h1 className="font-[family-name:var(--font-display)] text-2xl font-bold flex items-center gap-2">
|
||
<LayoutGrid size={22} className="text-amber-400" /> Plan Yönetimi
|
||
</h1>
|
||
<p className="text-sm text-[var(--color-text-muted)] mt-1">
|
||
Abonelik planlarını düzenle ve fiyatları güncelle
|
||
</p>
|
||
</div>
|
||
<button onClick={() => refetch()} className="btn-ghost text-sm flex items-center gap-2">
|
||
<RefreshCw size={14} /> Yenile
|
||
</button>
|
||
</motion.div>
|
||
|
||
{isLoading ? (
|
||
<div className="flex justify-center py-20">
|
||
<Loader2 size={28} className="animate-spin text-amber-400" />
|
||
</div>
|
||
) : (
|
||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-5">
|
||
{plans.map((plan) => {
|
||
const colorKey = plan.name?.toLowerCase() ?? "free";
|
||
const gradient = PLAN_COLORS[colorKey] ?? PLAN_COLORS.free;
|
||
const hasEdits = !!edits[plan.id];
|
||
|
||
return (
|
||
<motion.div
|
||
key={plan.id}
|
||
variants={fadeUp}
|
||
className={`card-surface p-5 bg-gradient-to-br ${gradient} space-y-4`}
|
||
>
|
||
<div className="flex items-center justify-between">
|
||
<h2 className="font-[family-name:var(--font-display)] text-xl font-bold capitalize">
|
||
{plan.name}
|
||
</h2>
|
||
<span className="text-xs text-[var(--color-text-ghost)]">
|
||
{plan._count?.subscriptions ?? 0} abone
|
||
</span>
|
||
</div>
|
||
|
||
<div className="space-y-3">
|
||
{PLAN_FIELDS.map((field) => {
|
||
const val = getValue(plan, field.key);
|
||
return (
|
||
<div key={field.key}>
|
||
<label className="text-[10px] text-[var(--color-text-muted)] mb-1 block">
|
||
{field.label} {field.suffix && <span className="text-[var(--color-text-ghost)]">({field.suffix})</span>}
|
||
</label>
|
||
{field.type === "toggle" ? (
|
||
<button
|
||
onClick={() => setEdit(plan.id, field.key, !val)}
|
||
className={`flex items-center gap-2 text-sm px-3 py-1.5 rounded-xl ${
|
||
val ? "bg-emerald-500/10 text-emerald-400" : "bg-rose-500/10 text-rose-400"
|
||
}`}
|
||
>
|
||
{val ? "✓ Aktif" : "✗ Pasif"}
|
||
</button>
|
||
) : field.type === "number" ? (
|
||
<input
|
||
type="number"
|
||
value={val ?? ""}
|
||
onChange={(e) => setEdit(plan.id, field.key, Number(e.target.value))}
|
||
className="input w-full text-sm"
|
||
/>
|
||
) : (
|
||
<input
|
||
type="text"
|
||
value={val ?? ""}
|
||
onChange={(e) => setEdit(plan.id, field.key, e.target.value)}
|
||
className="input w-full text-sm"
|
||
/>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
<button
|
||
onClick={() => savePlan(plan.id)}
|
||
disabled={!hasEdits || updatePlan.isPending}
|
||
className={`w-full flex items-center justify-center gap-2 py-2.5 rounded-xl text-sm font-medium transition-all ${
|
||
savedIds.has(plan.id)
|
||
? "bg-emerald-500/10 text-emerald-400"
|
||
: hasEdits
|
||
? "btn-primary"
|
||
: "opacity-40 cursor-not-allowed bg-[var(--color-bg-elevated)] text-[var(--color-text-muted)]"
|
||
}`}
|
||
>
|
||
{savedIds.has(plan.id) ? (
|
||
<><CheckCircle2 size={14} /> Kaydedildi</>
|
||
) : updatePlan.isPending ? (
|
||
<><Loader2 size={14} className="animate-spin" /> Kaydediliyor…</>
|
||
) : (
|
||
<><Save size={14} /> Değişiklikleri Kaydet</>
|
||
)}
|
||
</button>
|
||
</motion.div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</motion.div>
|
||
);
|
||
}
|