generated from fahricansecer/boilerplate-fe
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
Film,
|
||||
Trash2,
|
||||
MoreVertical,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
@@ -244,7 +245,7 @@ export default function ProjectDetailPage() {
|
||||
const statusInfo = STATUS_MAP[project.status] || STATUS_MAP.DRAFT;
|
||||
const StatusIcon = statusInfo.icon;
|
||||
const isRendering = ['PENDING', 'GENERATING_MEDIA', 'RENDERING', 'GENERATING_SCRIPT'].includes(project.status);
|
||||
const isEditable = !isRendering;
|
||||
const isEditable = !isRendering; // COMPLETED, DRAFT, FAILED → editable
|
||||
const hasScript = project.scenes && project.scenes.length > 0;
|
||||
const isCompleted = project.status === 'COMPLETED';
|
||||
const tweetData = project.sourceTweetData as Record<string, any> | undefined;
|
||||
@@ -350,38 +351,33 @@ export default function ProjectDetailPage() {
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock size={12} /> {project.targetDuration}s
|
||||
</span>
|
||||
{isEditable ? (
|
||||
<select
|
||||
value={project.videoStyle}
|
||||
onChange={(e) => handleStyleChange(e.target.value)}
|
||||
className="bg-[var(--color-bg-base)] border border-[var(--color-border-faint)] rounded-md px-2 py-0.5 text-xs text-[var(--color-text-secondary)] focus:outline-none focus:border-violet-500/50 cursor-pointer"
|
||||
>
|
||||
{videoStyles.map((s) => (
|
||||
<option key={s.id} value={s.id}>
|
||||
{s.emoji} {s.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<span>{currentStyle ? `${currentStyle.emoji} ${currentStyle.label}` : project.videoStyle}</span>
|
||||
)}
|
||||
|
||||
{project.videoStyle === 'CINEMATIC' && isEditable ? (
|
||||
<select
|
||||
value={project.videoStyle}
|
||||
onChange={(e) => handleStyleChange(e.target.value)}
|
||||
disabled={isRendering}
|
||||
className="bg-[var(--color-bg-base)] border border-[var(--color-border-faint)] rounded-md px-2 py-0.5 text-xs text-[var(--color-text-secondary)] focus:outline-none focus:border-violet-500/50 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
|
||||
>
|
||||
{videoStyles.map((s) => (
|
||||
<option key={s.id} value={s.id}>
|
||||
{s.emoji} {s.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{project.videoStyle === 'CINEMATIC' && (
|
||||
<select
|
||||
value={project.cinematicReference || ''}
|
||||
onChange={(e) => handleCinematicReferenceChange(e.target.value)}
|
||||
className="bg-[var(--color-bg-base)] border border-[var(--color-border-faint)] rounded-md px-2 py-0.5 text-xs text-[var(--color-text-secondary)] focus:outline-none focus:border-violet-500/50 cursor-pointer w-44 truncate"
|
||||
disabled={isRendering}
|
||||
className="bg-[var(--color-bg-base)] border border-[var(--color-border-faint)] rounded-md px-2 py-0.5 text-xs text-[var(--color-text-secondary)] focus:outline-none focus:border-violet-500/50 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer w-44 truncate"
|
||||
>
|
||||
<option value="">🎬 Sinematik Yönetmen/Film...</option>
|
||||
{CINEMATIC_REFERENCES.map(ref => (
|
||||
<option key={ref.value} value={ref.value}>{ref.label}</option>
|
||||
))}
|
||||
</select>
|
||||
) : project.videoStyle === 'CINEMATIC' && project.cinematicReference ? (
|
||||
<span className="flex items-center gap-1">
|
||||
🎬 <span className="truncate max-w-[150px]">{project.cinematicReference}</span>
|
||||
</span>
|
||||
) : null}
|
||||
)}
|
||||
|
||||
<span className="uppercase text-[10px] tracking-wider">{project.language}</span>
|
||||
<span className="text-[10px]">
|
||||
@@ -415,11 +411,11 @@ export default function ProjectDetailPage() {
|
||||
{/* Aksiyon butonları */}
|
||||
<div className="flex flex-wrap items-center gap-2 mt-4 pt-4 border-t border-[var(--color-border-faint)]">
|
||||
{/* Senaryo üret (draft, senaryo yok) */}
|
||||
{isEditable && !hasScript && (
|
||||
{!hasScript && (
|
||||
<button
|
||||
onClick={handleGenerateScript}
|
||||
disabled={generateScriptMutation.isPending}
|
||||
className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-gradient-to-r from-violet-500 to-violet-600 text-white text-sm font-medium shadow-lg shadow-violet-500/20 hover:shadow-violet-500/30 transition-shadow disabled:opacity-50"
|
||||
disabled={isRendering || generateScriptMutation.isPending}
|
||||
className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-gradient-to-r from-violet-500 to-violet-600 text-white text-sm font-medium shadow-lg shadow-violet-500/20 hover:shadow-violet-500/30 transition-shadow disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{generateScriptMutation.isPending ? (
|
||||
<Loader2 size={15} className="animate-spin" />
|
||||
@@ -431,27 +427,27 @@ export default function ProjectDetailPage() {
|
||||
)}
|
||||
|
||||
{/* Senaryo yeniden üret (draft/failed, senaryo var) */}
|
||||
{isEditable && hasScript && (
|
||||
{hasScript && (
|
||||
<button
|
||||
onClick={handleGenerateScript}
|
||||
disabled={generateScriptMutation.isPending}
|
||||
className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-[var(--color-bg-elevated)] text-[var(--color-text-secondary)] text-sm font-medium hover:bg-[var(--color-bg-surface)] transition-colors disabled:opacity-50"
|
||||
disabled={isRendering || generateScriptMutation.isPending}
|
||||
className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-[var(--color-bg-elevated)] text-[var(--color-text-secondary)] text-sm font-medium hover:bg-[var(--color-bg-surface)] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{generateScriptMutation.isPending ? (
|
||||
<Loader2 size={15} className="animate-spin" />
|
||||
) : (
|
||||
<RefreshCw size={15} />
|
||||
)}
|
||||
Yeniden Üret
|
||||
Senaryoyu Yeniden Üret
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Onayla ve video üretimini başlat */}
|
||||
{isEditable && hasScript && (
|
||||
{hasScript && (
|
||||
<button
|
||||
onClick={handleApprove}
|
||||
disabled={approveMutation.isPending}
|
||||
className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-gradient-to-r from-emerald-500 to-emerald-600 text-white text-sm font-medium shadow-lg shadow-emerald-500/20 hover:shadow-emerald-500/30 transition-shadow disabled:opacity-50"
|
||||
disabled={isRendering || approveMutation.isPending}
|
||||
className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-gradient-to-r from-emerald-500 to-emerald-600 text-white text-sm font-medium shadow-lg shadow-emerald-500/20 hover:shadow-emerald-500/30 transition-shadow disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{approveMutation.isPending ? (
|
||||
<Loader2 size={15} className="animate-spin" />
|
||||
@@ -463,14 +459,35 @@ export default function ProjectDetailPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Hata mesajı */}
|
||||
{/* Hata mesajı — kapatılabilir ve aksiyon butonlu */}
|
||||
{project.errorMessage && (
|
||||
<div className="mt-3 p-3 rounded-lg bg-red-500/10 border border-red-500/20">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<AlertCircle size={14} className="text-red-400" />
|
||||
<span className="text-xs font-medium text-red-400">Hata</span>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertCircle size={14} className="text-red-400" />
|
||||
<span className="text-xs font-medium text-red-400">Hata</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
// Sayfayı yeniden yükleyerek güncel veriyi al
|
||||
refetch();
|
||||
}}
|
||||
className="text-red-400/60 hover:text-red-400 transition-colors"
|
||||
title="Kapat"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-red-400/80">{project.errorMessage}</p>
|
||||
<p className="text-xs text-red-400/80 mb-2">{project.errorMessage}</p>
|
||||
{hasScript && (
|
||||
<button
|
||||
onClick={handleGenerateScript}
|
||||
disabled={generateScriptMutation.isPending}
|
||||
className="text-xs px-3 py-1.5 rounded-md bg-red-500/20 text-red-300 hover:bg-red-500/30 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{generateScriptMutation.isPending ? 'Üretiliyor...' : '🔄 Senaryoyu Yeniden Üret'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
@@ -512,6 +529,7 @@ export default function ProjectDetailPage() {
|
||||
key={scene.id}
|
||||
scene={scene}
|
||||
isEditable={isEditable}
|
||||
isRendering={isRendering}
|
||||
onUpdate={handleSceneUpdate}
|
||||
onRegenerate={handleSceneRegenerate}
|
||||
onGenerateImage={handleGenerateImage}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { useState, useMemo, useCallback } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import {
|
||||
Plus,
|
||||
Search,
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
ExternalLink,
|
||||
Loader2,
|
||||
Trash2,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -83,17 +84,28 @@ interface ProjectItem {
|
||||
export default function ProjectsPage() {
|
||||
const [activeFilter, setActiveFilter] = useState("all");
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [deleteTarget, setDeleteTarget] = useState<{ id: string; title: string } | null>(null);
|
||||
|
||||
const { data, isLoading } = useProjects({ limit: 100 });
|
||||
const deleteMutation = useDeleteProject();
|
||||
|
||||
const handleDelete = (e: React.MouseEvent, id: string) => {
|
||||
// Silme onay modal'ını aç (native confirm yerine)
|
||||
const openDeleteConfirm = useCallback((e: React.MouseEvent, project: ProjectItem) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (confirm("Bu projeyi silmek istediğinize emin misiniz?")) {
|
||||
deleteMutation.mutate(id);
|
||||
}
|
||||
};
|
||||
setDeleteTarget({ id: project.id, title: project.title });
|
||||
}, []);
|
||||
|
||||
// Silme işlemini gerçekleştir
|
||||
const confirmDelete = useCallback(() => {
|
||||
if (!deleteTarget) return;
|
||||
deleteMutation.mutate(deleteTarget.id, {
|
||||
onSettled: () => {
|
||||
setDeleteTarget(null);
|
||||
},
|
||||
});
|
||||
}, [deleteTarget, deleteMutation]);
|
||||
|
||||
// useProjects returns PaginatedResponse<Project> which has .data as Project[]
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const raw = data as any;
|
||||
@@ -220,63 +232,141 @@ export default function ProjectsPage() {
|
||||
const st = statusMap[project.status] ?? statusMap.draft;
|
||||
const StIcon = st.icon;
|
||||
return (
|
||||
<Link
|
||||
<div
|
||||
key={project.id}
|
||||
href={`/dashboard/projects/${project.id}`}
|
||||
className="flex items-center gap-4 p-4 rounded-xl card hover:border-violet-500/20 transition-all group"
|
||||
className="flex items-center rounded-xl card hover:border-violet-500/20 transition-all group relative"
|
||||
>
|
||||
<div
|
||||
className={`w-10 h-10 rounded-xl ${st.bgColor} flex items-center justify-center shrink-0 ${st.color}`}
|
||||
<Link
|
||||
href={`/dashboard/projects/${project.id}`}
|
||||
className="flex items-center gap-4 p-4 flex-1 min-w-0"
|
||||
>
|
||||
<StIcon size={18} />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-[var(--color-text-primary)] truncate group-hover:text-violet-300 transition-colors">
|
||||
{project.title}
|
||||
</p>
|
||||
<div className="flex items-center gap-3 mt-0.5 text-[11px] text-[var(--color-text-ghost)]">
|
||||
<span>
|
||||
{new Date(project.createdAt).toLocaleDateString(
|
||||
"tr-TR",
|
||||
{
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
},
|
||||
)}
|
||||
</span>
|
||||
{project.language && <span>• {project.language}</span>}
|
||||
{typeof project.creditsUsed === "number" &&
|
||||
project.creditsUsed > 0 && (
|
||||
<span>• {project.creditsUsed} kredi</span>
|
||||
)}
|
||||
<div
|
||||
className={`w-10 h-10 rounded-xl ${st.bgColor} flex items-center justify-center shrink-0 ${st.color}`}
|
||||
>
|
||||
<StIcon size={18} />
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className={`text-[10px] font-medium px-2.5 py-1 rounded-full border ${st.color} border-current/20 ${st.bgColor} shrink-0 mr-2`}
|
||||
>
|
||||
{st.label}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-[var(--color-text-primary)] truncate group-hover:text-violet-300 transition-colors">
|
||||
{project.title}
|
||||
</p>
|
||||
<div className="flex items-center gap-3 mt-0.5 text-[11px] text-[var(--color-text-ghost)]">
|
||||
<span>
|
||||
{new Date(project.createdAt).toLocaleDateString(
|
||||
"tr-TR",
|
||||
{
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
},
|
||||
)}
|
||||
</span>
|
||||
{project.language && <span>• {project.language}</span>}
|
||||
{typeof project.creditsUsed === "number" &&
|
||||
project.creditsUsed > 0 && (
|
||||
<span>• {project.creditsUsed} kredi</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className={`text-[10px] font-medium px-2.5 py-1 rounded-full border ${st.color} border-current/20 ${st.bgColor} shrink-0 mr-2`}
|
||||
>
|
||||
{st.label}
|
||||
</span>
|
||||
<ExternalLink
|
||||
size={14}
|
||||
className="text-[var(--color-text-ghost)] group-hover:text-violet-400 transition-colors shrink-0"
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<button
|
||||
onClick={(e) => handleDelete(e, project.id)}
|
||||
className="p-2 rounded-lg text-[var(--color-text-ghost)] hover:text-red-400 hover:bg-red-500/10 transition-colors shrink-0 z-10 mr-1"
|
||||
onClick={(e) => openDeleteConfirm(e, project)}
|
||||
className="p-2 rounded-lg text-[var(--color-text-ghost)] hover:text-red-400 hover:bg-red-500/10 transition-colors shrink-0 z-10 mr-3"
|
||||
title="Projeyi Sil"
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
|
||||
<ExternalLink
|
||||
size={14}
|
||||
className="text-[var(--color-text-ghost)] group-hover:text-violet-400 transition-colors shrink-0"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* ─── Silme Onay Modal ─── */}
|
||||
<AnimatePresence>
|
||||
{deleteTarget && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
||||
onClick={() => !deleteMutation.isPending && setDeleteTarget(null)}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95, y: 10 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95, y: 10 }}
|
||||
transition={{ duration: 0.2, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="relative bg-[var(--color-bg-elevated)] border border-[var(--color-border-default)] rounded-2xl p-6 max-w-sm w-full mx-4 shadow-2xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Kapatma butonu */}
|
||||
<button
|
||||
onClick={() => setDeleteTarget(null)}
|
||||
disabled={deleteMutation.isPending}
|
||||
className="absolute top-3 right-3 p-1 rounded-lg text-[var(--color-text-ghost)] hover:text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-surface)] transition-colors disabled:opacity-50"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
|
||||
{/* Icon */}
|
||||
<div className="flex items-center justify-center w-12 h-12 rounded-full bg-red-500/10 mb-4">
|
||||
<Trash2 size={22} className="text-red-400" />
|
||||
</div>
|
||||
|
||||
{/* İçerik */}
|
||||
<h3 className="text-base font-semibold text-[var(--color-text-primary)] mb-1.5">
|
||||
Projeyi Sil
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--color-text-muted)] mb-1">
|
||||
Bu projeyi silmek istediğinize emin misiniz?
|
||||
</p>
|
||||
<p className="text-xs text-[var(--color-text-ghost)] mb-5 line-clamp-2 italic">
|
||||
“{deleteTarget.title}”
|
||||
</p>
|
||||
|
||||
{/* Butonlar */}
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => setDeleteTarget(null)}
|
||||
disabled={deleteMutation.isPending}
|
||||
className="flex-1 px-4 py-2.5 rounded-xl border border-[var(--color-border-default)] text-sm font-medium text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-surface)] transition-colors disabled:opacity-50"
|
||||
>
|
||||
İptal
|
||||
</button>
|
||||
<button
|
||||
onClick={confirmDelete}
|
||||
disabled={deleteMutation.isPending}
|
||||
className="flex-1 flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl bg-red-500 hover:bg-red-600 text-white text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{deleteMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
Siliniyor...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Trash2 size={14} />
|
||||
Evet, Sil
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user