"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, widgetContentKey } from "@/lib/dashboard"; import type { SerializedWidgetMeta } from "@/modules/_core/registry"; import { saveDashboardLayout, resetDashboardLayout } 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 handleCancel() { 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) { setPlacements((current) => current.filter((_, i) => i !== index)); setIsDirty(true); } function addWidget(widgetId: string, config: unknown) { const meta = widgetMetas.find((m) => m.id === widgetId); if (!meta) return; const maxY = placements.reduce((m, p) => Math.max(m, p.y + p.h), 0); setPlacements((current) => [ ...current, { widgetId, config, x: 0, y: maxY, w: meta.defaultSize.w, h: meta.defaultSize.h }, ]); setIsDirty(true); setPickerOpen(false); } function updateConfig(index: number, config: unknown) { setPlacements((current) => current.map((p, i) => (i === index ? { ...p, config } : p))); setIsDirty(true); setConfiguringIndex(null); } function applyPreset(preset: PresetId) { const next = computePresetLayoutFromMetas(preset, widgetMetas); setPlacements(next.widgets); setIsDirty(true); setPresetMenuOpen(false); } 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[widgetContentKey(placement)]; 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} /> )}
); }