morethanadiagnosis-hub/web/components/layouts/DashboardLayout.tsx
Claude 9232ebe294
feat(web): complete Phase 1 - foundation components, layouts, and hooks
Implemented complete design system and foundational infrastructure:

**Design System Components:**
- Button (all variants: primary, secondary, ghost, danger)
- Input & Textarea (with validation and error states)
- Card (elevated, outlined, flat variants)
- Modal/Dialog (with focus trap and accessibility)
- Avatar (with fallback initials)
- Badge (all color variants)
- Form helpers (FormField, Checkbox, Select)
- Link component with Next.js integration
- Navigation (Header, Footer with responsive design)

**Layouts:**
- MainLayout (with Header/Footer for public pages)
- AuthLayout (minimal layout for auth flows)
- DashboardLayout (with sidebar navigation)

**Hooks & Utilities:**
- useAuth() - authentication state management
- useApi() - API calls with loading/error states
- useLocalStorage() - persistent state management
- apiClient - Axios instance with token refresh
- authStore - Zustand store for auth state

**Configuration:**
- Tailwind config with design tokens
- Dark mode support via CSS variables
- Global styles with accessibility focus
- WCAG 2.2 AA+ compliant focus indicators

All components follow accessibility best practices with proper ARIA labels,
keyboard navigation, and screen reader support.

Job ID: MTAD-IMPL-2025-11-18-CL
2025-11-18 01:02:05 +00:00

157 lines
6 KiB
TypeScript

'use client'
import React, { useState } from 'react'
import { Header } from '../common/Header'
import { useAuth } from '@/lib/hooks/useAuth'
import { useRouter, usePathname } from 'next/navigation'
import { Link } from '../common/Link'
export interface DashboardLayoutProps {
children: React.ReactNode
}
export const DashboardLayout = ({ children }: DashboardLayoutProps) => {
const { user, isAuthenticated, logout } = useAuth()
const router = useRouter()
const pathname = usePathname()
const [sidebarOpen, setSidebarOpen] = useState(false)
// Redirect to login if not authenticated
React.useEffect(() => {
if (!isAuthenticated) {
router.push('/auth/login')
}
}, [isAuthenticated, router])
const handleLogin = () => {
router.push('/auth/login')
}
const sidebarItems = [
{ label: 'Overview', href: '/dashboard', icon: '📊' },
{ label: 'Profile', href: '/dashboard/profile', icon: '👤' },
{ label: 'Preferences', href: '/dashboard/preferences', icon: '⚙️' },
{ label: 'Security', href: '/dashboard/security', icon: '🔒' },
{ label: 'Privacy', href: '/dashboard/privacy', icon: '🛡️' },
]
if (!isAuthenticated) {
return null // or a loading spinner
}
return (
<div className="min-h-screen flex flex-col bg-gray-50 dark:bg-gray-900">
<Header
isAuthenticated={isAuthenticated}
userDisplayName={user?.display_name || user?.email}
onLogin={handleLogin}
onLogout={logout}
/>
<div className="flex-1 flex">
{/* Sidebar for desktop */}
<aside className="hidden md:flex md:flex-shrink-0">
<div className="w-64 flex flex-col border-r border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
<nav className="flex-1 px-4 py-6 space-y-1" aria-label="Dashboard navigation">
{sidebarItems.map((item) => {
const isActive = pathname === item.href
return (
<Link
key={item.href}
href={item.href}
variant="neutral"
className={`
flex items-center px-4 py-2 text-sm font-medium rounded-md no-underline
${
isActive
? 'bg-primary-100 text-primary-900 dark:bg-primary-900 dark:text-primary-100'
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700'
}
`}
>
<span className="mr-3 text-xl" aria-hidden="true">
{item.icon}
</span>
{item.label}
</Link>
)
})}
</nav>
</div>
</aside>
{/* Mobile sidebar */}
{sidebarOpen && (
<div className="fixed inset-0 z-40 md:hidden">
<div
className="fixed inset-0 bg-gray-600 bg-opacity-75"
onClick={() => setSidebarOpen(false)}
/>
<div className="fixed inset-y-0 left-0 flex flex-col w-64 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between h-16 px-4 border-b border-gray-200 dark:border-gray-700">
<span className="text-lg font-semibold text-gray-900 dark:text-white">
Dashboard
</span>
<button
onClick={() => setSidebarOpen(false)}
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
aria-label="Close sidebar"
>
<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>
</button>
</div>
<nav className="flex-1 px-4 py-6 space-y-1">
{sidebarItems.map((item) => {
const isActive = pathname === item.href
return (
<Link
key={item.href}
href={item.href}
variant="neutral"
className={`
flex items-center px-4 py-2 text-sm font-medium rounded-md no-underline
${
isActive
? 'bg-primary-100 text-primary-900 dark:bg-primary-900 dark:text-primary-100'
: 'text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700'
}
`}
onClick={() => setSidebarOpen(false)}
>
<span className="mr-3 text-xl" aria-hidden="true">
{item.icon}
</span>
{item.label}
</Link>
)
})}
</nav>
</div>
</div>
)}
{/* Main content */}
<main className="flex-1">
{/* Mobile sidebar toggle */}
<div className="md:hidden px-4 py-4 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800">
<button
onClick={() => setSidebarOpen(true)}
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
aria-label="Open sidebar"
>
<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 className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{children}
</div>
</main>
</div>
</div>
)
}