This commit is contained in:
jpmvaz
2026-09-13 20:20:06 +01:00
commit 6994063c71
93 changed files with 7613 additions and 0 deletions
@@ -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>
)
}
+147
View File
@@ -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>
)
}