Download src/components/clean/layout/CleanAppLayout.tsx from asnannp/DocDoeAI: direct link, hf CLI and curl.
- Browser
- Download file 4.14 kB
-
https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/clean/layout/CleanAppLayout.tsx
- Command line
-
hf download hf://spaces/asnannp/DocDoeAI/src/components/clean/layout/CleanAppLayout.tsx
-
curl -L -o CleanAppLayout.tsx https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/clean/layout/CleanAppLayout.tsx
4.14 kB
| "use client"; | |
| 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 { CleanSidebar } from "./CleanSidebar"; | |
| import { CleanHeader } from "./CleanHeader"; | |
| const isLiveFrontendMode = getPublicDemoMode() === "live"; | |
| // ============================================================================= | |
| // CleanAppLayout β full-bleed light workspace. One top-left menu opens the nav | |
| // drawer at every breakpoint (no permanent rail, no bottom tab bar). | |
| // ============================================================================= | |
| type CleanAppLayoutProps = { | |
| children: React.ReactNode; | |
| }; | |
| function LoadingFallback() { | |
| return ( | |
| <div className="flex flex-1 items-center justify-center bg-[#F7F6F2]"> | |
| <div className="flex items-center gap-3"> | |
| <span className="size-5 animate-spin rounded-full border-2 border-[#E5E7EB] border-t-[#111]" /> | |
| <span className="text-[14px] text-[#9CA3AF]">Preparing your study spaceβ¦</span> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| export function CleanAppLayout({ children }: CleanAppLayoutProps) { | |
| const router = useRouter(); | |
| const pathname = usePathname(); | |
| const { isLoading: isAuthLoading } = useAuth(); | |
| const [navOpen, setNavOpen] = useState(false); | |
| const isLocalTuitionBetaRoute = | |
| pathname === "/tuition" || pathname?.startsWith("/tuition/"); | |
| // ββ Auth guard ββ | |
| const unauthenticated = | |
| !isLocalTuitionBetaRoute && | |
| isLiveFrontendMode && | |
| !hasLiveSession() && | |
| !isDemoPreviewSession(); | |
| useEffect(() => { | |
| if (!isLocalTuitionBetaRoute && !isAuthLoading && unauthenticated) { | |
| const next = encodeURIComponent(pathname ?? "/home"); | |
| router.replace(`/login?next=${next}`); | |
| } | |
| }, [isAuthLoading, isLocalTuitionBetaRoute, unauthenticated, pathname, router]); | |
| const blockChildren = | |
| !isLocalTuitionBetaRoute && (isAuthLoading || unauthenticated); | |
| // Close the drawer on route change. | |
| useEffect(() => { | |
| const t = window.setTimeout(() => setNavOpen(false), 0); | |
| return () => window.clearTimeout(t); | |
| }, [pathname]); | |
| // Escape closes; lock body scroll while the drawer is open. | |
| useEffect(() => { | |
| if (!navOpen) return; | |
| function onKeyDown(e: KeyboardEvent) { | |
| if (e.key === "Escape") setNavOpen(false); | |
| } | |
| window.addEventListener("keydown", onKeyDown); | |
| const prevOverflow = document.body.style.overflow; | |
| document.body.style.overflow = "hidden"; | |
| return () => { | |
| window.removeEventListener("keydown", onKeyDown); | |
| document.body.style.overflow = prevOverflow; | |
| }; | |
| }, [navOpen]); | |
| const closeNav = () => setNavOpen(false); | |
| return ( | |
| <div className="relative flex h-screen min-h-screen flex-col overflow-hidden bg-[#F7F6F2] text-[#171717]"> | |
| <CleanHeader onMenuClick={() => setNavOpen(true)} /> | |
| {/* ββ Nav drawer (all breakpoints) ββ */} | |
| {navOpen && ( | |
| <div className="fixed inset-0 z-50"> | |
| <button | |
| type="button" | |
| aria-label="Close navigation" | |
| className="absolute inset-0 bg-black/40 backdrop-blur-sm animate-in fade-in duration-200" | |
| onClick={closeNav} | |
| /> | |
| <div className="relative z-10 h-full w-[min(100%,300px)] max-w-[86vw] shadow-[8px_0_40px_rgba(0,0,0,0.25)] animate-in slide-in-from-left-full duration-300 ease-out"> | |
| <CleanSidebar onNavigate={closeNav} onClose={closeNav} /> | |
| </div> | |
| </div> | |
| )} | |
| {/* ββ Main content ββ */} | |
| <main className="relative min-h-0 flex-1 overflow-x-hidden overflow-y-auto bg-[#F7F6F2]"> | |
| <Suspense fallback={<LoadingFallback />}> | |
| {blockChildren ? ( | |
| <LoadingFallback /> | |
| ) : ( | |
| <div key={pathname} className="animate-in fade-in duration-200"> | |
| {children} | |
| </div> | |
| )} | |
| </Suspense> | |
| </main> | |
| </div> | |
| ); | |
| } | |