"use client" /** * Active school > program (or brand > site > location) selection for a product. * * The utility bar's scope switcher used to hold this in component state, which * meant the choice died on reload and nothing else could read it. The products * home needs to both show and set the scope before the user has even entered * the product, so the selection has to live somewhere both surfaces share. * * `usePersistedState` is deliberately not used here: it only syncs across * browser *tabs*, so two hook instances in the same document would hold * independent copies of the same key and drift apart the moment one of them * wrote. This is a small external store instead — one snapshot per key, every * subscriber notified on write — with the same storage envelope shape so it * can move to `usePersistedState` if that ever gains in-page sync. * * Persisted per product: Prism's school is not One — Sites' brand, and the two * scope hierarchies do not even have the same shape. */ import * as React from "react" import { getStorageItem, setStorageItem } from "@exxatdesignux/ui/lib/persisted-state" import type { Product } from "@/contexts/product-context" import { scopeConfigForProduct, type ScopeChild, type ScopeConfig, type ScopeParent, } from "@/lib/scope-switcher" import { productPersistKey, useAppStore } from "@/stores/app-store" const SCOPE_VERSION = 1 interface StoredScope { parentId: string childId: string } const listeners = new Set<() => void>() /** One canonical object per key so `getSnapshot` stays referentially stable. */ const snapshots = new Map() function subscribe(onChange: () => void) { listeners.add(onChange) return () => { listeners.delete(onChange) } } function readScope(key: string): StoredScope | null { const cached = snapshots.get(key) if (cached) return cached const raw = getStorageItem(key) if (!raw) return null try { const parsed: unknown = JSON.parse(raw) if ( typeof parsed === "object" && parsed !== null && (parsed as { v?: number }).v === SCOPE_VERSION ) { const data = (parsed as { d?: unknown }).d if ( typeof data === "object" && data !== null && typeof (data as StoredScope).parentId === "string" && typeof (data as StoredScope).childId === "string" ) { snapshots.set(key, data as StoredScope) return data as StoredScope } } } catch { // Unparseable payload — treat as absent and let the defaults win. } return null } function writeScope(key: string, value: StoredScope) { snapshots.set(key, value) setStorageItem(key, JSON.stringify({ v: SCOPE_VERSION, d: value })) for (const listener of listeners) listener() } export interface ActiveScope { config: ScopeConfig parent: ScopeParent child: ScopeChild /** Also moves the child to the new parent's first option. */ selectParent: (parent: ScopeParent) => void selectChild: (child: ScopeChild) => void } export function useActiveScope(product: Product, customIndex?: number): ActiveScope { const config = React.useMemo(() => scopeConfigForProduct(product), [product]) const customProducts = useAppStore(s => s.customProducts) const activeCustomIndex = useAppStore(s => s.activeCustomIndex) const key = React.useMemo( () => productPersistKey( product, "scope", customProducts, customIndex ?? activeCustomIndex, ), [product, customProducts, customIndex, activeCustomIndex], ) const stored = React.useSyncExternalStore( subscribe, () => readScope(key), () => null, ) // A stored id can go stale when the scope list changes underneath it, so // every read falls back to the default rather than trusting storage. const parent = React.useMemo( () => config.parents.find(p => p.id === stored?.parentId) ?? config.defaultParent, [config, stored], ) const child = React.useMemo(() => { const children = config.childrenOf(parent) return children.find(c => c.id === stored?.childId) ?? children[0] ?? config.defaultChild }, [config, parent, stored]) const selectParent = React.useCallback( (next: ScopeParent) => { const firstChild = config.childrenOf(next)[0] writeScope(key, { parentId: next.id, childId: firstChild?.id ?? config.defaultChild.id, }) }, [config, key], ) const selectChild = React.useCallback( (next: ScopeChild) => { writeScope(key, { parentId: parent.id, childId: next.id }) }, [key, parent.id], ) return { config, parent, child, selectParent, selectChild } }