import { Suspense, lazy } from "react" import { Navigate, type RouteObject } from "react-router" import { LoadingFallback } from "./pages/_loading" import { RouteError } from "./pages/_error" import { LibraryLayout } from "./pages/library/_layout" import ProductDashboard from "./pages/_product-dashboard" import { RedirectToDesignOsSubpath, designOsLegacyRootRedirects, designOsProductChildRoutes, } from "./pages/design-system/routes" 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_SCHOOLS_HUB_SEGMENTS, ONE_SITES_HUB_SEGMENTS, } from "@/lib/mock/navigation" import { FirstRunRedirect } from "./route-guards/first-run-redirect" import { RequireDirectoryAccess } from "./route-guards/require-directory-access" import { RequireProductScope } from "./route-guards/require-product-scope" import { RequireWorkspaceAdmin } from "./route-guards/require-workspace-admin" /** * 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`** and **`/whats-new`** stay 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. */ 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 } // Route guards live in `./route-guards/*` (FirstRunRedirect, RequireWorkspaceAdmin, // RequireDirectoryAccess, RequireProductScope). // ───────────────────────────────────────────────────────────────────────────── // 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. */ /** * Nav segments that have a real page and must not fall through to * `_product-shell-placeholder`. Keep in sync with the explicit routes in * `productChildren()`. */ const BUILT_HUB_SEGMENTS = new Set([ "learning-activities", "locations", "slot-requests", ]) 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 learningActivitiesChildren(): RouteObject[] { return [ { index: true, element: lazyPage(() => import("./pages/learning-activities")) }, { path: "courses/:offeringId", element: lazyPage(() => import("./pages/learning-activities-course-detail")), }, ] } 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: , }, ...designOsProductChildRoutes(), { path: "learning-activities", element: lazyPage(() => import("./pages/learning-activities/_layout")), children: learningActivitiesChildren(), errorElement: , }, // Exxat One hubs. These four are real surfaces now, so they must be // declared before the placeholder catch-all below and excluded from it. { path: "locations", element: lazyPage(() => import("./pages/one/locations")), errorElement: , }, { path: "slot-requests", element: lazyPage(() => import("./pages/one/slot-requests")), errorElement: , }, ...PRODUCT_SHELL_HUB_SEGMENTS.filter( segment => !BUILT_HUB_SEGMENTS.has(segment), ).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 → sign-in, then onboarding on the first visit, then the // products home. { index: true, element: }, // Sign-in. Shell-global and pre-auth: it belongs to no product, because which // products you have is not known until you are through it. { path: "login", element: lazyPage(() => import("./pages/login")), errorElement: , }, // Where the pick-a-role sign-in flow lands a student. Shell-global for the // same reason: a student holds no product, so there is no product root to // hang this under. See `lib/student-shell.ts`. { path: "student", element: lazyPage(() => import("./pages/student")), errorElement: , }, // Sign-in flow builder. Under `/builder` rather than a product root because a // single flow decides which product you end up in, so it belongs to none. { path: "builder/sign-in-flows", element: lazyPage(() => import("./pages/sign-in-flows")), errorElement: , }, // Pre-product back-compat for the only previously-rooted hub. { path: "dashboard", element: }, // ─────────────────────────────────────────────────────────────────────── // Products home — shell-global. The marketing pages live here rather than // under a product root because their audience is people without that // product; routing them through `/prism/*` would adopt a product context // they are not entitled to. // ─────────────────────────────────────────────────────────────────────── { path: "home", errorElement: , children: [ { index: true, element: lazyPage(() => import("./pages/product-home-index")), }, // Explicit layout links before `:productSlug`, so `/home/focus` is never // mistaken for a product marketing page. { path: "storefront", element: lazyPage(() => import("./pages/product-home")) }, { path: "storefront-v2", element: lazyPage(() => import("./pages/product-home")) }, { path: "focus", element: lazyPage(() => import("./pages/product-home")) }, { path: "spotlight", element: lazyPage(() => import("./pages/product-home")) }, { path: "spotlight-v2", element: lazyPage(() => import("./pages/product-home")) }, { path: "spotlight-v3", element: lazyPage(() => import("./pages/product-home")) }, { path: "spotlight-v4", element: lazyPage(() => import("./pages/product-home")) }, { path: "launcher", element: lazyPage(() => import("./pages/product-home")) }, { path: "campus", element: lazyPage(() => import("./pages/product-home")) }, { path: "campus-v3", element: lazyPage(() => import("./pages/product-home")) }, { path: "catalog", element: lazyPage(() => import("./pages/product-home")) }, { path: ":productSlug", element: lazyPage(() => import("./pages/product-marketing")), }, ], }, // ─────────────────────────────────────────────────────────────────────── // Shell-global routes (per-user, not per-product) // ─────────────────────────────────────────────────────────────────────── { 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: , }, { path: "whats-new", element: lazyPage(() => import("./pages/whats-new")), errorElement: , }, // ─────────────────────────────────────────────────────────────────────── // Pre-`/design-os` deep links (`/library`, `/design-system`, `/columns`, // `/tokens-themes`, `/exam`) → the Design OS product root. // ─────────────────────────────────────────────────────────────────────── ...designOsLegacyRootRedirects(), // ─────────────────────────────────────────────────────────────────────── // Directory products — People, Courses, Programs, Personnel (separate roots), plus // Administrator overview. Legacy `/admin/{hub}` paths redirect to the new // product roots so bookmarks keep working. // ─────────────────────────────────────────────────────────────────────── { path: "people", element: , errorElement: , children: [ { index: true, element: }, { path: "students", element: lazyPage(() => import("./pages/admin/people-students")) }, { path: "faculty", element: lazyPage(() => import("./pages/admin/people-faculty")) }, { path: "staff", element: lazyPage(() => import("./pages/admin/people-staff")) }, { path: ":personId", element: lazyPage(() => import("./pages/admin/person-detail")), }, { path: "dashboard", element: }, ], }, { path: "courses", element: , errorElement: , children: [ { index: true, element: lazyPage(() => import("./pages/admin/courses")) }, { path: ":courseId", element: lazyPage(() => import("./pages/admin/course-detail")), }, { path: "dashboard", element: }, ], }, { path: "programs", element: , errorElement: , children: [ { index: true, element: lazyPage(() => import("./pages/admin/programs")) }, { path: ":programId", element: lazyPage(() => import("./pages/admin/program-detail")), }, { path: "dashboard", element: }, ], }, { path: "personnel", element: , errorElement: , children: [ { index: true, element: lazyPage(() => import("./pages/admin/personnel")) }, { path: ":personnelId", element: lazyPage(() => import("./pages/admin/personnel-detail")), }, { path: "dashboard", element: }, ], }, { path: "admin", element: , errorElement: , children: [ { index: true, element: lazyPage(() => import("./pages/admin/index")) }, { path: "people", element: }, { path: "people/*", element: ( ), }, { path: "courses", element: }, { path: "courses/*", element: ( ), }, { path: "personnel", element: }, { path: "personnel/*", element: ( ), }, { path: "dashboard", 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: , errorElement: , // Pathless layout so the scope gate wraps every product page without any of // them naming it, and without adding a segment to their URLs. children: [ { element: , children: productChildren(), }, ], }, { path: "*", element: lazyPage(() => import("./pages/_not-found")), }, ]