Files
ContentGen_FE/src/app/[locale]/(dashboard)/dashboard/admin/projects/page.tsx
T
Harun CAN 5144ee4d9a
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Has been cancelled
main
2026-04-30 13:25:43 +02:00

201 lines
9.3 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 {
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>
);
}