Files
maternal-app/maternal-web/app/settings/page.tsx
Andrei 58c3a8d9d5
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: Complete Spanish, French, Portuguese, Chinese localization and add German/Italian support
- Updated all Spanish (es) translation files with comprehensive translations for tracking, AI, family, insights, children, and settings pages
- Updated French (fr), Portuguese (pt), and Chinese (zh) translations to match English structure
- Added German (de) and Italian (it) language support with complete translation files
- Fixed medicine tracker route from /track/medication to /track/medicine
- Updated i18n config to support 7 languages: en, es, fr, pt, zh, de, it
- All tracking pages now fully localized: sleep, feeding, diaper, medicine, activity
- AI assistant interface fully translated with thinking messages and suggested questions
- Family management and insights pages now support all languages

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-03 15:03:02 +00:00

380 lines
12 KiB
TypeScript

'use client';
import { Box, Typography, Card, CardContent, TextField, Button, Divider, Switch, FormControlLabel, Alert, CircularProgress, Snackbar } from '@mui/material';
import { Save, Logout } from '@mui/icons-material';
import { useAuth } from '@/lib/auth/AuthContext';
import { useState, useEffect } from 'react';
import { AppShell } from '@/components/layouts/AppShell/AppShell';
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
import { usersApi } from '@/lib/api/users';
import { MFASettings } from '@/components/settings/MFASettings';
import { SessionsManagement } from '@/components/settings/SessionsManagement';
import { DeviceTrustManagement } from '@/components/settings/DeviceTrustManagement';
import { BiometricSettings } from '@/components/settings/BiometricSettings';
import { DataExport } from '@/components/settings/DataExport';
import { AccountDeletion } from '@/components/settings/AccountDeletion';
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 { motion } from 'framer-motion';
import { useTranslation } from '@/hooks/useTranslation';
export default function SettingsPage() {
const { t } = useTranslation('settings');
const { user, logout, refreshUser } = useAuth();
const [name, setName] = useState(user?.name || '');
const [timezone, setTimezone] = useState(user?.timezone || 'UTC');
const [timeFormat, setTimeFormat] = useState<'12h' | '24h'>(user?.preferences?.timeFormat || '12h');
const [settings, setSettings] = useState({
notifications: true,
emailUpdates: false,
darkMode: false,
measurementUnit: 'metric' as 'metric' | 'imperial',
});
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const [nameError, setNameError] = useState<string | null>(null);
// Load preferences from user object when it changes
useEffect(() => {
if (user?.preferences) {
setSettings({
notifications: user.preferences.notifications ?? true,
emailUpdates: user.preferences.emailUpdates ?? false,
darkMode: user.preferences.darkMode ?? false,
measurementUnit: (user.preferences.measurementUnit as 'metric' | 'imperial') || 'metric',
});
setTimeFormat(user.preferences.timeFormat || '12h');
}
}, [user?.preferences]);
// Sync name and timezone state when user data changes
useEffect(() => {
if (user?.name) {
setName(user.name);
}
if (user?.timezone) {
setTimezone(user.timezone);
}
}, [user]);
const handleSaveAll = async () => {
// Validate name
if (!name || name.trim() === '') {
setNameError(t('profile.nameRequired'));
return;
}
setIsLoading(true);
setError(null);
setNameError(null);
try {
const response = await usersApi.updateProfile({
name: name.trim(),
timezone,
preferences: {
...settings,
timeFormat,
}
});
console.log('✅ All settings saved successfully:', response);
// Refresh user to get latest data from server
await refreshUser();
setSuccessMessage(t('saved'));
} catch (err: any) {
console.error('❌ Failed to save settings:', err);
console.error('Error response:', err.response);
setError(err.response?.data?.message || err.message || 'Failed to save settings. Please try again.');
} finally {
setIsLoading(false);
}
};
const handleLogout = async () => {
await logout();
};
return (
<ProtectedRoute>
<AppShell>
<Box sx={{ maxWidth: 'md', mx: 'auto' }}>
<Typography variant="h4" component="h1" fontWeight="600" gutterBottom>
{t('title')}
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 4 }}>
{t('profile.title')}
</Typography>
{/* Error Alert */}
{error && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
<Alert severity="error" sx={{ mb: 3 }} onClose={() => setError(null)}>
{error}
</Alert>
</motion.div>
)}
{/* Profile Settings */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4 }}
>
<Card sx={{ mb: 3, minHeight: '120px' }}>
<CardContent>
<Typography variant="h6" component="h2" fontWeight="600" gutterBottom>
{t('profile.title')}
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 2 }}>
<TextField
label={t('profile.name')}
value={name}
onChange={(e) => {
setName(e.target.value);
if (nameError) setNameError(null);
}}
fullWidth
error={!!nameError}
helperText={nameError}
disabled={isLoading}
/>
<TextField
label={t('profile.email')}
value={user?.email || ''}
fullWidth
disabled
helperText={t('profile.emailNotEditable')}
/>
</Box>
</CardContent>
</Card>
</motion.div>
{/* Preferences (Language, Measurement Units, Timezone, Time Format) */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.05 }}
>
<Card sx={{ mb: 3, minHeight: '120px' }}>
<CardContent>
<Typography variant="h6" component="h2" fontWeight="600" gutterBottom>
{t('preferences.title')}
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3, mt: 2 }}>
<LanguageSelector />
<Divider />
<MeasurementUnitSelector
value={settings.measurementUnit}
onChange={(value) => setSettings(prev => ({ ...prev, measurementUnit: value }))}
/>
<Divider />
<TimeZoneSelector value={timezone} onChange={setTimezone} />
<Divider />
<TimeFormatSelector value={timeFormat} onChange={setTimeFormat} />
</Box>
</CardContent>
</Card>
</motion.div>
{/* Notification Settings */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.1 }}
>
<Card sx={{ mb: 3, minHeight: '120px' }}>
<CardContent>
<Typography variant="h6" component="h2" fontWeight="600" gutterBottom>
{t('notifications.title')}
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, mt: 2 }}>
<FormControlLabel
control={
<Switch
checked={settings.notifications}
onChange={(e) => setSettings({ ...settings, notifications: e.target.checked })}
disabled={isLoading}
/>
}
label={t('notifications.push')}
/>
<FormControlLabel
control={
<Switch
checked={settings.emailUpdates}
onChange={(e) => setSettings({ ...settings, emailUpdates: e.target.checked })}
disabled={isLoading}
/>
}
label={t('notifications.email')}
/>
</Box>
</CardContent>
</Card>
</motion.div>
{/* Appearance Settings */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.2 }}
>
<Card sx={{ mb: 3, minHeight: '120px' }}>
<CardContent>
<Typography variant="h6" component="h2" fontWeight="600" gutterBottom>
{t('appearance.title')}
</Typography>
<Box sx={{ mt: 2 }}>
<FormControlLabel
control={
<Switch
checked={settings.darkMode}
onChange={(e) => setSettings({ ...settings, darkMode: e.target.checked })}
/>
}
label={t('appearance.darkMode')}
disabled
/>
</Box>
</CardContent>
</Card>
</motion.div>
{/* Security Settings - MFA */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.25 }}
>
<Box sx={{ mb: 3 }}>
<MFASettings />
</Box>
</motion.div>
{/* Sessions Management */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.3 }}
>
<Box sx={{ mb: 3 }}>
<SessionsManagement />
</Box>
</motion.div>
{/* Device Trust Management */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.33 }}
>
<Box sx={{ mb: 3 }}>
<DeviceTrustManagement />
</Box>
</motion.div>
{/* Biometric Authentication */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.36 }}
>
<Box sx={{ mb: 3 }}>
<BiometricSettings />
</Box>
</motion.div>
{/* Privacy & Compliance - Data Export */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.39 }}
>
<Box sx={{ mb: 3 }}>
<DataExport />
</Box>
</motion.div>
{/* Privacy & Compliance - Account Deletion */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.42 }}
>
<Box sx={{ mb: 3 }}>
<AccountDeletion />
</Box>
</motion.div>
{/* Global Save Button */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.44 }}
>
<Box sx={{ mb: 3, display: 'flex', justifyContent: 'center' }}>
<Button
variant="contained"
size="large"
startIcon={isLoading ? <CircularProgress size={20} color="inherit" /> : <Save />}
onClick={handleSaveAll}
disabled={isLoading}
sx={{ minWidth: 200 }}
>
{isLoading ? t('saving') : t('save')}
</Button>
</Box>
</motion.div>
{/* Account Actions */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.45 }}
>
<Card>
<CardContent>
<Typography variant="h6" component="h2" fontWeight="600" gutterBottom>
{t('accountActions.title')}
</Typography>
<Divider sx={{ my: 2 }} />
<Button
variant="outlined"
color="error"
startIcon={<Logout />}
onClick={handleLogout}
fullWidth
>
{t('accountActions.logout')}
</Button>
</CardContent>
</Card>
</motion.div>
{/* Success Snackbar */}
<Snackbar
open={!!successMessage}
autoHideDuration={4000}
onClose={() => setSuccessMessage(null)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
>
<Alert onClose={() => setSuccessMessage(null)} severity="success" sx={{ width: '100%' }}>
{successMessage}
</Alert>
</Snackbar>
</Box>
</AppShell>
</ProtectedRoute>
);
}