JavaScript 56.7%
TypeScript 42.6%
1"use client";23import { useEffect, useState, useCallback } from "react";4import { Folder, FolderOpen, Home, ChevronRight, ChevronDown, Search } from "lucide-react";5import Modal from "../ui/Modal";6import Button from "../ui/Button";78interface TreeFolder {9 id: string;10 name: string;11 children?: TreeFolder[];12 _count?: { files: number };13}1415interface MoveModalProps {16 isOpen: boolean;17 onClose: () => void;18 /** Appelée avec "root" ou l'id du dossier cible. */19 onSelect: (folderId: string) => void;20 /** Dossier (et son sous-arbre) à masquer — pour le déplacement d'un dossier. */21 excludeId?: string;22 title?: string;23 itemName?: string;24 /** Emplacement actuel (désactivé dans la liste) */25 currentFolderId?: string | null;26}2728function matches(node: TreeFolder, q: string): boolean {29 if (!q) return true;30 if (node.name.toLowerCase().includes(q)) return true;31 return (node.children ?? []).some((c) => matches(c, q));32}3334export default function MoveModal({35 isOpen,36 onClose,37 onSelect,38 excludeId,39 title = "Déplacer vers",40 itemName,41 currentFolderId,42}: MoveModalProps) {43 const [tree, setTree] = useState<TreeFolder[]>([]);44 const [expanded, setExpanded] = useState<Set<string>>(new Set());45 const [loading, setLoading] = useState(false);46 const [query, setQuery] = useState("");47 const [chosen, setChosen] = useState<string | null>(null);4849 useEffect(() => {50 if (!isOpen) return;51 setLoading(true);52 setQuery("");53 setChosen(null);54 fetch("/api/folders?tree=true")55 .then((r) => (r.ok ? r.json() : []))56 .then((data) => {57 const t: TreeFolder[] = Array.isArray(data) ? data : [];58 setTree(t);59 setExpanded(new Set(t.filter((n) => n.children?.length).map((n) => n.id)));60 })61 .catch(() => setTree([]))62 .finally(() => setLoading(false));63 }, [isOpen]);6465 const toggle = useCallback((id: string) => {66 setExpanded((prev) => {67 const next = new Set(prev);68 next.has(id) ? next.delete(id) : next.add(id);69 return next;70 });71 }, []);7273 const q = query.trim().toLowerCase();74 const current = currentFolderId ?? undefined;7576 function renderNode(node: TreeFolder, depth: number): React.ReactNode {77 if (node.id === excludeId) return null;78 if (!matches(node, q)) return null;79 const kids = node.children ?? [];80 const hasKids = kids.length > 0;81 const isOpen = q ? true : expanded.has(node.id);82 const isCurrent = node.id === current;83 const isChosen = node.id === chosen;84 return (85 <div key={node.id}>86 <div87 className={`flex items-center gap-1 rounded-xl transition-colors ${isChosen ? "bg-blue-500/10 ring-1 ring-blue-500/30" : "hover:bg-gray-100 dark:hover:bg-white/5"}`}88 style={{ paddingLeft: depth * 16 }}89 >90 <button91 type="button"92 onClick={() => hasKids && toggle(node.id)}93 className="w-7 h-10 flex items-center justify-center shrink-0 text-gray-400"94 tabIndex={hasKids ? 0 : -1}95 aria-label={hasKids ? (isOpen ? "Replier" : "Déplier") : undefined}96 >97 {hasKids ? (isOpen ? <ChevronDown className="w-4 h-4" /> : <ChevronRight className="w-4 h-4" />) : null}98 </button>99 <button100 type="button"101 disabled={isCurrent}102 onClick={() => setChosen(node.id)}103 onDoubleClick={() => !isCurrent && onSelect(node.id)}104 className={`flex items-center gap-2 flex-1 min-w-0 h-10 pr-2 text-sm text-left rounded-r-xl ${isCurrent ? "text-gray-400 cursor-not-allowed" : "text-gray-700 dark:text-gray-200"}`}105 >106 {isOpen && hasKids ? <FolderOpen className="w-4 h-4 text-blue-500 shrink-0" /> : <Folder className="w-4 h-4 text-blue-400 shrink-0" />}107 <span className="truncate flex-1">{node.name}</span>108 {isCurrent && <span className="text-[10px] uppercase tracking-wider text-gray-400">actuel</span>}109 {!isCurrent && node._count?.files ? <span className="text-[11px] text-gray-400 tabular-nums">{node._count.files}</span> : null}110 </button>111 </div>112 {hasKids && isOpen && <div className="border-l border-line dark:border-white/[0.07] ml-[13px]">{kids.map((c) => renderNode(c, depth + 1))}</div>}113 </div>114 );115 }116117 const rootIsCurrent = current === "root" || current === null;118119 return (120 <Modal121 isOpen={isOpen}122 onClose={onClose}123 title={title}124 description={itemName ? `Choisissez la destination de « ${itemName} »` : undefined}125 footer={126 <>127 <Button variant="ghost" onClick={onClose}>Annuler</Button>128 <Button variant="accent" disabled={!chosen} onClick={() => chosen && onSelect(chosen)}>Déplacer ici</Button>129 </>130 }131 >132 <div className="relative mb-3">133 <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400 pointer-events-none" />134 <input135 type="text"136 value={query}137 onChange={(e) => setQuery(e.target.value)}138 placeholder="Filtrer les dossiers…"139 className="field pl-9"140 />141 </div>142 <div className="space-y-0.5 max-h-[48vh] overflow-y-auto -mx-1 px-1">143 <button144 type="button"145 disabled={rootIsCurrent}146 onClick={() => setChosen("root")}147 onDoubleClick={() => !rootIsCurrent && onSelect("root")}148 className={`flex items-center gap-2 w-full px-2 h-10 text-sm font-medium rounded-xl transition-colors ${149 chosen === "root" ? "bg-blue-500/10 ring-1 ring-blue-500/30 text-gray-900 dark:text-white" : rootIsCurrent ? "text-gray-400 cursor-not-allowed" : "text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-white/5"150 }`}151 >152 <Home className="w-4 h-4 text-gray-500 shrink-0" />153 <span className="flex-1 text-left">Racine (Fichiers)</span>154 {rootIsCurrent && <span className="text-[10px] uppercase tracking-wider text-gray-400">actuel</span>}155 </button>156 {loading ? (157 <div className="space-y-2 p-2">158 {[0, 1, 2].map((i) => <div key={i} className="h-9 rounded-xl bg-gray-100 dark:bg-white/5 animate-pulse" />)}159 </div>160 ) : (161 tree.map((n) => renderNode(n, 0))162 )}163 {!loading && q && tree.every((n) => !matches(n, q)) && (164 <p className="px-2 py-4 text-sm text-gray-400 text-center">Aucun dossier ne correspond</p>165 )}166 </div>167 </Modal>168 );169}170