refactor(frontend): migrate Profile.tsx to the banner system

Load/save/delete failures now call showError instead of setting local
error state. The profile-not-loaded-yet fallback gains a
profileLoadFailed boolean so it can still distinguish "loading" from
"failed to load" without the error text itself, which now lives only
in the (transient) banner.
This commit is contained in:
2026-07-27 09:53:28 +02:00
parent a5fffff737
commit 940295510e

View File

@@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { api, BASE_URL } from "../api/client"; import { api, BASE_URL } from "../api/client";
import { showError } from "../banner";
import { ColorField } from "../components/ColorField"; import { ColorField } from "../components/ColorField";
import { GarminConnection } from "../components/GarminConnection"; import { GarminConnection } from "../components/GarminConnection";
import { NullableNumberField } from "../components/NullableNumberField"; import { NullableNumberField } from "../components/NullableNumberField";
@@ -87,7 +88,7 @@ const AUTO_SAVE_DELAY_MS = 600;
export function Profile({ onSaved }: { onSaved?: (p: ProfileType) => void }) { export function Profile({ onSaved }: { onSaved?: (p: ProfileType) => void }) {
const [profile, setProfile] = useState<ProfileType | null>(null); const [profile, setProfile] = useState<ProfileType | null>(null);
const [error, setError] = useState<string | null>(null); const [profileLoadFailed, setProfileLoadFailed] = useState(false);
const [saved, setSaved] = useState(false); const [saved, setSaved] = useState(false);
// Mirrors `profile` synchronously (state updates don't apply until the // Mirrors `profile` synchronously (state updates don't apply until the
// next render), so set() always debounces from the latest edit rather // next render), so set() always debounces from the latest edit rather
@@ -100,7 +101,10 @@ export function Profile({ onSaved }: { onSaved?: (p: ProfileType) => void }) {
const [deleting, setDeleting] = useState(false); const [deleting, setDeleting] = useState(false);
useEffect(() => { useEffect(() => {
api.getProfile().then(setProfile).catch((e) => setError(String(e))); api.getProfile().then(setProfile).catch((e) => {
showError(String(e));
setProfileLoadFailed(true);
});
}, []); }, []);
// Flush a still-pending debounced save on unmount (e.g. the user edits a // Flush a still-pending debounced save on unmount (e.g. the user edits a
@@ -121,11 +125,10 @@ export function Profile({ onSaved }: { onSaved?: (p: ProfileType) => void }) {
const updated = await api.updateProfile(next); const updated = await api.updateProfile(next);
profileRef.current = updated; profileRef.current = updated;
setProfile(updated); setProfile(updated);
setError(null);
setSaved(true); setSaved(true);
onSaved?.(updated); onSaved?.(updated);
} catch (e) { } catch (e) {
setError(String(e)); showError(String(e));
} }
} }
@@ -153,7 +156,6 @@ export function Profile({ onSaved }: { onSaved?: (p: ProfileType) => void }) {
async function deleteAccount() { async function deleteAccount() {
setDeleting(true); setDeleting(true);
setError(null);
try { try {
await api.deleteProfile(); await api.deleteProfile();
// Deletion doesn't clear the session cookie -- follow with a real // Deletion doesn't clear the session cookie -- follow with a real
@@ -166,7 +168,7 @@ export function Profile({ onSaved }: { onSaved?: (p: ProfileType) => void }) {
document.body.appendChild(form); document.body.appendChild(form);
form.submit(); form.submit();
} catch (e) { } catch (e) {
setError(String(e)); showError(String(e));
setDeleting(false); setDeleting(false);
} }
} }
@@ -174,14 +176,13 @@ export function Profile({ onSaved }: { onSaved?: (p: ProfileType) => void }) {
if (!profile) { if (!profile) {
return ( return (
<div className="page"> <div className="page">
{error ? <p className="error">{error}</p> : <p>Loading...</p>} {profileLoadFailed ? <p className="empty-state">Couldn't load profile.</p> : <p>Loading...</p>}
</div> </div>
); );
} }
return ( return (
<div className="page profile-page"> <div className="page profile-page">
{error && <p className="error">{error}</p>}
{saved && <p className="garmin-connection-message">Saved.</p>} {saved && <p className="garmin-connection-message">Saved.</p>}
<fieldset className="kind-editor"> <fieldset className="kind-editor">