morethanadiagnosis-hub/web/components/common/Avatar.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

72 lines
1.6 KiB
TypeScript

'use client'
import React from 'react'
export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
export interface AvatarProps {
src?: string
alt: string
size?: AvatarSize
fallbackText?: string
className?: string
}
const sizeStyles: Record<AvatarSize, string> = {
xs: 'w-6 h-6 text-xs',
sm: 'w-8 h-8 text-sm',
md: 'w-10 h-10 text-base',
lg: 'w-12 h-12 text-lg',
xl: 'w-16 h-16 text-xl',
}
export const Avatar = ({
src,
alt,
size = 'md',
fallbackText,
className = '',
}: AvatarProps) => {
const [imageError, setImageError] = React.useState(false)
const initials = React.useMemo(() => {
if (fallbackText) {
return fallbackText
.split(' ')
.map((word) => word[0])
.join('')
.toUpperCase()
.slice(0, 2)
}
return alt
.split(' ')
.map((word) => word[0])
.join('')
.toUpperCase()
.slice(0, 2)
}, [fallbackText, alt])
const showFallback = !src || imageError
const baseStyles = 'inline-flex items-center justify-center rounded-full overflow-hidden'
const fallbackStyles = 'bg-gradient-to-br from-primary-400 to-secondary-500 text-white font-semibold'
const combinedClassName = `${baseStyles} ${sizeStyles[size]} ${showFallback ? fallbackStyles : ''} ${className}`.trim()
if (showFallback) {
return (
<div className={combinedClassName} role="img" aria-label={alt}>
{initials}
</div>
)
}
return (
<img
src={src}
alt={alt}
className={combinedClassName}
onError={() => setImageError(true)}
/>
)
}