"use client"; import "react-grid-layout/css/styles.css"; import "react-resizable/css/styles.css"; import { useEffect, useState, useTransition, type ReactNode } from "react"; import { useRouter } from "next/navigation"; import { GridLayout } from "react-grid-layout"; import type { Layout } from "react-grid-layout"; import { GripVertical, Settings2, Trash2, RotateCcw, Plus, LayoutGrid } from "lucide-react"; import type { DashboardLayout, WidgetPlacement, PresetId } from "@/lib/dashboard"; import { computePresetLayoutFromMetas, widgetContentIndexKey } from "@/lib/dashboard"; import type { SerializedWidgetMeta } from "@/modules/_core/registry"; import { saveDashboardLayout, resetDashboardLayout, syncEditorDraftLayout, discardEditorDraftLayout, } from "@/app/d/actions"; import { Button } from "@/components/ui/button"; import { WidgetPicker } from "./widget-picker"; function placementKey(placement: WidgetPlacement, index: number) { return `${placement.widgetId}::${index}`; } export function DashboardEditor({ dashboard, layout: initialLayout, widgetMetas, widgetContents, }: { dashboard: { id: string; name: string; slug: string }; layout: DashboardLayout; widgetMetas: SerializedWidgetMeta[]; widgetContents: Record; }) { const router = useRouter(); const [isPending, startTransition] = useTransition(); const [placements, setPlacements] = useState(initialLayout.widgets); const [isDirty, setIsDirty] = useState(false); const [pickerOpen, setPickerOpen] = useState(false); const [configuringIndex, setConfiguringIndex] = useState(null); const [containerWidth, setContainerWidth] = useState(1200); const [presetMenuOpen, setPresetMenuOpen] = useState(false); useEffect(() => { function measure() { const el = document.getElementById("dashboard-editor-grid"); if (el) setContainerWidth(el.offsetWidth); } measure(); window.addEventListener("resize", measure); return () => window.removeEventListener("resize", measure); }, []); useEffect(() => { function onKey(e: KeyboardEvent) { if (e.key === "Escape") handleCancel(); } window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); // eslint-disable-next-line react-hooks/exhaustive-deps }, [dashboard.slug]); function publishDraft(next: WidgetPlacement[]) { startTransition(async () => { await syncEditorDraftLayout(dashboard.id, { version: 1, widgets: next }); router.refresh(); }); } function handleCancel() { startTransition(async () => { await discardEditorDraftLayout(dashboard.id); router.push(`/d/${dashboard.slug}`); }); } function handleSave() { startTransition(async () => { await saveDashboardLayout(dashboard.id, { version: 1, widgets: placements }); router.push(`/d/${dashboard.slug}`); }); } function handleReset() { startTransition(async () => { await resetDashboardLayout(dashboard.id); router.push(`/d/${dashboard.slug}`); }); } function handleLayoutChange(items: Layout) { setPlacements((current) => current.map((p, i) => { const key = placementKey(p, i); const item = items.find((it) => it.i === key); if (!item) return p; return { ...p, x: item.x, y: item.y, w: item.w, h: item.h }; }), ); setIsDirty(true); } function removeWidget(index: number) { const next = placements.filter((_, i) => i !== index); setPlacements(next); setIsDirty(true); publishDraft(next); } function addWidget(widgetId: string, config: unknown) { const meta = widgetMetas.find((m) => m.id === widgetId); if (!meta) return; const resolvedConfig = resolveWidgetConfig(meta, config); const maxY = placements.reduce((m, p) => Math.max(m, p.y + p.h), 0); const next = [ ...placements, { widgetId, config: resolvedConfig, x: 0, y: maxY, w: meta.defaultSize.w, h: meta.defaultSize.h, }, ]; setPlacements(next); setIsDirty(true); setPickerOpen(false); publishDraft(next); } function updateConfig(index: number, config: unknown) { const meta = widgetMetas.find((m) => m.id === placements[index]?.widgetId); const resolvedConfig = meta ? resolveWidgetConfig(meta, config) : config; const next = placements.map((p, i) => (i === index ? { ...p, config: resolvedConfig } : p)); setPlacements(next); setIsDirty(true); setConfiguringIndex(null); publishDraft(next); } function applyPreset(preset: PresetId) { const next = computePresetLayoutFromMetas(preset, widgetMetas); setPlacements(next.widgets); setIsDirty(true); setPresetMenuOpen(false); publishDraft(next.widgets); } const gridItems: Layout = placements.map((p, i) => ({ i: placementKey(p, i), x: p.x, y: p.y, w: p.w, h: p.h, minW: widgetMetas.find((m) => m.id === p.widgetId)?.minSize?.w ?? 2, minH: widgetMetas.find((m) => m.id === p.widgetId)?.minSize?.h ?? 1, maxW: widgetMetas.find((m) => m.id === p.widgetId)?.maxSize?.w ?? 12, })); return (

{dashboard.name}

{presetMenuOpen && (
setPresetMenuOpen(false)} >
)}

Drag to reorder · resize from the bottom-right corner · ESC to cancel

{placements.map((placement, i) => { const meta = widgetMetas.find((m) => m.id === placement.widgetId); const content = widgetContents[widgetContentIndexKey(i)]; return (
{meta?.title ?? placement.widgetId}
{content ?? (

{meta?.description ?? "Preview available after save"}

)}
); })}
{placements.length === 0 && (

No widgets yet.

)} {pickerOpen && ( setPickerOpen(false)} widgetMetas={widgetMetas} onAdd={addWidget} /> )} {configuringIndex !== null && ( setConfiguringIndex(null)} widgetMetas={widgetMetas} onAdd={(_, config) => updateConfig(configuringIndex, config)} initialWidgetId={placements[configuringIndex]?.widgetId} initialConfig={placements[configuringIndex]?.config} /> )}
); } function resolveWidgetConfig(meta: SerializedWidgetMeta, config: unknown): unknown { if (config != null && typeof config === "object" && !Array.isArray(config)) { return { ...(meta.defaultConfig as Record), ...(config as Record), }; } return meta.defaultConfig; }