main
Some checks failed
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Has been cancelled

This commit is contained in:
Harun CAN
2026-03-30 15:18:32 +03:00
parent 8bd995ea18
commit 0722faeee9
12 changed files with 12796 additions and 13 deletions

View File

@@ -0,0 +1,184 @@
"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>
);
}