All files / src/components/layout admin-layout.tsx

100% Statements 12/12
83.33% Branches 5/6
100% Functions 4/4
100% Lines 11/11

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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                          19x 19x 19x 19x   19x 19x 3x 3x     19x   12x       19x                                                                                                                
import { useRouterState } from "@tanstack/react-router";
import { type ReactNode, useCallback, useMemo, useRef, useState } from "react";
import { useAuth } from "../../lib/auth-context";
import { ThemeSwitcher } from "../ui/theme-switcher";
import { ADMIN_NAV, NAV_STORAGE_KEYS, resolveNav } from "./nav-config";
import { NavRail } from "./nav-rail";
import { TopBar } from "./top-bar";
 
type AdminLayoutProps = {
	children: ReactNode;
};
 
export function AdminLayout({ children }: AdminLayoutProps) {
	const [navOpen, setNavOpen] = useState(false);
	const menuButtonRef = useRef<HTMLButtonElement>(null);
	const { signOut, user, hasProAccess } = useAuth();
	const currentPath = useRouterState().location.pathname;
 
	const openNav = useCallback(() => setNavOpen(true), []);
	const closeNav = useCallback(() => {
		setNavOpen(false);
		menuButtonRef.current?.focus();
	}, []);
 
	const entries = useMemo(
		// isAdmin is implied: this layout only mounts behind AdminGuard.
		() => resolveNav(ADMIN_NAV, { isAdmin: true, hasProAccess }),
		[hasProAccess],
	);
 
	return (
		<div className="flex h-screen flex-col overflow-hidden bg-background-base">
			<a
				href="#main-content"
				className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:rounded-md focus:bg-background-elevated focus:p-4 focus:text-foreground-default focus:shadow-lg"
			>
				Skip to main content
			</a>
 
			<header className="z-30 flex-none">
				<TopBar
					onOpenNav={openNav}
					navOpen={navOpen}
					menuButtonRef={menuButtonRef}
					label="Admin"
				>
					<ThemeSwitcher variant="icon" size="sm" />
				</TopBar>
			</header>
 
			<div className="flex min-h-0 flex-1">
				{navOpen && (
					<button
						type="button"
						aria-label="Close sidebar overlay"
						className="fixed inset-0 z-40 cursor-default bg-overlay-light lg:hidden"
						onClick={closeNav}
					/>
				)}
 
				<NavRail
					entries={entries}
					currentPath={currentPath}
					storageKey={NAV_STORAGE_KEYS.admin}
					ariaLabel="Admin navigation"
					identity={{
						name: user?.name ?? "",
						meta: "Platform admin",
					}}
					onSignOut={signOut}
					isOpen={navOpen}
					onClose={closeNav}
				/>
 
				<div
					className="flex min-w-0 flex-1 flex-col overflow-hidden"
					aria-hidden={navOpen || undefined}
				>
					<main id="main-content" className="flex-1 overflow-auto">
						<div className="p-4 sm:p-6 lg:p-8">{children}</div>
					</main>
				</div>
			</div>
		</div>
	);
}