v2
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Clock } from 'lucide-react'
|
||||
|
||||
interface CountdownTimerProps {
|
||||
refreshInterval?: number; // in seconds
|
||||
}
|
||||
|
||||
export default function CountdownTimer({ refreshInterval = 300 }: CountdownTimerProps) {
|
||||
const [timeLeft, setTimeLeft] = useState<number>(refreshInterval)
|
||||
|
||||
useEffect(() => {
|
||||
// Reset when refresh interval changes
|
||||
setTimeLeft(refreshInterval);
|
||||
}, [refreshInterval]);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setTimeLeft((prev) => {
|
||||
if (prev <= 1) {
|
||||
// Trigger page reload for data refresh
|
||||
window.location.reload();
|
||||
return refreshInterval;
|
||||
}
|
||||
return prev - 1
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [refreshInterval])
|
||||
|
||||
const minutes = Math.floor((timeLeft ?? 0) / 60)
|
||||
const seconds = (timeLeft ?? 0) % 60
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 px-4 py-2 bg-[#E42518]/10 rounded-lg border border-[#E42518]/20">
|
||||
<Clock className="w-4 h-4 text-[#E42518]" />
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
Next update: {minutes}:{seconds?.toString()?.padStart(2, '0') ?? '00'}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Radio, Loader2, AlertCircle } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
|
||||
interface Fixture {
|
||||
fixture: {
|
||||
id: number
|
||||
status: {
|
||||
short: string
|
||||
elapsed: number | null
|
||||
}
|
||||
}
|
||||
teams: {
|
||||
home: {
|
||||
id: number
|
||||
name: string
|
||||
logo: string
|
||||
}
|
||||
away: {
|
||||
id: number
|
||||
name: string
|
||||
logo: string
|
||||
}
|
||||
}
|
||||
goals: {
|
||||
home: number | null
|
||||
away: number | null
|
||||
}
|
||||
}
|
||||
|
||||
const REFRESH_INTERVAL = 5 * 60 * 1000 // 5 minutes
|
||||
|
||||
export default function LiveMatches() {
|
||||
const [fixtures, setFixtures] = useState<Fixture[]>([])
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const fetchLiveMatches = async () => {
|
||||
try {
|
||||
setError(null)
|
||||
const res = await fetch('/api/fixtures?live=true')
|
||||
if (!res?.ok) throw new Error('Failed to fetch live matches')
|
||||
const data = await res.json()
|
||||
setFixtures(data?.response ?? [])
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err?.message : 'Failed to load live matches')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchLiveMatches()
|
||||
const interval = setInterval(fetchLiveMatches, REFRESH_INTERVAL)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<section className="bg-white rounded-xl shadow-lg p-6 border border-gray-100">
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-8 h-8 text-[#E42518] animate-spin" />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<section className="bg-white rounded-xl shadow-lg p-6 border border-gray-100">
|
||||
<div className="flex items-center justify-center gap-2 py-12 text-gray-500">
|
||||
<AlertCircle className="w-5 h-5" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
if ((fixtures?.length ?? 0) === 0) {
|
||||
return (
|
||||
<section className="bg-white rounded-xl shadow-lg p-6 border border-gray-100">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<Radio className="w-6 h-6 text-[#E42518]" />
|
||||
<h2 className="text-2xl font-bold text-gray-800">Live Matches</h2>
|
||||
</div>
|
||||
<p className="text-center py-8 text-gray-500">No live matches at the moment</p>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="bg-white rounded-xl shadow-lg p-6 border border-gray-100">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<Radio className="w-6 h-6 text-[#E42518] animate-pulse" />
|
||||
<h2 className="text-2xl font-bold text-gray-800">Live Matches</h2>
|
||||
<span className="ml-auto px-3 py-1 bg-red-500 text-white text-xs font-semibold rounded-full animate-pulse">
|
||||
LIVE
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{fixtures?.map((fixture) => (
|
||||
<div
|
||||
key={fixture?.fixture?.id}
|
||||
className="p-4 border border-gray-200 rounded-lg hover:shadow-md transition-shadow bg-gradient-to-r from-gray-50 to-white"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Home Team */}
|
||||
<div className="flex items-center gap-3 flex-1">
|
||||
<div className="relative w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center">
|
||||
<Image
|
||||
src={fixture?.teams?.home?.logo ?? ''}
|
||||
alt={fixture?.teams?.home?.name ?? ''}
|
||||
fill
|
||||
className="object-contain p-1"
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
<span className="font-semibold text-gray-800">{fixture?.teams?.home?.name}</span>
|
||||
</div>
|
||||
|
||||
{/* Score */}
|
||||
<div className="flex flex-col items-center mx-4">
|
||||
<div className="flex items-center gap-2 text-2xl font-bold">
|
||||
<span className="text-[#E42518]">{fixture?.goals?.home ?? 0}</span>
|
||||
<span className="text-gray-400">-</span>
|
||||
<span className="text-[#006600]">{fixture?.goals?.away ?? 0}</span>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500 mt-1">{fixture?.fixture?.status?.elapsed ?? 0}'</span>
|
||||
</div>
|
||||
|
||||
{/* Away Team */}
|
||||
<div className="flex items-center gap-3 flex-1 justify-end">
|
||||
<span className="font-semibold text-gray-800">{fixture?.teams?.away?.name}</span>
|
||||
<div className="relative w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center">
|
||||
<Image
|
||||
src={fixture?.teams?.away?.logo ?? ''}
|
||||
alt={fixture?.teams?.away?.name ?? ''}
|
||||
fill
|
||||
className="object-contain p-1"
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { CheckCircle2, Loader2, AlertCircle } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
|
||||
interface Fixture {
|
||||
fixture: {
|
||||
id: number
|
||||
date: string
|
||||
}
|
||||
teams: {
|
||||
home: {
|
||||
id: number
|
||||
name: string
|
||||
logo: string
|
||||
}
|
||||
away: {
|
||||
id: number
|
||||
name: string
|
||||
logo: string
|
||||
}
|
||||
}
|
||||
goals: {
|
||||
home: number | null
|
||||
away: number | null
|
||||
}
|
||||
}
|
||||
|
||||
const REFRESH_INTERVAL = 5 * 60 * 1000 // 5 minutes
|
||||
|
||||
export default function RecentResults() {
|
||||
const [fixtures, setFixtures] = useState<Fixture[]>([])
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const fetchRecentResults = async () => {
|
||||
try {
|
||||
setError(null)
|
||||
const res = await fetch('/api/fixtures?last=10')
|
||||
if (!res?.ok) throw new Error('Failed to fetch recent results')
|
||||
const data = await res.json()
|
||||
setFixtures(data?.response ?? [])
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err?.message : 'Failed to load recent results')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchRecentResults()
|
||||
const interval = setInterval(fetchRecentResults, REFRESH_INTERVAL)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<section className="bg-white rounded-xl shadow-lg p-6 border border-gray-100">
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-8 h-8 text-[#006600] animate-spin" />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<section className="bg-white rounded-xl shadow-lg p-6 border border-gray-100">
|
||||
<div className="flex items-center justify-center gap-2 py-12 text-gray-500">
|
||||
<AlertCircle className="w-5 h-5" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="bg-white rounded-xl shadow-lg p-6 border border-gray-100">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<CheckCircle2 className="w-6 h-6 text-[#006600]" />
|
||||
<h2 className="text-xl font-bold text-gray-800">Recent Results</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{fixtures?.slice(0, 8)?.map((fixture) => {
|
||||
const date = new Date(fixture?.fixture?.date ?? '')
|
||||
const formattedDate = date?.toLocaleDateString('en-GB', { day: 'numeric', month: 'short' })
|
||||
|
||||
return (
|
||||
<div
|
||||
key={fixture?.fixture?.id}
|
||||
className="p-3 border border-gray-200 rounded-lg hover:shadow-md transition-shadow bg-gray-50"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<div className="relative w-6 h-6">
|
||||
<Image
|
||||
src={fixture?.teams?.home?.logo ?? ''}
|
||||
alt={fixture?.teams?.home?.name ?? ''}
|
||||
fill
|
||||
className="object-contain"
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-800 truncate">
|
||||
{fixture?.teams?.home?.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 mx-3">
|
||||
<span className={`text-sm font-bold ${
|
||||
(fixture?.goals?.home ?? 0) > (fixture?.goals?.away ?? 0) ? 'text-[#006600]' : 'text-gray-600'
|
||||
}`}>
|
||||
{fixture?.goals?.home ?? 0}
|
||||
</span>
|
||||
<span className="text-xs text-gray-400">-</span>
|
||||
<span className={`text-sm font-bold ${
|
||||
(fixture?.goals?.away ?? 0) > (fixture?.goals?.home ?? 0) ? 'text-[#006600]' : 'text-gray-600'
|
||||
}`}>
|
||||
{fixture?.goals?.away ?? 0}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 flex-1 justify-end">
|
||||
<span className="text-sm font-medium text-gray-800 truncate">
|
||||
{fixture?.teams?.away?.name}
|
||||
</span>
|
||||
<div className="relative w-6 h-6">
|
||||
<Image
|
||||
src={fixture?.teams?.away?.logo ?? ''}
|
||||
alt={fixture?.teams?.away?.name ?? ''}
|
||||
fill
|
||||
className="object-contain"
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 text-center mt-2">{formattedDate}</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Trophy, Loader2, AlertCircle } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
|
||||
interface Standing {
|
||||
rank: number
|
||||
team: {
|
||||
id: number
|
||||
name: string
|
||||
logo: string
|
||||
}
|
||||
points: number
|
||||
all: {
|
||||
played: number
|
||||
win: number
|
||||
draw: number
|
||||
lose: number
|
||||
}
|
||||
goalsDiff: number
|
||||
form: string
|
||||
}
|
||||
|
||||
const REFRESH_INTERVAL = 5 * 60 * 1000 // 5 minutes
|
||||
|
||||
export default function Standings() {
|
||||
const [standings, setStandings] = useState<Standing[]>([])
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const fetchStandings = async () => {
|
||||
try {
|
||||
setError(null)
|
||||
const res = await fetch('/api/standings')
|
||||
if (!res?.ok) throw new Error('Failed to fetch standings')
|
||||
const data = await res.json()
|
||||
setStandings(data?.response?.[0]?.league?.standings?.[0] ?? [])
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err?.message : 'Failed to load standings')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchStandings()
|
||||
const interval = setInterval(fetchStandings, REFRESH_INTERVAL)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<section className="bg-white rounded-xl shadow-lg p-6 border border-gray-100">
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-8 h-8 text-[#006600] animate-spin" />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<section className="bg-white rounded-xl shadow-lg p-6 border border-gray-100">
|
||||
<div className="flex items-center justify-center gap-2 py-12 text-gray-500">
|
||||
<AlertCircle className="w-5 h-5" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="bg-white rounded-xl shadow-lg p-6 border border-gray-100">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<Trophy className="w-6 h-6 text-[#006600]" />
|
||||
<h2 className="text-2xl font-bold text-gray-800">League Standings</h2>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b-2 border-gray-200">
|
||||
<th className="text-left py-3 px-2 text-sm font-semibold text-gray-600">#</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-semibold text-gray-600">Team</th>
|
||||
<th className="text-center py-3 px-2 text-sm font-semibold text-gray-600">P</th>
|
||||
<th className="text-center py-3 px-2 text-sm font-semibold text-gray-600">W</th>
|
||||
<th className="text-center py-3 px-2 text-sm font-semibold text-gray-600">D</th>
|
||||
<th className="text-center py-3 px-2 text-sm font-semibold text-gray-600">L</th>
|
||||
<th className="text-center py-3 px-2 text-sm font-semibold text-gray-600">GD</th>
|
||||
<th className="text-center py-3 px-3 text-sm font-semibold text-gray-600">Pts</th>
|
||||
<th className="text-left py-3 px-2 text-sm font-semibold text-gray-600">Form</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{standings?.map((team, idx) => (
|
||||
<tr
|
||||
key={team?.team?.id}
|
||||
className={`border-b border-gray-100 hover:bg-gray-50 transition-colors ${
|
||||
idx < 3 ? 'bg-green-50/50' : idx < 6 ? 'bg-blue-50/30' : idx >= (standings?.length ?? 0) - 2 ? 'bg-red-50/30' : ''
|
||||
}`}
|
||||
>
|
||||
<td className="py-3 px-2 text-sm font-semibold text-gray-700">{team?.rank}</td>
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative w-8 h-8 rounded-full bg-gray-100 flex items-center justify-center">
|
||||
<Image
|
||||
src={team?.team?.logo ?? ''}
|
||||
alt={team?.team?.name ?? ''}
|
||||
fill
|
||||
className="object-contain p-1"
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
<span className="font-medium text-gray-800">{team?.team?.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="text-center py-3 px-2 text-sm text-gray-600">{team?.all?.played ?? 0}</td>
|
||||
<td className="text-center py-3 px-2 text-sm text-gray-600">{team?.all?.win ?? 0}</td>
|
||||
<td className="text-center py-3 px-2 text-sm text-gray-600">{team?.all?.draw ?? 0}</td>
|
||||
<td className="text-center py-3 px-2 text-sm text-gray-600">{team?.all?.lose ?? 0}</td>
|
||||
<td className="text-center py-3 px-2 text-sm text-gray-600">{team?.goalsDiff ?? 0}</td>
|
||||
<td className="text-center py-3 px-3 text-sm font-bold text-[#006600]">{team?.points ?? 0}</td>
|
||||
<td className="py-3 px-2">
|
||||
<div className="flex gap-1">
|
||||
{team?.form?.split('')?.slice(-5)?.map((result, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`w-5 h-5 rounded-full text-xs flex items-center justify-center font-semibold ${
|
||||
result === 'W' ? 'bg-green-500 text-white' :
|
||||
result === 'D' ? 'bg-gray-400 text-white' :
|
||||
result === 'L' ? 'bg-red-500 text-white' : 'bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
{result}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Target, Loader2, AlertCircle } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
|
||||
interface TopScorer {
|
||||
player: {
|
||||
id: number
|
||||
name: string
|
||||
photo: string
|
||||
}
|
||||
statistics: Array<{
|
||||
team: {
|
||||
id: number
|
||||
name: string
|
||||
logo: string
|
||||
}
|
||||
goals: {
|
||||
total: number | null
|
||||
}
|
||||
}>
|
||||
}
|
||||
|
||||
const REFRESH_INTERVAL = 5 * 60 * 1000 // 5 minutes
|
||||
|
||||
export default function TopScorers() {
|
||||
const [scorers, setScorers] = useState<TopScorer[]>([])
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const fetchTopScorers = async () => {
|
||||
try {
|
||||
setError(null)
|
||||
const res = await fetch('/api/topscorers')
|
||||
if (!res?.ok) throw new Error('Failed to fetch top scorers')
|
||||
const data = await res.json()
|
||||
setScorers(data?.response ?? [])
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err?.message : 'Failed to load top scorers')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchTopScorers()
|
||||
const interval = setInterval(fetchTopScorers, REFRESH_INTERVAL)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<section className="bg-white rounded-xl shadow-lg p-6 border border-gray-100">
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-8 h-8 text-[#E42518] animate-spin" />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<section className="bg-white rounded-xl shadow-lg p-6 border border-gray-100">
|
||||
<div className="flex items-center justify-center gap-2 py-12 text-gray-500">
|
||||
<AlertCircle className="w-5 h-5" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="bg-white rounded-xl shadow-lg p-6 border border-gray-100">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<Target className="w-6 h-6 text-[#E42518]" />
|
||||
<h2 className="text-2xl font-bold text-gray-800">Top Scorers</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{scorers?.slice(0, 12)?.map((scorer, idx) => {
|
||||
const stats = scorer?.statistics?.[0]
|
||||
const goals = stats?.goals?.total ?? 0
|
||||
|
||||
return (
|
||||
<div
|
||||
key={scorer?.player?.id}
|
||||
className="p-4 border border-gray-200 rounded-lg hover:shadow-md transition-all hover:-translate-y-1 bg-gradient-to-br from-white to-gray-50"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="relative w-12 h-12 rounded-full overflow-hidden bg-gray-200">
|
||||
<Image
|
||||
src={scorer?.player?.photo ?? ''}
|
||||
alt={scorer?.player?.name ?? ''}
|
||||
fill
|
||||
className="object-cover"
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-gray-800 text-sm">{scorer?.player?.name}</h3>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<div className="relative w-5 h-5">
|
||||
<Image
|
||||
src={stats?.team?.logo ?? ''}
|
||||
alt={stats?.team?.name ?? ''}
|
||||
fill
|
||||
className="object-contain"
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-gray-600">{stats?.team?.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between pt-3 border-t border-gray-200">
|
||||
<span className="text-sm text-gray-600">Goals</span>
|
||||
<span className="text-2xl font-bold text-[#E42518]">{goals}</span>
|
||||
</div>
|
||||
{idx === 0 && (
|
||||
<div className="mt-2 text-center">
|
||||
<span className="inline-block px-2 py-1 bg-yellow-100 text-yellow-800 text-xs font-semibold rounded">
|
||||
👑 Top Scorer
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Calendar, Loader2, AlertCircle } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
|
||||
interface Fixture {
|
||||
fixture: {
|
||||
id: number
|
||||
date: string
|
||||
}
|
||||
teams: {
|
||||
home: {
|
||||
id: number
|
||||
name: string
|
||||
logo: string
|
||||
}
|
||||
away: {
|
||||
id: number
|
||||
name: string
|
||||
logo: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const REFRESH_INTERVAL = 5 * 60 * 1000 // 5 minutes
|
||||
|
||||
export default function UpcomingFixtures() {
|
||||
const [fixtures, setFixtures] = useState<Fixture[]>([])
|
||||
const [loading, setLoading] = useState<boolean>(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const fetchUpcomingFixtures = async () => {
|
||||
try {
|
||||
setError(null)
|
||||
const res = await fetch('/api/fixtures?next=10')
|
||||
if (!res?.ok) throw new Error('Failed to fetch upcoming fixtures')
|
||||
const data = await res.json()
|
||||
setFixtures(data?.response ?? [])
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err?.message : 'Failed to load upcoming fixtures')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchUpcomingFixtures()
|
||||
const interval = setInterval(fetchUpcomingFixtures, REFRESH_INTERVAL)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<section className="bg-white rounded-xl shadow-lg p-6 border border-gray-100">
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-8 h-8 text-[#E42518] animate-spin" />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<section className="bg-white rounded-xl shadow-lg p-6 border border-gray-100">
|
||||
<div className="flex items-center justify-center gap-2 py-12 text-gray-500">
|
||||
<AlertCircle className="w-5 h-5" />
|
||||
<span>{error}</span>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="bg-white rounded-xl shadow-lg p-6 border border-gray-100">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<Calendar className="w-6 h-6 text-[#E42518]" />
|
||||
<h2 className="text-xl font-bold text-gray-800">Upcoming Fixtures</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{fixtures?.slice(0, 8)?.map((fixture) => {
|
||||
const date = new Date(fixture?.fixture?.date ?? '')
|
||||
const formattedDate = date?.toLocaleDateString('en-GB', { day: 'numeric', month: 'short' })
|
||||
const formattedTime = date?.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' })
|
||||
|
||||
return (
|
||||
<div
|
||||
key={fixture?.fixture?.id}
|
||||
className="p-3 border border-gray-200 rounded-lg hover:shadow-md transition-shadow bg-gradient-to-r from-white to-gray-50"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<div className="relative w-6 h-6">
|
||||
<Image
|
||||
src={fixture?.teams?.home?.logo ?? ''}
|
||||
alt={fixture?.teams?.home?.name ?? ''}
|
||||
fill
|
||||
className="object-contain"
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-gray-800 truncate">
|
||||
{fixture?.teams?.home?.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mx-3 text-sm text-gray-500 font-medium">
|
||||
vs
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 flex-1 justify-end">
|
||||
<span className="text-sm font-medium text-gray-800 truncate">
|
||||
{fixture?.teams?.away?.name}
|
||||
</span>
|
||||
<div className="relative w-6 h-6">
|
||||
<Image
|
||||
src={fixture?.teams?.away?.logo ?? ''}
|
||||
alt={fixture?.teams?.away?.name ?? ''}
|
||||
fill
|
||||
className="object-contain"
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 text-center mt-2">
|
||||
{formattedDate} • {formattedTime}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user