- Updated LiveEventsList to use live data from the live store and improved event rendering with links to event details. - Enhanced MatchDetailView to accept API sections for dynamic market rendering and improved state management for expanded sections. - Modified SportsNav to utilize search parameters for active sport highlighting. - Refactored TopMatches to fetch live match data and odds from the API, replacing static fallback data. - Improved UI elements for better responsiveness and user experience across components.
171 lines
7.5 KiB
TypeScript
171 lines
7.5 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import { ChevronRight } from "lucide-react"
|
|
import { useBetslipStore } from "@/lib/store/betslip-store"
|
|
import {
|
|
fetchEvents,
|
|
fetchOddsForEvent,
|
|
get1X2FromOddsResponse,
|
|
TOP_LEAGUES,
|
|
type ApiEvent,
|
|
} from "@/lib/betting-api"
|
|
import { cn } from "@/lib/utils"
|
|
|
|
type TopMatch = {
|
|
id: string
|
|
league: string
|
|
time: string
|
|
homeTeam: string
|
|
awayTeam: string
|
|
odds: { home: number; draw: number; away: number }
|
|
}
|
|
|
|
const FALLBACK_MATCHES: TopMatch[] = [
|
|
{ id: "tm1", league: "England - Premier League", time: "05:00 PM", homeTeam: "Nottingham Forest", awayTeam: "Liverpool", odds: { home: 4.09, draw: 3.93, away: 1.82 } },
|
|
{ id: "tm2", league: "England - Premier League", time: "11:00 PM", homeTeam: "Man City", awayTeam: "Newcastle", odds: { home: 1.50, draw: 5.17, away: 5.93 } },
|
|
{ id: "tm3", league: "England - Premier League", time: "06:00 PM", homeTeam: "Chelsea", awayTeam: "Burnley", odds: { home: 1.21, draw: 6.91, away: 11.50 } },
|
|
{ id: "tm4", league: "Spain - LaLiga", time: "07:30 PM", homeTeam: "Arsenal", awayTeam: "Wolves", odds: { home: 1.56, draw: 4.16, away: 5.80 } },
|
|
{ id: "tm5", league: "Italy - Serie A", time: "09:45 PM", homeTeam: "Inter Milan", awayTeam: "Napoli", odds: { home: 1.85, draw: 3.60, away: 4.20 } },
|
|
]
|
|
|
|
function parseTime(iso: string): string {
|
|
try {
|
|
const d = new Date(iso)
|
|
return d.toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit", hour12: true })
|
|
} catch {
|
|
return "--:--"
|
|
}
|
|
}
|
|
|
|
export function TopMatches() {
|
|
const { bets, addBet } = useBetslipStore()
|
|
const [matches, setMatches] = useState<TopMatch[]>(FALLBACK_MATCHES)
|
|
|
|
useEffect(() => {
|
|
let cancelled = false
|
|
const TOP_MATCHES_SIZE = 5
|
|
const leagueIds = TOP_LEAGUES.slice(0, 4).map((l) => l.id)
|
|
const nameById = Object.fromEntries(TOP_LEAGUES.map((l) => [l.id, l.name]))
|
|
|
|
Promise.all(leagueIds.map((league_id) => fetchEvents({ league_id, page_size: 2, page: 1 })))
|
|
.then(async (leagueResponses) => {
|
|
if (cancelled) return
|
|
const list: TopMatch[] = []
|
|
const eventMeta: { e: ApiEvent; leagueName: string }[] = []
|
|
leagueResponses.forEach((res, i) => {
|
|
const leagueName = nameById[leagueIds[i]] ?? ""
|
|
const events = res.data ?? []
|
|
for (const e of events) {
|
|
eventMeta.push({ e, leagueName })
|
|
if (eventMeta.length >= TOP_MATCHES_SIZE) break
|
|
}
|
|
})
|
|
const oddsResponses = await Promise.all(
|
|
eventMeta.slice(0, TOP_MATCHES_SIZE).map(({ e }) => fetchOddsForEvent(e.id).catch(() => ({ data: [] })))
|
|
)
|
|
eventMeta.slice(0, TOP_MATCHES_SIZE).forEach(({ e, leagueName }, i) => {
|
|
const mainOdds = get1X2FromOddsResponse(oddsResponses[i]?.data ?? [])
|
|
list.push({
|
|
id: String(e.id),
|
|
league: leagueName,
|
|
time: parseTime(e.start_time),
|
|
homeTeam: e.home_team,
|
|
awayTeam: e.away_team,
|
|
odds: mainOdds
|
|
? { home: mainOdds["1"], draw: mainOdds.X, away: mainOdds["2"] }
|
|
: { home: 0, draw: 0, away: 0 },
|
|
})
|
|
})
|
|
if (list.length > 0) setMatches(list)
|
|
})
|
|
.catch(() => {})
|
|
return () => { cancelled = true }
|
|
}, [])
|
|
|
|
return (
|
|
<div className="flex gap-3 overflow-x-auto pb-2 scrollbar-hide -mx-1 px-1">
|
|
{matches.map((match) => {
|
|
const eventName = `${match.homeTeam} - ${match.awayTeam}`
|
|
const leagueForBet = `Football - ${match.league}`
|
|
const outcomes = [
|
|
{ key: "1", label: "1", odds: match.odds.home },
|
|
{ key: "x", label: "X", odds: match.odds.draw },
|
|
{ key: "2", label: "2", odds: match.odds.away },
|
|
] as const
|
|
return (
|
|
<div
|
|
key={match.id}
|
|
className="min-w-[280px] bg-brand-surface border border-border/20 rounded-sm overflow-hidden flex flex-col relative group"
|
|
>
|
|
{/* Top Label Ribbon */}
|
|
<div className="absolute top-0 right-0 w-12 h-12 overflow-hidden z-10 pointer-events-none">
|
|
<div className="absolute top-[6px] right-[-14px] bg-brand-primary text-black text-[8px] font-black py-0.5 px-6 rotate-45 shadow-sm uppercase tracking-tighter">
|
|
TOP
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-brand-bg px-3 py-1.5 flex items-center justify-between text-[10px] text-muted-foreground border-b border-border/10">
|
|
<div className="flex items-center gap-1">
|
|
<span className="font-bold text-brand-primary">{match.league}</span>
|
|
<span className="font-black text-white ml-1 italic">{match.time}</span>
|
|
</div>
|
|
<ChevronRight className="size-3 text-white/20 mr-4" />
|
|
</div>
|
|
|
|
<div className="p-3 flex flex-col gap-3">
|
|
<div className="flex items-center justify-between gap-1">
|
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
<div className="size-4 shrink-0 bg-white/5 rounded-sm flex items-center justify-center text-[9px] border border-white/10">⚽</div>
|
|
<span className="text-[11px] font-black text-white truncate uppercase italic">{match.homeTeam}</span>
|
|
</div>
|
|
<span className="text-brand-primary text-[9.5px] font-black italic shrink-0 px-2 opacity-60">VS</span>
|
|
<div className="flex items-center gap-2 flex-1 min-w-0 justify-end">
|
|
<span className="text-[11px] font-black text-white truncate uppercase italic text-right">{match.awayTeam}</span>
|
|
<div className="size-4 shrink-0 bg-white/5 rounded-sm flex items-center justify-center text-[9px] border border-white/10">⚽</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-3 gap-px bg-white/5 mx-2 mb-2 rounded-sm overflow-hidden border border-white/5">
|
|
{outcomes.map(({ key, label, odds }) => {
|
|
const betId = `${match.id}-${key}`
|
|
const isSelected = bets.some((b) => b.id === betId)
|
|
return (
|
|
<button
|
|
key={key}
|
|
type="button"
|
|
onClick={() =>
|
|
addBet({
|
|
id: betId,
|
|
event: eventName,
|
|
league: leagueForBet,
|
|
market: "1X2",
|
|
selection: label,
|
|
odds,
|
|
})
|
|
}
|
|
className={cn(
|
|
"flex items-center justify-between px-2.5 py-1.5 transition-colors group/btn border-x border-white/5 first:border-x-0",
|
|
isSelected
|
|
? "bg-brand-primary text-black"
|
|
: "bg-brand-bg hover:bg-brand-surface-light"
|
|
)}
|
|
>
|
|
<span className={cn("text-[10px] font-black", isSelected ? "text-black" : "text-white/40 group-hover/btn:text-white")}>
|
|
{label}
|
|
</span>
|
|
<span className={cn("text-[11px] font-black tabular-nums", isSelected ? "text-black" : "text-brand-primary")}>
|
|
{odds > 0 ? odds.toFixed(2) : "—"}
|
|
</span>
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|