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
157 lines
6 KiB
TypeScript
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>
|
|
)
|
|
}
|