Sergei 1628501e75 Add Layout components for web application
Implemented responsive layout system with:
- Header: Top navigation with profile menu and mobile hamburger
- Sidebar: Desktop-only navigation sidebar (lg and above)
- Breadcrumbs: Auto-generated navigation breadcrumbs
- Layout: Main wrapper component with configurable options

Features:
- Responsive design (mobile, tablet, desktop)
- Active route highlighting
- User profile integration via auth store
- Click-outside dropdown closing
- Comprehensive test coverage (49 tests passing)

Updated (main) layout to use new Layout component system.
Updated dashboard page to work with new layout structure.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-31 18:20:13 -08:00

239 lines
8.9 KiB
TypeScript

'use client';
import { useState, useRef, useEffect } from 'react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useAuthStore } from '@/stores/authStore';
/**
* Navigation items for the header
*/
const navItems = [
{ href: '/dashboard', label: 'Dashboard' },
{ href: '/beneficiaries', label: 'Loved Ones' },
];
/**
* Header Component
*
* Responsive header with navigation and profile menu.
* - Desktop: Full navigation with profile dropdown
* - Mobile: Hamburger menu with slide-in navigation
*
* @example
* ```tsx
* <Header />
* ```
*/
export function Header() {
const pathname = usePathname();
const { user, logout } = useAuthStore();
const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const profileMenuRef = useRef<HTMLDivElement>(null);
// Close profile menu when clicking outside
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (profileMenuRef.current && !profileMenuRef.current.contains(event.target as Node)) {
setIsProfileMenuOpen(false);
}
}
if (isProfileMenuOpen) {
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}
}, [isProfileMenuOpen]);
// Close mobile menu when route changes
useEffect(() => {
setIsMobileMenuOpen(false);
}, [pathname]);
const handleLogout = async () => {
setIsProfileMenuOpen(false);
await logout();
};
const userInitials = user?.firstName && user?.lastName
? `${user.firstName[0]}${user.lastName[0]}`.toUpperCase()
: user?.email?.[0]?.toUpperCase() || '?';
const userName = user?.firstName && user?.lastName
? `${user.firstName} ${user.lastName}`
: user?.email || 'User';
return (
<header className="sticky top-0 z-50 w-full border-b border-slate-200 bg-white/80 backdrop-blur-md">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="flex h-16 items-center justify-between">
{/* Logo and brand */}
<div className="flex items-center gap-8">
<Link href="/dashboard" className="flex items-center gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-blue-600 to-blue-500">
<span className="text-lg font-bold text-white">W</span>
</div>
<span className="hidden text-xl font-semibold text-slate-900 sm:block">
WellNuo
</span>
</Link>
{/* Desktop navigation */}
<nav className="hidden md:flex md:gap-6">
{navItems.map((item) => {
const isActive = pathname === item.href || pathname.startsWith(`${item.href}/`);
return (
<Link
key={item.href}
href={item.href}
className={`relative px-3 py-2 text-sm font-medium transition-colors ${
isActive
? 'text-blue-600'
: 'text-slate-600 hover:text-slate-900'
}`}
>
{item.label}
{isActive && (
<span className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-600" />
)}
</Link>
);
})}
</nav>
</div>
{/* Right side: Profile menu */}
<div className="flex items-center gap-4">
{/* Desktop profile dropdown */}
<div className="relative hidden md:block" ref={profileMenuRef}>
<button
onClick={() => setIsProfileMenuOpen(!isProfileMenuOpen)}
className="flex items-center gap-3 rounded-lg px-3 py-2 transition-colors hover:bg-slate-50"
aria-expanded={isProfileMenuOpen}
aria-haspopup="true"
>
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-blue-100 text-sm font-semibold text-blue-700">
{userInitials}
</div>
<span className="hidden text-sm font-medium text-slate-700 lg:block">
{userName}
</span>
<svg
className={`h-4 w-4 text-slate-400 transition-transform ${
isProfileMenuOpen ? 'rotate-180' : ''
}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{/* Profile dropdown menu */}
{isProfileMenuOpen && (
<div className="absolute right-0 mt-2 w-56 origin-top-right rounded-lg border border-slate-200 bg-white shadow-lg ring-1 ring-black ring-opacity-5">
<div className="p-3">
<div className="mb-1 text-sm font-medium text-slate-900">{userName}</div>
{user?.email && (
<div className="text-xs text-slate-500">{user.email}</div>
)}
</div>
<div className="border-t border-slate-100">
<Link
href="/profile"
className="block px-4 py-2 text-sm text-slate-700 hover:bg-slate-50"
onClick={() => setIsProfileMenuOpen(false)}
>
Profile Settings
</Link>
<button
onClick={handleLogout}
className="block w-full px-4 py-2 text-left text-sm text-red-600 hover:bg-red-50"
>
Sign Out
</button>
</div>
</div>
)}
</div>
{/* Mobile menu button */}
<button
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
className="md:hidden rounded-lg p-2 text-slate-600 hover:bg-slate-50"
aria-label="Toggle menu"
aria-expanded={isMobileMenuOpen}
>
{isMobileMenuOpen ? (
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
) : (
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
)}
</button>
</div>
</div>
</div>
{/* Mobile menu */}
{isMobileMenuOpen && (
<div className="border-t border-slate-200 bg-white md:hidden">
<div className="space-y-1 px-4 pb-3 pt-2">
{/* User info */}
<div className="mb-4 flex items-center gap-3 rounded-lg bg-slate-50 p-3">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-blue-100 text-sm font-semibold text-blue-700">
{userInitials}
</div>
<div className="flex-1 min-w-0">
<div className="truncate text-sm font-medium text-slate-900">{userName}</div>
{user?.email && (
<div className="truncate text-xs text-slate-500">{user.email}</div>
)}
</div>
</div>
{/* Navigation links */}
{navItems.map((item) => {
const isActive = pathname === item.href || pathname.startsWith(`${item.href}/`);
return (
<Link
key={item.href}
href={item.href}
className={`block rounded-lg px-3 py-2 text-base font-medium ${
isActive
? 'bg-blue-50 text-blue-700'
: 'text-slate-700 hover:bg-slate-50'
}`}
>
{item.label}
</Link>
);
})}
{/* Profile link */}
<Link
href="/profile"
className="block rounded-lg px-3 py-2 text-base font-medium text-slate-700 hover:bg-slate-50"
>
Profile Settings
</Link>
{/* Sign out */}
<button
onClick={handleLogout}
className="block w-full rounded-lg px-3 py-2 text-left text-base font-medium text-red-600 hover:bg-red-50"
>
Sign Out
</button>
</div>
</div>
)}
</header>
);
}