import { Suspense, lazy } from "react" import { Navigate, useLocation, type RouteObject } from "react-router-dom" import { LoadingFallback } from "./pages/_loading" import { RouteError } from "./pages/_error" import { LibraryLayout } from "./pages/library/_layout" import ProductDashboard from "./pages/_product-dashboard" import { ProductRootGate } from "@/contexts/product-root-gate" import { DefaultProductRedirect } from "@/contexts/default-product-redirect" import { TENANT_PRODUCT_SHELL_HUB_SEGMENTS } from "@exxatdesignux/product-framework" import { PRISM_HUB_SEGMENTS, ONE_SITES_HUB_SEGMENTS, ONE_SCHOOLS_HUB_SEGMENTS } from "@/lib/mock/navigation" import { getStorageItem } from "@exxatdesignux/ui/lib/persisted-state" /** Per-device flag set when the builder onboarding flow completes. */ const ONBOARDING_COMPLETE_KEY = "builder:onboarding-complete:v1" /** * Vite route table — implements the multi-product routing pattern * (`apps/web/docs/multi-product-routing-pattern.md`): * * - **Per-product dashboards** live under `//dashboard` * (Rule 1). Switching products navigates to that root's dashboard * (Rule 2 — driven by `useProductSwitch` in * `contexts/product-route-sync.tsx`). * - **Back-compat redirects** at the workspace root (`/dashboard` → * `/prism/dashboard`) keep deep links from before this refactor working * without a server-side 308. * - **Library** mounts under product roots (`//library/*`). * The legacy root `/library/*` mount stays as a back-compat alias while * the Library subsystem normalizes product prefixes internally. * - **Shell-global settings** — profile prefs at `/settings/profile`; * org/workspace (products) at `/settings/organization` and * `//settings`. `/settings` redirects for back-compat. * - **`/help`** stays shell-global (per *user*, not per *product*). * - **DS demo routes** (`/columns`, `/tokens-themes`) also stay at the * root while they're still development surfaces — same exception. */ /** Back-compat: `/library`, `/columns`, `/tokens-themes`, `/exam` → Design OS product root. */ function RedirectToDesignOsSubpath({ fromPrefix, toPrefix }: { fromPrefix: string; toPrefix: string }) { const location = useLocation() const rest = location.pathname.startsWith(fromPrefix) ? location.pathname.slice(fromPrefix.length) : "" const normalizedRest = rest.startsWith("/") ? rest : rest ? `/${rest}` : "" return ( ) } function lazyPage( loader: () => Promise<{ default: React.ComponentType }>, fallback: React.ReactNode = , ) { const Lazy = lazy(loader) return ( ) } /** * Back-compat redirect for `/dashboard` → default product dashboard. */ function LegacyRedirectToDefaultDashboard() { return } // First-visit landing — run the builder onboarding flow on the very first // visit, then land on the default product dashboard on every subsequent load. // Onboarding marks itself complete via `builder:onboarding-complete:v1` // (set in `views/builder-onboarding.tsx`), so the redirect only fires once. // Re-run it from `Settings → Profile → Restart onboarding` (which clears the // flag) or by visiting `/builder/onboarding` directly. function FirstRunRedirect() { const onboardingComplete = getStorageItem(ONBOARDING_COMPLETE_KEY) === "true" if (!onboardingComplete) { return } return } // ───────────────────────────────────────────────────────────────────────────── // Per-product children // ───────────────────────────────────────────────────────────────────────────── // // Today only the dashboard is under a product root. Once Library / Placements // / Settings sub-screens move under products, append their children here (or // extract per-product factories like `prismChildren()`, `oneSitesChildren()`). const TENANT_PRODUCT_SHELL_HUBS = TENANT_PRODUCT_SHELL_HUB_SEGMENTS.filter( segment => segment !== "library", ) /** Prism + One — Schools + One — Sites + legacy tenant shell hubs. */ const PRODUCT_SHELL_HUB_SEGMENTS = [ ...new Set([ ...PRISM_HUB_SEGMENTS, ...ONE_SCHOOLS_HUB_SEGMENTS, ...ONE_SITES_HUB_SEGMENTS, ...TENANT_PRODUCT_SHELL_HUBS, ]), ] function libraryChildren(): RouteObject[] { return [ { index: true, element: lazyPage(() => import("./pages/library/index")) }, { path: "all", element: lazyPage(() => import("./pages/library/all")) }, { path: "find", element: lazyPage(() => import("./pages/library/find")) }, { path: "list", element: lazyPage(() => import("./pages/library/list")) }, { path: "new", element: lazyPage(() => import("./pages/library/new")) }, ] } function productChildren(): RouteObject[] { return [ { index: true, element: }, { path: "dashboard", element: , errorElement: , }, { path: "library", element: , children: libraryChildren(), errorElement: , }, { path: "leo", element: lazyPage(() => import("./pages/leo")), errorElement: , }, { path: "catalog", element: lazyPage(() => import("./pages/catalog")), errorElement: , }, { path: "columns", element: lazyPage(() => import("./pages/columns")), errorElement: , }, { path: "column-types-demo", element: lazyPage(() => import("./pages/column-types-demo")), errorElement: , }, { path: "tokens-themes", element: lazyPage(() => import("./pages/tokens-themes")), errorElement: , }, { path: "focus-workflow", element: lazyPage(() => import("./pages/focus-workflow")), errorElement: , }, { path: "exam-lock", element: lazyPage(() => import("./pages/exam-lock")), errorElement: , }, ...PRODUCT_SHELL_HUB_SEGMENTS.map(segment => ({ path: `${segment}/*`, element: lazyPage(() => import("./pages/_product-shell-placeholder")), errorElement: , })), { path: "settings", element: lazyPage(() => import("./pages/settings-organization")), errorElement: , }, ] } export const routes: RouteObject[] = [ // ─────────────────────────────────────────────────────────────────────── // Root + back-compat redirects // ─────────────────────────────────────────────────────────────────────── // Cold-start landing → tenant default product (Prism when unset). { index: true, element: }, // Pre-product back-compat for the only previously-rooted hub. { path: "dashboard", element: }, // ─────────────────────────────────────────────────────────────────────── // Shell-global routes (per-user, not per-product) // ─────────────────────────────────────────────────────────────────────── { path: "library/*", element: , }, { path: "settings", element: lazyPage(() => import("./pages/settings-layout")), errorElement: , children: [ { index: true, element: lazyPage(() => import("./pages/settings-legacy-redirect")), }, { path: "profile", element: lazyPage(() => import("./pages/settings-profile")), }, { path: "organization", element: lazyPage(() => import("./pages/settings-organization")), }, ], }, { path: "builder/onboarding", element: lazyPage(() => import("./pages/builder-onboarding")), errorElement: , }, { path: "builder/products", element: , }, { path: "help", element: lazyPage(() => import("./pages/help")), errorElement: , }, // ─────────────────────────────────────────────────────────────────────── // DS demo routes (development surfaces — still at root per the rule's // exception; once promoted into a product hub they adopt that subpath). // ─────────────────────────────────────────────────────────────────────── { path: "catalog/*", element: , }, { path: "columns/*", element: , }, { path: "tokens-themes/*", element: ( ), }, { path: "exam/*", element: , }, // ─────────────────────────────────────────────────────────────────────── // Product-owned roots (Rule 1) — built-in slugs + custom suffix slugs. // MUST stay after shell-global routes so `/settings` etc. are not captured. // ─────────────────────────────────────────────────────────────────────── { path: ":productRootSegment", element: , children: productChildren(), errorElement: , }, { path: "*", element: lazyPage(() => import("./pages/_not-found")), }, ]