Download src/components/layout/AppLayout.tsx from asnannp/DocDoeAI: direct link, hf CLI and curl.
- Browser
- Download file 5.35 kB
-
https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/layout/AppLayout.tsx
- Command line
-
hf download hf://spaces/asnannp/DocDoeAI/src/components/layout/AppLayout.tsx
-
curl -L -o AppLayout.tsx https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/layout/AppLayout.tsx
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> | |
| ); | |
| } | |