From dd65f7c4feeb2999e0f271392bc0b218ea996af7 Mon Sep 17 00:00:00 2001 From: Ronny Date: Sat, 11 Apr 2026 19:54:32 +0200 Subject: [PATCH] fix: StatsRing NaN with no data, Toast showing repeatedly, timer stability - StatsRing: compute all without || 1 fallback, guard segments/legend behind hasData, use seg.count in legend to avoid NaN - useRankChange: skip toast if already shown this session via sessionStorage - Toast: use ref for onDismiss to prevent timer reset on every render Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/components/StatsRing.tsx | 33 +++++++++++++++------------ frontend/src/components/Toast.tsx | 9 +++++--- frontend/src/hooks/useRankChange.ts | 5 ++++ 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/frontend/src/components/StatsRing.tsx b/frontend/src/components/StatsRing.tsx index 697a746..baf1edf 100644 --- a/frontend/src/components/StatsRing.tsx +++ b/frontend/src/components/StatsRing.tsx @@ -10,13 +10,14 @@ interface Props { export default function StatsRing({ exact, tendency, wrong, total }: Props) { const radius = 55; const circumference = 2 * Math.PI * radius; - const all = exact + tendency + wrong || 1; + const all = exact + tendency + wrong; + const hasData = all > 0; - const segments = [ - { value: exact / all, color: 'var(--gold)', label: 'Exakt' }, - { value: tendency / all, color: 'var(--success)', label: 'Tendenz' }, - { value: wrong / all, color: 'var(--error)', label: 'Falsch' }, - ]; + const segments = hasData ? [ + { value: exact / all, color: 'var(--gold)', label: 'Exakt', count: exact }, + { value: tendency / all, color: 'var(--success)', label: 'Tendenz', count: tendency }, + { value: wrong / all, color: 'var(--error)', label: 'Falsch', count: wrong }, + ] : []; let offset = 0; @@ -49,17 +50,19 @@ export default function StatsRing({ exact, tendency, wrong, total }: Props) { - Punkte + {hasData ? 'Punkte' : 'Keine Tipps'} -
- {segments.map((seg, i) => ( - - - {seg.label}: {Math.round(seg.value * all)} - - ))} -
+ {hasData && ( +
+ {segments.map((seg, i) => ( + + + {seg.label}: {seg.count} + + ))} +
+ )} ); } diff --git a/frontend/src/components/Toast.tsx b/frontend/src/components/Toast.tsx index 78a6577..bdcc09f 100644 --- a/frontend/src/components/Toast.tsx +++ b/frontend/src/components/Toast.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useRef } from 'react'; import styles from './Toast.module.css'; interface Props { @@ -8,10 +8,13 @@ interface Props { } export default function Toast({ message, onDismiss, duration = 5000 }: Props) { + const onDismissRef = useRef(onDismiss); + onDismissRef.current = onDismiss; + useEffect(() => { - const timer = setTimeout(onDismiss, duration); + const timer = setTimeout(() => onDismissRef.current(), duration); return () => clearTimeout(timer); - }, [onDismiss, duration]); + }, [duration]); return (
diff --git a/frontend/src/hooks/useRankChange.ts b/frontend/src/hooks/useRankChange.ts index 7227ea8..e131880 100644 --- a/frontend/src/hooks/useRankChange.ts +++ b/frontend/src/hooks/useRankChange.ts @@ -2,11 +2,15 @@ import { useState, useEffect } from 'react'; import { api } from '../api/client'; const RANK_KEY = 'tippspiel_last_rank'; +const SHOWN_KEY = 'tippspiel_rank_toast_shown'; export function useRankChange() { const [message, setMessage] = useState(null); useEffect(() => { + // Only show once per session + if (sessionStorage.getItem(SHOWN_KEY)) return; + api.getMyStats().then(stats => { if (!stats.rank) return; const lastRank = parseInt(localStorage.getItem(RANK_KEY) || '0'); @@ -16,6 +20,7 @@ export function useRankChange() { } else { setMessage(`⬇️ Du bist auf Platz ${stats.rank} gerutscht — hol dir die Punkte zurück!`); } + sessionStorage.setItem(SHOWN_KEY, '1'); } localStorage.setItem(RANK_KEY, String(stats.rank)); }).catch(() => {});