main
UI Deploy (Next-Auth Support) 🎨 / build-and-deploy (push) Has been cancelled

This commit is contained in:
Harun CAN
2026-04-05 20:37:03 +03:00
parent d8f9865dcf
commit dd8878d403
10 changed files with 767 additions and 285 deletions
+112 -23
View File
@@ -2,7 +2,7 @@
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Pencil, Check, X, RefreshCw, Clock, ArrowRight, Wand2, Image, Mic } from 'lucide-react';
import { Pencil, Check, X, RefreshCw, Clock, ArrowRight, Wand2, Image as ImageIcon, Mic, Maximize2 } from 'lucide-react';
interface SceneCardProps {
scene: {
@@ -19,13 +19,28 @@ interface SceneCardProps {
isEditable: boolean;
onUpdate?: (sceneId: string, data: { narrationText?: string; visualPrompt?: string; subtitleText?: string }) => void;
onRegenerate?: (sceneId: string) => void;
onGenerateImage?: (sceneId: string, customPrompt?: string) => void;
onUpscaleImage?: (sceneId: string) => void;
isRegenerating?: boolean;
isGeneratingImage?: boolean;
isUpscalingImage?: boolean;
}
export function SceneCard({ scene, isEditable, onUpdate, onRegenerate, isRegenerating }: SceneCardProps) {
export function SceneCard({
scene,
isEditable,
onUpdate,
onRegenerate,
onGenerateImage,
onUpscaleImage,
isRegenerating,
isGeneratingImage,
isUpscalingImage,
}: SceneCardProps) {
const [isEditing, setIsEditing] = useState(false);
const [editNarration, setEditNarration] = useState(scene.narrationText);
const [editVisual, setEditVisual] = useState(scene.visualPrompt);
const [lightboxOpen, setLightboxOpen] = useState(false);
const handleSave = () => {
onUpdate?.(scene.id, {
@@ -33,6 +48,7 @@ export function SceneCard({ scene, isEditable, onUpdate, onRegenerate, isRegener
visualPrompt: editVisual,
subtitleText: editNarration,
});
// If user edited visual prompt, and maybe wants to generate, they can click generate visual later.
setIsEditing(false);
};
@@ -42,6 +58,8 @@ export function SceneCard({ scene, isEditable, onUpdate, onRegenerate, isRegener
setIsEditing(false);
};
const thumbnailAsset = scene.mediaAssets?.find(a => a.type === 'THUMBNAIL');
return (
<motion.div
layout
@@ -119,7 +137,7 @@ export function SceneCard({ scene, isEditable, onUpdate, onRegenerate, isRegener
{/* Görsel prompt düzenleme */}
<div>
<label className="flex items-center gap-1.5 text-xs font-medium text-[var(--color-text-muted)] mb-1.5">
<Image size={12} /> Görsel Prompt
<ImageIcon size={12} /> Görsel Prompt
</label>
<textarea
value={editVisual}
@@ -160,30 +178,71 @@ export function SceneCard({ scene, isEditable, onUpdate, onRegenerate, isRegener
{/* Görsel Prompt */}
<div className="flex gap-2">
<div className="w-5 h-5 rounded-md bg-cyan-500/10 flex items-center justify-center shrink-0 mt-0.5">
<Image size={11} className="text-cyan-400" />
<ImageIcon size={11} className="text-cyan-400" />
</div>
<div className="flex-1 group/prompt relative">
<p className="text-xs text-[var(--color-text-ghost)] leading-relaxed italic pr-6">
{scene.visualPrompt}
</p>
<button
onClick={() => {
navigator.clipboard.writeText(scene.visualPrompt);
}}
className="absolute top-0 right-0 p-1 opacity-0 group-hover/prompt:opacity-100 transition-opacity bg-[var(--color-bg-elevated)] rounded-md text-[var(--color-text-muted)] hover:text-cyan-400 hover:bg-cyan-500/10"
title="Prompt'u Kopyala"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
</svg>
</button>
</div>
<p className="text-xs text-[var(--color-text-ghost)] leading-relaxed italic">
{scene.visualPrompt}
</p>
</div>
{/* Medya önizleme (varsa) */}
{scene.mediaAssets && scene.mediaAssets.length > 0 && (
<div className="flex gap-2 pt-1">
{scene.mediaAssets.slice(0, 3).map((asset) => (
<div
key={asset.id}
className="w-16 h-16 rounded-lg bg-[var(--color-bg-deep)] border border-[var(--color-border-faint)] flex items-center justify-center overflow-hidden"
>
{asset.url ? (
<img src={asset.url} alt="" className="w-full h-full object-cover" />
) : (
<Wand2 size={14} className="text-[var(--color-text-ghost)]" />
)}
{/* Görsel / Upscale Alanı */}
<div className="flex flex-col gap-2 pt-2">
{thumbnailAsset?.url ? (
<div className="relative group/thumb rounded-lg overflow-hidden border border-[var(--color-border-faint)] aspect-video max-w-sm">
<img
src={thumbnailAsset.url}
alt="Scene Thumbnail"
className="w-full h-full object-cover cursor-pointer hover:scale-105 transition-transform duration-500"
onClick={() => setLightboxOpen(true)}
/>
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover/thumb:opacity-100 transition-opacity flex items-center justify-center pointer-events-none">
<Maximize2 size={24} className="text-white" />
</div>
))}
</div>
)}
</div>
) : (
<div className="rounded-lg border border-dashed border-[var(--color-border-faint)] bg-[var(--color-bg-deep)] aspect-video max-w-sm flex flex-col items-center justify-center p-4">
<ImageIcon size={24} className="text-[var(--color-text-ghost)] mb-2" />
<p className="text-xs text-[var(--color-text-muted)] text-center">Görsel Henüz Üretilmedi</p>
</div>
)}
{isEditable && (
<div className="flex items-center gap-2 mt-1">
<button
onClick={() => 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"
>
{isGeneratingImage ? <RefreshCw size={13} className="animate-spin" /> : <ImageIcon size={13} />}
{thumbnailAsset ? "Görseli Yeniden Üret" : "Görsel Üret"}
</button>
{thumbnailAsset?.url && (
<button
onClick={() => 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 ? <RefreshCw size={13} className="animate-spin" /> : <Wand2 size={13} />}
Upscale (4K)
</button>
)}
</div>
)}
</div>
</motion.div>
)}
</AnimatePresence>
@@ -191,6 +250,36 @@ export function SceneCard({ scene, isEditable, onUpdate, onRegenerate, isRegener
{/* Sahne bağlantı çizgisi */}
<div className="absolute left-7 -bottom-3 w-px h-3 bg-gradient-to-b from-[var(--color-border-faint)] to-transparent" />
{/* Lightbox Modal */}
<AnimatePresence>
{lightboxOpen && thumbnailAsset?.url && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setLightboxOpen(false)}
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 backdrop-blur-sm p-4 md:p-10 cursor-zoom-out"
>
<motion.img
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
transition={{ type: "spring", damping: 25, stiffness: 300 }}
src={thumbnailAsset.url}
alt="Fullscreen Scene"
className="max-w-full max-h-full object-contain rounded-xl shadow-2xl"
onClick={(e) => e.stopPropagation()} // Click image prevents closing
/>
<button
onClick={() => setLightboxOpen(false)}
className="absolute top-6 right-6 p-2 rounded-full bg-black/50 text-white/70 hover:text-white hover:bg-black/70 transition-colors"
>
<X size={24} />
</button>
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
}