feat: local country flags replacing team crests
Build & Deploy Tippspiel / build (push) Successful in 50s

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>
This commit is contained in:
Ronny
2026-04-12 16:31:24 +02:00
parent 2dabd1f958
commit 7dc66e50bf
52 changed files with 80 additions and 10 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 854 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 940 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 989 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 625 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 789 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 932 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 928 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 947 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 566 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

+4 -2
View File
@@ -1,6 +1,7 @@
import { Check, TrendingUp, X } from 'lucide-react';
import { useEffect, useState } from 'react';
import { Match } from '../api/client';
import { getFlagUrl } from '../utils/flagUrl';
import styles from './MatchCard.module.css';
interface Props {
@@ -41,10 +42,11 @@ function formatKickoff(utcDate: string): string {
}
function FlagBox({ crest, name }: { crest: string | null; name: string }) {
const src = getFlagUrl(name, crest);
return (
<div className={styles.flagBox}>
{crest
? <img className={styles.crest} src={crest} alt={name} />
{src
? <img className={styles.crest} src={src} alt={name} />
: <span style={{ fontSize: 18 }}>🏳</span>
}
</div>
+5 -4
View File
@@ -1,5 +1,6 @@
import { useState } from 'react';
import { Match, api } from '../api/client';
import { getFlagUrl } from '../utils/flagUrl';
import styles from './TipModal.module.css';
interface Props {
@@ -63,8 +64,8 @@ export default function TipModal({ match, onClose, onSaved }: Props) {
<div className={styles.teamsRow}>
<div className={styles.teamBlock}>
<div className={styles.flagLarge}>
{match.homeTeam.crest
? <img src={match.homeTeam.crest} alt={match.homeTeam.name} className={styles.flagImg} />
{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>
@@ -75,8 +76,8 @@ export default function TipModal({ match, onClose, onSaved }: Props) {
<div className={styles.teamBlock}>
<div className={styles.flagLarge}>
{match.awayTeam.crest
? <img src={match.awayTeam.crest} alt={match.awayTeam.name} className={styles.flagImg} />
{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>
+5 -4
View File
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { TrendingUp, TrendingDown } from 'lucide-react';
import { api, DashboardData, Match } from '../api/client';
import { getFlagUrl } from '../utils/flagUrl';
import TipModal from '../components/TipModal';
import styles from './DashboardPage.module.css';
@@ -88,8 +89,8 @@ export default function DashboardPage(_props: Props) {
<div className={styles.heroTeams}>
<div className={styles.heroTeam}>
<div className={styles.heroCrestBox}>
{hero.match.homeTeam.crest ? (
<img src={hero.match.homeTeam.crest} alt={hero.match.homeTeam.name} className={styles.heroCrest} />
{hero.match.homeTeam.name ? (
<img src={getFlagUrl(hero.match.homeTeam.name, hero.match.homeTeam.crest)} alt={hero.match.homeTeam.name} className={styles.heroCrest} />
) : (
<span className={styles.heroCrestFallback}>🏳</span>
)}
@@ -108,8 +109,8 @@ export default function DashboardPage(_props: Props) {
<div className={styles.heroTeam}>
<div className={styles.heroCrestBox}>
{hero.match.awayTeam.crest ? (
<img src={hero.match.awayTeam.crest} alt={hero.match.awayTeam.name} className={styles.heroCrest} />
{hero.match.awayTeam.name ? (
<img src={getFlagUrl(hero.match.awayTeam.name, hero.match.awayTeam.crest)} alt={hero.match.awayTeam.name} className={styles.heroCrest} />
) : (
<span className={styles.heroCrestFallback}>🏳</span>
)}
+66
View File
@@ -0,0 +1,66 @@
/**
* Maps team names to local flag image paths.
* Flags are stored as static PNGs in /flags/{iso-code}.png
* Source: flagcdn.com (320px width, downloaded once)
*/
const TEAM_TO_FLAG: Record<string, string> = {
'Algeria': 'dz',
'Argentina': 'ar',
'Australia': 'au',
'Austria': 'at',
'Belgium': 'be',
'Bosnia-Herzegovina': 'ba',
'Brazil': 'br',
'Canada': 'ca',
'Cape Verde Islands': 'cv',
'Colombia': 'co',
'Congo DR': 'cd',
'Croatia': 'hr',
'Curaçao': 'cw',
'Czechia': 'cz',
'Ecuador': 'ec',
'Egypt': 'eg',
'England': 'gb-eng',
'France': 'fr',
'Germany': 'de',
'Ghana': 'gh',
'Haiti': 'ht',
'Iran': 'ir',
'Iraq': 'iq',
'Ivory Coast': 'ci',
'Japan': 'jp',
'Jordan': 'jo',
'Mexico': 'mx',
'Morocco': 'ma',
'Netherlands': 'nl',
'New Zealand': 'nz',
'Norway': 'no',
'Panama': 'pa',
'Paraguay': 'py',
'Portugal': 'pt',
'Qatar': 'qa',
'Saudi Arabia': 'sa',
'Scotland': 'gb-sct',
'Senegal': 'sn',
'South Africa': 'za',
'South Korea': 'kr',
'Spain': 'es',
'Sweden': 'se',
'Switzerland': 'ch',
'Tunisia': 'tn',
'Turkey': 'tr',
'United States': 'us',
'Uruguay': 'uy',
'Uzbekistan': 'uz',
};
/**
* Returns the local flag URL for a team name.
* Falls back to the original crest URL if no mapping exists.
*/
export function getFlagUrl(teamName: string, fallbackCrest: string | null): string {
const code = TEAM_TO_FLAG[teamName];
if (code) return `/flags/${code}.png`;
return fallbackCrest || '';
}