diff --git a/src/app/(dashboard)/dashboard-shell.tsx b/src/app/(dashboard)/dashboard-shell.tsx index 68c4d51..24a4afb 100644 --- a/src/app/(dashboard)/dashboard-shell.tsx +++ b/src/app/(dashboard)/dashboard-shell.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { AuthProvider, useAuth } from "@/hooks/use-auth"; import { Sidebar } from "@/components/layout/sidebar"; @@ -14,6 +14,11 @@ function DashboardShellInner({ children }: { children: React.ReactNode }) { const { user, loading } = useAuth(); const router = useRouter(); + // Sidebar drawer state — only used on mobile. On lg+ the sidebar is + // always visible and this stays at `false` (ignored by the component). + const [sidebarOpen, setSidebarOpen] = useState(false); + const closeSidebar = useCallback(() => setSidebarOpen(false), []); + useEffect(() => { if (!loading && !user) { router.push("/login"); @@ -35,10 +40,11 @@ function DashboardShellInner({ children }: { children: React.ReactNode }) { return (
- +
-
-
{children}
+
setSidebarOpen(true)} /> + {/* Thinner horizontal padding on mobile so cards have room to breathe. */} +
{children}
); diff --git a/src/components/layout/header.tsx b/src/components/layout/header.tsx index ab2038e..3030b86 100644 --- a/src/components/layout/header.tsx +++ b/src/components/layout/header.tsx @@ -2,7 +2,7 @@ import { usePathname } from "next/navigation"; import { useAuth } from "@/hooks/use-auth"; -import { LogOut } from "lucide-react"; +import { LogOut, Menu } from "lucide-react"; const pageTitles: Record = { "/dashboard": "Dashboard", @@ -16,24 +16,42 @@ const pageTitles: Record = { function getPageTitle(pathname: string): string { if (pageTitles[pathname]) return pageTitles[pathname]; - const match = Object.entries(pageTitles).find(([path]) => - pathname.startsWith(path) + pathname.startsWith(path), ); return match ? match[1] : "Dashboard"; } -export function Header() { +interface HeaderProps { + /** Wired to the shell's drawer state. Used only on mobile — the + * hamburger button is hidden on lg+. */ + onOpenSidebar?: () => void; +} + +export function Header({ onOpenSidebar }: HeaderProps) { const pathname = usePathname(); const { profile, signOut } = useAuth(); const title = getPageTitle(pathname); return ( -
-

{title}

+
+
+ {/* Hamburger — mobile only. 44×44 hit target per Apple HIG. */} + +

+ {title} +

+
-
-
+
+
{profile?.full_name?.charAt(0)?.toUpperCase() ?? "U"}
@@ -45,8 +63,9 @@ export function Header() {
diff --git a/src/components/layout/sidebar.tsx b/src/components/layout/sidebar.tsx index bf058ff..f01d2cb 100644 --- a/src/components/layout/sidebar.tsx +++ b/src/components/layout/sidebar.tsx @@ -2,6 +2,7 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; +import { useEffect } from "react"; import { cn } from "@/lib/utils"; import { useAuth } from "@/hooks/use-auth"; import { useTotalUnread } from "@/hooks/use-total-unread"; @@ -14,6 +15,7 @@ import { Zap, Settings, LogOut, + X, } from "lucide-react"; const navItems = [ @@ -29,115 +31,177 @@ const bottomNavItems = [ { href: "/settings", label: "Settings", icon: Settings }, ]; -export function Sidebar() { +interface SidebarProps { + /** Controlled on mobile by the Header's hamburger button. Ignored on lg+. */ + open?: boolean; + onClose?: () => void; +} + +export function Sidebar({ open = false, onClose }: SidebarProps) { const pathname = usePathname(); const { profile, signOut } = useAuth(); const totalUnread = useTotalUnread(); + // Close the drawer when route changes — users opened it to navigate, + // so once they pick a destination the drawer should get out of the way. + useEffect(() => { + onClose?.(); + // Only pathname drives this — onClose identity doesn't need to re-run it. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pathname]); + + // Lock body scroll and allow Escape to close while the drawer is open on + // mobile. No-ops on desktop because the sidebar isn't positioned there. + useEffect(() => { + if (!open) return; + const prev = document.body.style.overflow; + document.body.style.overflow = "hidden"; + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose?.(); + }; + window.addEventListener("keydown", onKey); + return () => { + document.body.style.overflow = prev; + window.removeEventListener("keydown", onKey); + }; + }, [open, onClose]); + return ( -
-
- + + {/* Main navigation */} + + + {/* User section */} +
+
+
+ {profile?.full_name?.charAt(0)?.toUpperCase() ?? "U"} +
+
+

+ {profile?.full_name ?? "User"} +

+

+ {profile?.email ?? ""} +

+
+ +
+
+ + ); }