Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 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 | 27x 22x 27x 1x 1x 1x 1x 2x 2x 2x 2x | import type { DragEvent } from "react";
import { type Media, getImageUrl } from "../../lib/api";
import { Trash2, Play, Star } from "lucide-react";
type MediaItemProps = {
item: Media;
index: number;
isDragging: boolean;
isDragOver: boolean;
onDragStart: (e: DragEvent<HTMLDivElement>, mediaId: number) => void;
onDragEnd: (e: DragEvent<HTMLDivElement>) => void;
onDragOver: (e: DragEvent<HTMLDivElement>, mediaId: number) => void;
onDrop: (e: DragEvent<HTMLDivElement>, targetMediaId: number) => void;
onClick: () => void;
onSetCover: (mediaId: number) => void;
onDelete: (mediaId: number) => void;
};
export function MediaItem({
item,
index,
isDragging,
isDragOver,
onDragStart,
onDragEnd,
onDragOver,
onDrop,
onClick,
onSetCover,
onDelete,
}: MediaItemProps) {
const getMediaUrl = (media: Media) => {
return getImageUrl(media.storageKey);
};
return (
<li
className={`relative group transition-all rounded-lg cursor-grab active:cursor-grabbing list-none ${
isDragOver ? "ring-2 ring-primary-500 scale-105" : ""
} ${isDragging ? "opacity-50 scale-95" : ""}`}
draggable
onDragStart={(e) =>
onDragStart(e as unknown as DragEvent<HTMLDivElement>, item.id)
}
onDragEnd={(e) => onDragEnd(e as unknown as DragEvent<HTMLDivElement>)}
onDragOver={(e) =>
onDragOver(e as unknown as DragEvent<HTMLDivElement>, item.id)
}
onDrop={(e) => onDrop(e as unknown as DragEvent<HTMLDivElement>, item.id)}
>
<button
type="button"
aria-label={`View ${item.caption || item.mediaType} in lightbox`}
className="aspect-square w-full bg-background-muted rounded-lg overflow-hidden shadow-sm hover:shadow-md transition-shadow"
onClick={onClick}
>
{item.mediaType === "video" ? (
<div className="w-full h-full relative">
{item.thumbnailKey ? (
<img
src={getImageUrl(item.thumbnailKey)}
alt={item.caption || "Video thumbnail"}
className="w-full h-full object-cover pointer-events-none"
draggable={false}
loading="lazy"
/>
) : (
<div className="w-full h-full bg-background-subtle flex items-center justify-center">
<Play className="h-8 w-8 text-foreground-subtle" />
</div>
)}
<div className="absolute inset-0 flex items-center justify-center bg-overlay-light">
<div className="w-12 h-12 rounded-full bg-background-elevated/80 flex items-center justify-center">
<Play className="h-6 w-6 text-foreground-default ml-1" />
</div>
</div>
{item.durationSeconds && (
<span className="absolute bottom-1 right-1 bg-overlay-dark text-foreground-inverse text-xs px-1.5 py-0.5 rounded">
{Math.floor(item.durationSeconds / 60)}:
{String(item.durationSeconds % 60).padStart(2, "0")}
</span>
)}
</div>
) : (
<img
src={getMediaUrl(item)}
alt={item.caption || "Media"}
className="w-full h-full object-cover pointer-events-none"
draggable={false}
loading="lazy"
/>
)}
</button>
{/* Order Badge */}
<div className="absolute top-1.5 left-1.5">
<span className="bg-overlay-dark text-foreground-inverse text-xs font-medium w-5 h-5 rounded-full flex items-center justify-center">
{index + 1}
</span>
</div>
{/* Cover Badge */}
{item.isCover && (
<div className="absolute top-1.5 left-8">
<span className="bg-yellow-500 text-foreground-inverse text-xs font-medium px-1.5 py-0.5 rounded flex items-center gap-0.5">
<Star className="h-3 w-3" />
Cover
</span>
</div>
)}
{/* Actions */}
<div className="absolute top-1.5 right-1.5 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
{item.mediaType === "image" && !item.isCover && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onSetCover(item.id);
}}
className="p-1 bg-yellow-500 hover:bg-yellow-600 rounded-full text-foreground-inverse"
title="Set as cover"
>
<Star className="h-3 w-3" />
</button>
)}
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onDelete(item.id);
}}
className="p-1 bg-error hover:bg-error/90 rounded-full text-foreground-inverse"
title="Delete"
>
<Trash2 className="h-3 w-3" />
</button>
</div>
</li>
);
}
|