Files
worldcup/components/match-card.tsx
T
valknar 58b4114159 feat: initial commit — World Cup stats app with pnpm, Traefik, Docker
Full-stack World Cup web app (1930–2026):
- Next.js 16 + TailwindCSS 4 + GraphQL Yoga + Apollo Client 4 + Drizzle + PostgreSQL 16
- 23 tournaments synced from openfootball/worldcup.json (matches, goals, teams, stadiums, squads, standings)
- Pages: home (live), groups, stats, history, search, /tournaments/[year], /teams/[slug], /players/[name]
- Live match detection via isLive() + Apollo 60 s poll
- pnpm with node-linker=hoisted for Docker compatibility
- docker-compose.yml with Traefik labels (HTTPS redirect, TLS, security middleware)
- docker-compose.dev.yml for local dev (DB only, port 5432 exposed)
- Dockerfile: multi-stage pnpm build, standalone Next.js output, sync script bundled
- .env.example with all required variables documented
- Comprehensive README with local dev, deployment, schema, and GraphQL API reference

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 15:36:44 +02:00

87 lines
4.2 KiB
TypeScript
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 Link from 'next/link'
import { TeamFlag } from './team-flag'
import { LiveBadge } from './live-badge'
interface Team { name: string; iso2?: string | null }
interface Match {
id: number
year: number
round: string
group?: string | null
date?: string | null
time?: string | null
team1: Team
team2: Team
scoreFt?: number[] | null
scoreEt?: number[] | null
scoreP?: number[] | null
isLive: boolean
}
function formatDate(d: string) {
return new Date(d).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' })
}
export function MatchCard({ match, compact = false }: { match: Match; compact?: boolean }) {
const ft = match.scoreFt
const hasScore = ft != null
const winner = ft ? (ft[0] > ft[1] ? 'home' : ft[0] < ft[1] ? 'away' : 'draw') : null
if (compact) {
return (
<Link href={`/tournaments/${match.year}#match-${match.id}`} className="block">
<div className="bg-[#0a1810] border border-[rgba(34,197,94,0.08)] rounded-xl p-3.5 hover:border-[rgba(34,197,94,0.22)] transition-colors">
<div className="text-[9px] text-[#2a5c35] tracking-[0.1em] uppercase mb-2.5">
{match.round}{match.group ? ` · ${match.group}` : ''} · {match.date ? formatDate(match.date) : ''}
</div>
<div className="flex items-center gap-2">
<div className="flex-1 flex items-center gap-2 overflow-hidden">
<TeamFlag name={match.team1.name} iso2={match.team1.iso2} size="sm" />
<span className={`text-sm font-medium truncate ${winner === 'home' ? 'text-[#dff5e8]' : 'text-[#4a7a55]'}`}>
{match.team1.name}
</span>
</div>
<div className="font-['Bebas_Neue'] text-xl text-[#22c55e] flex-shrink-0 min-w-[44px] text-center">
{hasScore ? `${ft![0]} ${ft![1]}` : match.isLive ? <LiveBadge label="•" /> : ''}
</div>
<div className="flex-1 flex items-center justify-end gap-2 overflow-hidden">
<span className={`text-sm font-medium truncate ${winner === 'away' ? 'text-[#dff5e8]' : 'text-[#4a7a55]'}`}>
{match.team2.name}
</span>
<TeamFlag name={match.team2.name} iso2={match.team2.iso2} size="sm" />
</div>
</div>
{match.scoreEt && <div className="text-[9px] text-[#2a5c35] mt-1 text-center">AET · {match.scoreP ? `PSO ${match.scoreP[0]}-${match.scoreP[1]}` : ''}</div>}
</div>
</Link>
)
}
return (
<Link href={`/tournaments/${match.year}#match-${match.id}`} className="block">
<div className="bg-gradient-to-br from-[#0d2016] to-[#102a1c] border border-[rgba(34,197,94,0.28)] rounded-2xl p-9 hover:border-[rgba(34,197,94,0.45)] transition-colors">
{match.isLive && <div className="mb-4"><LiveBadge label="Live Now" /></div>}
<div className="flex items-center justify-center gap-8 flex-wrap">
<div className="text-center flex-1 min-w-[100px]">
<TeamFlag name={match.team1.name} iso2={match.team1.iso2} size="xl" className="mb-2.5" />
<div className="font-['Bebas_Neue'] text-xl tracking-[0.07em] text-[#dff5e8]">{match.team1.name}</div>
</div>
<div className="text-center flex-shrink-0">
<div className="font-['Bebas_Neue'] text-[76px] text-[#22c55e] leading-none">
{hasScore ? `${ft![0]} ${ft![1]}` : '? ?'}
</div>
<div className="text-[10px] text-[#2a5c35] tracking-[0.12em] uppercase mt-1.5">{match.round}</div>
<div className="text-xs text-[#1a3a22] mt-1">{match.date ? formatDate(match.date) : ''}</div>
{match.scoreEt && <div className="text-[10px] text-[#2a5c35] mt-1">AET {match.scoreEt[0]}{match.scoreEt[1]}</div>}
{match.scoreP && <div className="text-[10px] text-[#4ade80] mt-0.5">PSO {match.scoreP[0]}{match.scoreP[1]}</div>}
</div>
<div className="text-center flex-1 min-w-[100px]">
<TeamFlag name={match.team2.name} iso2={match.team2.iso2} size="xl" className="mb-2.5" />
<div className="font-['Bebas_Neue'] text-xl tracking-[0.07em] text-[#dff5e8]">{match.team2.name}</div>
</div>
</div>
</div>
</Link>
)
}