DocDoeAI / src /components /layout /AppLayout.tsx
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
65d68f5
Raw History Blame Contribute Delete
5.35 kB
"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 (
<div className="flex flex-1 items-center justify-center">
<div className="flex items-center gap-3">
<span className="size-5 animate-spin rounded-full border-2 border-white/15 border-t-white/72" />
<span className="text-[14px] text-[var(--text-secondary)]">Preparing your study space...</span>
</div>
</div>
);
}
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 (
<div className="docdoe-workspace relative flex h-screen max-h-screen min-h-screen overflow-hidden bg-[#030303]">
<div className="pointer-events-none fixed inset-0 z-0 select-none overflow-hidden">
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(255,255,255,0.035)_0%,rgba(255,255,255,0.008)_38%,rgba(0,0,0,0)_100%)]" />
<div className="absolute inset-x-0 top-[-22rem] mx-auto h-[34rem] max-w-[70rem] rounded-full bg-white/[0.035] blur-3xl" />
<div className="absolute inset-0 bg-[radial-gradient(circle_at_1px_1px,rgba(255,255,255,0.014)_1px,transparent_0)] bg-[size:30px_30px] opacity-40" />
</div>
{mobileOpen ? (
<div className="fixed inset-0 z-modal-backdrop lg:hidden">
<button
type="button"
aria-label="Close navigation"
className="absolute inset-0 bg-black/70"
onClick={closeMobileNav}
/>
<div className="relative z-modal h-full w-[min(100%,280px)] max-w-[85vw] shadow-lg pointer-events-auto">
<Sidebar onNavigate={closeMobileNav} />
</div>
</div>
) : null}
<div className="relative z-10 flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
{hidesAppHeader ? null : <Header onMenuClick={() => setMobileOpen(true)} />}
<main
className={cn(
"docdoe-workspace relative z-10 min-h-0 flex-1 overflow-x-hidden",
ownsWorkspaceChrome ? "overflow-hidden pb-0" : "overflow-y-auto pb-10",
)}
>
<Suspense fallback={<LoadingFallback />}>
{blockChildren ? (
<LoadingFallback />
) : ownsWorkspaceChrome ? (
// These pages own their full-height scroll/layout β€” no transform wrapper.
children
) : (
// Every standard app screen breathes in on navigation (keyed per route).
<div key={pathname} className="dd-route-enter">
{children}
</div>
)}
</Suspense>
</main>
</div>
</div>
);
}