- 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.
188 lines
8.3 KiB
TypeScript
188 lines
8.3 KiB
TypeScript
"use client"
|
||
|
||
import { useEffect } from "react"
|
||
import Link from "next/link"
|
||
import { useBetslipStore } from "@/lib/store/betslip-store"
|
||
import { useLiveStore } from "@/lib/store/live-store"
|
||
import { SportEnum } from "@/lib/store/betting-types"
|
||
import { SPORT_ID_MAP } from "@/lib/store/betting-api"
|
||
import type { AppEvent } from "@/lib/store/betting-types"
|
||
import { cn } from "@/lib/utils"
|
||
import { BarChart2, Monitor, Loader2 } from "lucide-react"
|
||
|
||
function LiveEventRow({ event, isNoOdds }: { event: AppEvent; isNoOdds?: boolean }) {
|
||
const { addBet } = useBetslipStore()
|
||
const score = event.score ?? "0 - 0"
|
||
const time = event.matchMinute != null ? `${String(event.matchMinute).padStart(2, "0")}:00` : "—"
|
||
const period = "H2"
|
||
|
||
return (
|
||
<div className="bg-brand-bg border-b border-white/5 hover:bg-white/5 transition-colors h-[50px] flex items-center group">
|
||
{/* Match Info Column (Time & Score) */}
|
||
<div className="flex items-center gap-3 px-3 w-[360px] shrink-0 h-full border-r border-white/5">
|
||
<div className="flex flex-col text-[10px] font-black leading-tight italic w-[55px] shrink-0 tabular-nums">
|
||
<span className="text-brand-primary">{time}</span>
|
||
<span className="text-white/40">{period}</span>
|
||
</div>
|
||
<div className="flex items-center min-w-0 flex-1 gap-2">
|
||
<Link
|
||
href={`/event/${event.id}`}
|
||
className="text-[11.5px] font-black text-white truncate italic uppercase hover:text-brand-primary transition-colors"
|
||
>
|
||
{event.homeTeam} <span className="text-brand-primary mx-1 tabular-nums">{score}</span> {event.awayTeam}
|
||
</Link>
|
||
</div>
|
||
<div className="flex items-center gap-2 shrink-0 px-1 opacity-20 group-hover:opacity-100 transition-opacity">
|
||
<BarChart2 className="size-3.5 text-white" />
|
||
<Monitor className="size-3.5 text-white" />
|
||
</div>
|
||
</div>
|
||
|
||
{/* Odds Grid or Placeholder */}
|
||
<div className="flex-1 h-full flex items-center">
|
||
{isNoOdds ? (
|
||
<div className="flex-1 h-full bg-brand-surface-light flex items-center justify-center text-[10.5px] font-black text-white/20 uppercase italic tracking-tight">
|
||
Sorry, no odds for this match
|
||
</div>
|
||
) : (
|
||
<div className="flex-1 grid grid-cols-3 h-full">
|
||
{event.markets.slice(0, 3).map((m, idx) => {
|
||
const labels = ["Home", "Draw", "Away"]
|
||
return (
|
||
<button
|
||
key={m.id}
|
||
onClick={() => addBet({
|
||
id: `${event.id}-${m.id}`,
|
||
event: `${event.homeTeam} - ${event.awayTeam}`,
|
||
league: `${event.sport} - ${event.country} - ${event.league}`,
|
||
market: "1X2",
|
||
selection: m.label,
|
||
odds: m.odds,
|
||
})}
|
||
className="bg-brand-bg hover:bg-white/5 flex items-center justify-between px-4 h-full border-r border-white/5 transition-colors group/btn"
|
||
>
|
||
<span className="text-[10px] font-black text-white/40 group-hover/btn:text-white uppercase">{labels[idx]}</span>
|
||
<span className="text-[11px] font-black text-brand-primary tabular-nums">{m.odds.toFixed(2)}</span>
|
||
</button>
|
||
)
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Right Indicator */}
|
||
<div className="w-[4px] bg-brand-primary h-full" />
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const LIVE_SPORT_IDS = [
|
||
SportEnum.SOCCER,
|
||
SportEnum.BASKETBALL,
|
||
SportEnum.ICE_HOCKEY,
|
||
SportEnum.TENNIS,
|
||
SportEnum.HANDBALL,
|
||
SportEnum.RUGBY_UNION,
|
||
SportEnum.TABLE_TENNIS,
|
||
SportEnum.VOLLEYBALL,
|
||
SportEnum.FUTSAL,
|
||
SportEnum.E_SPORTS,
|
||
] as const
|
||
|
||
export function LiveEventsList() {
|
||
const { events, loading, error, sportId, setSportId, loadLiveEvents } = useLiveStore()
|
||
|
||
useEffect(() => {
|
||
loadLiveEvents()
|
||
}, [loadLiveEvents])
|
||
|
||
const groupedByLeague = events.reduce((acc, ev) => {
|
||
const key = ev.league || "Other"
|
||
if (!acc[key]) acc[key] = []
|
||
acc[key].push(ev)
|
||
return acc
|
||
}, {} as Record<string, AppEvent[]>)
|
||
|
||
return (
|
||
<div className="flex flex-col min-h-screen bg-brand-bg">
|
||
{/* Sport Navigation: SportEnum ids, no league — event?sport_id=1&first_start_time=RFC3339&is_live=true */}
|
||
<div className="bg-brand-surface border-b border-border/20 px-2 flex items-center h-[54px] overflow-x-auto scrollbar-hide">
|
||
<div className="flex items-center gap-0 h-full">
|
||
<button className="flex flex-col items-center justify-center px-4 h-full border-r border-white/5 min-w-[70px]">
|
||
<span className="text-[14px]">⭐</span>
|
||
<span className="text-[9px] font-bold text-white/40 uppercase mt-0.5">Favourites</span>
|
||
</button>
|
||
<button className="flex flex-col items-center justify-center px-4 h-full border-r border-white/5 min-w-[70px]">
|
||
<span className="text-[14px]">⏱️</span>
|
||
<span className="text-[9px] font-bold text-white/40 uppercase mt-0.5">Prematch</span>
|
||
</button>
|
||
{LIVE_SPORT_IDS.map((id) => {
|
||
const info = SPORT_ID_MAP[id]
|
||
if (!info) return null
|
||
const icon = id === SportEnum.SOCCER ? "⚽" : id === SportEnum.TENNIS ? "🎾" : id === SportEnum.BASKETBALL ? "🏀" : id === SportEnum.ICE_HOCKEY ? "🏒" : id === SportEnum.VOLLEYBALL ? "🏐" : id === SportEnum.HANDBALL ? "🤾" : id === SportEnum.E_SPORTS ? "🎮" : "⚽"
|
||
const active = sportId === id
|
||
return (
|
||
<button
|
||
key={id}
|
||
type="button"
|
||
onClick={() => setSportId(id)}
|
||
className={cn(
|
||
"flex flex-col items-center justify-center px-3 h-full border-r border-white/5 min-w-[75px] relative transition-colors",
|
||
active ? "bg-white/5" : "hover:bg-white/5"
|
||
)}
|
||
>
|
||
<span className="text-[16px]">{icon}</span>
|
||
<span className={cn(
|
||
"text-[9px] font-bold uppercase mt-1 tracking-tighter whitespace-nowrap",
|
||
active ? "text-brand-primary" : "text-white/40"
|
||
)}>
|
||
{info.name}
|
||
</span>
|
||
{active && <div className="absolute bottom-0 left-0 right-0 h-[2px] bg-brand-primary" />}
|
||
</button>
|
||
)
|
||
})}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Category Header */}
|
||
<div className="bg-brand-primary px-3 py-1.5 flex items-center gap-2 border-l-4 border-brand-primary">
|
||
<span className="text-[16px]">{SPORT_ID_MAP[sportId]?.name === "Soccer" ? "⚽" : "•"}</span>
|
||
<h2 className="text-[14px] font-black text-white uppercase tracking-tight">
|
||
{SPORT_ID_MAP[sportId]?.name ?? "Live"}
|
||
</h2>
|
||
</div>
|
||
|
||
{loading && events.length === 0 ? (
|
||
<div className="flex items-center justify-center py-16 gap-2 text-white/60">
|
||
<Loader2 className="size-5 animate-spin" />
|
||
<span className="text-sm">Loading live events…</span>
|
||
</div>
|
||
) : error && events.length === 0 ? (
|
||
<div className="py-8 px-4 text-center text-white/60 text-sm">{error}</div>
|
||
) : (
|
||
<div className="flex flex-col mb-10">
|
||
{Object.entries(groupedByLeague).map(([leagueName, matches]) => (
|
||
<div key={leagueName} className="flex flex-col">
|
||
<div className="bg-brand-surface px-3 py-1 border-b border-border/10 flex items-center gap-2">
|
||
<span className="text-[9.5px] font-black text-white/60 uppercase tracking-widest leading-none">
|
||
{leagueName}
|
||
</span>
|
||
</div>
|
||
<div className="flex flex-col">
|
||
{matches.map((match) => (
|
||
<LiveEventRow
|
||
key={match.id}
|
||
event={match}
|
||
isNoOdds={!match.markets?.length || match.markets.every((m) => m.odds <= 0)}
|
||
/>
|
||
))}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|