"use client" import * as React from "react" import { useLocation, useNavigate } from "react-router" import { useAppStore, hasConfiguredCustomProducts, productSlug, type Product, } from "@exxatdesignux/product-framework/app-store" import { productRouteSlug, resolveProductFromPath, } from "@exxatdesignux/product-framework/product-routing" import { useProduct } from "./product-context" import { usePersistHydrated } from "../../hooks/use-persist-hydrated" /** * Bidirectional sync between the active product (app store) and the URL. * * - **URL → store** (this component): when the user lands on or follows a * deep link to `//...` while the store says a different * product, the store adopts the URL's product. URL is source of truth. * * - **Store → URL** (`useProductSwitch` below, called from switcher click * handlers): the store updates AND we hard-navigate to * `//dashboard`. We do this in the click handler — not in an * effect — to keep the click ↔ navigate causality explicit. * * Mount once inside `ProductProvider` and inside the router. */ export function ProductRouteSync() { const { product, setProduct, customProducts, activeCustomIndex, setActiveCustomIndex, } = useProduct() const location = useLocation() const navigate = useNavigate() const setProductSwitching = useAppStore(s => s.setProductSwitching) const hydrated = usePersistHydrated() React.useEffect(() => { if (!hydrated) return const segment = location.pathname.replace(/^\/+/, "").split("/", 1)[0] ?? "" // Legacy `/custom/*` bookmarks → active custom suffix slug. if (segment === "custom") { if (!hasConfiguredCustomProducts(customProducts)) { if (product !== "exxat-prism") setProduct("exxat-prism") navigate(`/${productSlug("exxat-prism")}/dashboard`, { replace: true }) return } const targetSlug = productRouteSlug( "exxat-custom", customProducts, activeCustomIndex, ) const tail = location.pathname.replace(/^\/custom\/?/, "") navigate(`/${targetSlug}/${tail || "dashboard"}${location.search}${location.hash}`, { replace: true, }) return } const resolved = resolveProductFromPath(location.pathname, customProducts) // Shell-global routes (`/`, `/settings/profile`, `/help`, demo routes) leave // the store alone. The user's active product persists across visits. if (resolved == null) { setProductSwitching(false) return } if ( resolved.product === "exxat-custom" && !hasConfiguredCustomProducts(customProducts) ) { if (product !== "exxat-prism") setProduct("exxat-prism") navigate(`/${productSlug("exxat-prism")}/dashboard`, { replace: true }) return } if ( resolved.customIndex !== undefined && resolved.customIndex !== activeCustomIndex ) { setActiveCustomIndex(resolved.customIndex) } if (resolved.product !== product) { setProduct(resolved.product) } // When the active custom product is renamed, keep the URL on its slug. if ( product === "exxat-custom" && resolved.product === "exxat-custom" && hasConfiguredCustomProducts(customProducts) ) { const expectedSlug = productRouteSlug( "exxat-custom", customProducts, resolved.customIndex ?? activeCustomIndex, ) if (segment !== expectedSlug) { const tail = location.pathname.replace(/^\/[^/]+\/?/, "") navigate(`/${expectedSlug}/${tail || "dashboard"}${location.search}${location.hash}`, { replace: true, }) return } } setProductSwitching(false) }, [ activeCustomIndex, customProducts, hydrated, location, navigate, product, setActiveCustomIndex, setProduct, setProductSwitching, ]) return null } /** Product-scoped dashboard href for breadcrumbs and shell links. */ export function useProductDashboardHref(): string { const { product, customProducts, activeCustomIndex } = useProduct() const slug = productRouteSlug(product, customProducts, activeCustomIndex) return `/${slug}/dashboard` } /** Organization / workspace settings (products, tenant branding) under the active product root. */ export function useProductOrganizationSettingsHref(): string { const { product, customProducts, activeCustomIndex } = useProduct() const slug = productRouteSlug(product, customProducts, activeCustomIndex) return `/${slug}/settings` } /** * Hook for product-switcher click handlers. Returns a callback that flips * the active product AND hard-navigates to `//dashboard`. * * The two operations happen in the same tick so the user sees one visual * change (theme + URL flip together), not a brief flash of the old product * on the new dashboard. `replace: false` so browser `Back` returns the user * to where they were in the previous product. */ export function useProductSwitch() { const { setProduct, setActiveCustomIndex } = useProduct() const navigate = useNavigate() const setProductSwitching = useAppStore(s => s.setProductSwitching) return React.useCallback( (next: Product, customIndex?: number) => { const state = useAppStore.getState() const idx = customIndex ?? (next === "exxat-custom" ? state.activeCustomIndex : 0) const slug = productRouteSlug(next, state.customProducts, idx) setProductSwitching(true) if (customIndex !== undefined) { setActiveCustomIndex(customIndex) } setProduct(next) navigate(`/${slug}/dashboard`) }, [navigate, setActiveCustomIndex, setProduct, setProductSwitching], ) }