JavaScript 56.7%
TypeScript 42.6%
1/**2 * Glisser-déposer interne SPB Cloud (fichiers ↔ dossiers) + événements de synchro.3 */4import toast from "react-hot-toast";56export const DND_FILES = "application/x-spb-files";7export const DND_FOLDER = "application/x-spb-folder";8export const FOLDERS_CHANGED_EVENT = "spb:folders-changed";910/** Cible de dépôt : "root" ou id de dossier. */11export type DropTarget = string;1213export function notifyFoldersChanged() {14 if (typeof window !== "undefined") window.dispatchEvent(new Event(FOLDERS_CHANGED_EVENT));15}1617export function hasInternalDrag(dt: DataTransfer | null): boolean {18 if (!dt) return false;19 const types = Array.from(dt.types || []);20 return types.includes(DND_FILES) || types.includes(DND_FOLDER);21}2223export function hasNativeFiles(dt: DataTransfer | null): boolean {24 if (!dt) return false;25 const types = Array.from(dt.types || []);26 return types.includes("Files") && !hasInternalDrag(dt);27}2829export function readFileIds(dt: DataTransfer | null): string[] {30 try {31 const raw = dt?.getData(DND_FILES);32 if (!raw) return [];33 const arr = JSON.parse(raw);34 return Array.isArray(arr) ? arr.filter((x) => typeof x === "string") : [];35 } catch {36 return [];37 }38}3940export function readFolderId(dt: DataTransfer | null): string | null {41 try {42 const raw = dt?.getData(DND_FOLDER);43 return raw || null;44 } catch {45 return null;46 }47}4849/** Image fantôme personnalisée « N fichiers ». */50export function setDragGhost(e: React.DragEvent, label: string) {51 try {52 const ghost = document.createElement("div");53 ghost.textContent = label;54 ghost.style.cssText =55 "position:fixed;top:-1000px;left:-1000px;padding:8px 14px;border-radius:12px;font:600 13px system-ui,sans-serif;color:#fff;" +56 "background:linear-gradient(135deg,rgb(var(--accent-500)),rgb(var(--accent-700)));box-shadow:0 8px 24px rgb(0 0 0/.25);white-space:nowrap;";57 document.body.appendChild(ghost);58 e.dataTransfer.setDragImage(ghost, 16, 16);59 setTimeout(() => ghost.remove(), 0);60 } catch {}61}6263/** Déplace des fichiers vers un dossier ("root" accepté). Retourne le nombre déplacé. */64export async function moveFiles(ids: string[], target: DropTarget): Promise<number> {65 const results = await Promise.all(66 ids.map((id) =>67 fetch(`/api/files/${id}`, {68 method: "PATCH",69 headers: { "Content-Type": "application/json" },70 body: JSON.stringify({ folderId: target }),71 }).then((r) => r.ok).catch(() => false)72 )73 );74 const ok = results.filter(Boolean).length;75 if (ok === ids.length) toast.success(ok > 1 ? `${ok} fichiers déplacés` : "Fichier déplacé");76 else if (ok > 0) toast.error(`${ids.length - ok} fichier(s) non déplacé(s)`);77 else toast.error("Déplacement impossible");78 return ok;79}8081/** Déplace un dossier (anti-cycle géré côté serveur). */82export async function moveFolder(id: string, target: DropTarget): Promise<boolean> {83 if (id === target) return false;84 try {85 const res = await fetch(`/api/folders/${id}`, {86 method: "PATCH",87 headers: { "Content-Type": "application/json" },88 body: JSON.stringify({ parentId: target }),89 });90 if (res.ok) {91 toast.success("Dossier déplacé");92 notifyFoldersChanged();93 return true;94 }95 const d = await res.json().catch(() => ({}));96 toast.error(d.error || "Déplacement impossible");97 } catch {98 toast.error("Erreur réseau");99 }100 return false;101}102103/**104 * Gère un dépôt interne sur une cible. Retourne true si quelque chose a été déplacé.105 */106export async function handleInternalDrop(dt: DataTransfer | null, target: DropTarget): Promise<boolean> {107 const folderId = readFolderId(dt);108 if (folderId) return moveFolder(folderId, target);109 const ids = readFileIds(dt);110 if (ids.length) return (await moveFiles(ids, target)) > 0;111 return false;112}113