- Extended Tailwind config with 3xl (1920px) and 4xl (2560px) breakpoints - Added responsive max-widths (8xl, 9xl, 10xl) for large screens - Updated Layout component with scaling max-width and padding - Made Header container responsive for large displays - Added responsive Sidebar width (64→72→80 for lg→3xl→4xl) - Implemented responsive typography in globals.css - Updated Dashboard grids to utilize more columns on large screens - Added comprehensive unit tests for responsive classes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
239 lines
8.9 KiB
TypeScript
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 xl:px-10 3xl:max-w-8xl 3xl:px-12 4xl:max-w-9xl 4xl:px-16">
|
|
<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>
|
|
);
|
|
}
|