File size: 4,143 Bytes
65d68f5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 | "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>
);
}
|