Files
ToBePlayed-Frontend/src/pages/SettingsPage.jsx
T
lobo 28ff6eae52 refactor(settings): one card per section
The name field takes its value from the page's own load, not from the user context: the context can still be empty when the form mounts.
2026-09-30 08:46:51 +02:00

102 lines
4.1 KiB
React

import {useEffect, useRef, useState} from 'react';
import {useSearchParams} from 'react-router-dom';
import {useTranslation} from 'react-i18next';
import {toast} from 'sonner';
import {Skeleton} from '@/components/ui/skeleton';
import AccountInfoCard from '../components/AccountInfoCard.jsx';
import AppearanceCard from '../components/AppearanceCard.jsx';
import BuildVersionCard from '../components/BuildVersionCard.jsx';
import DeleteAccountCard from '../components/DeleteAccountCard.jsx';
import PreferencesCard from '../components/PreferencesCard.jsx';
import ProfileCard from '../components/ProfileCard.jsx';
import SteamAccountCard from '../components/SteamAccountCard.jsx';
import {useCurrentUserContext} from '../context/CurrentUserContext.jsx';
import {refreshSession} from '../utils/api.js';
import {markSession} from '../utils/auth.js';
// The `steam` query parameter Auth adds when "Sign in through Steam" sends the user back.
const STEAM_LINK_TOASTS = {
linked: {show: toast.success, key: 'SETTINGS.STEAM_LINK.LINKED'},
'in-use': {show: toast.error, key: 'SETTINGS.STEAM_LINK.IN_USE'},
cancelled: {show: toast.info, key: 'SETTINGS.STEAM_LINK.CANCELLED'},
failed: {show: toast.error, key: 'SETTINGS.STEAM_LINK.FAILED'},
};
export default function SettingsPage() {
const {t} = useTranslation();
const {user: profile, updateUser: updateProfile, reload: reloadUser} = useCurrentUserContext();
const [isLoading, setIsLoading] = useState(true);
const [loadError, setLoadError] = useState(null);
// From this page's own load: the context user can still be empty when the form mounts.
const [displayName, setDisplayName] = useState('');
const [searchParams, setSearchParams] = useSearchParams();
// Read once: removing the parameter below must not load the profile again.
const [steamOutcome] = useState(() => searchParams.get('steam'));
// A language change gives a new `t` and runs the effect again; the toast shows once.
const steamToastShown = useRef(false);
useEffect(() => {
const outcome = STEAM_LINK_TOASTS[steamOutcome];
if (!outcome || steamToastShown.current) return;
steamToastShown.current = true;
outcome.show(t(outcome.key));
setSearchParams((params) => {
params.delete('steam');
return params;
}, {replace: true});
}, [steamOutcome, setSearchParams, t]);
useEffect(() => {
const load = async () => {
if (steamOutcome === 'linked') {
// Refresh reads the new Steam ID from the database, so Core gets it in the token at once.
await refreshSession().catch(() => false);
markSession();
}
const {ok, status, data} = await reloadUser();
if (ok) setDisplayName(data.displayName ?? '');
else if (status !== 401) setLoadError(status === 0 ? 'INFO.ERROR.NETWORK_FAILURE' : 'INFO.ERROR.LOAD_PROFILE');
setIsLoading(false);
};
load();
}, [reloadUser, steamOutcome]);
if (isLoading) {
return (
<div className="w-full max-w-2xl mx-auto p-4 md:p-8 space-y-6">
<Skeleton className="h-9 w-48"/>
<Skeleton className="h-56 rounded-xl"/>
<Skeleton className="h-40 rounded-xl"/>
<Skeleton className="h-44 rounded-xl"/>
</div>
);
}
if (loadError) {
return <div className="py-24 text-center text-destructive">{t(loadError)}</div>;
}
return (
<div className="w-full max-w-2xl mx-auto p-4 md:p-8 space-y-6">
<h1 className="text-3xl font-bold tracking-tight">{t('SETTINGS.TITLE')}</h1>
<AccountInfoCard/>
<PreferencesCard/>
<AppearanceCard/>
<SteamAccountCard steamId={profile?.steamId} verified={!!profile?.steamIdVerified}
onUnlinked={() => updateProfile((p) => ({...p, steamId: null, steamIdVerified: false}))}/>
<ProfileCard displayName={displayName}/>
<BuildVersionCard/>
<DeleteAccountCard email={profile?.email}/>
</div>
);
}