"use client"; import { Sidebar } from "./Sidebar"; import { Header } from "./Header"; import { useEffect, useState, Suspense } from "react"; import { usePathname, useRouter } from "next/navigation"; import { useAuth } from "@/lib/auth/useAuth"; import { hasLiveSession } from "@/lib/api/mode"; import { isDemoPreviewSession } from "@/lib/auth/auth-client"; import { getPublicDemoMode } from "@/lib/env"; import { cn } from "@/lib/utils"; const isLiveFrontendMode = getPublicDemoMode() === "live"; type AppLayoutProps = { children: React.ReactNode; }; function LoadingFallback() { return (
Preparing your study space...
); } export function AppLayout({ children }: AppLayoutProps) { const router = useRouter(); const pathname = usePathname(); const { isLoading: isAuthLoading } = useAuth(); const [mobileOpen, setMobileOpen] = useState(false); const ownsWorkspaceChrome = pathname === "/home" || pathname === "/study-chat"; // startsWith, not exact match — a real bug found live: /learn-anything/[id] // (the roadmap view) kept AppLayout's own header, stacking it on top of // that section's own SectionTabs nav. const hidesAppHeader = pathname === "/home" || pathname === "/tuition" || pathname === "/learn-anything" || pathname?.startsWith("/learn-anything/") || pathname === "/study-chat"; const isLocalTuitionBetaRoute = pathname === "/tuition" || pathname?.startsWith("/tuition/"); // Auth guard — redirect unauthenticated users to login in live mode. // Runs on every student route wrapped by AppLayout. const unauthenticated = !isLocalTuitionBetaRoute && isLiveFrontendMode && !hasLiveSession() && !isDemoPreviewSession(); useEffect(() => { if (!isLocalTuitionBetaRoute && !isAuthLoading && unauthenticated) { const next = encodeURIComponent(pathname ?? "/study-chat"); router.replace(`/login?next=${next}`); } }, [isAuthLoading, isLocalTuitionBetaRoute, unauthenticated, pathname, router]); // Do NOT mount the page (and its authed API calls) while auth is still loading // or while we are redirecting an unauthenticated visitor — otherwise the page // fires a burst of 401 requests before the redirect lands. const blockChildren = !isLocalTuitionBetaRoute && (isAuthLoading || unauthenticated); // Close mobile drawer before paint on route change (avoids overlay flash blocking taps). useEffect(() => { const timer = window.setTimeout(() => setMobileOpen(false), 0); return () => window.clearTimeout(timer); }, [pathname]); const closeMobileNav = () => setMobileOpen(false); useEffect(() => { if (!mobileOpen) return; function onKeyDown(event: KeyboardEvent) { if (event.key === "Escape") setMobileOpen(false); } window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); }, [mobileOpen]); return (
{mobileOpen ? (
) : null}
{hidesAppHeader ? null :
setMobileOpen(true)} />}
}> {blockChildren ? ( ) : ownsWorkspaceChrome ? ( // These pages own their full-height scroll/layout — no transform wrapper. children ) : ( // Every standard app screen breathes in on navigation (keyed per route).
{children}
)}
); }