diff --git a/src/App.tsx b/src/App.tsx index 055c41a..659dae9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -128,6 +128,11 @@ export default function App() { const labels = useMemo(() => buildLabels(places), [places]); const placeById = useMemo(() => new Map(realPlaces.map((p) => [p.id, p])), [realPlaces]); + const savedCounts = useMemo(() => { + const c: Record = { match: 0, partial: 0, conflict: 0, incomplete: 0, fail: 0 }; + for (const a of assessments.values()) c[a.verdict]++; + return c; + }, [assessments]); const previewCounts = useCallback((p: Profile): Record => { const c: Record = { match: 0, partial: 0, conflict: 0, incomplete: 0, fail: 0 }; if (index) for (const pl of realPlaces) c[assessPlace(pl, p, index, reports).verdict]++; @@ -440,6 +445,7 @@ export default function App() { onDone={() => setView('places')} onSignOut={signOut} preview={previewCounts} + savedCounts={savedCounts} > {!needsProfile && ( { map.addSource('places', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } }); diff --git a/src/components/ProfileView.tsx b/src/components/ProfileView.tsx index 2b270a3..a165a74 100644 --- a/src/components/ProfileView.tsx +++ b/src/components/ProfileView.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import type { Account } from '../lib/auth'; import { useI18n, type Key } from '../lib/i18n'; import { DEFAULT_PROFILE, PRESETS, activePreset, bucket } from '../lib/profile'; @@ -13,23 +13,43 @@ interface Props { onDone: () => void; onSignOut: () => void; preview: (p: Profile) => Record; + /** Verdict counts for the saved profile, already computed by the app: saves a second pass over every place. */ + savedCounts: Record | null; children?: React.ReactNode; } const VERDICTS: Verdict[] = ['match', 'partial', 'conflict', 'incomplete', 'fail']; -export function ProfileView({ account, profile, onChange, onDone, onSignOut, preview, children }: Props) { +export function ProfileView({ account, profile, onChange, onDone, onSignOut, preview, savedCounts, children }: Props) { const { t } = useI18n(); const [draft, setDraft] = useState(profile ?? DEFAULT_PROFILE[account.preset ?? 'wheelchair']); - const update = (p: Profile) => { setDraft(p); if (profile) onChange(p); }; + // The draft follows the control instantly; the saved profile (which re-assesses every place) only catches up once changes pause. + const pending = useRef(null); + const timer = useRef(undefined); + const onChangeRef = useRef(onChange); + onChangeRef.current = onChange; + const flush = () => { + window.clearTimeout(timer.current); + if (pending.current) { onChangeRef.current(pending.current); pending.current = null; } + }; + const update = (p: Profile) => { + setDraft(p); + if (!profile) return; + pending.current = p; + window.clearTimeout(timer.current); + timer.current = window.setTimeout(flush, 300); + }; + useEffect(() => flush, []); // leaving the page must not drop the last change const set = (k: K, v: Profile[K]) => update({ ...draft, [k]: v }); - // Debounced live preview: how many places fall into each verdict with these limits. - const [counts, setCounts] = useState | null>(null); + // Live preview: how many places fall into each verdict with these limits. A saved profile reuses the app's own assessments. + const [onboardCounts, setOnboardCounts] = useState | null>(null); useEffect(() => { - const id = setTimeout(() => setCounts(preview(draft)), 250); + if (profile) return; + const id = setTimeout(() => setOnboardCounts(preview(draft)), 250); return () => clearTimeout(id); - }, [draft, preview]); + }, [profile, draft, preview]); + const counts = profile ? savedCounts : onboardCounts; const active = activePreset(draft); const presets = PRESETS.filter((p) => p.kind === draft.kind); @@ -151,7 +171,7 @@ export function ProfileView({ account, profile, onChange, onDone, onSignOut, pre

{t('profile.privacy')} {profile && t('profile.autosave')}

-