174 lines
7.1 KiB
TypeScript
174 lines
7.1 KiB
TypeScript
'use client';
|
|
|
|
import { X, ZoomIn, ZoomOut, Download, Film, Image as ImageIcon } from 'lucide-react';
|
|
import { useState, useRef, useEffect } from 'react';
|
|
|
|
interface AssetPreviewModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
assetUrl: string;
|
|
assetType: 'image' | 'video';
|
|
assetName?: string;
|
|
}
|
|
|
|
export default function AssetPreviewModal({
|
|
isOpen,
|
|
onClose,
|
|
assetUrl,
|
|
assetType,
|
|
assetName = 'Preview'
|
|
}: AssetPreviewModalProps) {
|
|
const [scale, setScale] = useState(1);
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
const [position, setPosition] = useState({ x: 0, y: 0 });
|
|
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Reset state when modal opens/closes or asset changes
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
setScale(1);
|
|
setPosition({ x: 0, y: 0 });
|
|
}
|
|
}, [isOpen, assetUrl]);
|
|
|
|
if (!isOpen) return null;
|
|
|
|
const handleZoomIn = () => setScale(s => Math.min(s + 0.5, 4));
|
|
const handleZoomOut = () => setScale(s => Math.max(s - 0.5, 1));
|
|
|
|
const handleWheel = (e: React.WheelEvent) => {
|
|
if (e.ctrlKey || e.metaKey) {
|
|
e.preventDefault();
|
|
const delta = e.deltaY > 0 ? -0.1 : 0.1;
|
|
setScale(s => Math.min(Math.max(s + delta, 0.5), 5));
|
|
}
|
|
};
|
|
|
|
const handleMouseDown = (e: React.MouseEvent) => {
|
|
if (scale > 1) {
|
|
setIsDragging(true);
|
|
setDragStart({ x: e.clientX - position.x, y: e.clientY - position.y });
|
|
}
|
|
};
|
|
|
|
const handleMouseMove = (e: React.MouseEvent) => {
|
|
if (isDragging && scale > 1) {
|
|
setPosition({
|
|
x: e.clientX - dragStart.x,
|
|
y: e.clientY - dragStart.y
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleMouseUp = () => setIsDragging(false);
|
|
|
|
return (
|
|
<div
|
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/95 backdrop-blur-md"
|
|
onClick={(e) => {
|
|
// Close if clicking the background
|
|
if (e.target === e.currentTarget) onClose();
|
|
}}
|
|
>
|
|
{/* Toolbar */}
|
|
<div className="absolute top-0 left-0 right-0 p-4 flex justify-between items-center z-50 bg-gradient-to-b from-black/80 to-transparent pointer-events-none">
|
|
<div className="pointer-events-auto flex items-center gap-3">
|
|
<div className="bg-white/10 backdrop-blur-md px-4 py-2 rounded-full border border-white/20 text-white font-medium text-sm flex items-center gap-2">
|
|
{assetType === 'video' ? <Film className="w-4 h-4 text-forge-yellow" /> : <ImageIcon className="w-4 h-4 text-forge-yellow" />}
|
|
{assetName}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="pointer-events-auto flex items-center gap-2">
|
|
{/* Zoom Controls (only for images) */}
|
|
{assetType === 'image' && (
|
|
<div className="bg-white/10 backdrop-blur-md rounded-full border border-white/20 flex overflow-hidden mr-2">
|
|
<button
|
|
onClick={handleZoomOut}
|
|
className="p-2 hover:bg-white/10 text-white transition-colors border-r border-white/10"
|
|
disabled={scale <= 1}
|
|
>
|
|
<ZoomOut className="w-5 h-5" />
|
|
</button>
|
|
<div className="px-3 py-2 text-xs font-mono text-white/70 min-w-[3rem] text-center flex items-center justify-center">
|
|
{(scale * 100).toFixed(0)}%
|
|
</div>
|
|
<button
|
|
onClick={handleZoomIn}
|
|
className="p-2 hover:bg-white/10 text-white transition-colors border-l border-white/10"
|
|
disabled={scale >= 4}
|
|
>
|
|
<ZoomIn className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
<a
|
|
href={assetUrl}
|
|
download={assetName}
|
|
className="p-3 rounded-full bg-white/10 border border-white/20 text-white hover:bg-white/20 transition-all hover:scale-105"
|
|
title="Download Original"
|
|
>
|
|
<Download className="w-5 h-5" />
|
|
</a>
|
|
|
|
<button
|
|
onClick={onClose}
|
|
className="p-3 rounded-full bg-white/10 border border-white/20 text-white hover:bg-red-500/20 hover:border-red-500/50 hover:text-red-400 transition-all hover:scale-105 ml-2"
|
|
title="Close Preview"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content Area */}
|
|
<div
|
|
ref={containerRef}
|
|
className="w-full h-full flex items-center justify-center overflow-hidden p-6"
|
|
onWheel={handleWheel}
|
|
>
|
|
{assetType === 'video' ? (
|
|
<video
|
|
src={assetUrl}
|
|
controls
|
|
autoPlay
|
|
className="max-w-full max-h-full rounded-lg shadow-2xl"
|
|
style={{
|
|
boxShadow: '0 0 50px rgba(0,0,0,0.5)'
|
|
}}
|
|
/>
|
|
) : (
|
|
<div
|
|
className={`relative transition-transform duration-100 ease-out ${isDragging ? 'cursor-grabbing' : scale > 1 ? 'cursor-grab' : ''}`}
|
|
style={{
|
|
transform: `scale(${scale}) translate(${position.x / scale}px, ${position.y / scale}px)`,
|
|
}}
|
|
onMouseDown={handleMouseDown}
|
|
onMouseMove={handleMouseMove}
|
|
onMouseUp={handleMouseUp}
|
|
onMouseLeave={handleMouseUp}
|
|
>
|
|
<img
|
|
src={assetUrl}
|
|
alt={assetName}
|
|
className="max-w-full max-h-[90vh] object-contain rounded-lg shadow-2xl"
|
|
draggable={false}
|
|
style={{
|
|
boxShadow: '0 0 50px rgba(0,0,0,0.5)'
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Hint overlay */}
|
|
{assetType === 'image' && scale === 1 && (
|
|
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 pointer-events-none opacity-50 text-white/50 text-xs px-4 py-2 rounded-full bg-black/40 backdrop-blur-sm border border-white/10">
|
|
Scroll to zoom • Drag to pan
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|