"use client"; import { FileText, Image, Video, Music, FileCode, FileSpreadsheet, File, Archive, MoreVertical, Star, Download, Check, type LucideIcon, } from "lucide-react"; import { formatFileSize, getFileIcon } from "@/lib/utils"; import { useState, useRef, useEffect, memo } from "react"; import FileActions from "./FileActions"; import type { FileData } from "@/types/files"; import { DND_FILES, setDragGhost } from "./dnd"; interface FileCardProps { file: FileData; onPreview: (file: FileData) => void; onRefresh: () => void; viewMode: "grid" | "list"; /** Sélection (explorateur) */ selected?: boolean; selectMode?: boolean; onToggleSelect?: (file: FileData, e: React.MouseEvent) => void; /** Ids à emporter dans le glisser (sélection multiple) */ dragIds?: string[]; /** Menu contextuel : géré par le parent (position souris) */ onContextMenu?: (file: FileData, e: React.MouseEvent) => void; /** Navigation clavier (roving tabindex) */ focusable?: boolean; onFocusItem?: (file: FileData) => void; /** Colonnes de la vue liste rendues par le parent (sticky header aligné) */ dense?: boolean; } const iconMap: Record = { image: Image, video: Video, audio: Music, pdf: FileText, code: FileCode, spreadsheet: FileSpreadsheet, archive: Archive, document: FileText, file: File, presentation: FileText, }; const iconColorMap: Record = { image: "text-emerald-500", video: "text-purple-500", audio: "text-pink-500", pdf: "text-rose-500", code: "text-amber-500", spreadsheet: "text-green-600", archive: "text-orange-500", document: "text-blue-500", presentation: "text-orange-500", file: "text-gray-400", }; const bgTintMap: Record = { image: "bg-emerald-50 dark:bg-emerald-950/30", video: "bg-purple-50 dark:bg-purple-950/30", audio: "bg-pink-50 dark:bg-pink-950/30", pdf: "bg-rose-50 dark:bg-rose-950/30", code: "bg-amber-50 dark:bg-amber-950/30", spreadsheet: "bg-green-50 dark:bg-green-950/30", archive: "bg-orange-50 dark:bg-orange-950/30", document: "bg-blue-50 dark:bg-blue-950/30", presentation: "bg-orange-50 dark:bg-orange-950/30", file: "bg-gray-50 dark:bg-white/[0.03]", }; const typeLabel: Record = { image: "Image", video: "Vidéo", audio: "Audio", pdf: "PDF", code: "Code", spreadsheet: "Tableur", archive: "Archive", document: "Document", presentation: "Présentation", file: "Fichier", }; export function formatRelativeDate(dateStr: string): string { const now = Date.now(); const then = new Date(dateStr).getTime(); const diff = now - then; const minutes = Math.floor(diff / 60000); const hours = Math.floor(diff / 3600000); const days = Math.floor(diff / 86400000); const months = Math.floor(days / 30); if (minutes < 1) return "à l'instant"; if (minutes < 60) return `il y a ${minutes} min`; if (hours < 24) return `il y a ${hours} h`; if (days < 30) return `il y a ${days} j`; if (months < 12) return `il y a ${months} mois`; return new Date(dateStr).toLocaleDateString("fr-CA", { year: "numeric", month: "short" }); } function extOf(name: string): string { const i = name.lastIndexOf("."); return i > 0 ? name.slice(i + 1).toUpperCase().slice(0, 5) : ""; } function FileCardInner({ file, onPreview, onRefresh, viewMode, selected = false, selectMode = false, onToggleSelect, dragIds, onContextMenu, focusable, onFocusItem, }: FileCardProps) { const [showActions, setShowActions] = useState(false); const [dragging, setDragging] = useState(false); const [favBusy, setFavBusy] = useState(false); const menuRef = useRef(null); const iconType = getFileIcon(file.mimeType); const Icon = iconMap[iconType] || File; const iconColor = iconColorMap[iconType] || "text-gray-400"; const bgTint = bgTintMap[iconType] || "bg-gray-50 dark:bg-white/[0.03]"; const isImage = file.mimeType.startsWith("image/"); const isFavorite = !!file.metadata?.isFavorite; const tags = file.tags ?? []; useEffect(() => { if (!showActions) return; function handleClick(e: MouseEvent) { if (menuRef.current && !menuRef.current.contains(e.target as Node)) setShowActions(false); } document.addEventListener("mousedown", handleClick); return () => document.removeEventListener("mousedown", handleClick); }, [showActions]); const onDragStart = (e: React.DragEvent) => { const ids = dragIds && dragIds.length && dragIds.includes(file.id) ? dragIds : [file.id]; e.dataTransfer.setData(DND_FILES, JSON.stringify(ids)); e.dataTransfer.setData("text/plain", file.name); e.dataTransfer.effectAllowed = "move"; setDragGhost(e, ids.length > 1 ? `${ids.length} fichiers` : file.name); setDragging(true); }; const onDragEnd = () => setDragging(false); const toggleFav = async (e: React.MouseEvent) => { e.stopPropagation(); if (favBusy) return; setFavBusy(true); try { await fetch(`/api/files/${file.id}/favorite`, { method: "POST" }); onRefresh(); } catch {} setFavBusy(false); }; const handleClick = (e: React.MouseEvent) => { if (selectMode || e.metaKey || e.ctrlKey || e.shiftKey) { onToggleSelect?.(file, e); return; } onPreview(file); }; const handleKey = (e: React.KeyboardEvent) => { if (e.key === "Enter") { e.preventDefault(); onPreview(file); } else if (e.key === " ") { e.preventDefault(); onToggleSelect?.(file, e as unknown as React.MouseEvent); } }; const ctx = (e: React.MouseEvent) => { if (!onContextMenu) return; e.preventDefault(); e.stopPropagation(); onContextMenu(file, e); }; const commonProps = { draggable: true, onDragStart, onDragEnd, onContextMenu: ctx, tabIndex: focusable ? 0 : -1, onFocus: () => onFocusItem?.(file), onKeyDown: handleKey, "data-file-id": file.id, role: "option", "aria-selected": selected, } as const; const Checkbox = ({ className = "" }: { className?: string }) => ( ); /* ================= LISTE ================= */ if (viewMode === "list") { return (
onPreview(file)} > {/* Case + icône */}
{isImage ? ( ) : ( )}
{/* Nom */}
{file.name} {isFavorite && } {tags.length > 0 && ( {tags.slice(0, 3).map(({ tag }) => ( ))} {tags.length > 3 && +{tags.length - 3}} )}
{/* Type */} {extOf(file.name) || typeLabel[iconType]} {/* Taille */} {formatFileSize(file.size)} {/* Date */} {formatRelativeDate(file.updatedAt || file.createdAt)} {/* Actions */}
{showActions && setShowActions(false)} onRefresh={onRefresh} onPreview={() => onPreview(file)} />}
); } /* ================= GRILLE ================= */ return (
{/* Vignette */}
{isImage ? ( {file.name} ) : (
{extOf(file.name) && {extOf(file.name)}}
)} {/* Voile hover */}
{/* Case (haut gauche) */}
{/* Favori (haut gauche, quand pas de case) */} {isFavorite && !selectMode && !selected && (
)} {/* Actions rapides (haut droite) */}
{showActions && setShowActions(false)} onRefresh={onRefresh} onPreview={() => onPreview(file)} />}
{/* Infos */}

{file.name}

{formatFileSize(file.size)} · {formatRelativeDate(file.updatedAt || file.createdAt)}

{tags.length > 0 && (
{tags.slice(0, 4).map(({ tag }) => ( ))} {tags.length > 4 && +{tags.length - 4}}
)}
); } const FileCard = memo(FileCardInner); export default FileCard;