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 (
{collapsed ? (
) : (
<>
>
)}
);
}
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}
);
}
export function WorkspaceLayout() {
const { workspaceSlug, pageId } = useParams({ strict: false }) as { workspaceSlug: string; pageId?: string };
return (
);
}