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

100% Statements 13/13
100% Branches 15/15
100% Functions 3/3
100% Lines 13/13

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 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 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149                                  59x 59x 59x 59x 59x 59x   59x 15x     59x 5x 5x       59x   59x                                                                                                                                                                                                                                
import { useState, useRef, useCallback, type ReactNode } from "react";
import { Menu, X, Bell } from "lucide-react";
import { Link } from "@tanstack/react-router";
import { Sidebar } from "./sidebar";
import { BottomNav } from "./bottom-nav";
import { LogoIcon } from "../ui/logo-icon";
import { ReleaseBadge } from "../ui/release-badge";
import { FeedbackButton } from "../feedback/FeedbackButton";
import { ErrorBoundary } from "../ui/error-boundary";
import { useUnreadCount } from "../../hooks/queries/useNotificationQueries";
import { useScrollDirection } from "../../hooks/useScrollDirection";
import { useIsMobile } from "../../hooks/useIsMobile";
type DashboardLayoutProps = {
	children: ReactNode;
};
 
export function DashboardLayout({ children }: DashboardLayoutProps) {
	const [sidebarOpen, setSidebarOpen] = useState(false);
	const menuButtonRef = useRef<HTMLButtonElement>(null);
	const mainRef = useRef<HTMLElement>(null);
	const { data: unreadCount = 0 } = useUnreadCount();
	const scrollDirection = useScrollDirection(mainRef);
	const isMobile = useIsMobile();
 
	const openSidebar = useCallback(() => {
		setSidebarOpen(true);
	}, []);
 
	const closeSidebar = useCallback(() => {
		setSidebarOpen(false);
		menuButtonRef.current?.focus();
	}, []);
 
	// Hide header/bottom-nav on scroll down, show on scroll up
	const barsVisible = scrollDirection !== "down";
 
	return (
		<div className="flex h-screen bg-background-base">
			{/* Skip to main content link */}
			<a
				href="#main-content"
				className="sr-only focus:not-sr-only focus:absolute focus:z-50 focus:p-4 focus:bg-background-elevated focus:text-foreground-default focus:rounded-md focus:shadow-lg focus:top-4 focus:left-4"
			>
				Skip to main content
			</a>
 
			{/* Mobile sidebar overlay — only used on tablet now; mobile uses BottomNav */}
			{sidebarOpen && (
				<button
					type="button"
					aria-label="Close sidebar overlay"
					className="fixed inset-0 z-40 bg-black/50 lg:hidden cursor-default"
					onClick={closeSidebar}
				/>
			)}
 
			{/* Sidebar */}
			<Sidebar isOpen={sidebarOpen} onClose={closeSidebar} />
 
			{/* Main content */}
			<div
				className="flex flex-1 flex-col overflow-hidden"
				aria-hidden={sidebarOpen || undefined}
			>
				{/* Mobile header — hidden on sm+, scroll-to-hide on mobile */}
				<header
					className={`flex h-14 items-center gap-4 border-b border-border-default bg-background-elevated px-4 sm:hidden transition-transform duration-200 ${
						barsVisible ? "translate-y-0" : "-translate-y-full"
					}`}
				>
					{sidebarOpen ? (
						<button
							type="button"
							onClick={closeSidebar}
							aria-label="Close sidebar"
							className="p-2 -ml-2 text-foreground-muted hover:text-foreground-default hover:bg-background-muted rounded-md"
						>
							<X className="h-6 w-6" />
						</button>
					) : (
						<button
							ref={menuButtonRef}
							type="button"
							onClick={openSidebar}
							aria-label="Open sidebar"
							aria-expanded={sidebarOpen}
							className="p-2 -ml-2 text-foreground-muted hover:text-foreground-default hover:bg-background-muted rounded-md"
						>
							<Menu className="h-6 w-6" />
						</button>
					)}
					<div className="flex items-center gap-1.5">
						<LogoIcon size={32} className="text-foreground-default" />
						<span className="font-semibold text-foreground-default">
							Interioring
						</span>
						<ReleaseBadge />
					</div>
				</header>
 
				{/* Tablet header — visible on sm-lg range only, keeps hamburger */}
				<header className="hidden sm:flex lg:hidden h-14 items-center gap-4 border-b border-border-default bg-background-elevated px-6">
					<button
						ref={menuButtonRef}
						type="button"
						onClick={openSidebar}
						aria-label="Open sidebar"
						aria-expanded={sidebarOpen}
						className="p-2 -ml-2 text-foreground-muted hover:text-foreground-default hover:bg-background-muted rounded-md"
					>
						<Menu className="h-6 w-6" />
					</button>
					<div className="flex items-center gap-1.5">
						<LogoIcon size={32} className="text-foreground-default" />
						<span className="font-semibold text-foreground-default">
							Interioring
						</span>
						<ReleaseBadge />
					</div>
					<div className="flex-1" />
					<Link to="/notifications" className="relative p-2 text-foreground-muted hover:text-foreground-default">
						<Bell className="h-5 w-5" />
						{unreadCount > 0 && (
							<span className="absolute -top-0.5 -right-0.5 bg-error text-white text-[9px] font-bold px-1 py-0.5 rounded-full min-w-[16px] text-center leading-tight">
								{unreadCount > 99 ? "99+" : unreadCount}
							</span>
						)}
					</Link>
				</header>
 
				{/* Page content — add bottom padding on mobile for BottomNav */}
				<main id="main-content" ref={mainRef} className="flex-1 overflow-auto">
					<div className="p-4 sm:p-6 lg:p-8 pb-20 sm:pb-6 lg:pb-8">
						<ErrorBoundary>
							{children}
						</ErrorBoundary>
					</div>
				</main>
 
				{/* Feedback button — desktop only */}
				<FeedbackButton />
			</div>
 
			{/* Bottom navigation — mobile only */}
			{isMobile && <BottomNav visible={barsVisible} />}
		</div>
	);
}