import { useState, useRef, useEffect } from 'react'; import { User, Mail, Lock, Palette, Save, Loader2, Globe, Camera, X, Calendar, Plus, Trash2, Copy, Eye, EyeOff, Shield, ShieldCheck, ShieldOff } from 'lucide-react'; import { useAuth } from '../contexts/AuthContext'; import { useTheme } from '../contexts/ThemeContext'; import { useLanguage } from '../contexts/LanguageContext'; import { themes, getThemeGroups } from '../themes'; import api from '../services/api'; import toast from 'react-hot-toast'; export default function Settings() { const { user, updateUser } = useAuth(); const { theme, setTheme } = useTheme(); const { t, language, setLanguage } = useLanguage(); const [profile, setProfile] = useState({ name: user?.name || '', display_name: user?.display_name || '', email: user?.email || '', }); const [passwords, setPasswords] = useState({ currentPassword: '', newPassword: '', confirmPassword: '', }); const [savingProfile, setSavingProfile] = useState(false); const [savingPassword, setSavingPassword] = useState(false); const handleLanguageChange = async (lang) => { setLanguage(lang); try { const res = await api.put('/auth/profile', { language: lang }); updateUser(res.data.user); } catch { // Language is still saved locally even if API fails } }; const [activeSection, setActiveSection] = useState('profile'); const [uploadingAvatar, setUploadingAvatar] = useState(false); const fileInputRef = useRef(null); // CalDAV token state const [caldavTokens, setCaldavTokens] = useState([]); const [caldavLoading, setCaldavLoading] = useState(false); const [newTokenName, setNewTokenName] = useState(''); const [creatingToken, setCreatingToken] = useState(false); const [newlyCreatedToken, setNewlyCreatedToken] = useState(null); const [tokenVisible, setTokenVisible] = useState(false); // 2FA state const [twoFaEnabled, setTwoFaEnabled] = useState(!!user?.totp_enabled); const [twoFaLoading, setTwoFaLoading] = useState(false); const [twoFaSetupData, setTwoFaSetupData] = useState(null); // { secret, uri, qrDataUrl } const [twoFaCode, setTwoFaCode] = useState(''); const [twoFaEnabling, setTwoFaEnabling] = useState(false); const [twoFaDisablePassword, setTwoFaDisablePassword] = useState(''); const [twoFaDisableCode, setTwoFaDisableCode] = useState(''); const [twoFaDisabling, setTwoFaDisabling] = useState(false); const [showDisableForm, setShowDisableForm] = useState(false); useEffect(() => { if (activeSection === 'caldav') { setCaldavLoading(true); api.get('/calendar/caldav-tokens') .then(r => setCaldavTokens(r.data.tokens || [])) .catch(() => {}) .finally(() => setCaldavLoading(false)); } if (activeSection === 'security') { setTwoFaLoading(true); api.get('/auth/2fa/status') .then(r => setTwoFaEnabled(r.data.enabled)) .catch(() => {}) .finally(() => setTwoFaLoading(false)); } }, [activeSection]); const handleCreateToken = async (e) => { e.preventDefault(); if (!newTokenName.trim()) return; setCreatingToken(true); try { const res = await api.post('/calendar/caldav-tokens', { name: newTokenName.trim() }); setNewlyCreatedToken(res.data.plainToken); setTokenVisible(false); setNewTokenName(''); const r = await api.get('/calendar/caldav-tokens'); setCaldavTokens(r.data.tokens || []); } catch (err) { toast.error(err.response?.data?.error || t('settings.caldav.createFailed')); } finally { setCreatingToken(false); } }; const handleRevokeToken = async (id) => { if (!confirm(t('settings.caldav.revokeConfirm'))) return; try { await api.delete(`/calendar/caldav-tokens/${id}`); setCaldavTokens(prev => prev.filter(tk => tk.id !== id)); toast.success(t('settings.caldav.revoked')); } catch { toast.error(t('settings.caldav.revokeFailed')); } }; // 2FA handlers const handleSetup2FA = async () => { setTwoFaLoading(true); try { const res = await api.post('/auth/2fa/setup'); // Generate QR code data URL client-side const QRCode = (await import('qrcode')).default; const qrDataUrl = await QRCode.toDataURL(res.data.uri, { width: 200, margin: 2, color: { dark: '#000000', light: '#ffffff' } }); setTwoFaSetupData({ secret: res.data.secret, uri: res.data.uri, qrDataUrl }); } catch (err) { toast.error(err.response?.data?.error || t('settings.security.setupFailed')); } finally { setTwoFaLoading(false); } }; const handleEnable2FA = async (e) => { e.preventDefault(); setTwoFaEnabling(true); try { await api.post('/auth/2fa/enable', { code: twoFaCode }); setTwoFaEnabled(true); setTwoFaSetupData(null); setTwoFaCode(''); toast.success(t('settings.security.enabled')); } catch (err) { toast.error(err.response?.data?.error || t('settings.security.enableFailed')); setTwoFaCode(''); } finally { setTwoFaEnabling(false); } }; const handleDisable2FA = async (e) => { e.preventDefault(); setTwoFaDisabling(true); try { await api.post('/auth/2fa/disable', { password: twoFaDisablePassword, code: twoFaDisableCode }); setTwoFaEnabled(false); setShowDisableForm(false); setTwoFaDisablePassword(''); setTwoFaDisableCode(''); toast.success(t('settings.security.disabled')); } catch (err) { toast.error(err.response?.data?.error || t('settings.security.disableFailed')); } finally { setTwoFaDisabling(false); } }; const groups = getThemeGroups(); const avatarColors = [ '#6366f1', '#8b5cf6', '#a855f7', '#d946ef', '#ec4899', '#f43f5e', '#ef4444', '#f97316', '#eab308', '#22c55e', '#14b8a6', '#06b6d4', '#3b82f6', '#2563eb', '#7c3aed', '#64748b', ]; const handleProfileSave = async (e) => { e.preventDefault(); setSavingProfile(true); try { const res = await api.put('/auth/profile', { name: profile.name, display_name: profile.display_name, email: profile.email, theme, language, avatar_color: user?.avatar_color, }); updateUser(res.data.user); toast.success(t('settings.profileSaved')); } catch (err) { toast.error(err.response?.data?.error || t('settings.profileSaveFailed')); } finally { setSavingProfile(false); } }; const handlePasswordSave = async (e) => { e.preventDefault(); if (passwords.newPassword !== passwords.confirmPassword) { toast.error(t('settings.passwordMismatch')); return; } setSavingPassword(true); try { await api.put('/auth/password', { currentPassword: passwords.currentPassword, newPassword: passwords.newPassword, }); setPasswords({ currentPassword: '', newPassword: '', confirmPassword: '' }); toast.success(t('settings.passwordChanged')); } catch (err) { toast.error(err.response?.data?.error || t('settings.passwordChangeFailed')); } finally { setSavingPassword(false); } }; const handleAvatarColor = async (color) => { try { const res = await api.put('/auth/profile', { avatar_color: color }); updateUser(res.data.user); } catch { // Ignore } }; const handleAvatarUpload = async (e) => { const file = e.target.files?.[0]; if (!file) return; if (!file.type.startsWith('image/')) { toast.error(t('settings.avatarInvalidType')); return; } if (file.size > 2 * 1024 * 1024) { toast.error(t('settings.avatarTooLarge')); return; } setUploadingAvatar(true); try { const res = await api.post('/auth/avatar', file, { headers: { 'Content-Type': file.type }, }); updateUser(res.data.user); toast.success(t('settings.avatarUploaded')); } catch (err) { toast.error(err.response?.data?.error || t('settings.avatarUploadFailed')); } finally { setUploadingAvatar(false); if (fileInputRef.current) fileInputRef.current.value = ''; } }; const handleAvatarRemove = async () => { try { const res = await api.delete('/auth/avatar'); updateUser(res.data.user); toast.success(t('settings.avatarRemoved')); } catch { toast.error(t('settings.avatarRemoveFailed')); } }; const sections = [ { id: 'profile', label: t('settings.profile'), icon: User }, { id: 'password', label: t('settings.password'), icon: Lock }, { id: 'security', label: t('settings.security.title'), icon: Shield }, { id: 'language', label: t('settings.language'), icon: Globe }, { id: 'themes', label: t('settings.themes'), icon: Palette }, { id: 'caldav', label: t('settings.caldav.title'), icon: Calendar }, ]; return (

{t('settings.title')}

{t('settings.subtitle')}

{/* Section nav */}
{/* Content */}
{/* Profile section */} {activeSection === 'profile' && (

{t('settings.editProfile')}

{/* Avatar */}
{user?.avatar_image ? ( Avatar ) : (
{(user?.display_name || user?.name)?.[0]?.toUpperCase() || '?'}
)}
{user?.avatar_image && ( )}

{t('settings.avatarHint')}

{/* Avatar Color (fallback) */}
{avatarColors.map(color => (

{t('settings.avatarColorHint')}

setProfile({ ...profile, name: e.target.value })} className="input-field pl-11" required />

{t('auth.usernameHint')}

setProfile({ ...profile, display_name: e.target.value })} className="input-field pl-11" />
setProfile({ ...profile, email: e.target.value })} className="input-field pl-11" required />
)} {/* Password section */} {activeSection === 'password' && (

{t('settings.changePassword')}

setPasswords({ ...passwords, currentPassword: e.target.value })} className="input-field pl-11" required />
setPasswords({ ...passwords, newPassword: e.target.value })} className="input-field pl-11" placeholder={t('auth.minPassword')} required minLength={6} />
setPasswords({ ...passwords, confirmPassword: e.target.value })} className="input-field pl-11" placeholder={t('auth.repeatPassword')} required minLength={6} />
)} {/* Security / 2FA section */} {activeSection === 'security' && (

{t('settings.security.title')}

{t('settings.security.subtitle')}

{twoFaLoading ? (
) : twoFaEnabled ? ( /* 2FA is enabled */

{t('settings.security.statusEnabled')}

{t('settings.security.statusEnabledDesc')}

{!showDisableForm ? ( ) : (

{t('settings.security.disableConfirm')}

setTwoFaDisablePassword(e.target.value)} className="input-field pl-11" required />
setTwoFaDisableCode(e.target.value.replace(/[^0-9\s]/g, '').slice(0, 7))} className="input-field text-center text-lg tracking-[0.3em] font-mono" placeholder="000 000" required maxLength={7} />
)}
) : twoFaSetupData ? ( /* Setup flow: show QR code + verification */

{t('settings.security.scanQR')}

TOTP QR Code

{t('settings.security.manualKey')}

{twoFaSetupData.secret}
setTwoFaCode(e.target.value.replace(/[^0-9\s]/g, '').slice(0, 7))} className="input-field text-center text-lg tracking-[0.3em] font-mono" placeholder="000 000" required maxLength={7} />
) : ( /* 2FA is disabled โ€” show enable button */

{t('settings.security.statusDisabled')}

{t('settings.security.statusDisabledDesc')}

)}
)} {/* Language section */} {activeSection === 'language' && (

{t('settings.selectLanguage')}

{t('settings.subtitle')}

{[ { code: 'en', label: 'English', flag: '๐Ÿ‡ฌ๐Ÿ‡ง' }, { code: 'de', label: 'Deutsch', flag: '๐Ÿ‡ฉ๐Ÿ‡ช' }, ].map(lang => ( ))}
)} {/* Themes section */} {activeSection === 'themes' && (
{Object.entries(groups).map(([groupName, groupThemes]) => (

{groupName}

{groupThemes.map(th => ( ))}
))}
)} {/* CalDAV section */} {activeSection === 'caldav' && (
{/* Info Card */}

{t('settings.caldav.title')}

{t('settings.caldav.subtitle')}

{t('settings.caldav.serverUrl')}

{`${window.location.origin}/caldav/`}

{t('settings.caldav.username')}

{user?.email}

{t('settings.caldav.hint')}

{/* New token was just created */} {newlyCreatedToken && (

{t('settings.caldav.newTokenCreated')}

{t('settings.caldav.newTokenHint')}

{tokenVisible ? newlyCreatedToken : 'โ€ข'.repeat(48)}
)} {/* Create new token */}

{t('settings.caldav.newToken')}

setNewTokenName(e.target.value)} placeholder={t('settings.caldav.tokenNamePlaceholder')} className="input-field flex-1 text-sm" required />
{/* Token list */}

{t('settings.caldav.existingTokens')}

{caldavLoading ? (
) : caldavTokens.length === 0 ? (

{t('settings.caldav.noTokens')}

) : (
{caldavTokens.map(tk => (

{tk.name}

{t('settings.caldav.created')}: {new Date(tk.created_at).toLocaleDateString()} {tk.last_used_at && ` ยท ${t('settings.caldav.lastUsed')}: ${new Date(tk.last_used_at).toLocaleDateString()}`}

))}
)}
)}
); }