"use client"; import { useState, useCallback, useEffect, useRef } from "react"; import { type DashboardLayoutConfig, type DashboardWidgetType, } from "@capakraken/shared/types"; import { createDashboardWidget, createDefaultDashboardLayout, getNextDashboardWidgetY, normalizeDashboardLayout, } from "@capakraken/shared/schemas"; import { trpc } from "~/lib/trpc/client.js"; const STORAGE_KEY = "capakraken_dashboard_v1"; function generateWidgetId() { return `widget-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`; } function loadFromStorage(): DashboardLayoutConfig | null { if (typeof window === "undefined") return null; try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return null; return normalizeDashboardLayout(JSON.parse(raw)); } catch { return null; } } function saveToStorage(config: DashboardLayoutConfig) { if (typeof window === "undefined") return; try { localStorage.setItem(STORAGE_KEY, JSON.stringify(config)); } catch {} } export function useDashboardLayout() { const [config, setConfig] = useState(() => loadFromStorage() ?? createDefaultDashboardLayout()); const saveTimeoutRef = useRef | null>(null); // eslint-disable-next-line @typescript-eslint/no-explicit-any const { data: dbData } = trpc.user.getDashboardLayout.useQuery(undefined, { staleTime: 30_000, }) as { data: { layout: DashboardLayoutConfig | null; updatedAt: unknown } | null | undefined }; const saveMutation = trpc.user.saveDashboardLayout.useMutation(); // Sync from DB on load (DB wins if it has data) useEffect(() => { if (dbData?.layout) { const dbConfig = normalizeDashboardLayout(dbData.layout); setConfig(dbConfig); saveToStorage(dbConfig); } }, [dbData]); const persist = useCallback((nextConfig: DashboardLayoutConfig) => { const newConfig = normalizeDashboardLayout(nextConfig); saveToStorage(newConfig); if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current); saveTimeoutRef.current = setTimeout(() => { saveMutation.mutate({ layout: newConfig }); }, 2000); }, [saveMutation]); const addWidget = useCallback((type: DashboardWidgetType) => { setConfig((prev) => { const newConfig = { ...prev, widgets: [ ...prev.widgets, createDashboardWidget(type, { id: generateWidgetId(), x: 0, y: getNextDashboardWidgetY(prev.widgets), }), ], }; persist(newConfig); return newConfig; }); }, [persist]); const removeWidget = useCallback((id: string) => { setConfig((prev) => { const newConfig = { ...prev, widgets: prev.widgets.filter((w) => w.id !== id) }; persist(newConfig); return newConfig; }); }, [persist]); const updateWidgetConfig = useCallback((id: string, configUpdate: Record) => { setConfig((prev) => { const newConfig = { ...prev, widgets: prev.widgets.map((w) => w.id === id ? { ...w, config: { ...w.config, ...configUpdate } } : w, ), }; persist(newConfig); return newConfig; }); }, [persist]); const onLayoutChange = useCallback( (layout: { i: string; x: number; y: number; w: number; h: number }[]) => { setConfig((prev) => { const layoutMap = new Map(layout.map((item) => [item.i, item])); const previousWidgetMap = new Map(prev.widgets.map((widget) => [widget.id, widget])); const updatedWidgets = prev.widgets.map((w) => { const item = layoutMap.get(w.id); if (!item) return w; return { ...w, x: item.x, y: item.y, w: item.w, h: item.h }; }); // Only persist when the user actually moved/resized something. // react-grid-layout fires onLayoutChange on mount too — we skip that // to avoid overwriting saved positions with compacted coordinates. const changed = updatedWidgets.some((w) => { const orig = previousWidgetMap.get(w.id); return orig && (w.x !== orig.x || w.y !== orig.y || w.w !== orig.w || w.h !== orig.h); }); const newConfig = { ...prev, widgets: updatedWidgets }; if (changed) persist(newConfig); return newConfig; }); }, [persist], ); const resetLayout = useCallback(() => { const defaultConfig = createDefaultDashboardLayout(); setConfig(defaultConfig); persist(defaultConfig); }, [persist]); return { config, addWidget, removeWidget, updateWidgetConfig, onLayoutChange, resetLayout, }; }