feat: Add photo upload component for user and child profiles
- 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:
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
110
maternal-web/components/common/PhotoUpload.tsx
Normal file
110
maternal-web/components/common/PhotoUpload.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user