import { zodResolver } from '@hookform/resolvers/zod'; import { changePasswordInputSchema, updateProfileInputSchema } from '@dorfteich/shared'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useState } from 'react'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { useAuth } from '../auth/auth-context'; import { Field, FormError, FormSuccess, applyFieldErrors } from '../components/forms'; import { useDataExport } from '../export/use-data-export'; import { apiDelete, apiGet, apiPatch, apiPost } from '../lib/api'; interface SessionView { id: string; createdAt: string; lastSeenAt: string; userAgent: string | null; current: boolean; } export function SettingsPage(): React.JSX.Element { const { t } = useTranslation(); return ( <>

{t('settings:title')}

); } function DataExportSection(): React.JSX.Element { const { t, i18n } = useTranslation(); const { status, expiresAt, request, download } = useDataExport(); const formatTime = (iso: string): string => new Intl.DateTimeFormat(i18n.language, { dateStyle: 'medium', timeStyle: 'short' }).format( new Date(iso), ); return (

{t('settings:dataExport.title')}

{t('settings:dataExport.description')}

{status === 'error' && (

{t('settings:dataExport.failed')}

)} {status === 'rateLimited' && (

{t('settings:dataExport.rateLimited')}

)} {status === 'ready' ? ( <> {expiresAt && (

{t('settings:dataExport.expiresHint', { when: formatTime(expiresAt) })}

)} ) : ( )}
); } function ProfileSection(): React.JSX.Element { const { t, i18n } = useTranslation(); const { user, refresh } = useAuth(); const [error, setError] = useState(null); const [saved, setSaved] = useState(false); const form = useForm<{ displayName?: string; locale?: 'de' | 'en' }>({ resolver: zodResolver(updateProfileInputSchema), values: { displayName: user?.displayName, locale: user?.locale }, }); const onSubmit = form.handleSubmit(async (input) => { setError(null); setSaved(false); try { await apiPatch('/users/me', input); // The new language applies immediately, before the refetch lands. if (input.locale) await i18n.changeLanguage(input.locale); await refresh(); setSaved(true); } catch (err) { setError(err); applyFieldErrors(err, (name, fieldError) => form.setError(name as 'displayName' | 'locale', fieldError), ); } }); return (

{t('settings:profile.title')}

); } function PasswordSection(): React.JSX.Element { const { t } = useTranslation(); const [error, setError] = useState(null); const [changed, setChanged] = useState(false); const form = useForm<{ currentPassword: string; newPassword: string }>({ resolver: zodResolver(changePasswordInputSchema), }); const onSubmit = form.handleSubmit(async (input) => { setError(null); setChanged(false); try { await apiPost('/users/me/change-password', input); form.reset(); setChanged(true); } catch (err) { setError(err); } }); return (

{t('settings:password.title')}

); } function SessionsSection(): React.JSX.Element { const { t, i18n } = useTranslation(); const queryClient = useQueryClient(); const sessions = useQuery({ queryKey: ['sessions'], queryFn: () => apiGet('/users/me/sessions'), }); const invalidate = () => queryClient.invalidateQueries({ queryKey: ['sessions'] }); const revoke = useMutation({ mutationFn: (id: string) => apiDelete(`/users/me/sessions/${id}`), onSuccess: invalidate, }); const revokeOthers = useMutation({ mutationFn: () => apiDelete('/users/me/sessions'), onSuccess: invalidate, }); const formatTime = (iso: string) => new Intl.DateTimeFormat(i18n.language, { dateStyle: 'medium', timeStyle: 'short' }).format( new Date(iso), ); const others = (sessions.data ?? []).filter((s) => !s.current); return (

{t('settings:sessions.title')}

{(sessions.data ?? []).map((session) => ( ))}
{t('settings:sessions.device')} {t('settings:sessions.created')} {t('settings:sessions.lastSeen')}
{session.userAgent ?? '—'} {session.current && {t('settings:sessions.current')}} {formatTime(session.createdAt)} {formatTime(session.lastSeenAt)} {!session.current && ( )}
{others.length > 0 ? ( ) : (

{t('settings:sessions.empty')}

)}
); }