forge/frontend/components/AssetPreviewModal.tsx

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>
);
}