chore(repo): initialize planarchy workspace
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useEffect, useRef } from "react";
|
||||
import {
|
||||
type DashboardLayoutConfig,
|
||||
type DashboardWidgetType,
|
||||
} from "@planarchy/shared/types";
|
||||
import {
|
||||
createDashboardWidget,
|
||||
createDefaultDashboardLayout,
|
||||
getNextDashboardWidgetY,
|
||||
normalizeDashboardLayout,
|
||||
} from "@planarchy/shared/schemas";
|
||||
import { trpc } from "~/lib/trpc/client.js";
|
||||
|
||||
const STORAGE_KEY = "planarchy_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<DashboardLayoutConfig>(() => loadFromStorage() ?? createDefaultDashboardLayout());
|
||||
const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | 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<string, unknown>) => {
|
||||
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 updatedWidgets = prev.widgets.map((w) => {
|
||||
const item = layout.find((l) => l.i === 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 = prev.widgets.find((o) => o.id === 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user