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/TipModal.tsx
T
Ronny 7dc66e50bf
Build & Deploy Tippspiel / build (push) Successful in 50s
feat: local country flags replacing team crests
48 country flags downloaded from flagcdn.com (320px PNG, ~55KB total)
stored in frontend/public/flags/{iso-code}.png.

New utility getFlagUrl() maps team names to local flag files.
Applied to MatchCard, DashboardPage, and TipModal.
Falls back to original crest URL if no mapping exists (e.g. TBD).

No external API calls at runtime — all flags served statically.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 16:31:24 +02:00

155 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { useState } from 'react';
import { Match, api } from '../api/client';
import { getFlagUrl } from '../utils/flagUrl';
import styles from './TipModal.module.css';
interface Props {
match: Match;
onClose: () => void;
onSaved: (matchId: number, home: number, away: number) => void;
}
type Tendency = 'home' | 'draw' | 'away';
function getTendency(home: number, away: number): Tendency {
if (home > away) return 'home';
if (away > home) return 'away';
return 'draw';
}
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 [showSuccess, setShowSuccess] = useState(false);
const [error, setError] = useState<string | null>(null);
const tendency = getTendency(home, away);
const tendencyLabel =
tendency === 'home' ? match.homeTeam.shortName || match.homeTeam.name :
tendency === 'away' ? match.awayTeam.shortName || match.awayTeam.name :
'Unentschieden';
const tendencyColor =
tendency === 'home' ? 'var(--primary)' :
tendency === 'away' ? 'var(--cyan)' :
'var(--gold)';
const handleSave = async () => {
setSaving(true);
setError(null);
try {
await api.submitTip(match.id, home, away);
setShowSuccess(true);
if (navigator.vibrate) navigator.vibrate(50); // haptic feedback
setTimeout(() => {
setShowSuccess(false);
onSaved(match.id, home, away);
onClose();
}, 1200);
} catch (e) {
setError((e as Error).message);
setSaving(false);
}
};
return (
<div className={styles.overlay} onClick={onClose}>
<div className={styles.sheet} onClick={e => e.stopPropagation()}>
{/* Drag handle */}
<div className={styles.handle} />
{/* Teams mit Flaggen */}
<div className={styles.teamsRow}>
<div className={styles.teamBlock}>
<div className={styles.flagLarge}>
{match.homeTeam.name
? <img src={getFlagUrl(match.homeTeam.name, match.homeTeam.crest)} alt={match.homeTeam.name} className={styles.flagImg} />
: <span className={styles.flagEmoji}>🏳</span>
}
</div>
<span className={styles.teamName}>{match.homeTeam.name}</span>
</div>
<div className={styles.vsBlock} />
<div className={styles.teamBlock}>
<div className={styles.flagLarge}>
{match.awayTeam.name
? <img src={getFlagUrl(match.awayTeam.name, match.awayTeam.crest)} alt={match.awayTeam.name} className={styles.flagImg} />
: <span className={styles.flagEmoji}>🏳</span>
}
</div>
<span className={styles.teamName}>{match.awayTeam.name}</span>
</div>
</div>
{/* Score Picker */}
<div className={styles.pickerSection}>
<p className={styles.pickerLabel}>Dein Tipp</p>
<div className={styles.pickerRow}>
<ScorePicker value={home} onChange={setHome} />
<div className={styles.pickerColon}>:</div>
<ScorePicker value={away} onChange={setAway} />
</div>
</div>
{/* Tendenz */}
<div className={styles.tendencyBar} style={{ '--tendency-color': tendencyColor } as React.CSSProperties}>
<span className={styles.tendencyIcon}>
{tendency === 'draw' ? '🤝' : tendency === 'home' ? '🏠' : '✈️'}
</span>
<span className={styles.tendencyText}>
Tendenz: <strong>{tendencyLabel}</strong>
</span>
</div>
{error && <div className={styles.error}>{error}</div>}
{showSuccess && (
<div className={styles.successOverlay}>
<div className={styles.successCheck}></div>
<div className={styles.successText}>Dein Tipp ist drin! 🎯</div>
</div>
)}
{/* CTA */}
<button
className={`btn-primary ${styles.saveBtn}`}
onClick={handleSave}
disabled={saving}
>
{saving ? '⏳ Wird gespeichert…' : '✓ Tipp bestätigen'}
</button>
<button className={styles.cancelBtn} onClick={onClose}>
Abbrechen
</button>
</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))}
aria-label="Erhöhen"
>
+
</button>
<span className={styles.pickerValue}>{value}</span>
<button
className={styles.pickerBtn}
onClick={() => onChange(Math.max(0, value - 1))}
aria-label="Verringern"
>
</button>
</div>
);
}