import React, { useEffect, useRef, useState, useCallback } from 'react'; import { useAssets } from '../../hooks/useAssets'; import { clickableProps } from '../../utils/a11y'; import { copyToClipboard } from '../../utils/clipboard'; /** How long the "Delete?" confirm state stays armed before auto-resetting. */ const DELETE_CONFIRM_TIMEOUT_MS = 4000; export const AssetsPanel: React.FC = () => { const { assets, loading, error, loadAssets, uploadAsset, deleteAsset } = useAssets(); const fileInputRef = useRef(null); const [isDragOver, setIsDragOver] = useState(false); const [copiedUrl, setCopiedUrl] = useState(null); const [copyErrorUrl, setCopyErrorUrl] = useState(null); const [confirmDeleteName, setConfirmDeleteName] = useState(null); const deleteConfirmTimeoutRef = useRef | null>(null); useEffect(() => { return () => { if (deleteConfirmTimeoutRef.current) clearTimeout(deleteConfirmTimeoutRef.current); }; }, []); const resetDeleteConfirm = useCallback(() => { if (deleteConfirmTimeoutRef.current) clearTimeout(deleteConfirmTimeoutRef.current); setConfirmDeleteName(null); }, []); const armDeleteConfirm = useCallback((name: string) => { setConfirmDeleteName(name); if (deleteConfirmTimeoutRef.current) clearTimeout(deleteConfirmTimeoutRef.current); deleteConfirmTimeoutRef.current = setTimeout(() => setConfirmDeleteName(null), DELETE_CONFIRM_TIMEOUT_MS); }, []); useEffect(() => { loadAssets(); }, [loadAssets]); const handleFileSelect = useCallback(async (files: FileList | null) => { if (!files) return; for (let i = 0; i < files.length; i++) { await uploadAsset(files[i]); } }, [uploadAsset]); const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragOver(false); handleFileSelect(e.dataTransfer.files); }, [handleFileSelect]); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragOver(true); }, []); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragOver(false); }, []); const copyUrl = useCallback(async (url: string) => { const ok = await copyToClipboard(url); if (ok) { setCopyErrorUrl(null); setCopiedUrl(url); setTimeout(() => setCopiedUrl(null), 2000); } else { setCopiedUrl(null); setCopyErrorUrl(url); setTimeout(() => setCopyErrorUrl(null), 2500); } }, []); const isImage = (type: string) => type.startsWith('image/') || /\.(jpg|jpeg|png|gif|svg|webp|ico)$/i.test(type); return (
{/* Upload button */} handleFileSelect(e.target.files)} /> {/* Drop zone */}
Drop files here to upload
{/* Error message */} {error && (
{error}
)} {/* Asset grid */} {assets.length === 0 && !loading && (
No assets uploaded yet
)}
{assets.map((asset) => { const isConfirmingDelete = confirmDeleteName === asset.name; return (
{ // While armed, clicking anywhere on the tile (other than the // delete control itself) counts as "click elsewhere" and // disarms the confirm instead of copying the URL. if (isConfirmingDelete) { resetDeleteConfirm(); return; } copyUrl(asset.url); })} style={{ position: 'relative', background: 'var(--color-bg-elevated)', border: '1px solid var(--color-border)', borderRadius: 'var(--radius-md)', overflow: 'visible', cursor: 'pointer', transition: 'border-color var(--transition-fast)', }} title={`Click to copy URL: ${asset.url}`} > {/* Thumbnail */}
{isImage(asset.type) ? ( {asset.name} ) : ( 📄 )}
{/* Name / status */}
{copyErrorUrl === asset.url ? 'Copy failed' : copiedUrl === asset.url ? 'Copied!' : asset.name}
{/* Delete control: idle icon, or an in-app two-step confirm */} {isConfirmingDelete ? (
) : ( )}
); })}
{/* Loading indicator */} {loading && assets.length > 0 && (
Loading...
)}
); };