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>
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 118 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 292 B |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 154 B |
|
After Width: | Height: | Size: 252 B |
|
After Width: | Height: | Size: 231 B |
|
After Width: | Height: | Size: 854 B |
|
After Width: | Height: | Size: 604 B |
|
After Width: | Height: | Size: 940 B |
|
After Width: | Height: | Size: 151 B |
|
After Width: | Height: | Size: 989 B |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 254 B |
|
After Width: | Height: | Size: 245 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 625 B |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 789 B |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 658 B |
|
After Width: | Height: | Size: 932 B |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 928 B |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 153 B |
|
After Width: | Height: | Size: 323 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 307 B |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 138 B |
|
After Width: | Height: | Size: 681 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 947 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 566 B |
|
After Width: | Height: | Size: 982 B |
@@ -1,6 +1,7 @@
|
|||||||
import { Check, TrendingUp, X } from 'lucide-react';
|
import { Check, TrendingUp, X } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Match } from '../api/client';
|
import { Match } from '../api/client';
|
||||||
|
import { getFlagUrl } from '../utils/flagUrl';
|
||||||
import styles from './MatchCard.module.css';
|
import styles from './MatchCard.module.css';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -41,10 +42,11 @@ function formatKickoff(utcDate: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function FlagBox({ crest, name }: { crest: string | null; name: string }) {
|
function FlagBox({ crest, name }: { crest: string | null; name: string }) {
|
||||||
|
const src = getFlagUrl(name, crest);
|
||||||
return (
|
return (
|
||||||
<div className={styles.flagBox}>
|
<div className={styles.flagBox}>
|
||||||
{crest
|
{src
|
||||||
? <img className={styles.crest} src={crest} alt={name} />
|
? <img className={styles.crest} src={src} alt={name} />
|
||||||
: <span style={{ fontSize: 18 }}>🏳️</span>
|
: <span style={{ fontSize: 18 }}>🏳️</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Match, api } from '../api/client';
|
import { Match, api } from '../api/client';
|
||||||
|
import { getFlagUrl } from '../utils/flagUrl';
|
||||||
import styles from './TipModal.module.css';
|
import styles from './TipModal.module.css';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -63,8 +64,8 @@ export default function TipModal({ match, onClose, onSaved }: Props) {
|
|||||||
<div className={styles.teamsRow}>
|
<div className={styles.teamsRow}>
|
||||||
<div className={styles.teamBlock}>
|
<div className={styles.teamBlock}>
|
||||||
<div className={styles.flagLarge}>
|
<div className={styles.flagLarge}>
|
||||||
{match.homeTeam.crest
|
{match.homeTeam.name
|
||||||
? <img src={match.homeTeam.crest} alt={match.homeTeam.name} className={styles.flagImg} />
|
? <img src={getFlagUrl(match.homeTeam.name, match.homeTeam.crest)} alt={match.homeTeam.name} className={styles.flagImg} />
|
||||||
: <span className={styles.flagEmoji}>🏳️</span>
|
: <span className={styles.flagEmoji}>🏳️</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -75,8 +76,8 @@ export default function TipModal({ match, onClose, onSaved }: Props) {
|
|||||||
|
|
||||||
<div className={styles.teamBlock}>
|
<div className={styles.teamBlock}>
|
||||||
<div className={styles.flagLarge}>
|
<div className={styles.flagLarge}>
|
||||||
{match.awayTeam.crest
|
{match.awayTeam.name
|
||||||
? <img src={match.awayTeam.crest} alt={match.awayTeam.name} className={styles.flagImg} />
|
? <img src={getFlagUrl(match.awayTeam.name, match.awayTeam.crest)} alt={match.awayTeam.name} className={styles.flagImg} />
|
||||||
: <span className={styles.flagEmoji}>🏳️</span>
|
: <span className={styles.flagEmoji}>🏳️</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { TrendingUp, TrendingDown } from 'lucide-react';
|
import { TrendingUp, TrendingDown } from 'lucide-react';
|
||||||
import { api, DashboardData, Match } from '../api/client';
|
import { api, DashboardData, Match } from '../api/client';
|
||||||
|
import { getFlagUrl } from '../utils/flagUrl';
|
||||||
import TipModal from '../components/TipModal';
|
import TipModal from '../components/TipModal';
|
||||||
import styles from './DashboardPage.module.css';
|
import styles from './DashboardPage.module.css';
|
||||||
|
|
||||||
@@ -88,8 +89,8 @@ export default function DashboardPage(_props: Props) {
|
|||||||
<div className={styles.heroTeams}>
|
<div className={styles.heroTeams}>
|
||||||
<div className={styles.heroTeam}>
|
<div className={styles.heroTeam}>
|
||||||
<div className={styles.heroCrestBox}>
|
<div className={styles.heroCrestBox}>
|
||||||
{hero.match.homeTeam.crest ? (
|
{hero.match.homeTeam.name ? (
|
||||||
<img src={hero.match.homeTeam.crest} alt={hero.match.homeTeam.name} className={styles.heroCrest} />
|
<img src={getFlagUrl(hero.match.homeTeam.name, hero.match.homeTeam.crest)} alt={hero.match.homeTeam.name} className={styles.heroCrest} />
|
||||||
) : (
|
) : (
|
||||||
<span className={styles.heroCrestFallback}>🏳️</span>
|
<span className={styles.heroCrestFallback}>🏳️</span>
|
||||||
)}
|
)}
|
||||||
@@ -108,8 +109,8 @@ export default function DashboardPage(_props: Props) {
|
|||||||
|
|
||||||
<div className={styles.heroTeam}>
|
<div className={styles.heroTeam}>
|
||||||
<div className={styles.heroCrestBox}>
|
<div className={styles.heroCrestBox}>
|
||||||
{hero.match.awayTeam.crest ? (
|
{hero.match.awayTeam.name ? (
|
||||||
<img src={hero.match.awayTeam.crest} alt={hero.match.awayTeam.name} className={styles.heroCrest} />
|
<img src={getFlagUrl(hero.match.awayTeam.name, hero.match.awayTeam.crest)} alt={hero.match.awayTeam.name} className={styles.heroCrest} />
|
||||||
) : (
|
) : (
|
||||||
<span className={styles.heroCrestFallback}>🏳️</span>
|
<span className={styles.heroCrestFallback}>🏳️</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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 || '';
|
||||||
|
}
|
||||||