From a2d76be65ad57841bb4510e81e384e241405537d Mon Sep 17 00:00:00 2001 From: lashman Date: Fri, 2 Jan 2026 18:56:15 +0200 Subject: [PATCH] leaderboards --- frontend/src/components/Leaderboard.tsx | 78 +++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 frontend/src/components/Leaderboard.tsx diff --git a/frontend/src/components/Leaderboard.tsx b/frontend/src/components/Leaderboard.tsx new file mode 100644 index 0000000..c8c8fab --- /dev/null +++ b/frontend/src/components/Leaderboard.tsx @@ -0,0 +1,78 @@ +import { useState, useEffect } from 'react' +import { useTranslation } from 'react-i18next' +import { IconTrophyFilled, IconClockFilled, IconBulbFilled, IconAwardFilled, IconRosetteDiscountCheckFilled } from '@tabler/icons-react' +import { motion } from 'motion/react' +import * as api from '../api' +import type { LeaderboardEntry } from '../api' +import { useStore } from '../store' + +type Entry = LeaderboardEntry + +function formatTime(secs: number) { + const m = Math.floor(secs / 60) + const s = secs % 60 + return m > 0 ? `${m}:${s.toString().padStart(2, '0')}` : `${s}s` +} + +function RankIcon({ rank }: { rank: number }) { + if (rank === 1) return + if (rank === 2) return + if (rank === 3) return + return {rank}. +} + +export default function Leaderboard() { + const { t } = useTranslation() + const puzzleId = useStore(s => s.puzzleId) + const elapsed = useStore(s => s.elapsed) + const hintsUsed = useStore(s => s.hintsUsed) + const [entries, setEntries] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + if (!puzzleId) return + const t0 = setTimeout(() => setLoading(true), 0) + api.fetchLeaderboard(puzzleId).then(data => { + setEntries(data.entries || []) + setLoading(false) + }).catch(() => setLoading(false)) + return () => clearTimeout(t0) + }, [puzzleId]) + + if (loading || entries.length === 0) return null + + const userRank = entries.findIndex(e => e.time > elapsed) + 1 || entries.length + 1 + + return ( + +
+
+
+ {entries.slice(0, 10).map((entry, i) => ( +
+ + {entry.name || t('common.anonymous')} + + + {entry.hints > 0 && ( + + + )} +
+ ))} +
+
+ {t('leaderboard.yourTime', { time: formatTime(elapsed), detail: (hintsUsed > 0 ? (hintsUsed === 1 ? t('leaderboard.hintCount', { count: hintsUsed }) : t('leaderboard.hintCount_plural', { count: hintsUsed })) + ', ' : '') + '#' + userRank })} +
+
+ ) +}