import { lazy, Suspense, useCallback, useEffect, useState, type ReactNode } from "react"; import { Outlet, useLocation, useNavigate, useParams } from "@tanstack/react-router"; import { AlertCircle } from "lucide-react"; import { STORAGE_KEYS } from "@/client/lib/constants"; import { isResolvingWorkspace, isWorkspaceReady } from "@/client/lib/workspace-route-model"; import { shouldBlockMemberOnlyRouteContent, shouldRedirectMemberOnlyRoute } from "@/client/lib/workspace-layout-model"; import { WorkspaceViewProvider } from "./view-provider"; import { useWorkspaceView } from "./use-workspace-view"; import { Header } from "@/client/components/header"; import { Footer } from "@/client/components/footer"; import { Banners } from "@/client/components/ui/banners"; import { Skeleton } from "@/client/components/ui/skeleton"; import { Button } from "@/client/components/ui/button"; import { useMobileDrawer } from "@/client/hooks/use-mobile-drawer"; import { readStorageString, writeStorageString } from "@/client/lib/storage"; import { WorkspaceLayoutModeProvider } from "@/client/components/workspace/layout-mode-context"; const Sidebar = lazy(() => import("@/client/components/sidebar/sidebar").then((mod) => ({ default: mod.Sidebar }))); const NARROW_SHELL_CONTENT_CLASS = "mx-auto w-full max-w-7xl"; const NARROW_SHELL_ROW_CLASS = `${NARROW_SHELL_CONTENT_CLASS} border-l border-zinc-800/60`; function SidebarFallback({ collapsed = false }: { collapsed?: boolean }) { return ( ); } function TerminalRouteError({ message, onRetry }: { message: string; onRetry?: () => void }) { return (

{message}

{onRetry && ( )}
); } function WorkspaceLayoutInner() { const { route, canonicalSlug } = useWorkspaceView(); const params = useParams({ strict: false }) as { workspaceSlug: string; pageId?: string }; const location = useLocation(); const navigate = useNavigate(); const { open: mobileDrawerOpen, close: closeMobileDrawer, toggle: toggleMobileDrawer } = useMobileDrawer(); const [expanded, setExpanded] = useState(() => readStorageString(STORAGE_KEYS.LAYOUT) === "expanded"); const [sidebarCollapsed, setSidebarCollapsed] = useState(() => { if (typeof window === "undefined") return false; const stored = readStorageString(STORAGE_KEYS.SIDEBAR); if (stored !== null) return stored === "true"; return window.innerWidth < 768; }); const [sidebarManualToggle, setSidebarManualToggle] = useState(() => { if (typeof window === "undefined") return false; return readStorageString(STORAGE_KEYS.SIDEBAR) !== null; }); const toggleLayout = useCallback(() => { setExpanded((prev) => { const next = !prev; writeStorageString(STORAGE_KEYS.LAYOUT, next ? "expanded" : "default"); return next; }); }, []); useEffect(() => { if (sidebarManualToggle) return; const mq = window.matchMedia("(max-width: 767px)"); const handleChange = (event: MediaQueryListEvent) => setSidebarCollapsed(event.matches); mq.addEventListener("change", handleChange); return () => mq.removeEventListener("change", handleChange); }, [sidebarManualToggle]); const setPersistedSidebarCollapsed = useCallback((next: boolean) => { setSidebarManualToggle(true); setSidebarCollapsed(next); writeStorageString(STORAGE_KEYS.SIDEBAR, String(next)); }, []); // Canonical slug redirect — preserves search and hash state. useEffect(() => { if (!canonicalSlug) return; const segments = location.pathname.split("/"); if (segments[1] === canonicalSlug) return; segments[1] = canonicalSlug; const corrected = segments.join("/") + location.searchStr + (location.hash ? `#${location.hash}` : ""); navigate({ to: corrected, replace: true }); }, [canonicalSlug, location.pathname, location.searchStr, location.hash, navigate]); // Member-access gating: routes without a pageId param (workspace index, // settings) require confirmed member access. const isMemberOnlyRoute = !params.pageId; const memberOnlyRouteBlocked = shouldBlockMemberOnlyRouteContent(route, isMemberOnlyRoute); useEffect(() => { if (shouldRedirectMemberOnlyRoute(route, isMemberOnlyRoute)) { navigate({ to: "/", replace: true }); } }, [route, isMemberOnlyRoute, navigate]); let mainContent: ReactNode; if (memberOnlyRouteBlocked) { // Member-only route without confirmed member access. The redirect effect // above will swap us onto "/"; during that one render, show nothing. mainContent = null; } else if (route.phase === "error") { mainContent = ( window.location.reload() : undefined} /> ); } else { // Ready, loading, or degraded (with a pageId — memberOnlyRouteBlocked // already trapped the non-page case). ActivePageProvider owns cache-vs- // live fallback for the degraded branch via its internal page-load target. mainContent = ; } const showFullSidebar = isWorkspaceReady(route); const showSkeleton = isResolvingWorkspace(route); return ( Skip to content
{showFullSidebar ? ( }> ) : showSkeleton ? ( ) : null}
{mainContent}