Compare commits
2 Commits
a6111d7beb
...
3cb619d7fa
| Author | SHA1 | Date | |
|---|---|---|---|
| 3cb619d7fa | |||
| c3ddb6e874 |
@@ -3,6 +3,7 @@ import { useQuery, gql } from '@/lib/graphql/hooks'
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { TeamFlag } from '@/components/team-flag'
|
import { TeamFlag } from '@/components/team-flag'
|
||||||
|
import { FireIcon, CalendarDaysIcon, TrophyIcon } from '@heroicons/react/24/outline'
|
||||||
|
|
||||||
const HISTORY_QUERY = gql`
|
const HISTORY_QUERY = gql`
|
||||||
query History {
|
query History {
|
||||||
@@ -92,14 +93,14 @@ export default function HistoryPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex gap-3.5 text-[11px] text-[#2a5c35] flex-wrap">
|
<div className="flex gap-3.5 text-[11px] text-[#2a5c35] flex-wrap">
|
||||||
{t.totalGoals != null && <span>⚽ {t.totalGoals}</span>}
|
{t.totalGoals != null && <span className="inline-flex items-center gap-1"><FireIcon className="w-3 h-3" />{t.totalGoals}</span>}
|
||||||
{t.matchesCount != null && <span>🗓 {t.matchesCount} games</span>}
|
{t.matchesCount != null && <span className="inline-flex items-center gap-1"><CalendarDaysIcon className="w-3 h-3" />{t.matchesCount} games</span>}
|
||||||
{t.teamsCount != null && <span>🏳 {t.teamsCount} teams</span>}
|
{t.teamsCount != null && <span>🏳 {t.teamsCount} teams</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{topScorer && (
|
{topScorer && (
|
||||||
<div className="mt-2 text-[10px] text-[#1a3a22]">
|
<div className="mt-2 text-[10px] text-[#1a3a22]">
|
||||||
Golden Boot: <span className="text-[#2a5c35]">{topScorer.playerName} ({topScorer.goals}⚽)</span>
|
Golden Boot: <span className="text-[#2a5c35]">{topScorer.playerName} (<span className="inline-flex items-center gap-0.5"><FireIcon className="w-2.5 h-2.5 inline" />{topScorer.goals}</span>)</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+5
-4
@@ -4,6 +4,7 @@ import { useSearchParams, useRouter } from 'next/navigation'
|
|||||||
import { useState, useEffect, Suspense } from 'react'
|
import { useState, useEffect, Suspense } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { TeamFlag } from '@/components/team-flag'
|
import { TeamFlag } from '@/components/team-flag'
|
||||||
|
import { TrophyIcon, FireIcon } from '@heroicons/react/24/outline'
|
||||||
|
|
||||||
const SEARCH_QUERY = gql`
|
const SEARCH_QUERY = gql`
|
||||||
query Search($q: String!) {
|
query Search($q: String!) {
|
||||||
@@ -111,7 +112,7 @@ function SearchContent() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-semibold text-[#dff5e8]">{t.name}</div>
|
<div className="text-sm font-semibold text-[#dff5e8]">{t.name}</div>
|
||||||
<div className="text-[10px] text-[#2a5c35]">
|
<div className="text-[10px] text-[#2a5c35]">
|
||||||
{t.stats?.appearances ?? 0} WCs{t.stats?.titles ? ` · ${t.stats.titles} 🏆` : ''}
|
{t.stats?.appearances ?? 0} WCs{t.stats?.titles ? <span className="inline-flex items-center gap-0.5 ml-1">· {t.stats.titles}<TrophyIcon className="w-3 h-3 inline" /></span> : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -135,7 +136,7 @@ function SearchContent() {
|
|||||||
<div className="text-sm font-semibold text-[#dff5e8] truncate">{p.playerName}</div>
|
<div className="text-sm font-semibold text-[#dff5e8] truncate">{p.playerName}</div>
|
||||||
<div className="text-[10px] text-[#2a5c35]">{p.team?.name} · {p.tournaments} WC{p.tournaments !== 1 ? 's' : ''}</div>
|
<div className="text-[10px] text-[#2a5c35]">{p.team?.name} · {p.tournaments} WC{p.tournaments !== 1 ? 's' : ''}</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-['Bebas_Neue'] text-xl text-[#22c55e] flex-shrink-0">{p.goals}⚽</span>
|
<span className="font-['Bebas_Neue'] text-xl text-[#22c55e] flex-shrink-0 inline-flex items-center gap-0.5">{p.goals}<FireIcon className="w-3.5 h-3.5" /></span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
@@ -154,8 +155,8 @@ function SearchContent() {
|
|||||||
style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.12)' }}>
|
style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.12)' }}>
|
||||||
<div className="font-['Bebas_Neue'] text-3xl text-[#22c55e]">{t.year}</div>
|
<div className="font-['Bebas_Neue'] text-3xl text-[#22c55e]">{t.year}</div>
|
||||||
<div className="text-sm text-[#dff5e8]">{t.host}</div>
|
<div className="text-sm text-[#dff5e8]">{t.host}</div>
|
||||||
{t.winner && <div className="text-[10px] text-[#2a5c35] mt-1">🏆 {t.winner}</div>}
|
{t.winner && <div className="text-[10px] text-[#2a5c35] mt-1 flex items-center gap-1"><TrophyIcon className="w-3 h-3 flex-shrink-0" />{t.winner}</div>}
|
||||||
{t.totalGoals && <div className="text-[10px] text-[#1a3a22]">⚽ {t.totalGoals} goals</div>}
|
{t.totalGoals && <div className="text-[10px] text-[#1a3a22] flex items-center gap-1"><FireIcon className="w-3 h-3 flex-shrink-0" />{t.totalGoals} goals</div>}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
|
|||||||
+22
-13
@@ -3,6 +3,10 @@ import { useQuery, gql } from '@/lib/graphql/hooks'
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { TeamFlag } from '@/components/team-flag'
|
import { TeamFlag } from '@/components/team-flag'
|
||||||
|
import {
|
||||||
|
ChartBarIcon, StarIcon, TrophyIcon, ClockIcon, BoltIcon,
|
||||||
|
FireIcon, SparklesIcon, ArrowPathIcon, GlobeEuropeAfricaIcon, TableCellsIcon,
|
||||||
|
} from '@heroicons/react/24/outline'
|
||||||
|
|
||||||
const STATS_QUERY = gql`
|
const STATS_QUERY = gql`
|
||||||
query Stats {
|
query Stats {
|
||||||
@@ -36,8 +40,13 @@ const STATS_QUERY = gql`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
function SectionTitle({ children }: { children: React.ReactNode }) {
|
function SectionTitle({ children, icon: Icon }: { children: React.ReactNode; icon: React.ComponentType<{ className?: string }> }) {
|
||||||
return <h2 className="text-[11px] font-bold tracking-[0.14em] uppercase text-[#2a5c35] mb-4">{children}</h2>
|
return (
|
||||||
|
<h2 className="flex items-center gap-1.5 text-[11px] font-bold tracking-[0.14em] uppercase text-[#2a5c35] mb-4">
|
||||||
|
<Icon className="w-3.5 h-3.5 flex-shrink-0" />
|
||||||
|
{children}
|
||||||
|
</h2>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Card({ children, className = '' }: { children: React.ReactNode; className?: string }) {
|
function Card({ children, className = '' }: { children: React.ReactNode; className?: string }) {
|
||||||
@@ -92,7 +101,7 @@ export default function StatsPage() {
|
|||||||
{/* ── Goals per tournament bar chart ── */}
|
{/* ── Goals per tournament bar chart ── */}
|
||||||
{tournaments.length > 0 && (
|
{tournaments.length > 0 && (
|
||||||
<div className="mb-12">
|
<div className="mb-12">
|
||||||
<SectionTitle>⚽ Goals Scored per Tournament</SectionTitle>
|
<SectionTitle icon={ChartBarIcon}>Goals Scored per Tournament</SectionTitle>
|
||||||
<Card>
|
<Card>
|
||||||
<div className="px-3 pt-4 pb-0 sm:px-7 sm:pt-7">
|
<div className="px-3 pt-4 pb-0 sm:px-7 sm:pt-7">
|
||||||
<div className="flex items-end gap-[2px] sm:gap-[3px] h-[170px]">
|
<div className="flex items-end gap-[2px] sm:gap-[3px] h-[170px]">
|
||||||
@@ -125,7 +134,7 @@ export default function StatsPage() {
|
|||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-12">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-12">
|
||||||
{/* ── All-time top scorers ── */}
|
{/* ── All-time top scorers ── */}
|
||||||
<div>
|
<div>
|
||||||
<SectionTitle>🏅 All-Time Top Scorers</SectionTitle>
|
<SectionTitle icon={StarIcon}>All-Time Top Scorers</SectionTitle>
|
||||||
<Card>
|
<Card>
|
||||||
{scorers.map((s, i) => (
|
{scorers.map((s, i) => (
|
||||||
<Link key={s.playerName} href={`/players/${encodeURIComponent(s.playerName)}`}>
|
<Link key={s.playerName} href={`/players/${encodeURIComponent(s.playerName)}`}>
|
||||||
@@ -149,7 +158,7 @@ export default function StatsPage() {
|
|||||||
|
|
||||||
{/* ── World Cup titles ── */}
|
{/* ── World Cup titles ── */}
|
||||||
<div>
|
<div>
|
||||||
<SectionTitle>🏆 World Cup Titles by Nation</SectionTitle>
|
<SectionTitle icon={TrophyIcon}>World Cup Titles by Nation</SectionTitle>
|
||||||
<Card>
|
<Card>
|
||||||
{titlesByNation.map((t, i) => (
|
{titlesByNation.map((t, i) => (
|
||||||
<Link key={t.name} href={`/teams/${t.slug}`}>
|
<Link key={t.name} href={`/teams/${t.slug}`}>
|
||||||
@@ -160,7 +169,7 @@ export default function StatsPage() {
|
|||||||
<div className="flex-1 min-w-0 text-sm font-semibold text-[#dff5e8] truncate">{t.name}</div>
|
<div className="flex-1 min-w-0 text-sm font-semibold text-[#dff5e8] truncate">{t.name}</div>
|
||||||
<div className="hidden sm:flex gap-0.5 flex-shrink-0">
|
<div className="hidden sm:flex gap-0.5 flex-shrink-0">
|
||||||
{Array.from({ length: t.stats?.titles ?? 0 }).map((_, j) => (
|
{Array.from({ length: t.stats?.titles ?? 0 }).map((_, j) => (
|
||||||
<span key={j} className="text-sm">🏆</span>
|
<TrophyIcon key={j} className="w-4 h-4 text-[#22c55e]" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<span className="font-['Bebas_Neue'] text-[28px] text-[#22c55e] flex-shrink-0">{t.stats?.titles}</span>
|
<span className="font-['Bebas_Neue'] text-[28px] text-[#22c55e] flex-shrink-0">{t.stats?.titles}</span>
|
||||||
@@ -174,7 +183,7 @@ export default function StatsPage() {
|
|||||||
{/* ── Goals by minute heatmap ── */}
|
{/* ── Goals by minute heatmap ── */}
|
||||||
{minuteBuckets.length > 0 && (
|
{minuteBuckets.length > 0 && (
|
||||||
<div className="mb-12">
|
<div className="mb-12">
|
||||||
<SectionTitle>⏱ Goals by Minute (All-Time)</SectionTitle>
|
<SectionTitle icon={ClockIcon}>Goals by Minute (All-Time)</SectionTitle>
|
||||||
<Card>
|
<Card>
|
||||||
<div className="px-3 py-4 sm:p-6">
|
<div className="px-3 py-4 sm:p-6">
|
||||||
<div className="flex items-end gap-1 sm:gap-3 h-24">
|
<div className="flex items-end gap-1 sm:gap-3 h-24">
|
||||||
@@ -197,7 +206,7 @@ export default function StatsPage() {
|
|||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-12">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-12">
|
||||||
{/* ── Biggest wins ── */}
|
{/* ── Biggest wins ── */}
|
||||||
<div>
|
<div>
|
||||||
<SectionTitle>💥 Biggest Victories</SectionTitle>
|
<SectionTitle icon={BoltIcon}>Biggest Victories</SectionTitle>
|
||||||
<Card>
|
<Card>
|
||||||
{biggestWins.map(m => (
|
{biggestWins.map(m => (
|
||||||
<Link key={m.id} href={`/tournaments/${m.year}#match-${m.id}`}>
|
<Link key={m.id} href={`/tournaments/${m.year}#match-${m.id}`}>
|
||||||
@@ -220,7 +229,7 @@ export default function StatsPage() {
|
|||||||
|
|
||||||
{/* ── Highest scoring matches ── */}
|
{/* ── Highest scoring matches ── */}
|
||||||
<div>
|
<div>
|
||||||
<SectionTitle>🔥 Highest Scoring Matches</SectionTitle>
|
<SectionTitle icon={FireIcon}>Highest Scoring Matches</SectionTitle>
|
||||||
<Card>
|
<Card>
|
||||||
{highScoring.map(m => (
|
{highScoring.map(m => (
|
||||||
<Link key={m.id} href={`/tournaments/${m.year}#match-${m.id}`}>
|
<Link key={m.id} href={`/tournaments/${m.year}#match-${m.id}`}>
|
||||||
@@ -245,7 +254,7 @@ export default function StatsPage() {
|
|||||||
{/* ── Hat-tricks ── */}
|
{/* ── Hat-tricks ── */}
|
||||||
{hatTricks.length > 0 && (
|
{hatTricks.length > 0 && (
|
||||||
<div className="mb-12">
|
<div className="mb-12">
|
||||||
<SectionTitle>🎩 Hat-Tricks</SectionTitle>
|
<SectionTitle icon={SparklesIcon}>Hat-Tricks</SectionTitle>
|
||||||
<div className="grid grid-cols-[repeat(auto-fill,minmax(240px,1fr))] gap-3">
|
<div className="grid grid-cols-[repeat(auto-fill,minmax(240px,1fr))] gap-3">
|
||||||
{hatTricks.map((h, i) => (
|
{hatTricks.map((h, i) => (
|
||||||
<div key={i} className="rounded-xl p-4" style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.12)' }}>
|
<div key={i} className="rounded-xl p-4" style={{ background: '#0a1810', border: '1px solid rgba(34,197,94,0.12)' }}>
|
||||||
@@ -270,7 +279,7 @@ export default function StatsPage() {
|
|||||||
{/* ── ET & Penalty stats ── */}
|
{/* ── ET & Penalty stats ── */}
|
||||||
{etStats && (
|
{etStats && (
|
||||||
<div className="mb-12">
|
<div className="mb-12">
|
||||||
<SectionTitle>⚡ Extra Time & Penalty Shootouts</SectionTitle>
|
<SectionTitle icon={ArrowPathIcon}>Extra Time & Penalty Shootouts</SectionTitle>
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||||
{[
|
{[
|
||||||
{ label: 'Knockout Matches', value: etStats.totalKnockoutMatches },
|
{ label: 'Knockout Matches', value: etStats.totalKnockoutMatches },
|
||||||
@@ -290,7 +299,7 @@ export default function StatsPage() {
|
|||||||
{/* ── Confederation stats ── */}
|
{/* ── Confederation stats ── */}
|
||||||
{confStats.length > 0 && (
|
{confStats.length > 0 && (
|
||||||
<div className="mb-12">
|
<div className="mb-12">
|
||||||
<SectionTitle>🌍 Performance by Confederation</SectionTitle>
|
<SectionTitle icon={GlobeEuropeAfricaIcon}>Performance by Confederation</SectionTitle>
|
||||||
<Card>
|
<Card>
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -319,7 +328,7 @@ export default function StatsPage() {
|
|||||||
{/* ── All-time team table ── */}
|
{/* ── All-time team table ── */}
|
||||||
{teams.length > 0 && (
|
{teams.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<SectionTitle>📊 All-Time Team Table</SectionTitle>
|
<SectionTitle icon={TableCellsIcon}>All-Time Team Table</SectionTitle>
|
||||||
<Card>
|
<Card>
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full" style={{ minWidth: '560px' }}>
|
<table className="w-full" style={{ minWidth: '560px' }}>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useQuery, gql } from '@/lib/graphql/hooks'
|
|||||||
import { use, useEffect } from 'react'
|
import { use, useEffect } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { TeamFlag } from '@/components/team-flag'
|
import { TeamFlag } from '@/components/team-flag'
|
||||||
|
import { TrophyIcon } from '@heroicons/react/24/outline'
|
||||||
|
|
||||||
const TEAM_QUERY = gql`
|
const TEAM_QUERY = gql`
|
||||||
query Team($slug: String!) {
|
query Team($slug: String!) {
|
||||||
@@ -103,8 +104,9 @@ export default function TeamPage({ params }: { params: Promise<{ slug: string }>
|
|||||||
{team.confederation && <span className="text-[11px] text-[#2a5c35]">{team.confederation}</span>}
|
{team.confederation && <span className="text-[11px] text-[#2a5c35]">{team.confederation}</span>}
|
||||||
{team.continent && <span className="text-[11px] text-[#2a5c35]">{team.continent}</span>}
|
{team.continent && <span className="text-[11px] text-[#2a5c35]">{team.continent}</span>}
|
||||||
{(s?.titles ?? 0) > 0 && (
|
{(s?.titles ?? 0) > 0 && (
|
||||||
<span className="text-[11px] text-[#22c55e] font-bold">
|
<span className="inline-flex items-center gap-1 text-[11px] text-[#22c55e] font-bold">
|
||||||
{Array.from({ length: s?.titles ?? 0 }).map(() => '🏆').join('')} {s?.titles} title{(s?.titles ?? 0) !== 1 ? 's' : ''}
|
{Array.from({ length: s?.titles ?? 0 }).map((_, i) => <TrophyIcon key={i} className="w-3.5 h-3.5" />)}
|
||||||
|
{s?.titles} title{(s?.titles ?? 0) !== 1 ? 's' : ''}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { db } from '@/lib/db'
|
import { db } from '@/lib/db'
|
||||||
import { tournaments, teams, matches, goals, groupStandings, stadiums, squads } from '@/lib/db/schema'
|
import { tournaments, teams, matches, goals, groupStandings, stadiums, squads } from '@/lib/db/schema'
|
||||||
import { slugify, getIso } from '@/lib/iso-codes'
|
import { slugify, getIso } from '@/lib/iso-codes'
|
||||||
import { eq, and, desc, asc, sql, ilike, or, isNotNull, lt, gt, gte } from 'drizzle-orm'
|
import { eq, and, desc, asc, sql, ilike, or, isNotNull, lt, lte, gt, gte } from 'drizzle-orm'
|
||||||
|
|
||||||
function teamWithSlug(t: typeof teams.$inferSelect) {
|
function teamWithSlug(t: typeof teams.$inferSelect) {
|
||||||
return { ...t, slug: slugify(t.name), iso2: t.iso2 ?? getIso(t.name) }
|
return { ...t, slug: slugify(t.name), iso2: t.iso2 ?? getIso(t.name) }
|
||||||
@@ -116,7 +116,7 @@ export const resolvers = {
|
|||||||
const today = new Date().toISOString().slice(0, 10)
|
const today = new Date().toISOString().slice(0, 10)
|
||||||
const rows = await db.select().from(matches)
|
const rows = await db.select().from(matches)
|
||||||
.where(and(
|
.where(and(
|
||||||
lt(matches.date, today),
|
lte(matches.date, today),
|
||||||
isNotNull(matches.scoreFtHome),
|
isNotNull(matches.scoreFtHome),
|
||||||
eq(matches.isQualiPlayoff, false),
|
eq(matches.isQualiPlayoff, false),
|
||||||
))
|
))
|
||||||
|
|||||||
Generated
+8097
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^4.2.3",
|
"@apollo/client": "^4.2.3",
|
||||||
"@graphql-tools/schema": "^10.0.33",
|
"@graphql-tools/schema": "^10.0.33",
|
||||||
|
"@heroicons/react": "^2.2.0",
|
||||||
"drizzle-orm": "^0.45.2",
|
"drizzle-orm": "^0.45.2",
|
||||||
"flag-icons": "^7.5.0",
|
"flag-icons": "^7.5.0",
|
||||||
"graphql": "^16.14.2",
|
"graphql": "^16.14.2",
|
||||||
|
|||||||
Reference in New Issue
Block a user