Sergei 5b04765b0d Add responsive design support for 768px to 4K screens
- 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>
2026-02-01 11:34:33 -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 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>
);
}