DocDoeAI / src /components /clean /layout /CleanAppLayout.tsx
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
65d68f5
Raw History Blame Contribute Delete
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>
);
}