import React, { useEffect, useRef, useState, useCallback } from 'react'; import { useAssets } from '../../hooks/useAssets'; import { clickableProps } from '../../utils/a11y'; import { copyToClipboard } from '../../utils/clipboard'; import { useIsMobile } from '../../hooks/useIsMobile'; /** 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 isMobile = useIsMobile(); 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 -- a single element that doubles as the empty state. Previously this was a small always-visible dropzone PLUS a separate italic "No assets uploaded yet" line stacked underneath it when empty; merged into one tall dropzone (icon + copy, click-or-drag) so the empty state isn't two redundant messages. Once assets exist it collapses back to a slim persistent drop target above the grid. */}
fileInputRef.current?.click()) : {})} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 8, padding: assets.length === 0 ? '36px 20px' : 16, border: `2px dashed ${isDragOver ? 'var(--color-accent)' : 'var(--color-border)'}`, borderRadius: 'var(--radius-md)', background: isDragOver ? 'var(--color-accent-subtle)' : 'transparent', textAlign: 'center', color: isDragOver ? 'var(--color-accent)' : 'var(--color-text-dim)', fontSize: 11, cursor: assets.length === 0 ? 'pointer' : 'default', transition: 'all var(--transition-fast)', }} > {assets.length === 0 && ( )} {assets.length === 0 ? (isMobile ? 'Tap to upload images' : 'Drag images here or click to upload') : 'Drop files here to upload'}
{/* Error message */} {error && (
{error}
)} {/* Asset grid */}
{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...
)}
); };