refactor: Consolidate settings page save buttons for better UX

Improved the settings page by removing individual save buttons from each
preference component and adding unified save buttons per section:

## Changes Made

### Component Updates
- **TimeZoneSelector**: Converted to controlled component with value/onChange props
  * Removed internal state management and save button
  * Removed success/error alerts (now handled by parent)
  * Added auto-detect as simple button without save

- **TimeFormatSelector**: Converted to controlled component with value/onChange props
  * Removed internal state management and save button
  * Removed success/error alerts (now handled by parent)
  * Simplified to just radio buttons with preview

### Settings Page Improvements
- Added timezone and timeFormat to local state
- Created separate save handlers:
  * `handleSaveProfile` - for name/email changes
  * `handleSavePreferences` - for timezone and time format
- Three clear sections with dedicated save buttons:
  1. **Profile Information** → "Save Profile" button
  2. **Preferences** (Language, Units, Timezone, Time Format) → "Save Preferences" button
  3. **Notifications** → "Save Notification Settings" button

### User Experience Benefits
- Clearer separation between different types of settings
- Single save action per logical section instead of multiple buttons
- Consistent save pattern across all settings cards
- Reduced visual clutter with fewer buttons on page
- Better organization: related settings grouped with one save action

Files changed: 3 files (TimeZoneSelector, TimeFormatSelector, settings page)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-03 11:59:27 +00:00
parent 49ac1dd58a
commit c27f72e41d
5 changed files with 878 additions and 129 deletions

View File

@@ -1,53 +1,33 @@
'use client';
import { useState } from 'react';
import { useEffect } from 'react';
import {
Box,
FormControl,
RadioGroup,
FormControlLabel,
Radio,
Button,
CircularProgress,
Alert,
Typography,
} from '@mui/material';
import { Save, Schedule } from '@mui/icons-material';
import { 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();
interface TimeFormatSelectorProps {
value: '12h' | '24h';
onChange: (timeFormat: '12h' | '24h') => void;
}
export function TimeFormatSelector({ value, onChange }: TimeFormatSelectorProps) {
const { user } = 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);
// Initialize with user's time format on mount
useEffect(() => {
if (user?.preferences?.timeFormat && !value) {
onChange(user.preferences.timeFormat);
}
};
}, [user?.preferences?.timeFormat]);
const currentTime = new Date();
const preview12h = currentTime.toLocaleTimeString('en-US', {
@@ -70,26 +50,14 @@ export function TimeFormatSelector() {
</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 }}>
<FormControl component="fieldset">
<RadioGroup
value={timeFormat}
onChange={(e) => setTimeFormat(e.target.value as '12h' | '24h')}
value={value || user?.preferences?.timeFormat || '12h'}
onChange={(e) => onChange(e.target.value as '12h' | '24h')}
>
<FormControlLabel
value="12h"
control={<Radio disabled={isLoading} />}
control={<Radio />}
label={
<Box>
<Typography variant="body1">12-hour format</Typography>
@@ -101,7 +69,7 @@ export function TimeFormatSelector() {
/>
<FormControlLabel
value="24h"
control={<Radio disabled={isLoading} />}
control={<Radio />}
label={
<Box>
<Typography variant="body1">24-hour format</Typography>
@@ -113,15 +81,6 @@ export function TimeFormatSelector() {
/>
</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>
);
}

View File

@@ -1,6 +1,6 @@
'use client';
import { useState } from 'react';
import { useEffect } from 'react';
import {
Box,
FormControl,
@@ -8,13 +8,10 @@ import {
Select,
MenuItem,
Button,
CircularProgress,
Alert,
Typography,
} from '@mui/material';
import { Save, AccessTime } from '@mui/icons-material';
import { AccessTime } from '@mui/icons-material';
import { useAuth } from '@/lib/auth/AuthContext';
import { usersApi } from '@/lib/api/users';
import { useTranslation } from '@/hooks/useTranslation';
// Common timezones grouped by region
@@ -71,35 +68,25 @@ const TIMEZONES = {
],
};
export function TimeZoneSelector() {
const { user, refreshUser } = useAuth();
interface TimeZoneSelectorProps {
value: string;
onChange: (timezone: string) => void;
}
export function TimeZoneSelector({ value, onChange }: TimeZoneSelectorProps) {
const { user } = useAuth();
const { t } = useTranslation('settings');
const [timezone, setTimezone] = useState(user?.timezone || 'UTC');
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({ timezone });
await refreshUser();
setSuccessMessage('Timezone updated successfully');
} catch (err: any) {
console.error('Failed to update timezone:', err);
setError(err.response?.data?.message || 'Failed to update timezone');
} finally {
setIsLoading(false);
// Initialize with user's timezone on mount
useEffect(() => {
if (user?.timezone && !value) {
onChange(user.timezone);
}
};
}, [user?.timezone]);
const handleAutoDetect = () => {
const detectedTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
setTimezone(detectedTimezone);
setSuccessMessage(`Auto-detected timezone: ${detectedTimezone}`);
onChange(detectedTimezone);
};
return (
@@ -111,25 +98,12 @@ export function TimeZoneSelector() {
</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 fullWidth sx={{ mb: 2 }}>
<InputLabel>Time Zone</InputLabel>
<Select
value={timezone}
onChange={(e) => setTimezone(e.target.value)}
value={value || user?.timezone || 'UTC'}
onChange={(e) => onChange(e.target.value)}
label="Time Zone"
disabled={isLoading}
>
{Object.entries(TIMEZONES).map(([region, zones]) => [
<MenuItem key={region} disabled sx={{ fontWeight: 'bold', color: 'text.primary' }}>
@@ -145,23 +119,13 @@ export function TimeZoneSelector() {
</Select>
</FormControl>
<Box sx={{ display: 'flex', gap: 2 }}>
<Button
variant="outlined"
onClick={handleAutoDetect}
disabled={isLoading}
>
Auto-Detect
</Button>
<Button
variant="contained"
startIcon={isLoading ? <CircularProgress size={20} color="inherit" /> : <Save />}
onClick={handleSave}
disabled={isLoading || timezone === user?.timezone}
>
{isLoading ? 'Saving...' : 'Save'}
</Button>
</Box>
<Button
variant="outlined"
onClick={handleAutoDetect}
size="small"
>
Auto-Detect
</Button>
</Box>
);
}