feat: Add photo upload component for user and child profiles
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

- Created reusable PhotoUpload component with avatar preview
- Added photo upload to child create/edit dialog
- Added profile photo upload to settings page
- Show photo preview with fallback icon
- Display camera button for future file upload integration
- Support URL paste for immediate photo display
- Updated API types to support photoUrl field

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-04 08:11:38 +00:00
parent 426b5a309e
commit ac59e6fe82
5 changed files with 132 additions and 7 deletions

View File

@@ -17,6 +17,7 @@ import { LanguageSelector } from '@/components/settings/LanguageSelector';
import { MeasurementUnitSelector } from '@/components/settings/MeasurementUnitSelector';
import { TimeZoneSelector } from '@/components/settings/TimeZoneSelector';
import { TimeFormatSelector } from '@/components/settings/TimeFormatSelector';
import { PhotoUpload } from '@/components/common/PhotoUpload';
import { motion } from 'framer-motion';
import { useTranslation } from '@/hooks/useTranslation';
@@ -24,6 +25,7 @@ export default function SettingsPage() {
const { t } = useTranslation('settings');
const { user, logout, refreshUser } = useAuth();
const [name, setName] = useState(user?.name || '');
const [photoUrl, setPhotoUrl] = useState(user?.photoUrl || '');
const [timezone, setTimezone] = useState(user?.timezone || 'UTC');
const [timeFormat, setTimeFormat] = useState<'12h' | '24h'>(user?.preferences?.timeFormat || '12h');
const [settings, setSettings] = useState({
@@ -50,11 +52,14 @@ export default function SettingsPage() {
}
}, [user?.preferences]);
// Sync name and timezone state when user data changes
// Sync name, photo, and timezone state when user data changes
useEffect(() => {
if (user?.name) {
setName(user.name);
}
if (user?.photoUrl) {
setPhotoUrl(user.photoUrl);
}
if (user?.timezone) {
setTimezone(user.timezone);
}
@@ -74,6 +79,7 @@ export default function SettingsPage() {
try {
const response = await usersApi.updateProfile({
name: name.trim(),
photoUrl: photoUrl || undefined,
timezone,
preferences: {
...settings,
@@ -135,6 +141,13 @@ export default function SettingsPage() {
{t('profile.title')}
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 2 }}>
<PhotoUpload
label="Profile Photo"
value={photoUrl}
onChange={setPhotoUrl}
disabled={isLoading}
size={100}
/>
<TextField
label={t('profile.name')}
value={name}

View File

@@ -14,6 +14,7 @@ import {
} from '@mui/material';
import { Child, CreateChildData } from '@/lib/api/children';
import { useTranslation } from '@/hooks/useTranslation';
import { PhotoUpload } from '@/components/common/PhotoUpload';
interface ChildDialogProps {
open: boolean;
@@ -146,13 +147,12 @@ export function ChildDialog({ open, onClose, onSubmit, child, isLoading = false
<MenuItem value="other">{t('gender.other')}</MenuItem>
</TextField>
<TextField
<PhotoUpload
label={t('dialog.photoUrl')}
value={formData.photoUrl}
onChange={handleChange('photoUrl')}
fullWidth
placeholder={t('dialog.photoPlaceholder')}
value={formData.photoUrl || ''}
onChange={(url) => setFormData({ ...formData, photoUrl: url })}
disabled={isLoading}
size={80}
/>
</Box>
</DialogContent>

View File

@@ -0,0 +1,110 @@
'use client';
import { useState } from 'react';
import {
Box,
Avatar,
IconButton,
TextField,
Typography,
Paper,
} from '@mui/material';
import { PhotoCamera, Person } from '@mui/icons-material';
interface PhotoUploadProps {
value: string;
onChange: (url: string) => void;
label: string;
disabled?: boolean;
size?: number;
}
export function PhotoUpload({
value,
onChange,
label,
disabled = false,
size = 100
}: PhotoUploadProps) {
const [imageError, setImageError] = useState(false);
const handleImageError = () => {
setImageError(true);
};
const handleImageLoad = () => {
setImageError(false);
};
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>
<Box sx={{ position: 'relative' }}>
<Avatar
src={!imageError && value ? value : undefined}
sx={{
width: size,
height: size,
bgcolor: 'primary.light',
fontSize: size / 3,
}}
onError={handleImageError}
onLoad={handleImageLoad}
>
{!value || imageError ? <Person sx={{ fontSize: size / 2 }} /> : null}
</Avatar>
<IconButton
sx={{
position: 'absolute',
bottom: -4,
right: -4,
bgcolor: 'background.paper',
border: 2,
borderColor: 'divider',
'&:hover': {
bgcolor: 'action.hover',
},
}}
size="small"
disabled={disabled}
onClick={() => {
// Future: Open file picker for actual upload
// For now, user can paste URL below
}}
>
<PhotoCamera fontSize="small" />
</IconButton>
</Box>
<TextField
label="Photo URL"
value={value}
onChange={(e) => onChange(e.target.value)}
fullWidth
size="small"
placeholder="https://example.com/photo.jpg"
disabled={disabled}
helperText="Paste an image URL or upload a photo"
/>
</Paper>
</Box>
);
}

View File

@@ -8,6 +8,8 @@ export interface UserPreferences {
export interface UpdateProfileData {
name?: string;
photoUrl?: string;
timezone?: string;
preferences?: UserPreferences;
}

File diff suppressed because one or more lines are too long