/** * Glisser-déposer interne SPB Cloud (fichiers ↔ dossiers) + événements de synchro. */ import toast from "react-hot-toast"; export const DND_FILES = "application/x-spb-files"; export const DND_FOLDER = "application/x-spb-folder"; export const FOLDERS_CHANGED_EVENT = "spb:folders-changed"; /** Cible de dépôt : "root" ou id de dossier. */ export type DropTarget = string; export function notifyFoldersChanged() { if (typeof window !== "undefined") window.dispatchEvent(new Event(FOLDERS_CHANGED_EVENT)); } export function hasInternalDrag(dt: DataTransfer | null): boolean { if (!dt) return false; const types = Array.from(dt.types || []); return types.includes(DND_FILES) || types.includes(DND_FOLDER); } export function hasNativeFiles(dt: DataTransfer | null): boolean { if (!dt) return false; const types = Array.from(dt.types || []); return types.includes("Files") && !hasInternalDrag(dt); } export function readFileIds(dt: DataTransfer | null): string[] { try { const raw = dt?.getData(DND_FILES); if (!raw) return []; const arr = JSON.parse(raw); return Array.isArray(arr) ? arr.filter((x) => typeof x === "string") : []; } catch { return []; } } export function readFolderId(dt: DataTransfer | null): string | null { try { const raw = dt?.getData(DND_FOLDER); return raw || null; } catch { return null; } } /** Image fantôme personnalisée « N fichiers ». */ export function setDragGhost(e: React.DragEvent, label: string) { try { const ghost = document.createElement("div"); ghost.textContent = label; ghost.style.cssText = "position:fixed;top:-1000px;left:-1000px;padding:8px 14px;border-radius:12px;font:600 13px system-ui,sans-serif;color:#fff;" + "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;"; document.body.appendChild(ghost); e.dataTransfer.setDragImage(ghost, 16, 16); setTimeout(() => ghost.remove(), 0); } catch {} } /** Déplace des fichiers vers un dossier ("root" accepté). Retourne le nombre déplacé. */ export async function moveFiles(ids: string[], target: DropTarget): Promise { const results = await Promise.all( ids.map((id) => fetch(`/api/files/${id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ folderId: target }), }).then((r) => r.ok).catch(() => false) ) ); const ok = results.filter(Boolean).length; if (ok === ids.length) toast.success(ok > 1 ? `${ok} fichiers déplacés` : "Fichier déplacé"); else if (ok > 0) toast.error(`${ids.length - ok} fichier(s) non déplacé(s)`); else toast.error("Déplacement impossible"); return ok; } /** Déplace un dossier (anti-cycle géré côté serveur). */ export async function moveFolder(id: string, target: DropTarget): Promise { if (id === target) return false; try { const res = await fetch(`/api/folders/${id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ parentId: target }), }); if (res.ok) { toast.success("Dossier déplacé"); notifyFoldersChanged(); return true; } const d = await res.json().catch(() => ({})); toast.error(d.error || "Déplacement impossible"); } catch { toast.error("Erreur réseau"); } return false; } /** * Gère un dépôt interne sur une cible. Retourne true si quelque chose a été déplacé. */ export async function handleInternalDrop(dt: DataTransfer | null, target: DropTarget): Promise { const folderId = readFolderId(dt); if (folderId) return moveFolder(folderId, target); const ids = readFileIds(dt); if (ids.length) return (await moveFiles(ids, target)) > 0; return false; }