generated from fahricansecer/boilerplate-fe
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user