import { createRoot } from "react-dom/client"; import { lazy, Suspense, useEffect, useLayoutEffect, useState } from "react"; import { createBrowserRouter, createPath, Link, NavLink, Outlet, RouterProvider, useLocation, useNavigationType, type RouteObject } from "react-router-dom"; import type { AuthUser } from "@stc/contracts"; import { AppShell, installRouteScroll, reducedMotion, routeScroll } from "@stc/ui"; import { webFeatures } from "./features"; import { changesPage, visibleNav } from "./nav"; function Home() { return

No features installed yet

; } /** The signed-in user (GET /api/me), refreshed on navigation so sign-in and sign-out update the nav. */ function useSessionUser(): AuthUser | null { const { pathname } = useLocation(); const [user, setUser] = useState(null); useEffect(() => { let live = true; fetch("/api/me", { credentials: "same-origin" }) .then((r) => (r.ok ? r.json() : { user: null })) .then((b: { user: AuthUser | null }) => { if (live) setUser(b.user ?? null); }) .catch(() => { if (live) setUser(null); }); return () => { live = false; }; }, [pathname]); return user; } /** Scroll per history entry: top on a new page, the saved position on back/forward (a page may defer it). */ function useRouteScroll() { const loc = useLocation(); const type = useNavigationType(); useEffect(() => installRouteScroll(), []); // A layout effect, so with a view transition the scroll is part of the captured new state. Pages' layout // effects run before this one, so a page keeping its old content on screen can defer it first. useLayoutEffect(() => { routeScroll().routeChanged(loc.key, type === "POP", loc.hash); }, [loc.key]); } function Layout() { useRouteScroll(); // Links the user cannot use are hidden; the pages and APIs still enforce access themselves. const nav = visibleNav(webFeatures, useSessionUser()); const header = ( <> STCourses {nav.length ? ( ) : null} ); return ( ); } const featureRoutes: RouteObject[] = webFeatures.flatMap((f) => f.routes.map((r) => { const Element = lazy(r.element); return { path: r.path, element: }; }), ); const router = createBrowserRouter([ { element: , children: [...featureRoutes, ...(featureRoutes.some((r) => r.path === "/") ? [] : [{ path: "*", element: }])] }, ]); // Navigating to another kind of page (library, course, lesson, account, studio…) runs as a view transition: the old // page dissolves while the new one's content rises in (styles in @stc/ui tokens.css). Steps within a page (lesson // to lesson) are animated by the page itself. Reduced motion and browsers without the API navigate plainly. const routePatterns = featureRoutes.map((r) => ({ path: r.path })); const plainNavigate = router.navigate.bind(router); router.navigate = ((to: Parameters[0], opts?: Parameters[1]) => { const animate = typeof to !== "number" && to !== null && "startViewTransition" in document && !reducedMotion() && changesPage(routePatterns, router.state.location.pathname, typeof to === "string" ? to : createPath(to)); return plainNavigate(to as never, animate ? { ...opts, viewTransition: opts?.viewTransition ?? true, flushSync: opts?.flushSync ?? true } : opts); }) as typeof router.navigate; createRoot(document.getElementById("root")!).render();