main
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
@@ -0,0 +1,200 @@
"use client";
import { useState } from "react";
import { motion } from "framer-motion";
import {
FolderOpen,
Search,
Trash2,
ChevronLeft,
ChevronRight,
Loader2,
RefreshCw,
ExternalLink,
} from "lucide-react";
import Link from "next/link";
import { useAdminProjects, useAdminDeleteProject } from "@/hooks/use-api";
const STATUS_LABELS: Record<string, { label: string; color: string }> = {
DRAFT: { label: "Taslak", color: "bg-gray-500/10 text-gray-400" },
GENERATING_SCRIPT: { label: "Senaryo Üretiliyor", color: "bg-blue-500/10 text-blue-400" },
PENDING: { label: "Kuyrukta", color: "bg-amber-500/10 text-amber-400" },
GENERATING_MEDIA: { label: "Medya Üretiliyor", color: "bg-cyan-500/10 text-cyan-400" },
RENDERING: { label: "Render", color: "bg-violet-500/10 text-violet-400" },
COMPLETED: { label: "Tamamlandı", color: "bg-emerald-500/10 text-emerald-400" },
FAILED: { label: "Başarısız", color: "bg-rose-500/10 text-rose-400" },
};
const fadeUp = {
hidden: { opacity: 0, y: 14 },
show: { opacity: 1, y: 0, transition: { duration: 0.4 } },
};
export default function AdminProjectsPage() {
const [page, setPage] = useState(1);
const [statusFilter, setStatusFilter] = useState("");
const [search, setSearch] = useState("");
const { data, isLoading, refetch } = useAdminProjects({
page,
limit: 20,
status: statusFilter || undefined,
});
const deleteProject = useAdminDeleteProject();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const response = (data as any)?.data ?? data;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const projects = (response?.data ?? []) as any[];
const meta = response?.meta ?? {};
const filtered = search
? projects.filter(
(p) =>
p.title?.toLowerCase().includes(search.toLowerCase()) ||
p.user?.email?.toLowerCase().includes(search.toLowerCase())
)
: projects;
return (
<motion.div
initial="hidden"
animate="show"
variants={{ show: { transition: { staggerChildren: 0.06 } } }}
className="space-y-5 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">
<FolderOpen size={22} className="text-cyan-400" /> Proje Yönetimi
</h1>
<p className="text-sm text-[var(--color-text-muted)] mt-1">
Toplam {meta.total ?? "—"} proje
</p>
</div>
<button onClick={() => refetch()} className="btn-ghost text-sm flex items-center gap-2">
<RefreshCw size={14} /> Yenile
</button>
</motion.div>
{/* Filtreler */}
<motion.div variants={fadeUp} className="flex gap-3 flex-wrap">
<div className="relative">
<Search size={15} className="absolute left-3.5 top-1/2 -translate-y-1/2 text-[var(--color-text-ghost)]" />
<input
type="text"
placeholder="Başlık veya email ara..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="input pl-10"
/>
</div>
<select
value={statusFilter}
onChange={(e) => { setStatusFilter(e.target.value); setPage(1); }}
className="input"
>
<option value="">Tüm Durumlar</option>
{Object.entries(STATUS_LABELS).map(([key, { label }]) => (
<option key={key} value={key}>{label}</option>
))}
</select>
</motion.div>
<motion.div variants={fadeUp} className="card-surface overflow-hidden">
{isLoading ? (
<div className="flex justify-center py-16">
<Loader2 size={28} className="animate-spin text-cyan-400" />
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-[var(--color-border-faint)]">
<th className="text-left px-4 py-3 text-xs text-[var(--color-text-muted)] font-medium">Proje</th>
<th className="text-left px-4 py-3 text-xs text-[var(--color-text-muted)] font-medium">Kullanıcı</th>
<th className="text-left px-4 py-3 text-xs text-[var(--color-text-muted)] font-medium">Durum</th>
<th className="text-left px-4 py-3 text-xs text-[var(--color-text-muted)] font-medium">Kredi</th>
<th className="text-left px-4 py-3 text-xs text-[var(--color-text-muted)] font-medium">Kaynak</th>
<th className="text-left px-4 py-3 text-xs text-[var(--color-text-muted)] font-medium">Tarih</th>
<th className="text-right px-4 py-3 text-xs text-[var(--color-text-muted)] font-medium">İşlem</th>
</tr>
</thead>
<tbody>
{filtered.map((project) => {
const st = STATUS_LABELS[project.status] ?? { label: project.status, color: "bg-gray-500/10 text-gray-400" };
return (
<tr
key={project.id}
className="border-b border-[var(--color-border-faint)] last:border-0 hover:bg-[var(--color-bg-elevated)] transition-colors"
>
<td className="px-4 py-3">
<div className="font-medium max-w-[200px] truncate">{project.title}</div>
<div className="text-[10px] text-[var(--color-text-ghost)]">{project.language} · {project._count?.scenes ?? 0} sahne</div>
</td>
<td className="px-4 py-3 text-xs text-[var(--color-text-muted)]">
{project.user?.email}
</td>
<td className="px-4 py-3">
<span className={`text-[10px] px-2 py-0.5 rounded-full ${st.color}`}>
{st.label}
</span>
{project.status !== "DRAFT" && project.status !== "COMPLETED" && project.status !== "FAILED" && (
<div className="w-16 h-1 rounded-full bg-[var(--color-border-faint)] mt-1">
<div className="h-full rounded-full bg-violet-500" style={{ width: `${project.progress}%` }} />
</div>
)}
</td>
<td className="px-4 py-3 text-xs font-mono">{project.creditsUsed}</td>
<td className="px-4 py-3 text-xs text-[var(--color-text-ghost)]">
{project.sourceType === "X_TWEET" ? "𝕏 Tweet" : project.sourceType === "YOUTUBE" ? "YouTube" : "Manuel"}
</td>
<td className="px-4 py-3 text-xs text-[var(--color-text-muted)]">
{new Date(project.createdAt).toLocaleDateString("tr")}
</td>
<td className="px-4 py-3">
<div className="flex items-center justify-end gap-2">
<Link
href={`/dashboard/projects/${project.id}`}
className="p-1.5 rounded-lg hover:bg-cyan-500/10 text-cyan-400 transition-colors"
title="Detay"
>
<ExternalLink size={13} />
</Link>
<button
onClick={() => {
if (confirm(`"${project.title}" projesini silmek istiyor musunuz?`)) {
deleteProject.mutate(project.id);
}
}}
disabled={deleteProject.isPending}
className="p-1.5 rounded-lg hover:bg-rose-500/10 text-rose-400 transition-colors"
title="Sil"
>
<Trash2 size={13} />
</button>
</div>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</motion.div>
{meta.totalPages > 1 && (
<motion.div variants={fadeUp} className="flex items-center justify-center gap-3">
<button onClick={() => setPage((p) => Math.max(1, p - 1))} disabled={page === 1} className="btn-ghost text-sm flex items-center gap-1 disabled:opacity-40">
<ChevronLeft size={14} /> Önceki
</button>
<span className="text-sm text-[var(--color-text-muted)]">{page} / {meta.totalPages}</span>
<button onClick={() => setPage((p) => Math.min(meta.totalPages, p + 1))} disabled={page === meta.totalPages} className="btn-ghost text-sm flex items-center gap-1 disabled:opacity-40">
Sonraki <ChevronRight size={14} />
</button>
</motion.div>
)}
</motion.div>
);
}