"use client"; import { createContext, type Dispatch, type ReactNode, useContext, useReducer } from "react"; import type { RootTab, TreeNodeData, TreeNodeUpdate } from "@/types/catalog"; // ── State ───────────────────────────────────────────────────── export interface CatalogManagerState { // Root tabs rootPages: RootTab[]; activeTabId: number | null; loadingRoot: boolean; // Tree nodes: Map; childrenMap: Map; expandedIds: Set; loadingIds: Set; selectedPageId: number | null; multiSelectedIds: Set; focusedId: number | null; sortMode: boolean; searchQuery: string; searchResults: TreeNodeData[] | null; searching: boolean; // Editor editorDirty: boolean; // Breadcrumb ancestors: Array<{ id: number; caption: string }>; } const initialState: CatalogManagerState = { rootPages: [], activeTabId: null, loadingRoot: false, nodes: new Map(), childrenMap: new Map(), expandedIds: new Set(), loadingIds: new Set(), selectedPageId: null, multiSelectedIds: new Set(), focusedId: null, sortMode: false, searchQuery: "", searchResults: null, searching: false, editorDirty: false, ancestors: [], }; // ── Actions ─────────────────────────────────────────────────── export type CatalogAction = | { type: "SET_ROOT_PAGES"; pages: RootTab[] } | { type: "SET_LOADING_ROOT"; loading: boolean } | { type: "SET_ACTIVE_TAB"; id: number | null } | { type: "SELECT_PAGE"; id: number | null } | { type: "TOGGLE_EXPAND"; id: number } | { type: "SET_EXPANDED"; id: number; expanded: boolean } | { type: "EXPAND_PATH"; ids: number[] } | { type: "COLLAPSE_ALL" } | { type: "SET_NODES"; nodes: TreeNodeData[] } | { type: "SET_CHILDREN"; parentId: number; children: TreeNodeData[] } | { type: "UPDATE_NODE"; update: TreeNodeUpdate } | { type: "REMOVE_NODE"; id: number } | { type: "SET_LOADING_IDS"; ids: Set } | { type: "SET_SEARCH_QUERY"; query: string } | { type: "SET_SEARCH_RESULTS"; results: TreeNodeData[] | null } | { type: "SET_SEARCHING"; searching: boolean } | { type: "TOGGLE_SORT_MODE" } | { type: "TOGGLE_MULTI_SELECT"; id: number } | { type: "RANGE_SELECT"; from: number; to: number; flatIds: number[] } | { type: "CLEAR_MULTI_SELECT" } | { type: "SET_FOCUSED"; id: number | null } | { type: "SET_DIRTY"; dirty: boolean } | { type: "SET_ANCESTORS"; ancestors: Array<{ id: number; caption: string }> } | { type: "ON_DIALOG_CLOSE" } | { type: "CLEAR_TREE" }; // ── Reducer ─────────────────────────────────────────────────── function reducer(state: CatalogManagerState, action: CatalogAction): CatalogManagerState { switch (action.type) { case "SET_ROOT_PAGES": return { ...state, rootPages: action.pages, loadingRoot: false }; case "SET_LOADING_ROOT": return { ...state, loadingRoot: action.loading }; case "SET_ACTIVE_TAB": return { ...state, activeTabId: action.id, selectedPageId: null, multiSelectedIds: new Set(), focusedId: null, ancestors: [], }; case "SELECT_PAGE": return { ...state, selectedPageId: action.id, multiSelectedIds: new Set(), focusedId: action.id, }; case "TOGGLE_EXPAND": { const next = new Set(state.expandedIds); if (next.has(action.id)) next.delete(action.id); else next.add(action.id); return { ...state, expandedIds: next }; } case "SET_EXPANDED": { const next = new Set(state.expandedIds); if (action.expanded) next.add(action.id); else next.delete(action.id); return { ...state, expandedIds: next }; } case "EXPAND_PATH": { const next = new Set(state.expandedIds); for (const id of action.ids) next.add(id); return { ...state, expandedIds: next }; } case "COLLAPSE_ALL": return { ...state, expandedIds: new Set() }; case "SET_NODES": { const next = new Map(state.nodes); for (const n of action.nodes) next.set(n.id, n); return { ...state, nodes: next }; } case "SET_CHILDREN": { const nextNodes = new Map(state.nodes); const nextChildren = new Map(state.childrenMap); const childIds: number[] = []; for (const child of action.children) { nextNodes.set(child.id, child); childIds.push(child.id); } nextChildren.set(action.parentId, childIds); return { ...state, nodes: nextNodes, childrenMap: nextChildren }; } case "UPDATE_NODE": { const existing = state.nodes.get(action.update.id); if (!existing) return state; const nextNodes = new Map(state.nodes); nextNodes.set(action.update.id, { ...existing, ...(action.update.caption !== undefined && { caption: action.update.caption }), ...(action.update.enabled !== undefined && { enabled: action.update.enabled }), ...(action.update.visible !== undefined && { visible: action.update.visible }), ...(action.update.pageLayout !== undefined && { pageLayout: action.update.pageLayout }), ...(action.update.iconImage !== undefined && { iconImage: action.update.iconImage }), }); return { ...state, nodes: nextNodes }; } case "REMOVE_NODE": { const nextNodes = new Map(state.nodes); nextNodes.delete(action.id); const nextChildren = new Map(state.childrenMap); // Remove from parent's children list for (const [parentId, children] of nextChildren) { const idx = children.indexOf(action.id); if (idx !== -1) { nextChildren.set( parentId, children.filter((cid) => cid !== action.id), ); } } // Remove this node's children entry nextChildren.delete(action.id); const nextSelected = state.selectedPageId === action.id ? null : state.selectedPageId; return { ...state, nodes: nextNodes, childrenMap: nextChildren, selectedPageId: nextSelected }; } case "SET_LOADING_IDS": return { ...state, loadingIds: action.ids }; case "SET_SEARCH_QUERY": return { ...state, searchQuery: action.query }; case "SET_SEARCH_RESULTS": return { ...state, searchResults: action.results, searching: false }; case "SET_SEARCHING": return { ...state, searching: action.searching }; case "TOGGLE_SORT_MODE": return { ...state, sortMode: !state.sortMode, multiSelectedIds: new Set() }; case "TOGGLE_MULTI_SELECT": { const next = new Set(state.multiSelectedIds); if (next.has(action.id)) next.delete(action.id); else next.add(action.id); return { ...state, multiSelectedIds: next, focusedId: action.id }; } case "RANGE_SELECT": { const { flatIds, from, to } = action; const fromIdx = flatIds.indexOf(from); const toIdx = flatIds.indexOf(to); if (fromIdx === -1 || toIdx === -1) return state; const lo = Math.min(fromIdx, toIdx); const hi = Math.max(fromIdx, toIdx); const next = new Set(state.multiSelectedIds); for (let i = lo; i <= hi; i++) next.add(flatIds[i]); return { ...state, multiSelectedIds: next, focusedId: action.to }; } case "CLEAR_MULTI_SELECT": return { ...state, multiSelectedIds: new Set() }; case "SET_FOCUSED": return { ...state, focusedId: action.id }; case "SET_DIRTY": return { ...state, editorDirty: action.dirty }; case "SET_ANCESTORS": return { ...state, ancestors: action.ancestors }; case "ON_DIALOG_CLOSE": return { ...state, selectedPageId: null, editorDirty: false, multiSelectedIds: new Set(), ancestors: [], }; case "CLEAR_TREE": return { ...state, nodes: new Map(), childrenMap: new Map(), expandedIds: new Set(), loadingIds: new Set(), selectedPageId: null, multiSelectedIds: new Set(), focusedId: null, searchQuery: "", searchResults: null, ancestors: [], }; default: return state; } } // ── Context ─────────────────────────────────────────────────── interface CatalogManagerContextValue { state: CatalogManagerState; dispatch: Dispatch; } const CatalogManagerContext = createContext(null); export function CatalogManagerProvider({ children }: { children: ReactNode }) { const [state, dispatch] = useReducer(reducer, initialState); return ( {children} ); } export function useCatalogManager() { const ctx = useContext(CatalogManagerContext); if (!ctx) throw new Error("useCatalogManager must be used within CatalogManagerProvider"); return ctx; }