feat: Add timezone and time format preferences with auto-detection
This commit implements comprehensive timezone and time format customization:
## Backend Changes
- Added timeFormat field ('12h' | '24h') to user preferences JSONB in user entity
- Timezone field already existed in user entity, now actively used
- Backend ready to accept timezone on registration
## Frontend Components (2 new)
- TimeZoneSelector: Dropdown with timezones grouped by region (Americas, Europe, Asia, Pacific, Africa)
* Auto-detect button to detect browser timezone
* Save functionality with success/error feedback
* Integrated into Settings > Preferences section
- TimeFormatSelector: Radio buttons to choose 12h vs 24h format
* Live preview showing current time in selected format
* Save functionality with user feedback
* Integrated into Settings > Preferences section
## Timezone Auto-Detection
- Register function now auto-detects user's timezone via Intl.DateTimeFormat()
- Detected timezone sent to backend during registration
- Timezone stored in user profile for persistent preference
## Enhanced useLocalizedDate Hook
- Added useAuth integration to access user timezone and timeFormat preferences
- Installed and integrated date-fns-tz for timezone conversion
- New format() function with timezone support via useTimezone option
- New formatTime() function respecting user's 12h/24h preference
- New formatDateTime() function combining date, time, and timezone
- All formatting now respects user's:
* Language (existing: en, es, fr, pt-BR, zh-CN)
* Timezone (user-selected or auto-detected)
* Time format (12h with AM/PM or 24h)
## Settings Page Updates
- Added TimeZoneSelector to Preferences card
- Added TimeFormatSelector to Preferences card
- Visual separators (Dividers) between preference sections
- Settings now show: Language | Units | Timezone | Time Format
## Translations
- Enhanced settings.json with timezone and time format keys:
* preferences.timezone, autoDetectTimezone, timezoneUpdated
* preferences.12hour, 24hour, timeFormatUpdated
## User Experience Flow
1. User registers → timezone auto-detected and saved
2. User can change timezone in Settings > Preferences > Time Zone
3. User can change time format in Settings > Preferences > Time Format
4. All dates/times throughout app respect these preferences
5. Changes persist across sessions
Files changed: 10 files
New dependencies: date-fns-tz
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
127
maternal-web/components/settings/TimeFormatSelector.tsx
Normal file
127
maternal-web/components/settings/TimeFormatSelector.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
FormControl,
|
||||
RadioGroup,
|
||||
FormControlLabel,
|
||||
Radio,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Alert,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { Save, Schedule } from '@mui/icons-material';
|
||||
import { useAuth } from '@/lib/auth/AuthContext';
|
||||
import { usersApi } from '@/lib/api/users';
|
||||
import { useTranslation } from '@/hooks/useTranslation';
|
||||
|
||||
export function TimeFormatSelector() {
|
||||
const { user, refreshUser } = useAuth();
|
||||
const { t } = useTranslation('settings');
|
||||
const [timeFormat, setTimeFormat] = useState<'12h' | '24h'>(
|
||||
user?.preferences?.timeFormat || '12h'
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
setSuccessMessage(null);
|
||||
|
||||
try {
|
||||
await usersApi.updateProfile({
|
||||
preferences: {
|
||||
...user?.preferences,
|
||||
timeFormat,
|
||||
},
|
||||
});
|
||||
await refreshUser();
|
||||
setSuccessMessage('Time format updated successfully');
|
||||
} catch (err: any) {
|
||||
console.error('Failed to update time format:', err);
|
||||
setError(err.response?.data?.message || 'Failed to update time format');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const currentTime = new Date();
|
||||
const preview12h = currentTime.toLocaleTimeString('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true,
|
||||
});
|
||||
const preview24h = currentTime.toLocaleTimeString('en-US', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
|
||||
<Schedule color="action" />
|
||||
<Typography variant="subtitle1" fontWeight="600">
|
||||
Time Format
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }} onClose={() => setError(null)}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{successMessage && (
|
||||
<Alert severity="success" sx={{ mb: 2 }} onClose={() => setSuccessMessage(null)}>
|
||||
{successMessage}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<FormControl component="fieldset" sx={{ mb: 2 }}>
|
||||
<RadioGroup
|
||||
value={timeFormat}
|
||||
onChange={(e) => setTimeFormat(e.target.value as '12h' | '24h')}
|
||||
>
|
||||
<FormControlLabel
|
||||
value="12h"
|
||||
control={<Radio disabled={isLoading} />}
|
||||
label={
|
||||
<Box>
|
||||
<Typography variant="body1">12-hour format</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Example: {preview12h}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="24h"
|
||||
control={<Radio disabled={isLoading} />}
|
||||
label={
|
||||
<Box>
|
||||
<Typography variant="body1">24-hour format</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Example: {preview24h}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={isLoading ? <CircularProgress size={20} color="inherit" /> : <Save />}
|
||||
onClick={handleSave}
|
||||
disabled={isLoading || timeFormat === user?.preferences?.timeFormat}
|
||||
>
|
||||
{isLoading ? 'Saving...' : 'Save'}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user