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 <noreply@anthropic.com>
This commit is contained in:
@@ -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) {
|
||||
</text>
|
||||
<text x="70" y="85" textAnchor="middle"
|
||||
fill="var(--text-secondary)" fontSize="11">
|
||||
Punkte
|
||||
{hasData ? 'Punkte' : 'Keine Tipps'}
|
||||
</text>
|
||||
</svg>
|
||||
<div className={styles.legend}>
|
||||
{segments.map((seg, i) => (
|
||||
<span key={i} className={styles.legendItem}>
|
||||
<span className={styles.dot} style={{ background: seg.color }} />
|
||||
{seg.label}: {Math.round(seg.value * all)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{hasData && (
|
||||
<div className={styles.legend}>
|
||||
{segments.map((seg, i) => (
|
||||
<span key={i} className={styles.legendItem}>
|
||||
<span className={styles.dot} style={{ background: seg.color }} />
|
||||
{seg.label}: {seg.count}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className={`card ${styles.toast}`} onClick={onDismiss}>
|
||||
|
||||
@@ -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<string | null>(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(() => {});
|
||||
|
||||
Reference in New Issue
Block a user