This repository has been archived on 2026-05-06. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
tippspiel/frontend/src/components/MatchCard.js
T
Ronny 89046a2e29 feat: tip confirmation animation with haptic feedback
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>
2026-04-11 19:08:39 +02:00

86 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
import { Check, TrendingUp, X } from 'lucide-react';
import { useEffect, useState } from 'react';
import styles from './MatchCard.module.css';
function getCardState(match) {
if (match.status === 'IN_PLAY' || match.status === 'PAUSED')
return 'live';
if (match.status === 'FINISHED') {
return match.userTip ? 'finished' : 'missed';
}
// SCHEDULED or TIMED
return match.userTip ? 'tipped' : 'open';
}
function useCountdown(minutesUntilKickoff) {
const [remaining, setRemaining] = useState(minutesUntilKickoff);
useEffect(() => {
if (minutesUntilKickoff > 60)
return; // only active for <1h
setRemaining(minutesUntilKickoff);
const interval = setInterval(() => {
setRemaining(r => Math.max(0, r - 1 / 60));
}, 1000);
return () => clearInterval(interval);
}, [minutesUntilKickoff]);
return remaining;
}
const STATUS_LABELS = {
SCHEDULED: 'Geplant',
TIMED: 'Terminiert',
IN_PLAY: 'Live',
PAUSED: 'Pause',
FINISHED: 'Beendet',
POSTPONED: 'Verschoben',
CANCELLED: 'Abgesagt',
};
function formatKickoff(utcDate) {
return new Date(utcDate).toLocaleString('de-DE', {
hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Berlin',
}) + ' Uhr';
}
function CountdownBadge({ minutes }) {
if (minutes <= 0)
return null;
if (minutes < 60)
return _jsxs("span", { className: styles.badgeUrgent, children: ["\u26A1 in ", minutes, " Min."] });
const h = Math.floor(minutes / 60);
const m = minutes % 60;
if (h < 24)
return _jsxs("span", { className: styles.badge, children: ["in ", h, "h", m > 0 ? ` ${m}m` : ''] });
const d = Math.floor(h / 24);
return _jsxs("span", { className: styles.badge, children: ["in ", d, " Tag", d > 1 ? 'en' : ''] });
}
function FlagBox({ crest, name }) {
return (_jsx("div", { className: styles.flagBox, children: crest
? _jsx("img", { className: styles.crest, src: crest, alt: name })
: _jsx("span", { style: { fontSize: 18 }, children: "\uD83C\uDFF3\uFE0F" }) }));
}
export default function MatchCard({ match, onTip }) {
const state = getCardState(match);
const remaining = useCountdown(match.minutesUntilKickoff);
const remainingMins = Math.ceil(remaining);
const isFinished = state === 'finished' || state === 'missed';
const isLive = state === 'live';
const hasTip = !!match.userTip;
const points = match.userTip?.points ?? null;
const resultClass = points === 3 ? styles.exact :
points === 1 ? styles.tendency :
(points === 0 && isFinished) ? styles.wrong : '';
const glowClass = isFinished && points === 3 ? styles.glowExact :
isFinished && points === 1 ? styles.glowTendency :
isFinished && points === 0 ? styles.glowWrong : '';
return (_jsxs("div", { className: `card ${styles.card} ${styles[`card_${state}`]} ${isLive ? styles.live : ''} ${glowClass}`, children: [_jsxs("div", { className: styles.topRow, children: [_jsxs("span", { className: `${styles.status} ${isLive ? styles.statusLive : ''}`, children: [isLive && _jsx("span", { className: styles.liveDot }), STATUS_LABELS[match.status] ?? match.status] }), match.group && (_jsx("span", { className: styles.group, children: match.group.replace('GROUP_', 'Gruppe ') })), (state === 'open' || state === 'tipped') && match.tippable && (match.minutesUntilKickoff < 60 ? (_jsxs("span", { className: `${styles.countdown} ${remainingMins < 5 ? styles.countdownUrgent : ''}`, children: ["Noch ", remainingMins, " Min!"] })) : (_jsx(CountdownBadge, { minutes: match.minutesUntilKickoff })))] }), _jsxs("div", { className: styles.matchRow, children: [_jsxs("div", { className: styles.teamHome, children: [_jsx("span", { className: styles.teamName, children: match.homeTeam.name }), _jsx(FlagBox, { crest: match.homeTeam.crest, name: match.homeTeam.name })] }), _jsx("div", { className: styles.scoreBox, children: isFinished || isLive ? (_jsxs("div", { className: styles.scoreStack, children: [_jsxs("span", { className: styles.score, children: [match.score.home ?? '', "\u00A0:\u00A0", match.score.away ?? ''] }), isLive && hasTip && (_jsxs("span", { className: styles.liveTipCompare, children: ["Tipp: ", match.userTip.home, ":", match.userTip.away] }))] })) : (_jsx("div", { className: styles.kickoffCenter, children: _jsx("span", { className: styles.kickoffCenterTime, children: formatKickoff(match.utcDate) }) })) }), _jsxs("div", { className: styles.teamAway, children: [_jsx(FlagBox, { crest: match.awayTeam.crest, name: match.awayTeam.name }), _jsx("span", { className: styles.teamName, children: match.awayTeam.name })] })] }), _jsx("div", { className: `${styles.tipRow} ${hasTip && points !== null ? `${styles.resultBanner} ${resultClass}` : ''}`, children: state === 'missed' ? (
/* ── Missed: no tip, match finished ── */
_jsx("span", { className: styles.missedLabel, children: "Nicht getippt" })) : state === 'live' ? (
/* ── Live: no tip input, locked ── */
hasTip ? (_jsxs("div", { className: styles.tipDisplay, children: [_jsx("div", { className: styles.tipLeft }), _jsxs("div", { className: styles.tipCenter, children: [_jsx("span", { className: styles.tipLabel, children: "DEIN TIPP" }), _jsxs("span", { className: styles.tipScore, children: [match.userTip.home, " : ", match.userTip.away] })] }), _jsx("div", { className: styles.tipRight })] })) : (_jsx("span", { className: styles.noTip, children: "Kein Tipp abgegeben" }))) : hasTip ? (points !== null ? (
/* ── Auswertungs-Banner ── */
_jsxs("div", { className: styles.tipDisplay, children: [_jsxs("div", { className: `${styles.tipLeft} ${styles.bannerLeft}`, children: [_jsx("span", { className: styles.resultIcon, children: points === 3 ? _jsx(Check, { size: 14, strokeWidth: 3 }) :
points === 1 ? _jsx(TrendingUp, { size: 14, strokeWidth: 2.5 }) :
_jsx(X, { size: 14, strokeWidth: 3 }) }), _jsx("span", { className: styles.resultLabel, children: points === 3 ? 'Exakt' : points === 1 ? 'Tendenz' : 'Falsch' })] }), _jsx("div", { className: styles.tipCenter, children: _jsxs("span", { className: styles.tipScoreBanner, children: [match.userTip.home, " : ", match.userTip.away] }) }), _jsx("div", { className: styles.tipRight, children: _jsx("span", { className: `${styles.pointsBadge} ${points === 3 ? styles.pointsBadge_exact :
points === 1 ? styles.pointsBadge_tendency :
styles.pointsBadge_wrong}`, children: points === 0 ? '0 Pkt.' : `+${points} Pkt.` }) })] })) : (
/* ── Tipp vorhanden, noch nicht ausgewertet (tipped state) ── */
_jsxs("div", { className: styles.tipDisplay, children: [_jsx("div", { className: styles.tipLeft, children: match.tippable && (_jsx("button", { className: styles.changeBtn, onClick: onTip, children: "\u00C4ndern" })) }), _jsxs("div", { className: styles.tipCenter, children: [!match.tippable && _jsx("span", { className: styles.tipLabel, children: "DEIN TIPP" }), _jsxs("span", { className: styles.tipDisplay_score, children: [_jsx(Check, { size: 13, strokeWidth: 3, style: { color: 'var(--success)', flexShrink: 0 } }), _jsxs("span", { className: styles.tipScore, children: [match.userTip.home, " : ", match.userTip.away] })] })] }), _jsx("div", { className: styles.tipRight })] }))) : match.tippable ? (_jsx("button", { className: `btn-primary ${styles.tipBtn}`, onClick: onTip, children: "Tipp abgeben" })) : (_jsx("span", { className: styles.noTip, children: "Kein Tipp abgegeben" })) })] }));
}