"use client"; import { useEffect, useMemo, useState } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { DndContext, closestCenter, PointerSensor, TouchSensor, KeyboardSensor, useSensor, useSensors, type DragEndEvent } from "@dnd-kit/core"; import { SortableContext, rectSortingStrategy, useSortable, arrayMove, sortableKeyboardCoordinates } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { GripVertical, X, Plus, Settings2, RotateCcw, Check, Columns2, Columns3, RectangleHorizontal } from "lucide-react"; import { nanoid } from "nanoid"; import { api } from "@/lib/client/api"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Popover, PopoverTrigger, PopoverContent, Spinner, Tip } from "@/components/ui/primitives"; import { toast } from "@/components/ui/toast"; import { WIDGET_META, WIDGET_TYPES, type Widget, type WidgetSize, type WidgetType } from "./types"; import { WIDGET_COMPONENTS, type DashboardData } from "./widgets"; const SIZE_CLASS: Record = { third: "md:col-span-2", half: "md:col-span-3", wide: "md:col-span-6" }; const SIZES: { id: WidgetSize; label: string; icon: React.ReactNode }[] = [{ id: "third", label: "Third", icon: }, { id: "half", label: "Half", icon: }, { id: "wide", label: "Full width", icon: }]; function greeting(d: Date) { const h = d.getHours(); return h < 5 ? "Good night" : h < 12 ? "Good morning" : h < 18 ? "Good afternoon" : "Good evening"; } function WidgetCard({ w, editing, data, refresh, onRemove, onResize }: { w: Widget; editing: boolean; data: DashboardData | undefined; refresh: () => void; onRemove: () => void; onResize: (s: WidgetSize) => void }) { const { attributes, listeners, setNodeRef, setActivatorNodeRef, transform, transition, isDragging } = useSortable({ id: w.id, disabled: !editing }); const meta = WIDGET_META[w.type]; const Comp = WIDGET_COMPONENTS[w.type]; return (
{editing && }

{meta.label}

{editing ? (
{SIZES.map((s) => )}
) : null}
{data ? :
}
); } export function Dashboard({ displayName }: { displayName: string }) { const qc = useQueryClient(); const { data, isError, error } = useQuery({ queryKey: ["dashboard"], queryFn: () => api("/api/dashboard"), staleTime: 20_000 }); const refresh = () => { qc.invalidateQueries({ queryKey: ["dashboard"] }); }; const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(null); const [saving, setSaving] = useState(false); const widgets = draft ?? data?.layout.widgets ?? []; const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } }), useSensor(TouchSensor, { activationConstraint: { delay: 180, tolerance: 6 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })); const now = useMemo(() => new Date(), []); const [dateLabel, setDateLabel] = useState(""); useEffect(() => { setDateLabel(new Intl.DateTimeFormat("en-CA", { weekday: "long", month: "long", day: "numeric", year: "numeric" }).format(new Date())); }, []); useEffect(() => { if (!editing) return; const k = (e: KeyboardEvent) => { if (e.key === "Escape" && !e.defaultPrevented && !document.querySelector("[role=dialog][data-state=open]")) cancel(); }; window.addEventListener("keydown", k); return () => window.removeEventListener("keydown", k); /* eslint-disable-next-line */ }, [editing]); function start() { setDraft(data?.layout.widgets ?? []); setEditing(true); } function cancel() { setDraft(null); setEditing(false); } function onDragEnd(e: DragEndEvent) { const { active, over } = e; if (!over || active.id === over.id) return; setDraft((d) => { const list = d ?? []; const from = list.findIndex((w) => w.id === active.id); const to = list.findIndex((w) => w.id === over.id); return from < 0 || to < 0 ? list : arrayMove(list, from, to); }); } function add(type: WidgetType) { setDraft((d) => [...(d ?? []), { id: `w-${nanoid(8)}`, type, size: WIDGET_META[type].defaultSize }]); } async function save() { if (!draft) return; setSaving(true); try { const r = await api("/api/dashboard", { method: "PUT", json: { widgets: draft } }); qc.setQueryData(["dashboard"], (old) => (old ? { ...old, layout: r } : old)); setEditing(false); setDraft(null); toast("Dashboard saved", { tone: "success" }); refresh(); } catch (e: any) { toast("Could not save layout", { description: e.message, tone: "danger" }); } finally { setSaving(false); } } async function reset() { setSaving(true); try { const r = await api("/api/dashboard", { method: "PUT", json: { reset: true } }); qc.setQueryData(["dashboard"], (old) => (old ? { ...old, layout: r } : old)); setDraft(null); setEditing(false); toast("Default layout restored"); refresh(); } catch (e: any) { toast("Reset failed", { description: e.message, tone: "danger" }); } finally { setSaving(false); } } const missing = WIDGET_TYPES.filter((t) => !widgets.some((w) => w.type === t)); return (

{greeting(now)}, {displayName.split(" ")[0]}

{dateLabel || " "}{data && data.tasks.length > 0 && · {data.tasks.length} task{data.tasks.length === 1 ? "" : "s"} due soon}

{editing ? (
{missing.length ? "Available widgets" : "All widgets are on the board"}
{(missing.length ? missing : WIDGET_TYPES).map((t) => )}
) : }
{isError &&
Could not load the dashboard: {(error as Error).message}
} w.id)} strategy={rectSortingStrategy}>
{widgets.map((w) => setDraft((d) => (d ?? []).filter((x) => x.id !== w.id))} onResize={(size) => setDraft((d) => (d ?? []).map((x) => (x.id === w.id ? { ...x, size } : x)))} />)} {!widgets.length && data &&
{editing ? "Add widgets with the button above." : "Your dashboard is empty."} {!editing && }
}
{editing &&

Drag the handle to reorder (touch: press and hold). Resize with the column buttons. Esc cancels.

}
); }