generated from fahricansecer/boilerplate-fe
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user