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

795 lines
36 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 { useParams, useRouter } from 'next/navigation';
import { motion, AnimatePresence } from 'framer-motion';
import {
ArrowLeft,
Play,
Sparkles,
RefreshCw,
Clock,
CheckCircle2,
AlertCircle,
Loader2,
FileText,
Film,
Trash2,
MoreVertical,
X,
Languages,
} from 'lucide-react';
import Link from 'next/link';
import { useState, useEffect } from 'react';
import {
useProject,
useGenerateScript,
useApproveAndQueue,
useUpdateProject,
useDeleteProject,
useGenerateSceneImage,
useUpscaleSceneImage,
useRegenerateScene,
useCancelRender
} from '@/hooks/use-api';
import { useRenderProgress } from '@/hooks/use-render-progress';
import { SceneCard } from '@/components/project/scene-card';
import { RenderProgress } from '@/components/project/render-progress';
import { VideoPlayer } from '@/components/project/video-player';
import { projectsApi, apiClient } from '@/lib/api/api-service';
import { CINEMATIC_REFERENCES } from '@/constants/cinematic-references';
import { languages } from '@/components/projects/ProjectConfiguration';
import { toaster as toast } from '@/components/ui/feedback/toaster';
// X (Twitter) ikonunu burada da tanımlıyoruz
const XIcon = ({ size = 16 }: { size?: number }) => (
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
);
const STATUS_MAP: Record<string, { label: string; color: string; icon: React.ElementType; bgClass: string }> = {
DRAFT: { label: 'Taslak', color: 'text-neutral-400', icon: FileText, bgClass: 'bg-neutral-500/10 border-neutral-500/20' },
GENERATING_SCRIPT: { label: 'Senaryo Üretiliyor', color: 'text-neutral-300', icon: Sparkles, bgClass: 'bg-neutral-500/10 border-neutral-500/20' },
PENDING: { label: 'Kuyrukta', color: 'text-neutral-400', icon: Clock, bgClass: 'bg-neutral-500/10 border-neutral-500/20' },
GENERATING_MEDIA: { label: 'Medya Üretiliyor', color: 'text-neutral-300', icon: Sparkles, bgClass: 'bg-neutral-500/10 border-neutral-500/20' },
RENDERING: { label: 'Video İşleniyor', color: 'text-neutral-300', icon: Film, bgClass: 'bg-neutral-500/10 border-neutral-500/20' },
COMPLETED: { label: 'Tamamlandı', color: 'text-neutral-100', icon: CheckCircle2, bgClass: 'bg-neutral-500/20 border-neutral-500/30' },
FAILED: { label: 'Başarısız', color: 'text-neutral-500', icon: AlertCircle, bgClass: 'bg-neutral-800/50 border-neutral-800' },
};
const videoStyles = [
// Film & Sinema
{ id: "CINEMATIC", label: "Sinematik", emoji: "🎬", desc: "Film kalitesinde görseller", category: "Film & Sinema" },
{ id: "DOCUMENTARY", label: "Belgesel", emoji: "📹", desc: "Bilimsel ve ciddi ton", category: "Film & Sinema" },
{ id: "STORYTELLING", label: "Hikâye Anlatımı", emoji: "📖", desc: "Anlatı odaklı, sürükleyici", category: "Film & Sinema" },
{ id: "NEWS", label: "Haber", emoji: "📰", desc: "Güncel ve bilgilendirici", category: "Film & Sinema" },
{ id: "ARTISTIC", label: "Sanatsal", emoji: "🎨", desc: "Yaratıcı ve sıra dışı", category: "Film & Sinema" },
{ id: "NOIR", label: "Film Noir", emoji: "🖤", desc: "Karanlık, dramatik", category: "Film & Sinema" },
{ id: "VLOG", label: "Vlog", emoji: "📱", desc: "Günlük, samimi", category: "Film & Sinema" },
// Animasyon
{ id: "ANIME", label: "Anime", emoji: "⛩️", desc: "Japon animasyon stili", category: "Animasyon" },
{ id: "ANIMATION_3D", label: "3D Animasyon", emoji: "🧊", desc: "Pixar kalitesi", category: "Animasyon" },
{ id: "ANIMATION_2D", label: "2D Animasyon", emoji: "✏️", desc: "Klasik el çizimi", category: "Animasyon" },
{ id: "STOP_MOTION", label: "Stop Motion", emoji: "🧸", desc: "Kare kare animasyon", category: "Animasyon" },
{ id: "MOTION_COMIC", label: "Hareketli Çizgi Roman", emoji: "💥", desc: "Panel bazlı anlatım", category: "Animasyon" },
{ id: "CARTOON", label: "Karikatür", emoji: "🎭", desc: "Çizgi film stili", category: "Animasyon" },
{ id: "CLAYMATION", label: "Claymation", emoji: "🏺", desc: "Kil animasyon", category: "Animasyon" },
{ id: "PIXEL_ART", label: "Pixel Art", emoji: "👾", desc: "8-bit retro oyun", category: "Animasyon" },
{ id: "ISOMETRIC", label: "İzometrik", emoji: "🔷", desc: "İzometrik animasyon", category: "Animasyon" },
// Eğitim & Bilgi
{ id: "EDUCATIONAL", label: "Eğitim", emoji: "🎓", desc: "Öğretici ve açıklayıcı", category: "Eğitim & Bilgi" },
{ id: "INFOGRAPHIC", label: "İnfografik", emoji: "📊", desc: "Veri görselleştirme", category: "Eğitim & Bilgi" },
{ id: "WHITEBOARD", label: "Whiteboard", emoji: "📝", desc: "Tahta animasyonu", category: "Eğitim & Bilgi" },
{ id: "EXPLAINER", label: "Explainer", emoji: "💡", desc: "Ürün/konsept anlatımı", category: "Eğitim & Bilgi" },
{ id: "DATA_VIZ", label: "Veri Görselleştirme", emoji: "📈", desc: "Grafikler ve tablolar", category: "Eğitim & Bilgi" },
// Retro & Nostaljik
{ id: "RETRO_80S", label: "Retro 80s", emoji: "🕹️", desc: "Synthwave estetik", category: "Retro & Nostaljik" },
{ id: "VINTAGE_FILM", label: "Vintage Film", emoji: "📽️", desc: "Super 8 filmi", category: "Retro & Nostaljik" },
{ id: "VHS", label: "VHS", emoji: "📼", desc: "Kaset estetik", category: "Retro & Nostaljik" },
{ id: "POLAROID", label: "Polaroid", emoji: "📸", desc: "Analog fotoğraf", category: "Retro & Nostaljik" },
{ id: "RETRO_90S", label: "Retro 90s Y2K", emoji: "💿", desc: "Y2K & internet", category: "Retro & Nostaljik" },
// Sanat Akımları
{ id: "WATERCOLOR", label: "Suluboya", emoji: "🎨", desc: "Suluboya resim", category: "Sanat Akımları" },
{ id: "OIL_PAINTING", label: "Yağlı Boya", emoji: "🖌️", desc: "Klasik tuval", category: "Sanat Akımları" },
{ id: "IMPRESSIONIST", label: "Empresyonist", emoji: "🌅", desc: "Monet tarzı", category: "Sanat Akımları" },
{ id: "POP_ART", label: "Pop Art", emoji: "🎯", desc: "Warhol stili", category: "Sanat Akımları" },
{ id: "UKIYO_E", label: "Ukiyo-e", emoji: "🏯", desc: "Japon gravür", category: "Sanat Akımları" },
{ id: "ART_DECO", label: "Art Deco", emoji: "✨", desc: "1920s zarafet", category: "Sanat Akımları" },
{ id: "SURREAL", label: "Sürrealist", emoji: "🌀", desc: "Dalí tarzı", category: "Sanat Akımları" },
{ id: "COMIC_BOOK", label: "Çizgi Roman", emoji: "💬", desc: "Marvel/DC stili", category: "Sanat Akımları" },
{ id: "SKETCH", label: "Karakalem", emoji: "✍️", desc: "Kalem çizim", category: "Sanat Akımları" },
// Modern & Minimal
{ id: "MINIMALIST", label: "Minimalist", emoji: "⚪", desc: "Apple estetiği", category: "Modern & Minimal" },
{ id: "GLASSMORPHISM", label: "Glassmorphism", emoji: "🔮", desc: "Cam efekti", category: "Modern & Minimal" },
{ id: "NEON", label: "Neon Glow", emoji: "💜", desc: "Neon ışıkları", category: "Modern & Minimal" },
{ id: "CYBERPUNK", label: "Cyberpunk", emoji: "🤖", desc: "Gelecek distopya", category: "Modern & Minimal" },
{ id: "STEAMPUNK", label: "Steampunk", emoji: "⚙️", desc: "Viktoryan mekanik", category: "Modern & Minimal" },
{ id: "ABSTRACT", label: "Soyut", emoji: "🔵", desc: "Abstract sanat", category: "Modern & Minimal" },
// Fotoğrafik
{ id: "PRODUCT", label: "Ürün Fotoğrafı", emoji: "📦", desc: "Studio çekim", category: "Fotoğrafik" },
{ id: "FASHION", label: "Moda", emoji: "👗", desc: "Editöryal çekim", category: "Fotoğrafik" },
{ id: "AERIAL", label: "Havadan", emoji: "🚁", desc: "Drone görüntüsü", category: "Fotoğrafik" },
{ id: "MACRO", label: "Makro", emoji: "🔬", desc: "Yakın çekim", category: "Fotoğrafik" },
{ id: "PORTRAIT", label: "Portre", emoji: "🧑", desc: "Portre fotoğraf", category: "Fotoğrafik" },
];
const stagger = {
hidden: { opacity: 0 },
show: { opacity: 1, transition: { staggerChildren: 0.06 } },
};
const fadeUp = {
hidden: { opacity: 0, y: 12 },
show: { opacity: 1, y: 0, transition: { duration: 0.4, ease: [0.16, 1, 0.3, 1] as const } },
};
export default function ProjectDetailPage() {
const { id } = useParams<{ id: string }>();
const router = useRouter();
const [showMenu, setShowMenu] = useState(false);
const [regeneratingSceneId, setRegeneratingSceneId] = useState<string | null>(null);
const [mounted, setMounted] = useState(false);
const [showTranslateModal, setShowTranslateModal] = useState(false);
const [targetLanguage, setTargetLanguage] = useState<string>("");
const [isTranslating, setIsTranslating] = useState(false);
const confirmTranslate = async () => {
if (!id || !targetLanguage) return;
try {
setIsTranslating(true);
const res = await apiClient.post(`/projects/${id}/translate`, { targetLanguage });
toast.success("Proje başarıyla çevrildi!");
setShowTranslateModal(false);
setTargetLanguage("");
// refetch() to maybe update some states, or router.push to the new project
router.push(`/dashboard/projects/${res.data.id}`);
} catch (err: any) {
toast.error(err?.response?.data?.message || "Çeviri sırasında bir hata oluştu.");
} finally {
setIsTranslating(false);
}
};
useEffect(() => {
setMounted(true);
}, []);
// Veri hook'ları (Aktif işlem varsa 3 saniyede bir polling yap)
const { data: project, isLoading, error, refetch } = useProject(id, {
refetchInterval: (data: any) => {
if (!data) return false;
const isGenerating = data.status === 'GENERATING_MEDIA' ||
data.renderJobs?.some((j: any) => j.status === 'QUEUED' || j.status === 'PROCESSING');
return isGenerating ? 3000 : false;
}
});
const generateScriptMutation = useGenerateScript();
const approveMutation = useApproveAndQueue();
const deleteMutation = useDeleteProject();
const cancelRenderMutation = useCancelRender();
const generateImageMutation = useGenerateSceneImage();
const upscaleImageMutation = useUpscaleSceneImage();
const regenerateSceneMutation = useRegenerateScene();
const [generatingImageId, setGeneratingImageId] = useState<string | null>(null);
const [upscalingImageId, setUpscalingImageId] = useState<string | null>(null);
// WebSocket progress
const renderState = useRenderProgress(
project?.status && ['PENDING', 'GENERATING_MEDIA', 'RENDERING'].includes(project.status) ? id : undefined,
);
// Sahne güncelleme
const handleSceneUpdate = async (sceneId: string, data: { narrationText?: string; visualPrompt?: string; subtitleText?: string }) => {
try {
await projectsApi.update(`${id}/scenes/${sceneId}` as unknown as string, data as any);
refetch();
} catch (err) {
console.error('Sahne güncelleme hatası:', err);
}
};
// Sahne yeniden üretim
const handleSceneRegenerate = async (sceneId: string) => {
setRegeneratingSceneId(sceneId);
regenerateSceneMutation.mutate(
{ projectId: id, sceneId },
{
onSettled: () => setRegeneratingSceneId(null),
onSuccess: () => refetch(),
}
);
};
// Sahne görseli oluşturma
const handleGenerateImage = (sceneId: string, customPrompt?: string) => {
setGeneratingImageId(sceneId);
generateImageMutation.mutate(
{ projectId: id, sceneId, customPrompt },
{
onSettled: () => setGeneratingImageId(null),
onSuccess: () => refetch(),
}
);
};
// Sahne görseli upscale
const handleUpscaleImage = (sceneId: string) => {
setUpscalingImageId(sceneId);
upscaleImageMutation.mutate(
{ projectId: id, sceneId },
{
onSettled: () => setUpscalingImageId(null),
onSuccess: () => refetch(),
}
);
};
// Senaryo üret
const handleGenerateScript = () => {
generateScriptMutation.mutate(id, {
onSuccess: () => refetch(),
});
};
// Onayla ve gönder
const handleApprove = () => {
approveMutation.mutate(id, {
onSuccess: () => refetch(),
});
};
// İptal et
const handleCancelRender = () => {
if (confirm('Aktif video üretimini iptal etmek istediğinize emin misiniz?')) {
cancelRenderMutation.mutate(id, {
onSuccess: () => refetch(),
});
}
};
// Sil
const handleDelete = () => {
if (confirm('Bu projeyi silmek istediğinize emin misiniz?')) {
deleteMutation.mutate(id, {
onSuccess: () => router.push('/dashboard/projects'),
});
}
};
// ── Loading ──
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<div className="flex flex-col items-center gap-3">
<Loader2 size={32} className="animate-spin text-neutral-400" />
<p className="text-sm text-[var(--color-text-muted)]">Proje yükleniyor...</p>
</div>
</div>
);
}
// ── Error ──
if (error || !project) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<div className="card-surface p-8 text-center max-w-md">
<AlertCircle size={40} className="text-red-400 mx-auto mb-3" />
<h2 className="text-lg font-semibold mb-2">Proje Bulunamadı</h2>
<p className="text-sm text-[var(--color-text-muted)] mb-4">
Bu proje silinmiş veya erişim izniniz yok.
</p>
<Link href="/dashboard/projects" className="btn-primary text-sm">
Projelere Dön
</Link>
</div>
</div>
);
}
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; // 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;
const currentStyle = videoStyles.find(s => s.id === project.videoStyle);
const handleStyleChange = async (newStyleId: string) => {
if (newStyleId === project.videoStyle) return;
try {
await projectsApi.update(id, { videoStyle: newStyleId } as any);
refetch();
} catch (error) {
console.error('Failed to update style:', error);
}
};
const handleCinematicReferenceChange = async (newRef: string) => {
if (newRef === project.cinematicReference) return;
try {
await projectsApi.update(id, { cinematicReference: newRef || undefined } as any);
refetch();
} catch (error) {
console.error('Failed to update cinematic reference:', error);
}
};
return (
<motion.div
variants={stagger}
initial="hidden"
animate="show"
className="space-y-5 max-w-4xl mx-auto pb-8"
>
{/* ── Üst Bar — Geri + Aksiyonlar ── */}
<motion.div variants={fadeUp} className="flex items-center justify-between">
<Link
href="/dashboard/projects"
className="flex items-center gap-2 text-sm text-[var(--color-text-muted)] hover:text-[var(--color-text-primary)] transition-colors"
>
<ArrowLeft size={16} />
Projeler
</Link>
<div className="relative">
<button
onClick={() => setShowMenu(!showMenu)}
className="w-8 h-8 rounded-lg flex items-center justify-center text-[var(--color-text-muted)] hover:bg-[var(--color-bg-elevated)] transition-colors"
>
<MoreVertical size={16} />
</button>
{showMenu && (
<div className="absolute right-0 top-10 card-surface p-1.5 min-w-[160px] z-50 shadow-xl">
<button
onClick={() => { refetch(); setShowMenu(false); }}
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-elevated)] rounded-lg transition-colors"
>
<RefreshCw size={14} /> Yenile
</button>
<button
onClick={() => { setShowMenu(false); setShowTranslateModal(true); }}
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-[var(--color-text-secondary)] hover:bg-[var(--color-bg-elevated)] rounded-lg transition-colors"
>
<Languages size={14} /> Çevir
</button>
<button
onClick={() => { handleDelete(); setShowMenu(false); }}
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-red-400 hover:bg-red-500/10 rounded-lg transition-colors"
>
<Trash2 size={14} /> Sil
</button>
</div>
)}
</div>
</motion.div>
{/* ── Proje Header ── */}
<motion.div variants={fadeUp} className="card-surface p-5 md:p-6">
<div className="flex flex-col md:flex-row md:items-start md:justify-between gap-4">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2">
{/* Durum badge */}
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-medium border ${statusInfo.bgClass} ${statusInfo.color}`}>
<StatusIcon size={12} />
{statusInfo.label}
</span>
{/* Tweet kaynak badge */}
{project.sourceType === 'X_TWEET' && (
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-lg text-[10px] font-medium bg-sky-500/10 border border-sky-500/20 text-sky-400">
<XIcon size={10} /> Tweet
</span>
)}
</div>
<h1 className="font-[family-name:var(--font-display)] text-xl md:text-2xl font-bold tracking-tight text-[var(--color-text-primary)] truncate">
{project.title}
</h1>
{project.description && (
<p className="text-sm text-[var(--color-text-muted)] mt-1.5 line-clamp-2">
{project.description}
</p>
)}
</div>
{/* Meta bilgiler */}
<div className="flex flex-wrap items-center gap-3 text-xs text-[var(--color-text-ghost)] shrink-0">
<span className="flex items-center gap-1">
<Clock size={12} /> {project.targetDuration}s
</span>
<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-neutral-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)}
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-neutral-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>
)}
<span className="uppercase text-[10px] tracking-wider">{project.language}</span>
<span className="text-[10px]">
{new Date(project.createdAt).toLocaleDateString('tr-TR', {
day: 'numeric', month: 'short', year: 'numeric',
})}
</span>
</div>
</div>
{/* Tweet kaynak bilgisi */}
{tweetData && (
<div className="mt-4 p-3 rounded-xl bg-sky-500/5 border border-sky-500/10">
<div className="flex items-center gap-2 mb-1">
<XIcon size={14} />
<span className="text-xs font-medium text-sky-400">
@{tweetData.authorUsername as string}
</span>
{tweetData.viralScore && (
<span className="text-[10px] px-1.5 py-0.5 rounded-md bg-amber-500/15 text-amber-400 font-medium">
🔥 {String(tweetData.viralScore)}/100
</span>
)}
</div>
<p className="text-xs text-[var(--color-text-muted)] line-clamp-2">
{tweetData.text as string}
</p>
</div>
)}
{/* 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) */}
{!hasScript && (
<button
onClick={handleGenerateScript}
disabled={isRendering || generateScriptMutation.isPending}
className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)] text-sm font-medium hover:bg-neutral-800 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{generateScriptMutation.isPending ? (
<Loader2 size={15} className="animate-spin" />
) : (
<Sparkles size={15} />
)}
AI ile Senaryo Üret
</button>
)}
{/* Senaryo yeniden üret (draft/failed, senaryo var) */}
{hasScript && (
<button
onClick={handleGenerateScript}
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} />
)}
Senaryoyu Yeniden Üret
</button>
)}
{/* Onayla ve video üretimini başlat */}
{hasScript && (
<button
onClick={handleApprove}
disabled={isRendering || approveMutation.isPending}
className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)] text-sm font-medium hover:bg-neutral-800 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{approveMutation.isPending ? (
<Loader2 size={15} className="animate-spin" />
) : (
<Play size={15} />
)}
Onayla & Video Üret
</button>
)}
{/* Üretimi İptal Et (Sadece aktif işlem varsa) */}
{isRendering && (
<button
onClick={handleCancelRender}
disabled={cancelRenderMutation.isPending}
className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-red-500/10 text-red-400 text-sm font-medium hover:bg-red-500/20 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{cancelRenderMutation.isPending ? (
<Loader2 size={15} className="animate-spin" />
) : (
<X size={15} />
)}
Üretimi İptal Et
</button>
)}
</div>
{/* 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 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 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>
{/* ── Render Progress (WebSocket) ── */}
{isRendering && (
<motion.div variants={fadeUp}>
<RenderProgress renderState={renderState} projectStatus={project.status} />
</motion.div>
)}
{/* ── Video Player (tamamlandıysa) ── */}
{isCompleted && project.finalVideoUrl && (
<motion.div variants={fadeUp}>
<VideoPlayer
videoUrl={project.finalVideoUrl}
thumbnailUrl={project.thumbnailUrl}
title={project.title}
/>
</motion.div>
)}
{/* ── Sahneler ── */}
{hasScript && (
<motion.div variants={fadeUp}>
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold text-[var(--color-text-primary)] flex items-center gap-2">
<Film size={15} className="text-neutral-400" />
Senaryo {project.scenes!.length} sahne
</h2>
<span className="text-[10px] text-[var(--color-text-ghost)]">
Toplam: {project.scenes!.reduce((sum, s) => sum + s.duration, 0)}s
</span>
</div>
<div className="space-y-3">
{project.scenes!.map((scene) => (
<SceneCard
key={scene.id}
scene={scene}
isEditable={isEditable}
isRendering={isRendering}
onUpdate={handleSceneUpdate}
onRegenerate={handleSceneRegenerate}
onGenerateImage={handleGenerateImage}
onUpscaleImage={handleUpscaleImage}
isRegenerating={regeneratingSceneId === scene.id}
isGeneratingImage={generatingImageId === scene.id}
isUpscalingImage={upscalingImageId === scene.id}
/>
))}
</div>
</motion.div>
)}
{/* ── Boş durum (senaryo yok) ── */}
{!hasScript && isEditable && (
<motion.div variants={fadeUp} className="card-surface p-8 text-center">
<div className="w-16 h-16 rounded-2xl bg-[var(--color-bg-elevated)] mx-auto mb-4 flex items-center justify-center">
<Sparkles size={28} className="text-neutral-400" />
</div>
<h3 className="text-base font-semibold mb-1.5">Henüz senaryo üretilmedi</h3>
<p className="text-sm text-[var(--color-text-muted)] mb-4 max-w-sm mx-auto">
AI'ın projeniz için etkileyici bir video senaryosu oluşturmasını sağlayın.
</p>
<button
onClick={handleGenerateScript}
disabled={generateScriptMutation.isPending}
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-xl bg-[var(--color-bg-inverted)] text-[var(--color-text-inverted)] text-sm font-medium hover:bg-neutral-800 transition-colors disabled:opacity-50"
>
{generateScriptMutation.isPending ? (
<Loader2 size={15} className="animate-spin" />
) : (
<Sparkles size={15} />
)}
Senaryo Üret
</button>
</motion.div>
)}
{/* ── Render Geçmişi ── */}
{project.renderJobs && project.renderJobs.length > 0 && (
<motion.div variants={fadeUp}>
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold text-[var(--color-text-primary)] flex items-center gap-2">
<Clock size={15} className="text-neutral-400" />
Render Geçmişi
</h2>
{project.status === 'GENERATING_MEDIA' || project.renderJobs.some((j: any) => j.status === 'QUEUED' || j.status === 'PROCESSING') ? (
<button
onClick={handleCancelRender}
disabled={cancelRenderMutation.isPending}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-red-500/10 text-red-400 hover:bg-red-500/20 text-xs font-medium transition-colors"
>
{cancelRenderMutation.isPending ? <Loader2 size={13} className="animate-spin" /> : <X size={13} />}
İptal Et
</button>
) : null}
</div>
<div className="space-y-2">
{project.renderJobs.map((job) => (
<div key={job.id} className="card-surface p-3 flex flex-col gap-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2.5">
<div className={`w-2 h-2 rounded-full ${
job.status === 'COMPLETED' ? 'bg-neutral-100' :
job.status === 'FAILED' ? 'bg-neutral-500' :
job.status === 'CANCELLED' ? 'bg-neutral-600' :
'bg-neutral-300 animate-pulse'
}`} />
<span className="text-xs text-[var(--color-text-secondary)]">
Deneme #{job.attemptNumber}
</span>
<span className="text-[10px] text-[var(--color-text-ghost)]">
{job.status}
</span>
</div>
<div className="flex items-center gap-3 text-[10px] text-[var(--color-text-ghost)]">
{job.processingTimeMs && (
<span>{(job.processingTimeMs / 1000).toFixed(1)}s</span>
)}
{mounted ? (
<span>
{new Date(job.createdAt).toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit' })}
</span>
) : (
<span>--:--</span>
)}
</div>
</div>
{(job.status === 'QUEUED' || job.status === 'PROCESSING') && (
<div className="w-full bg-[var(--color-bg-surface)] rounded-full h-1.5 mt-1 overflow-hidden">
<div
className="bg-neutral-300 h-1.5 rounded-full transition-all duration-1000 ease-out relative"
style={{ width: `${job.progress || (job.status === 'QUEUED' ? 5 : 50)}%` }}
>
<div className="absolute inset-0 bg-white/20 animate-pulse" />
</div>
</div>
)}
</div>
))}
</div>
</motion.div>
)}
{/* ─── Çeviri Modal ─── */}
<AnimatePresence>
{showTranslateModal && (
<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={() => !isTranslating && setShowTranslateModal(false)}
>
<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={() => setShowTranslateModal(false)}
disabled={isTranslating}
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-blue-500/10 mb-4">
<Languages size={22} className="text-blue-400" />
</div>
{/* İçerik */}
<h3 className="text-base font-semibold text-[var(--color-text-primary)] mb-1.5">
Projeyi Çevir
</h3>
<p className="text-sm text-[var(--color-text-muted)] mb-3">
"{project?.title}" projesini başka bir dile çevirin. (1 kredi)
</p>
<select
value={targetLanguage}
onChange={(e) => setTargetLanguage(e.target.value)}
disabled={isTranslating}
className="w-full bg-[var(--color-bg-surface)] border border-[var(--color-border-default)] rounded-xl px-4 py-2.5 text-sm text-[var(--color-text-primary)] mb-5 outline-none focus:border-blue-500"
>
<option value="">Dil Seçin...</option>
{languages.map((lang) => (
<option key={lang.code} value={lang.code}>
{lang.flag} {lang.label}
</option>
))}
</select>
{/* Butonlar */}
<div className="flex items-center gap-3">
<button
onClick={() => setShowTranslateModal(false)}
disabled={isTranslating}
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={confirmTranslate}
disabled={isTranslating || !targetLanguage}
className="flex-1 flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl bg-blue-500 hover:bg-blue-600 text-white text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isTranslating ? (
<>
<Loader2 size={14} className="animate-spin" />
Çevriliyor...
</>
) : (
<>
<Languages size={14} />
Çevir (1 🪙)
</>
)}
</button>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
}