Files
ContentGen_FE/src/app/[locale]/(dashboard)/dashboard/projects/[id]/page.tsx
Harun CAN dd8878d403
Some checks failed
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Has been cancelled
main
2026-04-05 20:37:03 +03:00

559 lines
25 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 } from 'framer-motion';
import {
ArrowLeft,
Play,
Sparkles,
RefreshCw,
Clock,
CheckCircle2,
AlertCircle,
Loader2,
FileText,
Film,
Trash2,
MoreVertical,
} from 'lucide-react';
import Link from 'next/link';
import { useState } from 'react';
import {
useProject,
useGenerateScript,
useApproveAndQueue,
useUpdateProject,
useDeleteProject,
useGenerateSceneImage,
useUpscaleSceneImage,
useRegenerateScene
} 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 } from '@/lib/api/api-service';
// 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-slate-400', icon: FileText, bgClass: 'bg-slate-500/10 border-slate-500/20' },
GENERATING_SCRIPT: { label: 'Senaryo Üretiliyor', color: 'text-violet-400', icon: Sparkles, bgClass: 'bg-violet-500/10 border-violet-500/20' },
PENDING: { label: 'Kuyrukta', color: 'text-amber-400', icon: Clock, bgClass: 'bg-amber-500/10 border-amber-500/20' },
GENERATING_MEDIA: { label: 'Medya Üretiliyor', color: 'text-cyan-400', icon: Sparkles, bgClass: 'bg-cyan-500/10 border-cyan-500/20' },
RENDERING: { label: 'Video İşleniyor', color: 'text-blue-400', icon: Film, bgClass: 'bg-blue-500/10 border-blue-500/20' },
COMPLETED: { label: 'Tamamlandı', color: 'text-emerald-400', icon: CheckCircle2, bgClass: 'bg-emerald-500/10 border-emerald-500/20' },
FAILED: { label: 'Başarısız', color: 'text-red-400', icon: AlertCircle, bgClass: 'bg-red-500/10 border-red-500/20' },
};
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);
// Veri hook'ları
const { data: project, isLoading, error, refetch } = useProject(id);
const generateScriptMutation = useGenerateScript();
const approveMutation = useApproveAndQueue();
const deleteMutation = useDeleteProject();
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(),
});
};
// 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-violet-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;
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 (err) {
console.error('Üslup (Stil) değiştirme hatası:', err);
}
};
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={() => { 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>
{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>
)}
<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) */}
{isEditable && !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"
>
{generateScriptMutation.isPending ? (
<Loader2 size={15} className="animate-spin" />
) : (
<Sparkles size={15} />
)}
AI ile Senaryo Üret
</button>
)}
{/* Senaryo yeniden üret (draft/failed, senaryo var) */}
{isEditable && 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"
>
{generateScriptMutation.isPending ? (
<Loader2 size={15} className="animate-spin" />
) : (
<RefreshCw size={15} />
)}
Yeniden Üret
</button>
)}
{/* Onayla ve video üretimini başlat */}
{isEditable && 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"
>
{approveMutation.isPending ? (
<Loader2 size={15} className="animate-spin" />
) : (
<Play size={15} />
)}
Onayla & Video Üret
</button>
)}
</div>
{/* Hata mesajı */}
{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>
<p className="text-xs text-red-400/80">{project.errorMessage}</p>
</div>
)}
</motion.div>
{/* ── Render Progress (WebSocket) ── */}
{isRendering && (
<motion.div variants={fadeUp}>
<RenderProgress renderState={renderState} />
</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-violet-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}
onUpdate={handleSceneUpdate}
onRegenerate={handleSceneRegenerate}
isRegenerating={regeneratingSceneId === 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-gradient-to-br from-violet-500/15 to-cyan-400/10 mx-auto mb-4 flex items-center justify-center">
<Sparkles size={28} className="text-violet-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-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"
>
{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}>
<h2 className="text-sm font-semibold text-[var(--color-text-primary)] mb-3 flex items-center gap-2">
<Clock size={15} className="text-cyan-400" />
Render Geçmişi
</h2>
<div className="space-y-2">
{project.renderJobs.map((job) => (
<div key={job.id} className="card-surface p-3 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-emerald-400' :
job.status === 'FAILED' ? 'bg-red-400' :
'bg-amber-400 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>
)}
<span>
{new Date(job.createdAt).toLocaleTimeString('tr-TR', { hour: '2-digit', minute: '2-digit' })}
</span>
</div>
</div>
))}
</div>
</motion.div>
)}
</motion.div>
);
}