diff --git a/next-env.d.ts b/next-env.d.ts index 9edff1c..c4b7818 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./.next/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/src/app/[locale]/(dashboard)/dashboard/projects/[id]/page.tsx b/src/app/[locale]/(dashboard)/dashboard/projects/[id]/page.tsx index 0de4ecd..6d47528 100644 --- a/src/app/[locale]/(dashboard)/dashboard/projects/[id]/page.tsx +++ b/src/app/[locale]/(dashboard)/dashboard/projects/[id]/page.tsx @@ -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 | undefined; @@ -350,38 +351,33 @@ export default function ProjectDetailPage() { {project.targetDuration}s - {isEditable ? ( - 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) => ( - - {s.emoji} {s.label} - - ))} - - ) : ( - {currentStyle ? `${currentStyle.emoji} ${currentStyle.label}` : project.videoStyle} - )} - {project.videoStyle === 'CINEMATIC' && isEditable ? ( + 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) => ( + + {s.emoji} {s.label} + + ))} + + + {project.videoStyle === 'CINEMATIC' && ( 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" > 🎬 Sinematik Yönetmen/Film... {CINEMATIC_REFERENCES.map(ref => ( {ref.label} ))} - ) : project.videoStyle === 'CINEMATIC' && project.cinematicReference ? ( - - 🎬 {project.cinematicReference} - - ) : null} + )} {project.language} @@ -415,11 +411,11 @@ export default function ProjectDetailPage() { {/* Aksiyon butonları */} {/* Senaryo üret (draft, senaryo yok) */} - {isEditable && !hasScript && ( + {!hasScript && ( {generateScriptMutation.isPending ? ( @@ -431,27 +427,27 @@ export default function ProjectDetailPage() { )} {/* Senaryo yeniden üret (draft/failed, senaryo var) */} - {isEditable && hasScript && ( + {hasScript && ( {generateScriptMutation.isPending ? ( ) : ( )} - Yeniden Üret + Senaryoyu Yeniden Üret )} {/* Onayla ve video üretimini başlat */} - {isEditable && hasScript && ( + {hasScript && ( {approveMutation.isPending ? ( @@ -463,14 +459,35 @@ export default function ProjectDetailPage() { )} - {/* Hata mesajı */} + {/* Hata mesajı — kapatılabilir ve aksiyon butonlu */} {project.errorMessage && ( - - - Hata + + + + Hata + + { + // Sayfayı yeniden yükleyerek güncel veriyi al + refetch(); + }} + className="text-red-400/60 hover:text-red-400 transition-colors" + title="Kapat" + > + + - {project.errorMessage} + {project.errorMessage} + {hasScript && ( + + {generateScriptMutation.isPending ? 'Üretiliyor...' : '🔄 Senaryoyu Yeniden Üret'} + + )} )} @@ -512,6 +529,7 @@ export default function ProjectDetailPage() { key={scene.id} scene={scene} isEditable={isEditable} + isRendering={isRendering} onUpdate={handleSceneUpdate} onRegenerate={handleSceneRegenerate} onGenerateImage={handleGenerateImage} diff --git a/src/app/[locale]/(dashboard)/dashboard/projects/page.tsx b/src/app/[locale]/(dashboard)/dashboard/projects/page.tsx index 6cd8512..89369b4 100644 --- a/src/app/[locale]/(dashboard)/dashboard/projects/page.tsx +++ b/src/app/[locale]/(dashboard)/dashboard/projects/page.tsx @@ -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 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 ( - - - - - - - {project.title} - - - - {new Date(project.createdAt).toLocaleDateString( - "tr-TR", - { - day: "numeric", - month: "short", - year: "numeric", - }, - )} - - {project.language && • {project.language}} - {typeof project.creditsUsed === "number" && - project.creditsUsed > 0 && ( - • {project.creditsUsed} kredi - )} + + - - - {st.label} - + + + {project.title} + + + + {new Date(project.createdAt).toLocaleDateString( + "tr-TR", + { + day: "numeric", + month: "short", + year: "numeric", + }, + )} + + {project.language && • {project.language}} + {typeof project.creditsUsed === "number" && + project.creditsUsed > 0 && ( + • {project.creditsUsed} kredi + )} + + + + {st.label} + + + 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} > - - - + ); }) )} )} + + {/* ─── Silme Onay Modal ─── */} + + {deleteTarget && ( + !deleteMutation.isPending && setDeleteTarget(null)} + > + e.stopPropagation()} + > + {/* Kapatma butonu */} + 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" + > + + + + {/* Icon */} + + + + + {/* İçerik */} + + Projeyi Sil + + + Bu projeyi silmek istediğinize emin misiniz? + + + “{deleteTarget.title}” + + + {/* Butonlar */} + + 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 + + + {deleteMutation.isPending ? ( + <> + + Siliniyor... + > + ) : ( + <> + + Evet, Sil + > + )} + + + + + )} + ); } diff --git a/src/components/project/scene-card.tsx b/src/components/project/scene-card.tsx index a5990dc..05bf20c 100644 --- a/src/components/project/scene-card.tsx +++ b/src/components/project/scene-card.tsx @@ -17,6 +17,7 @@ interface SceneCardProps { mediaAssets?: Array<{ id: string; type: string; url?: string }>; }; isEditable: boolean; + isRendering?: boolean; onUpdate?: (sceneId: string, data: { narrationText?: string; visualPrompt?: string; subtitleText?: string }) => void; onRegenerate?: (sceneId: string) => void; onGenerateImage?: (sceneId: string, customPrompt?: string) => void; @@ -29,6 +30,7 @@ interface SceneCardProps { export function SceneCard({ scene, isEditable, + isRendering = false, onUpdate, onRegenerate, onGenerateImage, @@ -91,19 +93,20 @@ export function SceneCard({ {/* Aksiyon butonları */} - {isEditable && !isEditing && ( + {!isEditing && ( setIsEditing(true)} - className="w-7 h-7 rounded-lg flex items-center justify-center text-[var(--color-text-muted)] hover:text-violet-400 hover:bg-violet-500/10 transition-colors" + disabled={!isEditable || isRendering} + className="w-7 h-7 rounded-lg flex items-center justify-center text-[var(--color-text-muted)] hover:text-violet-400 hover:bg-violet-500/10 transition-colors disabled:opacity-40 disabled:cursor-not-allowed" title="Düzenle" > onRegenerate?.(scene.id)} - disabled={isRegenerating} - className="w-7 h-7 rounded-lg flex items-center justify-center text-[var(--color-text-muted)] hover:text-cyan-400 hover:bg-cyan-500/10 transition-colors disabled:opacity-40" + disabled={!isEditable || isRendering || isRegenerating} + className="w-7 h-7 rounded-lg flex items-center justify-center text-[var(--color-text-muted)] hover:text-cyan-400 hover:bg-cyan-500/10 transition-colors disabled:opacity-40 disabled:cursor-not-allowed" title="AI ile yeniden üret" > @@ -235,28 +238,27 @@ export function SceneCard({ )} - {isEditable && ( - + {/* Görsel üretim butonları — tüm projelerde her zaman göster, render sürecinde disable et */} + + onGenerateImage?.(scene.id, scene.visualPrompt)} + disabled={isRendering || isGeneratingImage || isUpscalingImage} + className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-emerald-500/15 text-emerald-400 text-xs font-medium hover:bg-emerald-500/25 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" + > + {isGeneratingImage ? : } + {thumbnailAsset ? "Görseli Yeniden Üret" : "Görsel Üret"} + + {thumbnailAsset?.url && ( onGenerateImage?.(scene.id, scene.visualPrompt)} - disabled={isGeneratingImage || isUpscalingImage} - className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-emerald-500/15 text-emerald-400 text-xs font-medium hover:bg-emerald-500/25 transition-colors disabled:opacity-50" + onClick={() => onUpscaleImage?.(scene.id)} + disabled={isRendering || isUpscalingImage || isGeneratingImage} + className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-orange-500/15 text-orange-400 text-xs font-medium hover:bg-orange-500/25 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" > - {isGeneratingImage ? : } - {thumbnailAsset ? "Görseli Yeniden Üret" : "Görsel Üret"} + {isUpscalingImage ? : } + Upscale (4K) - {thumbnailAsset?.url && ( - onUpscaleImage?.(scene.id)} - disabled={isUpscalingImage || isGeneratingImage} - className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-orange-500/15 text-orange-400 text-xs font-medium hover:bg-orange-500/25 transition-colors disabled:opacity-50" - > - {isUpscalingImage ? : } - Upscale (4K) - - )} - - )} + )} + )}
{project.errorMessage}
- {project.title} -
+ {project.title} +
+ Bu projeyi silmek istediğinize emin misiniz? +
+ “{deleteTarget.title}” +