89046a2e29
Success overlay with animated checkmark and 'Dein Tipp ist drin! 🎯' message. Haptic vibration on mobile. Auto-closes after 1.2s. - Add showSuccess state to TipModal - Trigger vibration feedback on successful submit - Display success overlay with popIn animation for checkmark - Auto-close modal after success animation completes - Add CSS animations (fadeIn, popIn) to TipModal.module.css Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
56 lines
5.7 KiB
JavaScript
56 lines
5.7 KiB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
import { useState, useEffect } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { api } from '../api/client';
|
|
import styles from './LeaderboardPage.module.css';
|
|
function initials(name) {
|
|
return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
|
|
}
|
|
function TrendIcon({ entry, prev }) {
|
|
if (!prev)
|
|
return _jsx("span", { className: styles.trendNeutral, children: "\u2192" });
|
|
if (entry.total_points > prev.total_points)
|
|
return _jsx("span", { className: styles.trendUp, children: "\u2197" });
|
|
if (entry.total_points < prev.total_points)
|
|
return _jsx("span", { className: styles.trendDown, children: "\u2198" });
|
|
return _jsx("span", { className: styles.trendNeutral, children: "\u2192" });
|
|
}
|
|
export default function LeaderboardPage() {
|
|
const [data, setData] = useState(null);
|
|
const [tippableCount, setTippableCount] = useState(0);
|
|
const [loading, setLoading] = useState(true);
|
|
const navigate = useNavigate();
|
|
useEffect(() => {
|
|
Promise.all([
|
|
api.getLeaderboard(),
|
|
api.getMatches(),
|
|
]).then(([lb, matches]) => {
|
|
setData(lb);
|
|
setTippableCount(matches.matches.filter(m => m.tippable && !m.userTip).length);
|
|
}).finally(() => setLoading(false));
|
|
}, []);
|
|
if (loading)
|
|
return (_jsx("div", { className: styles.loading, children: _jsx("div", { className: styles.spinner }) }));
|
|
if (!data)
|
|
return null;
|
|
const { entries, currentUserId, currentUserRank, totalParticipants } = data;
|
|
const top3 = entries.slice(0, 3);
|
|
const rest = entries.slice(3);
|
|
const podiumSlots = [];
|
|
if (top3[1])
|
|
podiumSlots.push({ entry: top3[1], rank: 2, medal: '🥈', colorClass: styles.silver, barHeight: '64px' });
|
|
if (top3[0])
|
|
podiumSlots.push({ entry: top3[0], rank: 1, medal: '🥇', colorClass: styles.gold, barHeight: '96px' });
|
|
if (top3[2])
|
|
podiumSlots.push({ entry: top3[2], rank: 3, medal: '🥉', colorClass: styles.bronze, barHeight: '48px' });
|
|
return (_jsxs("div", { className: styles.page, children: [_jsxs("div", { className: styles.pageHeader, children: [_jsx("h1", { className: `font-display ${styles.title}`, children: "Rangliste" }), _jsxs("div", { className: styles.meta, children: [totalParticipants, " Teilnehmer", currentUserRank ? ` · Du: Platz ${currentUserRank}` : ''] })] }), entries.length === 0 ? (_jsx("div", { className: styles.empty, children: "Noch keine Punkte vergeben. Spiele m\u00FCssen erst abgeschlossen sein." })) : (_jsxs(_Fragment, { children: [top3.length > 0 && (_jsx("div", { className: styles.podiumWrap, children: podiumSlots.map(({ entry, rank, medal, colorClass, barHeight }) => {
|
|
const isMe = entry.user_id === currentUserId;
|
|
const isFirst = rank === 1;
|
|
return (_jsxs("div", { className: `${styles.podiumCard} ${isFirst ? styles.podiumFirst : ''}`, children: [_jsx("div", { className: styles.podiumMedal, children: medal }), _jsx("div", { className: `${styles.podiumAvatar} ${colorClass} ${isFirst ? styles.podiumAvatarLarge : ''} ${isMe ? styles.podiumAvatarMe : ''}`, children: initials(entry.full_name) }), _jsxs("div", { className: `${styles.podiumName} ${isMe ? styles.nameMe : ''}`, children: [entry.full_name.split(' ')[0], isMe ? ' (Ich)' : ''] }), _jsxs("div", { className: `${styles.podiumPoints} ${colorClass}`, children: [entry.total_points.toLocaleString('de-DE'), _jsx("span", { className: styles.podiumPtLabel, children: " Pkt" })] }), _jsx("div", { className: `${styles.podiumBar} ${colorClass}Bar`, style: { height: barHeight } })] }, entry.user_id));
|
|
}) })), rest.length > 0 && (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.listHeader, children: [_jsx("span", { children: "POS" }), _jsx("span", { children: "SPIELER" }), _jsx("span", { children: "TREND" }), _jsx("span", { children: "PUNKTE" })] }), _jsx("div", { className: styles.list, children: rest.map((entry, i) => {
|
|
const isMe = entry.user_id === currentUserId;
|
|
const prev = rest[i - 1];
|
|
return (_jsxs("div", { className: `card ${styles.row} ${isMe ? styles.rowMe : ''}`, children: [_jsx("div", { className: styles.rankCol, children: _jsx("span", { className: styles.rankNum, children: entry.rank }) }), _jsx("div", { className: `${styles.avatarSmall} ${isMe ? styles.avatarSmallMe : ''}`, children: initials(entry.full_name) }), _jsxs("div", { className: styles.nameCol, children: [_jsx("div", { className: styles.nameRow, children: _jsxs("span", { className: `${styles.rowName} ${isMe ? styles.nameMe : ''}`, children: [entry.full_name, isMe ? ' (Ich)' : ''] }) }), entry.team && _jsx("div", { className: styles.rowTeam, children: entry.team }), isMe && _jsx("div", { className: styles.aufholjagd, children: "AUFHOLJAGD!" })] }), _jsx("div", { className: styles.trendCol, children: _jsx(TrendIcon, { entry: entry, prev: prev }) }), _jsx("div", { className: styles.pointsCol, children: entry.total_points.toLocaleString('de-DE') })] }, entry.user_id));
|
|
}) })] })), tippableCount > 0 && (_jsxs("div", { className: `card ${styles.ctaCard}`, children: [_jsxs("div", { className: styles.ctaText, children: [_jsx("div", { className: styles.ctaTitle, children: "Punkte sichern!" }), _jsxs("div", { className: styles.ctaBody, children: [tippableCount, " Spiel", tippableCount !== 1 ? 'e' : '', " noch ohne Tipp \u2014 kletter nach oben."] })] }), _jsx("button", { className: `btn-primary ${styles.ctaBtn}`, onClick: () => navigate('/'), children: "TIPPEN" })] }))] }))] }));
|
|
}
|