generated from fahricansecer/boilerplate-fe
201 lines
9.3 KiB
TypeScript
201 lines
9.3 KiB
TypeScript
"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-[var(--color-bg-surface)] text-[var(--color-text-ghost)] border border-[var(--color-border-faint)]" },
|
||
GENERATING_SCRIPT: { label: "Senaryo Üretiliyor", color: "bg-neutral-500/10 text-neutral-400 border border-neutral-500/20" },
|
||
PENDING: { label: "Kuyrukta", color: "bg-neutral-500/10 text-neutral-400 border border-neutral-500/20" },
|
||
GENERATING_MEDIA: { label: "Medya Üretiliyor", color: "bg-neutral-500/10 text-neutral-400 border border-neutral-500/20" },
|
||
RENDERING: { label: "Render", color: "bg-neutral-500/10 text-neutral-400 border border-neutral-500/20" },
|
||
COMPLETED: { label: "Tamamlandı", color: "bg-neutral-500/10 text-neutral-400 border border-neutral-500/20" },
|
||
FAILED: { label: "Başarısız", color: "bg-neutral-500/10 text-neutral-400 border border-neutral-500/20" },
|
||
};
|
||
|
||
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-neutral-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-neutral-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-neutral-400" 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-neutral-500/10 text-neutral-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-neutral-500/10 text-neutral-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>
|
||
);
|
||
}
|