"use client"; import { useSyncExternalStore } from "react"; import { adminFetch } from "@/lib/admin-fetch"; import type { TreeNode } from "@/types/catalog"; import { applyCatalogTotals, applyCatalogTreeDelta, type CatalogTotals, type CatalogTreeDelta, isEmptyCatalogDelta, normalizeTreePages, } from "./live-catalog-merge"; export type CatalogMode = "normal" | "bc"; interface LiveCatalogSnapshot { tree: TreeNode[]; totals: Record; treeLoaded: boolean; } const EMPTY_SNAPSHOT: LiveCatalogSnapshot = { tree: [], totals: { normal: null, bc: null }, treeLoaded: false, }; // A module-level external store keeps the catalog tree alive across client // navigations and — more importantly — lets an import push its delta straight // into the mounted tree instead of re-rendering the whole route. let snapshot: LiveCatalogSnapshot = EMPTY_SNAPSHOT; const listeners = new Set<() => void>(); function emit(next: LiveCatalogSnapshot) { snapshot = next; for (const listener of listeners) listener(); } function subscribe(listener: () => void) { listeners.add(listener); return () => { listeners.delete(listener); }; } const getSnapshot = () => snapshot; const getServerSnapshot = () => EMPTY_SNAPSHOT; export function useLiveCatalog(): LiveCatalogSnapshot { return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); } // ── Reads ────────────────────────────────────────────────────────────────── /** * Adopt server-rendered totals as the baseline. Only the first value per mode * wins: after an import the store holds counts that are newer than any re-render * of the server prop (which is itself `unstable_cache`d for 30s). */ export function seedCatalogTotals( mode: CatalogMode, totals: CatalogTotals | null | undefined, ): void { if (!totals) return; if (snapshot.totals[mode]) return; emit({ ...snapshot, totals: { ...snapshot.totals, [mode]: { ...totals } }, }); } /** * Fold an import run's changes into the live tree and totals. Safe to call from * an event handler; a delta that changes nothing leaves the snapshot untouched. */ export function applyCatalogDelta(delta: CatalogTreeDelta): void { if (isEmptyCatalogDelta(delta)) return; const normal = snapshot.totals.normal; emit({ ...snapshot, tree: applyCatalogTreeDelta(snapshot.tree, delta), totals: { ...snapshot.totals, normal: normal ? applyCatalogTotals(normal, delta) : null, }, }); } async function loadTree(): Promise { const res = await adminFetch("/api/admin/catalog/tree?mode=full"); if (!res.ok) throw new Error(`Catalog tree refresh failed (${res.status})`); const data = await res.json(); emit({ ...snapshot, tree: normalizeTreePages(data?.pages), treeLoaded: true, }); } let treeRequest: Promise | null = null; /** * Reload the whole tree over the API. Used where the server decides the shape of * the result — the furniture importers derive their category pages from * furnidata, so there is nothing for the client to predict. Still a plain * in-place data update: no route re-render, no remount, no lost editor state. */ export function refreshCatalogTree(): Promise { if (treeRequest) return treeRequest; treeRequest = loadTree() .catch(() => undefined) .finally(() => { treeRequest = null; }); return treeRequest; } /** Load the tree once per session; later calls are no-ops. */ export function ensureCatalogTreeLoaded(): Promise { return snapshot.treeLoaded ? Promise.resolve() : refreshCatalogTree(); } /** Test seam: drop all live catalog state between cases. */ export function resetLiveCatalogForTests(): void { treeRequest = null; emit(EMPTY_SNAPSHOT); }