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 { 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 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')}
| {t('settings:sessions.device')} |
{t('settings:sessions.created')} |
{t('settings:sessions.lastSeen')} |
|
{(sessions.data ?? []).map((session) => (
|
{session.userAgent ?? '—'}
{session.current && {t('settings:sessions.current')}}
|
{formatTime(session.createdAt)} |
{formatTime(session.lastSeenAt)} |
{!session.current && (
)}
|
))}
{others.length > 0 ? (
) : (
{t('settings:sessions.empty')}
)}
);
}