Complete Phase 1 accessibility implementation with comprehensive WCAG 2.1 Level AA compliance foundation. **Accessibility Tools Setup:** - ESLint jsx-a11y plugin with 18 accessibility rules - Axe-core for runtime accessibility testing in dev mode - jest-axe for automated testing - Accessibility utility functions (9 functions) **Core Features:** - Skip navigation link (WCAG 2.4.1 Bypass Blocks) - 45+ ARIA attributes across 15 components - Keyboard navigation fixes (Quick Actions now keyboard accessible) - Focus management on route changes with screen reader announcements - Color contrast WCAG AA compliance (4.5:1+ ratio, tested with Axe) - Proper heading hierarchy (h1→h2) across all pages - Semantic landmarks (header, nav, main) **Components Enhanced:** - 6 dialogs with proper ARIA labels (Child, InviteMember, DeleteConfirm, RemoveMember, JoinFamily, MFAVerification) - Voice input with aria-live regions - Navigation components with semantic landmarks - Quick Action cards with keyboard support **WCAG Success Criteria Met (8):** - 1.3.1 Info and Relationships (Level A) - 2.1.1 Keyboard (Level A) - 2.4.1 Bypass Blocks (Level A) - 4.1.2 Name, Role, Value (Level A) - 1.4.3 Contrast Minimum (Level AA) - 2.4.3 Focus Order (Level AA) - 2.4.6 Headings and Labels (Level AA) - 2.4.7 Focus Visible (Level AA) **Files Created (7):** - .eslintrc.json - ESLint accessibility config - components/providers/AxeProvider.tsx - Dev-time testing - components/common/SkipNavigation.tsx - Skip link - lib/accessibility.ts - Utility functions - hooks/useFocusManagement.ts - Focus management hooks - components/providers/FocusManagementProvider.tsx - Provider - docs/ACCESSIBILITY_PROGRESS.md - Progress tracking **Files Modified (17):** - Frontend: 20 components/pages with accessibility improvements - Backend: ai-rate-limit.service.ts (del → delete method) - Docs: implementation-gaps.md updated 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
118 lines
3.3 KiB
TypeScript
118 lines
3.3 KiB
TypeScript
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { Alert, Button, Snackbar, Box } from '@mui/material';
|
|
import { Email } from '@mui/icons-material';
|
|
import { useAuth } from '@/lib/auth/AuthContext';
|
|
import apiClient from '@/lib/api/client';
|
|
|
|
export const EmailVerificationBanner: React.FC = () => {
|
|
const { user } = useAuth();
|
|
const [dismissed, setDismissed] = useState(false);
|
|
const [loading, setLoading] = useState(false);
|
|
const [snackbar, setSnackbar] = useState<{ open: boolean; message: string; severity: 'success' | 'error' }>({
|
|
open: false,
|
|
message: '',
|
|
severity: 'success',
|
|
});
|
|
|
|
// Don't show if user is not logged in, email is verified, or banner was dismissed
|
|
if (!user || user.emailVerified || dismissed) {
|
|
return null;
|
|
}
|
|
|
|
const handleResendVerification = async () => {
|
|
setLoading(true);
|
|
try {
|
|
await apiClient.post('/api/v1/auth/email/send-verification');
|
|
setSnackbar({
|
|
open: true,
|
|
message: 'Verification email sent! Please check your inbox.',
|
|
severity: 'success',
|
|
});
|
|
} catch (error: any) {
|
|
console.error('Failed to resend verification email:', error);
|
|
setSnackbar({
|
|
open: true,
|
|
message: error.response?.data?.message || 'Failed to send verification email. Please try again.',
|
|
severity: 'error',
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleDismiss = () => {
|
|
setDismissed(true);
|
|
// Store dismissal in localStorage to persist across sessions
|
|
localStorage.setItem('emailVerificationBannerDismissed', 'true');
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Alert
|
|
severity="warning"
|
|
icon={<Email />}
|
|
onClose={handleDismiss}
|
|
sx={{
|
|
borderRadius: 2,
|
|
mb: 2,
|
|
'& .MuiAlert-message': {
|
|
width: '100%',
|
|
},
|
|
}}
|
|
>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
gap: 2,
|
|
flexWrap: 'wrap',
|
|
}}
|
|
>
|
|
<Box>
|
|
<strong>Verify your email address</strong>
|
|
<br />
|
|
Please check your inbox and click the verification link to access all features.
|
|
</Box>
|
|
<Button
|
|
variant="outlined"
|
|
size="small"
|
|
onClick={handleResendVerification}
|
|
disabled={loading}
|
|
sx={{
|
|
borderRadius: 2,
|
|
textTransform: 'none',
|
|
fontWeight: 600,
|
|
borderColor: '#D97706',
|
|
color: '#92400E',
|
|
'&:hover': {
|
|
borderColor: '#92400E',
|
|
bgcolor: '#FEF3C7',
|
|
},
|
|
}}
|
|
>
|
|
{loading ? 'Sending...' : 'Resend Email'}
|
|
</Button>
|
|
</Box>
|
|
</Alert>
|
|
|
|
<Snackbar
|
|
open={snackbar.open}
|
|
autoHideDuration={6000}
|
|
onClose={() => setSnackbar({ ...snackbar, open: false })}
|
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
|
>
|
|
<Alert
|
|
severity={snackbar.severity}
|
|
onClose={() => setSnackbar({ ...snackbar, open: false })}
|
|
sx={{ borderRadius: 2 }}
|
|
>
|
|
{snackbar.message}
|
|
</Alert>
|
|
</Snackbar>
|
|
</>
|
|
);
|
|
};
|