767236739b
Diagonal ±45° goal-net texture on body background. All card surfaces converted from opaque #0a1810 to glass-card (backdrop-blur + semi-transparent rgba) or glass-card-hero (gradient rgba) so the net pattern shows through. Covers all pages: home, groups, history, search, stats, teams, tournaments, players, match cards, and 404. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
195 lines
9.0 KiB
TypeScript
195 lines
9.0 KiB
TypeScript
'use client'
|
||
import { useQuery, gql } from '@/lib/graphql/hooks'
|
||
import { useSearchParams, useRouter } from 'next/navigation'
|
||
import { useState, useEffect, Suspense } from 'react'
|
||
import Link from 'next/link'
|
||
import { TeamFlag } from '@/components/team-flag'
|
||
import { TrophyIcon, FireIcon } from '@heroicons/react/24/outline'
|
||
|
||
const SEARCH_QUERY = gql`
|
||
query Search($q: String!) {
|
||
search(query: $q) {
|
||
tournaments { year host winner totalGoals matchesCount }
|
||
teams { name iso2 slug stats { appearances titles } }
|
||
players { playerName goals tournaments team { name iso2 } }
|
||
matches {
|
||
id year round group date scoreFt isQualiPlayoff
|
||
team1 { name iso2 } team2 { name iso2 }
|
||
}
|
||
}
|
||
}
|
||
`
|
||
|
||
interface SearchMatch {
|
||
id: number; year: number; round: string; group?: string | null
|
||
date?: string | null; scoreFt?: number[] | null; isQualiPlayoff: boolean
|
||
team1: { name: string; iso2?: string | null }
|
||
team2: { name: string; iso2?: string | null }
|
||
}
|
||
|
||
function SearchContent() {
|
||
const searchParams = useSearchParams()
|
||
const router = useRouter()
|
||
const initialQ = searchParams.get('q') ?? ''
|
||
const [q, setQ] = useState(initialQ)
|
||
const [debouncedQ, setDebouncedQ] = useState(initialQ)
|
||
|
||
useEffect(() => {
|
||
const t = setTimeout(() => {
|
||
setDebouncedQ(q)
|
||
if (q.trim()) router.replace(`/search?q=${encodeURIComponent(q.trim())}`, { scroll: false })
|
||
}, 300)
|
||
return () => clearTimeout(t)
|
||
}, [q, router])
|
||
|
||
useEffect(() => {
|
||
document.title = q.trim() ? `"${q.trim()}" · World Cup` : 'Search · World Cup'
|
||
}, [q])
|
||
|
||
const skip = debouncedQ.trim().length < 2
|
||
const { data, loading } = useQuery(SEARCH_QUERY, {
|
||
variables: { q: debouncedQ },
|
||
skip,
|
||
})
|
||
|
||
const results = data?.search
|
||
const total = skip ? 0 : (
|
||
(results?.tournaments?.length ?? 0) +
|
||
(results?.teams?.length ?? 0) +
|
||
(results?.players?.length ?? 0) +
|
||
(results?.matches?.length ?? 0)
|
||
)
|
||
|
||
return (
|
||
<div className="max-w-[1200px] mx-auto px-7 py-10 pb-16">
|
||
<h1 className="font-['Bebas_Neue'] text-[52px] tracking-[0.04em] text-[#22c55e] leading-none mb-6">Search</h1>
|
||
|
||
{/* Search input */}
|
||
<div className="relative max-w-lg mb-8">
|
||
<input
|
||
type="text" value={q} onChange={e => setQ(e.target.value)}
|
||
placeholder="Search teams, players, tournaments…"
|
||
autoFocus
|
||
className="w-full pl-10 pr-4 py-3 rounded-2xl text-[#dff5e8] text-sm outline-none"
|
||
style={{ background: 'rgba(34,197,94,0.06)', border: '1px solid rgba(34,197,94,0.2)' }}
|
||
/>
|
||
<svg className="absolute left-3.5 top-1/2 -translate-y-1/2 opacity-40" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#dff5e8" strokeWidth="2.5">
|
||
<circle cx="11" cy="11" r="8" /><line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||
</svg>
|
||
{loading && <div className="absolute right-3.5 top-1/2 -translate-y-1/2 w-4 h-4 border-2 border-[#22c55e] border-t-transparent rounded-full animate-spin" />}
|
||
</div>
|
||
|
||
{/* Prompt */}
|
||
{skip && (
|
||
<div className="flex flex-col items-center py-20 text-center">
|
||
<div className="text-[56px] mb-5">🔍</div>
|
||
<div className="text-[#2a5c35] text-base">Search for nations, players, or tournaments…</div>
|
||
<div className="text-[#1a3a22] text-sm mt-2">Examples: "Brazil", "Ronaldo", "1966"</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* No results */}
|
||
{!skip && !loading && total === 0 && (
|
||
<div className="text-center text-[#1a3a22] py-16 text-sm">No results for "{debouncedQ}"</div>
|
||
)}
|
||
|
||
{/* Results count */}
|
||
{!skip && total > 0 && (
|
||
<div className="text-[13px] text-[#2a5c35] mb-6">{total} result{total !== 1 ? 's' : ''} for "{debouncedQ}"</div>
|
||
)}
|
||
|
||
<div className="flex flex-col gap-6">
|
||
{/* Teams */}
|
||
{results?.teams?.length > 0 && (
|
||
<section>
|
||
<h3 className="text-[11px] text-[#2a5c35] font-bold tracking-[0.12em] uppercase mb-3">Teams</h3>
|
||
<div className="grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-2.5">
|
||
{results.teams.map((t: { name: string; iso2?: string | null; slug: string; stats?: { appearances: number; titles: number } | null }) => (
|
||
<Link key={t.name} href={`/teams/${t.slug}`}>
|
||
<div className="glass-card flex items-center gap-3 p-3 px-4 rounded-xl hover:border-[rgba(34,197,94,0.25)] transition-colors cursor-pointer">
|
||
<TeamFlag name={t.name} iso2={t.iso2} size="md" />
|
||
<div>
|
||
<div className="text-sm font-semibold text-[#dff5e8]">{t.name}</div>
|
||
<div className="text-[10px] text-[#2a5c35]">
|
||
{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>
|
||
</Link>
|
||
))}
|
||
</div>
|
||
</section>
|
||
)}
|
||
|
||
{/* Players */}
|
||
{results?.players?.length > 0 && (
|
||
<section>
|
||
<h3 className="text-[11px] text-[#2a5c35] font-bold tracking-[0.12em] uppercase mb-3">Players</h3>
|
||
<div className="grid grid-cols-[repeat(auto-fill,minmax(220px,1fr))] gap-2.5">
|
||
{results.players.map((p: { playerName: string; goals: number; tournaments: number; team?: { name: string; iso2?: string | null } | null }) => (
|
||
<Link key={p.playerName} href={`/players/${encodeURIComponent(p.playerName)}`}>
|
||
<div className="glass-card flex items-center gap-3 p-3 px-4 rounded-xl hover:border-[rgba(34,197,94,0.25)] transition-colors cursor-pointer">
|
||
{p.team && <TeamFlag name={p.team.name} iso2={p.team.iso2} size="sm" />}
|
||
<div className="flex-1 min-w-0">
|
||
<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>
|
||
<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>
|
||
</Link>
|
||
))}
|
||
</div>
|
||
</section>
|
||
)}
|
||
|
||
{/* Tournaments */}
|
||
{results?.tournaments?.length > 0 && (
|
||
<section>
|
||
<h3 className="text-[11px] text-[#2a5c35] font-bold tracking-[0.12em] uppercase mb-3">Tournaments</h3>
|
||
<div className="grid grid-cols-[repeat(auto-fill,minmax(180px,1fr))] gap-2.5">
|
||
{results.tournaments.map((t: { year: number; host: string; winner?: string | null; totalGoals?: number | null; matchesCount?: number | null }) => (
|
||
<Link key={t.year} href={`/tournaments/${t.year}`}>
|
||
<div className="glass-card p-4 rounded-xl hover:border-[rgba(34,197,94,0.25)] transition-colors cursor-pointer">
|
||
<div className="font-['Bebas_Neue'] text-3xl text-[#22c55e]">{t.year}</div>
|
||
<div className="text-sm text-[#dff5e8]">{t.host}</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] flex items-center gap-1"><FireIcon className="w-3 h-3 flex-shrink-0" />{t.totalGoals} goals</div>}
|
||
</div>
|
||
</Link>
|
||
))}
|
||
</div>
|
||
</section>
|
||
)}
|
||
|
||
{/* Matches */}
|
||
{results?.matches?.length > 0 && (
|
||
<section>
|
||
<h3 className="text-[11px] text-[#2a5c35] font-bold tracking-[0.12em] uppercase mb-3">Matches</h3>
|
||
<div className="flex flex-col gap-2">
|
||
{results.matches.map((m: SearchMatch) => (
|
||
<Link key={m.id} href={`/tournaments/${m.year}#match-${m.id}`}>
|
||
<div className="glass-card flex items-center gap-3 p-3 px-4 rounded-xl hover:border-[rgba(34,197,94,0.25)] transition-colors cursor-pointer">
|
||
<TeamFlag name={m.team1.name} iso2={m.team1.iso2} size="sm" />
|
||
<div className="flex-1 text-sm text-[#dff5e8]">{m.team1.name} vs {m.team2.name}</div>
|
||
{m.scoreFt && <span className="font-['Bebas_Neue'] text-lg text-[#22c55e]">{m.scoreFt[0]}–{m.scoreFt[1]}</span>}
|
||
<TeamFlag name={m.team2.name} iso2={m.team2.iso2} size="sm" />
|
||
<div className="text-[10px] text-[#2a5c35] whitespace-nowrap">{m.year} · {m.round}</div>
|
||
</div>
|
||
</Link>
|
||
))}
|
||
</div>
|
||
</section>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default function SearchPage() {
|
||
return (
|
||
<Suspense fallback={<div className="p-10 text-[#2a5c35]">Loading…</div>}>
|
||
<SearchContent />
|
||
</Suspense>
|
||
)
|
||
}
|