Files
ContentGen_FE/src/app/[locale]/(dashboard)/dashboard/admin/plans/page.tsx
Harun CAN 0722faeee9
Some checks failed
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Has been cancelled
main
2026-03-30 15:18:32 +03:00

185 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
);
}