import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode, } from "react"; import { api } from "@/api"; import type { Board, Item, ItemInput, ItemPatch, Prefs, RendererName, Section, StatusMap, Theme, } from "@/types"; import { parsePrefs } from "@/types"; import { setLocale } from "@/i18n"; // Board store — single renderer-agnostic source of truth for the board tree, // reachability statuses, and persisted settings. Renderers consume via useBoard. interface BoardState { sections: Section[]; boards: Board[]; boardId: string; boardSlug: string; statuses: StatusMap; theme: Theme; renderer: RendererName; accent: string; wallpaper: string; customCss: string; locale: string; loaded: boolean; refresh(): Promise; // slug is the URL form; "" means the default (first) board. setActiveSlug(slug: string): void; addBoard(name: string): Promise; renameBoard(id: string, name: string): Promise; deleteBoard(id: string): Promise; addSection(name: string): Promise
; patchSection(id: string, patch: { name?: string; collapsed?: boolean }): Promise; deleteSection(id: string): Promise; addItem(input: ItemInput): Promise; patchItem(id: string, patch: ItemPatch): Promise; deleteItem(id: string): Promise; uploadIcon(id: string, file: File): Promise; moveItemLocal(itemId: string, toSectionId: string, index: number): void; moveSectionLocal(id: string, index: number): void; persistItemMove(itemId: string, sectionId: string, beforeId?: string, afterId?: string): Promise; persistSectionMove(id: string, beforeId?: string, afterId?: string): Promise; setSetting(key: keyof Prefs, value: string): Promise; } const Ctx = createContext(null); export function BoardProvider({ children }: { children: ReactNode }) { const [sections, setSections] = useState([]); const [boards, setBoards] = useState([]); const [activeSlug, setActiveSlug] = useState(""); const [statuses, setStatuses] = useState({}); const [prefs, setPrefs] = useState({ theme: "dark", renderer: "bento", accent: "", wallpaper: "", customCss: "", locale: "" }); const [loaded, setLoaded] = useState(false); // Resolve slug → id; unknown/empty slugs fall back to the first board. const board = boards.find((b) => b.slug === activeSlug) ?? boards[0]; const boardId = board?.id ?? ""; const boardSlug = board?.slug ?? ""; const refresh = useCallback(async () => { if (!boardId) return; const { data } = await api.GET("/api/sections", { params: { query: { board: boardId } } }); if (data) setSections(data); }, [boardId]); const refreshStatus = useCallback(async () => { const { data } = await api.GET("/api/status"); if (data) setStatuses(data); }, []); useEffect(() => { void (async () => { const [{ data: bs }, { data: cfg }] = await Promise.all([ api.GET("/api/boards"), api.GET("/api/settings"), ]); if (bs) setBoards(bs); if (cfg) setPrefs(parsePrefs(cfg)); setLoaded(true); void refreshStatus(); })(); const t = setInterval(() => void refreshStatus(), 60_000); return () => clearInterval(t); }, [refreshStatus]); // Sections reload whenever the active board changes. useEffect(() => { void refresh(); }, [refresh]); const addBoard = useCallback(async (name: string) => { const { data } = await api.POST("/api/boards", { body: { name } }); if (data) setBoards((bs) => [...bs, data]); return data ?? null; }, []); const renameBoard = useCallback(async (id: string, name: string) => { const { data } = await api.PATCH("/api/boards/{id}", { params: { path: { id } }, body: { name } }); if (data) setBoards((bs) => bs.map((b) => (b.id === id ? data : b))); }, []); const deleteBoard = useCallback(async (id: string) => { const { error } = await api.DELETE("/api/boards/{id}", { params: { path: { id } } }); if (!error) { setBoards((bs) => bs.filter((b) => b.id !== id)); setActiveSlug(""); } }, []); const { theme, renderer, accent, wallpaper, customCss, locale } = prefs; // t() reads a module-level dict; this effect swaps it when the pref lands. useEffect(() => setLocale(locale), [locale]); // Apply theme: drives all CSS vars. localStorage mirrors // the server setting so index.html can pre-paint without a flash. useEffect(() => { if (!loaded) return; document.documentElement.dataset.theme = theme; localStorage.setItem("dash.theme", theme); }, [theme, loaded]); // Appearance overrides: accent is a CSS var the whole theme references, // wallpaper hangs off , customCss lands in a managed