"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}
)}
);
}