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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user