File size: 6,757 Bytes
dea9ad9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | import { useState } from "react";
import { Check, Trash2, X, Play, Wand2, ArrowRight } from "lucide-react";
import type { MediaItem } from "../types";
interface MediaTileProps {
item: MediaItem;
onOpen: () => void;
onDelete: (item: MediaItem) => Promise<void> | void;
onGenerateVideo?: (item: MediaItem, motionPrompt: string) => void;
}
export function MediaTile({ item, onOpen, onDelete, onGenerateVideo }: MediaTileProps) {
const [confirmDelete, setConfirmDelete] = useState(false);
const [deleting, setDeleting] = useState(false);
const [showI2VInput, setShowI2VInput] = useState(false);
const [motionPrompt, setMotionPrompt] = useState("");
async function confirm(event: React.MouseEvent) {
event.stopPropagation();
if (deleting) return;
setDeleting(true);
try {
await onDelete(item);
} finally {
setDeleting(false);
setConfirmDelete(false);
}
}
return (
<div
onClick={onOpen}
className="cursor-pointer rounded-xl overflow-hidden border border-gray-800/60 hover:border-gray-600 aspect-[3/4] bg-gray-900/50 relative group transition-all duration-200 hover:shadow-xl hover:shadow-black/30 hover:-translate-y-0.5"
>
{/* Media */}
{item.type === "video" ? (
<video src={item.thumb || item.url} className="w-full h-full object-cover" preload="metadata" muted />
) : (
<img src={item.thumb || item.url} alt={item.name || ""} className="w-full h-full object-cover group-hover:scale-105 transition duration-500" loading="lazy" />
)}
{/* Delete button */}
{!confirmDelete && (
<button
onClick={(event) => {
event.stopPropagation();
setConfirmDelete(true);
}}
className="absolute top-2 right-2 z-10 w-8 h-8 rounded-lg bg-black/60 text-white/80 backdrop-blur-sm flex items-center justify-center opacity-0 group-hover:opacity-100 hover:bg-red-600 hover:text-white transition-all"
title="Delete"
>
<Trash2 className="w-4 h-4" />
</button>
)}
{/* I2V button / prompt — only for images */}
{!confirmDelete && item.type === "image" && onGenerateVideo && (
<>
{!showI2VInput ? (
<button
onClick={(event) => {
event.stopPropagation();
setShowI2VInput(true);
}}
className="absolute top-2 left-2 z-10 flex items-center gap-1.5 rounded-lg bg-violet-600/80 text-white text-[10px] font-medium px-2 py-1.5 backdrop-blur-sm opacity-0 group-hover:opacity-100 hover:bg-violet-500 transition-all"
title="Generate Video"
>
<Wand2 className="w-3 h-3" />
I2V
</button>
) : (
<div
className="absolute inset-0 z-20 bg-black/90 backdrop-blur-sm flex flex-col items-center justify-center gap-2 px-4"
onClick={(event) => event.stopPropagation()}
>
<p className="text-[10px] uppercase tracking-wider text-white/60">Motion Prompt</p>
<input
autoFocus
value={motionPrompt}
onChange={(e) => setMotionPrompt(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
onGenerateVideo(item, motionPrompt);
setShowI2VInput(false);
setMotionPrompt("");
}
if (e.key === "Escape") {
setShowI2VInput(false);
setMotionPrompt("");
}
}}
placeholder="camera push in, rain falling..."
className="w-full rounded-lg bg-gray-800 border border-gray-700 px-3 py-2 text-xs text-white focus:outline-none focus:border-violet-500 placeholder:text-gray-600"
/>
<div className="flex gap-2">
<button
onClick={() => {
setShowI2VInput(false);
setMotionPrompt("");
}}
className="text-[10px] text-gray-500 hover:text-white transition"
>
Cancel
</button>
<button
onClick={() => {
onGenerateVideo(item, motionPrompt);
setShowI2VInput(false);
setMotionPrompt("");
}}
className="flex items-center gap-1 text-[10px] text-violet-400 hover:text-violet-200 transition font-medium"
>
Generate <ArrowRight className="w-3 h-3" />
</button>
</div>
</div>
)}
</>
)}
{/* Bottom info bar */}
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 via-black/40 to-transparent p-3 pt-6 opacity-0 group-hover:opacity-100 transition pointer-events-none">
<p className="text-xs font-medium truncate text-white/90">{item.name || "Untitled"}</p>
</div>
{/* Video badge */}
{item.type === "video" && (
<div className="absolute top-2 left-2 bg-black/60 backdrop-blur-sm text-white text-[10px] font-medium px-2 py-1 rounded-md flex items-center gap-1">
<Play className="w-2.5 h-2.5 fill-white" />
VIDEO
</div>
)}
{/* Delete confirmation overlay */}
{confirmDelete && (
<div
className="absolute inset-0 z-20 bg-black/80 backdrop-blur-sm flex flex-col items-center justify-center gap-3"
onClick={(event) => {
event.stopPropagation();
setConfirmDelete(false);
}}
>
<span className="text-xs font-semibold uppercase tracking-wider text-white/80">Delete this?</span>
<div className="flex gap-3" onClick={(event) => event.stopPropagation()}>
<button
onClick={() => setConfirmDelete(false)}
className="w-10 h-10 rounded-full bg-gray-800/90 text-gray-400 hover:text-white hover:bg-gray-700 flex items-center justify-center transition"
title="Cancel"
>
<X className="w-5 h-5" />
</button>
<button
onClick={confirm}
disabled={deleting}
className="w-10 h-10 rounded-full bg-red-600/90 text-white hover:bg-red-500 disabled:bg-gray-800 disabled:text-gray-600 flex items-center justify-center transition"
title="Confirm delete"
>
<Check className="w-5 h-5" />
</button>
</div>
</div>
)}
</div>
);
}
|