Files
maternal-app/maternal-web/components/common/PhotoUpload.tsx
Andrei 2110359307
Some checks failed
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled
feat: Add comprehensive accessibility improvements and medical tracking
- **EULA Persistence Fix**: Fixed EULA dialog showing on every login
  - Added eulaAcceptedAt/eulaVersion to AuthResponse interface
  - Updated login/register/getUserById endpoints to return EULA fields
  - Changed EULACheck to use refreshUser() instead of window.reload()

- **Touch Target Accessibility**: All interactive elements now meet 48x48px minimum
  - Fixed 14 undersized IconButtons across 5 files
  - Changed size="small" to size="medium" with minWidth/minHeight constraints
  - Updated children page, AI chat, analytics cards, legal viewer

- **Alt Text for Images**: Complete image accessibility for screen readers
  - Added photoAlt field to children table (Migration V009)
  - PhotoUpload component now includes alt text input field
  - All Avatar components have meaningful alt text
  - Default alt text: "Photo of {childName}", "{userName}'s profile photo"

- **Medical Tracking Consolidation**: Unified medical page with tabs
  - Medicine page now has 3 tabs: Medication, Temperature, Doctor Visit
  - Backward compatibility for legacy 'medicine' activity type
  - Created dedicated /track/growth page for physical measurements

- **Track Page Updates**:
  - Simplified to 6 options: Feeding, Sleep, Diaper, Medical, Activity, Growth
  - Fixed grid layout to 3 cards per row with minWidth: 200px
  - Updated terminology from "Medicine" to "Medical"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-04 13:15:23 +00:00

192 lines
4.9 KiB
TypeScript

'use client';
import { useState, useRef } from 'react';
import {
Box,
Avatar,
IconButton,
TextField,
Typography,
Paper,
Alert,
CircularProgress,
} from '@mui/material';
import { PhotoCamera, Person } from '@mui/icons-material';
interface PhotoUploadProps {
value: string;
onChange: (url: string) => void;
label: string;
disabled?: boolean;
size?: number;
altText?: string;
onAltTextChange?: (altText: string) => void;
showAltText?: boolean;
}
export function PhotoUpload({
value,
onChange,
label,
disabled = false,
size = 100,
altText = '',
onAltTextChange,
showAltText = true,
}: PhotoUploadProps) {
const [imageError, setImageError] = useState(false);
const [uploadError, setUploadError] = useState<string>('');
const [uploading, setUploading] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const handleImageError = () => {
setImageError(true);
};
const handleImageLoad = () => {
setImageError(false);
};
const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
// Validate file type
if (!file.type.startsWith('image/')) {
setUploadError('Please select an image file');
return;
}
// Validate file size (max 5MB for base64)
const maxSize = 5 * 1024 * 1024; // 5MB
if (file.size > maxSize) {
setUploadError('Image size must be less than 5MB');
return;
}
try {
setUploading(true);
setUploadError('');
// Convert to base64 (simple, works everywhere)
const reader = new FileReader();
reader.onload = (e) => {
const base64String = e.target?.result as string;
onChange(base64String);
setUploading(false);
setImageError(false);
};
reader.onerror = () => {
setUploadError('Failed to read image file');
setUploading(false);
};
reader.readAsDataURL(file);
} catch (error: any) {
console.error('Photo processing failed:', error);
setUploadError('Failed to process photo');
setUploading(false);
} finally {
// Reset input
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}
};
const handleCameraClick = () => {
fileInputRef.current?.click();
};
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, alignItems: 'center' }}>
<Paper
elevation={0}
sx={{
p: 2,
border: 1,
borderColor: 'divider',
borderRadius: 2,
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 2
}}
>
<Typography variant="body2" color="text.secondary">
{label}
</Typography>
{uploadError && (
<Alert severity="error" onClose={() => setUploadError('')} sx={{ width: '100%' }}>
{uploadError}
</Alert>
)}
<Box sx={{ position: 'relative' }}>
<Avatar
src={!imageError && value ? value : undefined}
alt={altText || 'Profile photo'}
sx={{
width: size,
height: size,
bgcolor: 'primary.light',
fontSize: size / 3,
}}
onError={handleImageError}
onLoad={handleImageLoad}
>
{!value || imageError ? <Person sx={{ fontSize: size / 2 }} /> : null}
</Avatar>
<input
ref={fileInputRef}
type="file"
accept="image/*"
style={{ display: 'none' }}
onChange={handleFileSelect}
disabled={disabled || uploading}
/>
<IconButton
sx={{
position: 'absolute',
bottom: -4,
right: -4,
bgcolor: 'background.paper',
border: 2,
borderColor: 'divider',
'&:hover': {
bgcolor: 'action.hover',
},
}}
size="small"
disabled={disabled || uploading}
onClick={handleCameraClick}
>
{uploading ? (
<CircularProgress size={20} />
) : (
<PhotoCamera fontSize="small" />
)}
</IconButton>
</Box>
{/* Alt text input for accessibility */}
{showAltText && onAltTextChange && (
<TextField
fullWidth
label="Photo description (for accessibility)"
placeholder="e.g., Smiling baby with blue eyes"
value={altText}
onChange={(e) => onAltTextChange(e.target.value)}
disabled={disabled}
helperText="Describe the photo for screen reader users"
size="small"
/>
)}
</Paper>
</Box>
);
}