feat: WM2026 Tippspiel - Initial Backend + Frontend
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
import { useState } from 'react';
|
||||
import { Match, api } from '../api/client';
|
||||
import styles from './TipModal.module.css';
|
||||
|
||||
interface Props {
|
||||
match: Match;
|
||||
onClose: () => void;
|
||||
onSaved: (matchId: number, home: number, away: number) => void;
|
||||
}
|
||||
|
||||
export default function TipModal({ match, onClose, onSaved }: Props) {
|
||||
const existing = match.userTip;
|
||||
const [home, setHome] = useState(existing?.home ?? 0);
|
||||
const [away, setAway] = useState(existing?.away ?? 0);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const adjust = (setter: React.Dispatch<React.SetStateAction<number>>, val: number, delta: number) => {
|
||||
setter(Math.max(0, Math.min(20, val + delta)));
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
await api.submitTip(match.id, home, away);
|
||||
onSaved(match.id, home, away);
|
||||
} catch (e) {
|
||||
setError((e as Error).message);
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Tendenz-Anzeige
|
||||
const tendency = home > away ? match.homeTeam.shortName :
|
||||
away > home ? match.awayTeam.shortName : 'Unentschieden';
|
||||
|
||||
return (
|
||||
<div className={styles.overlay} onClick={onClose}>
|
||||
<div className={styles.modal} onClick={e => e.stopPropagation()}>
|
||||
{/* Header */}
|
||||
<div className={styles.header}>
|
||||
<h2 className={styles.title}>Tipp abgeben</h2>
|
||||
<button className={styles.closeBtn} onClick={onClose}>✕</button>
|
||||
</div>
|
||||
|
||||
{/* Teams */}
|
||||
<div className={styles.teamsRow}>
|
||||
<div className={styles.team}>
|
||||
{match.homeTeam.crest && <img className={styles.crest} src={match.homeTeam.crest} alt="" />}
|
||||
<span className={styles.teamName}>{match.homeTeam.name}</span>
|
||||
</div>
|
||||
<span className={styles.vs}>vs</span>
|
||||
<div className={`${styles.team} ${styles.teamRight}`}>
|
||||
<span className={styles.teamName}>{match.awayTeam.name}</span>
|
||||
{match.awayTeam.crest && <img className={styles.crest} src={match.awayTeam.crest} alt="" />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Score Picker */}
|
||||
<div className={styles.pickerRow}>
|
||||
<ScorePicker value={home} onChange={v => setHome(v)} />
|
||||
<div className={styles.colon}>:</div>
|
||||
<ScorePicker value={away} onChange={v => setAway(v)} />
|
||||
</div>
|
||||
|
||||
{/* Tendenz */}
|
||||
<div className={styles.tendencyRow}>
|
||||
<span className={styles.tendencyLabel}>Tendenz:</span>
|
||||
<span className={styles.tendencyValue}>{tendency}</span>
|
||||
</div>
|
||||
|
||||
{error && <div className={styles.error}>{error}</div>}
|
||||
|
||||
{/* Buttons */}
|
||||
<div className={styles.actions}>
|
||||
<button className="btn-ghost" onClick={onClose}>Abbrechen</button>
|
||||
<button className="btn-primary" onClick={handleSave} disabled={saving}>
|
||||
{saving ? 'Wird gespeichert…' : '✓ Tipp speichern'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ScorePicker({ value, onChange }: { value: number; onChange: (v: number) => void }) {
|
||||
return (
|
||||
<div className={styles.picker}>
|
||||
<button className={styles.pickerBtn} onClick={() => onChange(Math.min(20, value + 1))}>+</button>
|
||||
<span className={styles.pickerValue}>{value}</span>
|
||||
<button className={styles.pickerBtn} onClick={() => onChange(Math.max(0, value - 1))}>–</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user