Refactor live events list and match detail view components

- 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.
This commit is contained in:
brooktewabe 2026-03-02 19:08:52 +03:00
parent 86ffd88e46
commit 8941c45555
6 changed files with 495 additions and 295 deletions

View File

@ -5,8 +5,11 @@ import Link from "next/link"
import { useSearchParams } from "next/navigation" import { useSearchParams } from "next/navigation"
import { useBetslipStore } from "@/lib/store/betslip-store" import { useBetslipStore } from "@/lib/store/betslip-store"
import { mockEvents, popularLeagues, type Event } from "@/lib/mock-data" import { mockEvents, popularLeagues, type Event } from "@/lib/mock-data"
import { useBettingStore } from "@/lib/store/betting-store"
import type { AppEvent } from "@/lib/store/betting-types"
import { SPORT_SLUG_TO_ID, getMarketsForTab, type ApiOdds, type MarketTabKey } from "@/lib/store/betting-api"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { ChevronDown, BarChart2, TrendingUp, Plus } from "lucide-react" import { ChevronDown, BarChart2, TrendingUp, Plus, Loader2 } from "lucide-react"
function OddsButton({ odds, onClick, isSelected }: { function OddsButton({ odds, onClick, isSelected }: {
odds: number odds: number
@ -26,7 +29,7 @@ function OddsButton({ odds, onClick, isSelected }: {
) )
} }
function EventRow({ event }: { event: Event }) { function EventRow({ event }: { event: Event | AppEvent }) {
const { bets, addBet } = useBetslipStore() const { bets, addBet } = useBetslipStore()
return ( return (
@ -82,22 +85,47 @@ function EventRow({ event }: { event: Event }) {
) )
} }
const MARKET_HEADERS = ["1", "x", "2", "Over (2.5)", "Under (2.5)", "1X", "12", "X2", "Yes", "No"]; const MARKET_HEADERS = ["1", "X", "2", "Over (2.5)", "Under (2.5)", "1X", "12", "X2", "Yes", "No"]
export function EventsList({ filter = "All", sport = "all", search = "" }: { const ROW1_TABS: { key: MarketTabKey; label: string }[] = [
{ key: "main", label: "Main" },
{ key: "goals", label: "Goals" },
{ key: "handicap", label: "Handicap" },
{ key: "half_time", label: "Half Time / Full Time" },
{ key: "correct_score", label: "Correct Score" },
]
const ROW2_TABS: { key: MarketTabKey; label: string }[] = [
{ key: "1st_half", label: "1st Half" },
{ key: "2nd_half", label: "2nd Half" },
{ key: "combo", label: "Combo" },
{ key: "chance_mix", label: "Chance Mix" },
{ key: "home", label: "Home" },
]
export function EventsList({ filter = "All", sport: sportProp = "all", search = "" }: {
filter?: string filter?: string
sport?: string sport?: string
search?: string search?: string
}) { }) {
const searchParams = useSearchParams() const searchParams = useSearchParams()
const leagueQuery = searchParams.get("league") const leagueQuery = searchParams.get("league")
const sportQuery = searchParams.get("sport") ?? sportProp
const [selectedLeague, setSelectedLeague] = useState<string | null>(leagueQuery) const [selectedLeague, setSelectedLeague] = useState<string | null>(leagueQuery)
const [activeTab, setActiveTab] = useState<MarketTabKey>("main")
const { bets, addBet } = useBetslipStore() const { bets, addBet } = useBetslipStore()
const sportId = sportQuery === "all" ? null : (SPORT_SLUG_TO_ID[sportQuery] ?? null)
const leagueId = selectedLeague && !Number.isNaN(Number(selectedLeague)) ? selectedLeague : null
const { events: apiEvents, loading, error, hasMore, loadMore, setFilters } = useBettingStore()
useEffect(() => { useEffect(() => {
setSelectedLeague(leagueQuery) setSelectedLeague(leagueQuery)
}, [leagueQuery]) }, [leagueQuery])
useEffect(() => {
setFilters(sportId, leagueId)
}, [sportId, leagueId, setFilters])
const handleClose = () => { const handleClose = () => {
const url = new URL(window.location.href) const url = new URL(window.location.href)
url.searchParams.delete("league") url.searchParams.delete("league")
@ -105,13 +133,17 @@ export function EventsList({ filter = "All", sport = "all", search = "" }: {
setSelectedLeague(null) setSelectedLeague(null)
} }
const events = selectedLeague const useApi = !(error && apiEvents.length === 0)
? mockEvents.filter(e => e.league.toLowerCase() === selectedLeague.toLowerCase()) const events = useApi
: mockEvents.filter((e) => { ? (filter === "Live" ? apiEvents.filter((e) => e.isLive) : apiEvents)
if (filter === "Live" && !e.isLive) return false : selectedLeague
if (sport !== "all" && e.sport.toLowerCase() !== sport.toLowerCase()) return false ? mockEvents.filter((e) => e.league.toLowerCase() === selectedLeague.toLowerCase())
return true : mockEvents.filter((e) => {
}) if (filter === "Live" && !e.isLive) return false
if (sportProp !== "all" && e.sport.toLowerCase() !== sportProp.toLowerCase()) return false
return true
})
const showLoadMore = useApi && hasMore && events.length > 0
// Common Header Rendering // Common Header Rendering
const renderTableHeaders = () => ( const renderTableHeaders = () => (
@ -135,75 +167,130 @@ export function EventsList({ filter = "All", sport = "all", search = "" }: {
</> </>
) )
const renderColumnHeaders = () => ( const getHeadersForTab = (tab: MarketTabKey) => {
<div className="bg-brand-surface border-b border-white/5 h-8 flex items-center text-[9px] font-black text-white/40 uppercase"> const first = events[0]
<div className="w-[180px] px-3 flex items-center gap-1.5 border-r border-border/10 h-full">Main</div> const rawOdds: ApiOdds[] = first && "rawOdds" in first && Array.isArray((first as AppEvent).rawOdds) ? (first as AppEvent).rawOdds! : []
<div className="w-[180px] flex items-center justify-center border-r border-border/10 h-full">Over/Under</div> return getMarketsForTab(rawOdds, tab).headers
<div className="flex-1 grid grid-cols-10 text-center h-full items-center tracking-tighter"> }
{MARKET_HEADERS.map(h => <span key={h}>{h}</span>)}
</div>
<div className="w-10 border-l border-border/10 h-full" />
</div>
)
const renderEventItem = (event: Event) => ( const renderColumnHeaders = (tab: MarketTabKey, eventList: (Event | AppEvent)[]) => {
<div key={event.id} className="h-[34px] group flex items-center border-b border-white/5 bg-brand-bg hover:bg-white/5 transition-colors"> let headers = eventList.length ? getHeadersForTab(tab) : getMarketsForTab([], tab).headers
{/* Stats & Icons */} if (!headers.length) headers = getMarketsForTab([], "main").headers
<div className="w-[35px] flex items-center justify-center gap-1 px-2 border-r border-white/5 h-full opacity-40 group-hover:opacity-100"> const n = Math.max(headers.length, 1)
<BarChart2 className="size-3 cursor-pointer hover:text-primary" /> return (
<div className="bg-brand-surface border-b border-white/5 flex flex-col">
<div className="h-8 flex items-center text-[9px] font-black text-white/40 uppercase">
<div className="w-[35px] shrink-0 px-1 border-r border-border/10 h-full flex items-center justify-center" />
<div className="w-[45px] shrink-0 border-r border-border/10 h-full flex items-center justify-center">ID</div>
<div className="w-[50px] shrink-0 border-r border-border/10 h-full flex items-center justify-center">Time</div>
<div className="flex-1 min-w-0 px-3 border-r border-border/10 h-full flex items-center">Event</div>
<div
className="flex-1 grid text-center h-full items-center tracking-tighter border-r border-border/10"
style={{ gridTemplateColumns: `repeat(${n}, minmax(0, 1fr))` }}
>
{headers.map((h, i) => (
<span key={i} className="border-r border-white/5 last:border-r-0 px-0.5 truncate">
{h}
</span>
))}
</div>
<div className="w-10 shrink-0 border-l border-border/10 h-full" />
</div>
</div> </div>
{/* ID */} )
<div className="w-[45px] text-[10px] font-black text-brand-primary italic tabular-nums text-center border-r border-white/5 h-full flex items-center justify-center"> }
{event.id}
const renderEventItem = (event: Event | AppEvent, tab: MarketTabKey) => {
const hasRawOdds = event && "rawOdds" in event && Array.isArray((event as AppEvent).rawOdds)
const rawOdds = hasRawOdds ? (event as AppEvent).rawOdds! : []
const { cells } = getMarketsForTab(rawOdds, tab)
const useMainMarkets = !hasRawOdds && event.markets?.length && (tab === "main" || tab === "combo" || tab === "chance_mix" || tab === "home")
const displayCells = useMainMarkets
? event.markets.slice(0, 10).map((m, i) => ({ id: m.id, label: MARKET_HEADERS[i] ?? m.label, odds: m.odds }))
: cells
const n = Math.max(displayCells.length, 1)
return (
<div key={event.id} className="h-[34px] group flex items-center border-b border-white/5 bg-brand-bg hover:bg-white/5 transition-colors">
<div className="w-[35px] flex items-center justify-center gap-1 px-2 border-r border-white/5 h-full opacity-40 group-hover:opacity-100">
<BarChart2 className="size-3 cursor-pointer hover:text-primary" />
</div>
<div className="w-[45px] text-[10px] font-black text-brand-primary italic tabular-nums text-center border-r border-white/5 h-full flex items-center justify-center">
{event.id}
</div>
<div className="w-[50px] flex flex-col items-center justify-center border-r border-white/5 h-full leading-none italic font-black text-[9px]">
<span>{event.time}</span>
<span className="text-[7px] text-white/30 uppercase mt-0.5">PM</span>
</div>
<Link
href={`/event/${event.id}`}
className="flex-1 px-4 text-[10.5px] font-black text-white truncate max-w-[200px] hover:text-brand-primary transition-colors"
>
{event.homeTeam} - {event.awayTeam}
</Link>
<div
className="flex-1 grid h-full"
style={{ gridTemplateColumns: `repeat(${n}, minmax(0, 1fr))` }}
>
{displayCells.map((cell) => {
const betId = `${event.id}-${cell.id}`
const isSelected = bets.some((b) => b.id === betId)
const hasOdds = cell.odds > 0
return (
<button
key={cell.id}
type="button"
disabled={!hasOdds}
onClick={() =>
hasOdds &&
addBet({
id: betId,
event: `${event.homeTeam} - ${event.awayTeam}`,
league: `${event.sport} - ${event.country} - ${event.league}`,
market: cell.label,
selection: cell.label,
odds: cell.odds,
})
}
className={cn(
"flex items-center justify-center text-[10.5px] font-black tabular-nums transition-all border-r border-white/5",
isSelected ? "bg-brand-primary text-black" : "text-brand-primary hover:bg-white/5",
!hasOdds && "text-white/30 cursor-default hover:bg-transparent"
)}
>
{hasOdds ? cell.odds.toFixed(2) : "—"}
</button>
)
})}
</div>
<Link
href={`/event/${event.id}`}
className="w-10 flex items-center justify-center h-full hover:bg-white/5 transition-colors border-l border-white/5 text-white/40 hover:text-white"
aria-label="View all markets"
>
<Plus className="size-3" />
</Link>
</div> </div>
{/* Time */} )
<div className="w-[50px] flex flex-col items-center justify-center border-r border-white/5 h-full leading-none italic font-black text-[9px]"> }
<span>{event.time}</span>
<span className="text-[7px] text-white/30 uppercase mt-0.5">PM</span> if (loading && events.length === 0) {
return (
<div className="flex flex-col bg-brand-bg rounded overflow-hidden py-16 items-center justify-center gap-3">
<Loader2 className="size-8 animate-spin text-brand-primary" aria-hidden />
<p className="text-white/80 text-sm font-medium">Loading events and odds</p>
{/* <p className="text-white/50 text-xs">Resolving odds for each event</p> */}
</div> </div>
{/* Event Name -> same route as + icon (match detail) */} )
<Link }
href={`/event/${event.id}`}
className="flex-1 px-4 text-[10.5px] font-black text-white truncate max-w-[200px] hover:text-brand-primary transition-colors" if (error && apiEvents.length === 0 && events.length === 0) {
> return (
{event.homeTeam} - {event.awayTeam} <div className="flex flex-col bg-brand-bg rounded overflow-hidden py-8 px-4 text-center">
</Link> <p className="text-white/60 text-sm mb-2">{error}</p>
{/* Odds Grid */} <p className="text-white/40 text-xs">Check NEXT_PUBLIC_BETTING_API_BASE_URL and tenant.</p>
<div className="flex-1 grid grid-cols-10 h-full">
{event.markets.slice(0, 10).map((market) => {
const betId = `${event.id}-${market.id}`
const isSelected = bets.some((b) => b.id === betId)
return (
<button
key={market.id}
onClick={() => addBet({
id: betId,
event: `${event.homeTeam} - ${event.awayTeam}`,
league: `${event.sport} - ${event.country} - ${event.league}`,
market: "1X2",
selection: market.label,
odds: market.odds,
})}
className={cn(
"flex items-center justify-center text-[10.5px] font-black tabular-nums transition-all border-r border-white/5",
isSelected ? "bg-brand-primary text-black" : "text-brand-primary hover:bg-white/5"
)}
>
{market.odds.toFixed(2)}
</button>
)
})}
</div> </div>
{/* More Button -> match detail page */} )
<Link }
href={`/event/${event.id}`}
className="w-10 flex items-center justify-center h-full hover:bg-white/5 transition-colors border-l border-white/5 text-white/40 hover:text-white"
aria-label="View all markets"
>
<Plus className="size-3" />
</Link>
</div>
)
if (selectedLeague) { if (selectedLeague) {
// Group by date for league view // Group by date for league view
@ -211,7 +298,7 @@ export function EventsList({ filter = "All", sport = "all", search = "" }: {
if (!acc[event.date]) acc[event.date] = [] if (!acc[event.date]) acc[event.date] = []
acc[event.date].push(event) acc[event.date].push(event)
return acc return acc
}, {} as Record<string, Event[]>) }, {} as Record<string, (Event | AppEvent)[]>)
return ( return (
<div className="flex flex-col bg-brand-bg rounded overflow-hidden shadow-2xl"> <div className="flex flex-col bg-brand-bg rounded overflow-hidden shadow-2xl">
@ -231,26 +318,41 @@ export function EventsList({ filter = "All", sport = "all", search = "" }: {
</button> </button>
</div> </div>
{/* Large Market Tab Grid */} {/* Market category tabs row 1: Main, Goals, Handicap, Half Time / Full Time, Correct Score */}
<div className="grid grid-cols-5 bg-brand-bg border-b border-border/10"> <div className="flex flex-wrap gap-1 p-2 pb-1 bg-brand-bg border-b border-border/10">
{[ {ROW1_TABS.map(({ key, label }) => (
{ label: "Main", active: true }, { label: "Goals" }, { label: "Handicap" }, { label: "Half Time / Full Time" }, { label: "Correct Score" }, <button
{ label: "1st Half" }, { label: "2nd Half" }, { label: "Asian Markets" }, { label: "Corners" }, { label: "Home" } key={key}
].map((m, i) => ( type="button"
<button onClick={() => setActiveTab(key)}
key={i} className={cn(
className={cn( "px-3 py-1.5 text-[10px] font-black uppercase rounded-sm transition-all",
"h-8 border-r border-b border-border/10 flex items-center justify-center text-[10px] font-black uppercase transition-all", activeTab === key ? "bg-brand-primary text-black" : "text-white/60 hover:bg-brand-surface hover:text-white"
m.active ? "bg-brand-primary text-black" : "text-white/60 hover:bg-brand-surface" )}
)} >
> {label}
{m.label} </button>
</button> ))}
))} </div>
{/* Row 2: 1st Half, 2nd Half, Combo, Chance Mix, Home */}
<div className="flex flex-wrap gap-1 px-2 pb-2 bg-brand-bg border-b border-border/10">
{ROW2_TABS.map(({ key, label }) => (
<button
key={key}
type="button"
onClick={() => setActiveTab(key)}
className={cn(
"px-3 py-1.5 text-[10px] font-black uppercase rounded-sm transition-all",
activeTab === key ? "bg-brand-primary text-black" : "text-white/60 hover:bg-brand-surface hover:text-white"
)}
>
{label}
</button>
))}
</div> </div>
{/* Column Headers */} {/* Column Headers (dynamic by tab) */}
{renderColumnHeaders()} {renderColumnHeaders(activeTab, events)}
{/* Grouped Events */} {/* Grouped Events */}
<div className="overflow-y-auto max-h-[700px]"> <div className="overflow-y-auto max-h-[700px]">
@ -259,10 +361,29 @@ export function EventsList({ filter = "All", sport = "all", search = "" }: {
<div className="bg-brand-surface px-2 py-1 text-[10px] font-black text-white border-b border-white/5"> <div className="bg-brand-surface px-2 py-1 text-[10px] font-black text-white border-b border-white/5">
{date} {date}
</div> </div>
{dateEvents.map(event => renderEventItem(event))} {dateEvents.map((event) => renderEventItem(event, activeTab))}
</div> </div>
))} ))}
</div> </div>
{showLoadMore && (
<div className="p-3 border-t border-white/10">
<button
type="button"
onClick={loadMore}
disabled={loading}
className="w-full py-2.5 text-[11px] font-bold uppercase text-brand-primary hover:bg-white/5 disabled:opacity-70 flex items-center justify-center gap-2"
>
{loading ? (
<>
<Loader2 className="size-4 animate-spin shrink-0" aria-hidden />
Loading
</>
) : (
"Load more"
)}
</button>
</div>
)}
</div> </div>
) )
} }
@ -276,6 +397,11 @@ export function EventsList({ filter = "All", sport = "all", search = "" }: {
return ( return (
<div className="flex flex-col bg-brand-bg rounded overflow-hidden"> <div className="flex flex-col bg-brand-bg rounded overflow-hidden">
{error && (
<div className="px-3 py-1.5 bg-amber-500/20 border-b border-amber-500/30 text-amber-200 text-[10px]">
Showing sample data. API: {error}
</div>
)}
<div className="flex flex-col"> <div className="flex flex-col">
{Object.entries(homeEventsByLeague).map(([leagueName, leagueEvents]) => ( {Object.entries(homeEventsByLeague).map(([leagueName, leagueEvents]) => (
<div key={leagueName} className="flex flex-col border-b border-white/5 last:border-none"> <div key={leagueName} className="flex flex-col border-b border-white/5 last:border-none">
@ -312,11 +438,30 @@ export function EventsList({ filter = "All", sport = "all", search = "" }: {
{/* Matches in this league */} {/* Matches in this league */}
<div className="flex flex-col"> <div className="flex flex-col">
{leagueEvents.map(event => renderEventItem(event))} {leagueEvents.map((event) => renderEventItem(event, "main"))}
</div> </div>
</div> </div>
))} ))}
</div> </div>
{showLoadMore && (
<div className="p-3 border-t border-white/10">
<button
type="button"
onClick={loadMore}
disabled={loading}
className="w-full py-2.5 text-[11px] font-bold uppercase text-brand-primary hover:bg-white/5 disabled:opacity-70 flex items-center justify-center gap-2"
>
{loading ? (
<>
<Loader2 className="size-4 animate-spin shrink-0" aria-hidden />
Loading
</>
) : (
"Load more"
)}
</button>
</div>
)}
</div> </div>
) )
} }

View File

@ -1,17 +1,19 @@
"use client" "use client"
import { useEffect } from "react"
import Link from "next/link"
import { useBetslipStore } from "@/lib/store/betslip-store" import { useBetslipStore } from "@/lib/store/betslip-store"
import { mockEvents, type Event } from "@/lib/mock-data" 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 { cn } from "@/lib/utils"
import { BarChart2, TrendingUp, Monitor, Tv } from "lucide-react" import { BarChart2, Monitor, Loader2 } from "lucide-react"
function LiveEventRow({ event, isNoOdds }: { event: AppEvent; isNoOdds?: boolean }) {
function LiveEventRow({ event, isNoOdds }: { event: Event, isNoOdds?: boolean }) { const { addBet } = useBetslipStore()
const { bets, addBet } = useBetslipStore() const score = event.score ?? "0 - 0"
const time = event.matchMinute != null ? `${String(event.matchMinute).padStart(2, "0")}:00` : "—"
// Dummy data for demonstration
const score = event.homeScore !== undefined ? `${event.homeScore} - ${event.awayScore}` : "0 - 0"
const time = event.liveMinute ? `${event.liveMinute}:00` : "83:10"
const period = "H2" const period = "H2"
return ( return (
@ -23,9 +25,12 @@ function LiveEventRow({ event, isNoOdds }: { event: Event, isNoOdds?: boolean })
<span className="text-white/40">{period}</span> <span className="text-white/40">{period}</span>
</div> </div>
<div className="flex items-center min-w-0 flex-1 gap-2"> <div className="flex items-center min-w-0 flex-1 gap-2">
<span className="text-[11.5px] font-black text-white truncate italic uppercase"> <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} {event.homeTeam} <span className="text-brand-primary mx-1 tabular-nums">{score}</span> {event.awayTeam}
</span> </Link>
</div> </div>
<div className="flex items-center gap-2 shrink-0 px-1 opacity-20 group-hover:opacity-100 transition-opacity"> <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" /> <BarChart2 className="size-3.5 text-white" />
@ -71,64 +76,38 @@ function LiveEventRow({ event, isNoOdds }: { event: Event, isNoOdds?: boolean })
) )
} }
const liveSports = [ const LIVE_SPORT_IDS = [
{ id: "soccer", label: "Soccer", icon: "⚽", count: 25, active: true }, SportEnum.SOCCER,
{ id: "basketball", label: "Basketball", icon: "🏀", count: 39 }, SportEnum.BASKETBALL,
{ id: "ice-hockey", label: "Ice Hockey", icon: "🏒", count: 3 }, SportEnum.ICE_HOCKEY,
{ id: "tennis", label: "Tennis", icon: "🎾", count: 4 }, SportEnum.TENNIS,
{ id: "handball", label: "Handball", icon: "🤾", count: 10 }, SportEnum.HANDBALL,
{ id: "rugby", label: "Rugby", icon: "🏉", count: 2 }, SportEnum.RUGBY_UNION,
{ id: "table-tennis", label: "Table Tennis", icon: "🏓", count: 8 }, SportEnum.TABLE_TENNIS,
{ id: "volleyball", label: "Volleyball", icon: "🏐", count: 7 }, SportEnum.VOLLEYBALL,
{ id: "futsal", label: "Futsal", icon: "⚽", count: 2 }, SportEnum.FUTSAL,
{ id: "esport-counter-strike", label: "ESport Cou...", icon: "🎮", count: 2 }, SportEnum.E_SPORTS,
{ id: "esport-league-of-legends", label: "ESport Lea...", icon: "🎮", count: 1 }, ] as const
{ id: "esport-dota-2", label: "ESport Dota", icon: "🎮", count: 1 },
{ id: "efootball", label: "eFootball", icon: "⚽", count: 4 },
{ id: "ebasketball", label: "eBasketball", icon: "🏀", count: 1 },
]
export function LiveEventsList() { export function LiveEventsList() {
// Enhanced mock data local to live view to match screenshot exactly const { events, loading, error, sportId, setSportId, loadLiveEvents } = useLiveStore()
const liveMatches = [
{ useEffect(() => {
league: "Algeria - Ligue 1", loadLiveEvents()
flag: "https://flagcdn.com/w20/dz.png", }, [loadLiveEvents])
matches: [
{ ...mockEvents[0], id: "l1", homeTeam: "Paradou AC", awayTeam: "Ben Aknoun", homeScore: 3, awayScore: 5, liveMinute: 91, noOdds: true } const groupedByLeague = events.reduce((acc, ev) => {
] const key = ev.league || "Other"
}, if (!acc[key]) acc[key] = []
{ acc[key].push(ev)
league: "Australia - U23 Victoria NPL", return acc
flag: "https://flagcdn.com/w20/au.png", }, {} as Record<string, AppEvent[]>)
matches: [
{ ...mockEvents[1], id: "l2", homeTeam: "Oakleigh Cannons FC", awayTeam: "Altona Magic SC", homeScore: 5, awayScore: 1, liveMinute: 87, noOdds: true }
]
},
{
league: "Australia - U23 Victoria Premier League 1",
flag: "https://flagcdn.com/w20/au.png",
matches: [
{ ...mockEvents[2], id: "l3", homeTeam: "Northcote City FC", awayTeam: "Western United FC", homeScore: 4, awayScore: 0, liveMinute: 83, noOdds: false },
{ ...mockEvents[3], id: "l4", homeTeam: "Melbourne Knights FC", awayTeam: "Melbourne Victory FC", homeScore: 0, awayScore: 3, liveMinute: 81, noOdds: true }
]
},
{
league: "Australia - Victoria NPL, Women",
flag: "https://flagcdn.com/w20/au.png",
matches: [
{ ...mockEvents[4], id: "l5", homeTeam: "Preston Lions FC", awayTeam: "South Melbourne FC", homeScore: 1, awayScore: 1, liveMinute: 52, noOdds: true },
{ ...mockEvents[0], id: "l6", homeTeam: "Bentleigh Greens SC", awayTeam: "Box Hill United", homeScore: 0, awayScore: 6, liveMinute: 83, noOdds: true }
]
}
]
return ( return (
<div className="flex flex-col min-h-screen bg-brand-bg"> <div className="flex flex-col min-h-screen bg-brand-bg">
{/* Sport Navigation Carousel */} {/* 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="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"> <div className="flex items-center gap-0 h-full">
{/* Favourites & Prematch */}
<button className="flex flex-col items-center justify-center px-4 h-full border-r border-white/5 min-w-[70px]"> <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-[14px]"></span>
<span className="text-[9px] font-bold text-white/40 uppercase mt-0.5">Favourites</span> <span className="text-[9px] font-bold text-white/40 uppercase mt-0.5">Favourites</span>
@ -137,59 +116,72 @@ export function LiveEventsList() {
<span className="text-[14px]"></span> <span className="text-[14px]"></span>
<span className="text-[9px] font-bold text-white/40 uppercase mt-0.5">Prematch</span> <span className="text-[9px] font-bold text-white/40 uppercase mt-0.5">Prematch</span>
</button> </button>
{LIVE_SPORT_IDS.map((id) => {
{/* Live Sports */} const info = SPORT_ID_MAP[id]
{liveSports.map((sport) => ( if (!info) return null
<button 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 ? "🎮" : "⚽"
key={sport.id} const active = sportId === id
className={cn( return (
"flex flex-col items-center justify-center px-3 h-full border-r border-white/5 min-w-[75px] relative transition-colors", <button
sport.active ? "bg-white/5" : "hover:bg-white/5" key={id}
)} type="button"
> onClick={() => setSportId(id)}
<span className="absolute top-1 right-2 text-[8.5px] font-black text-white/40">{sport.count}</span> className={cn(
<span className="text-[16px]">{sport.icon}</span> "flex flex-col items-center justify-center px-3 h-full border-r border-white/5 min-w-[75px] relative transition-colors",
<span className={cn( active ? "bg-white/5" : "hover:bg-white/5"
"text-[9px] font-bold uppercase mt-1 tracking-tighter whitespace-nowrap", )}
sport.active ? "text-brand-primary" : "text-white/40" >
)}> <span className="text-[16px]">{icon}</span>
{sport.label} <span className={cn(
</span> "text-[9px] font-bold uppercase mt-1 tracking-tighter whitespace-nowrap",
{sport.active && ( active ? "text-brand-primary" : "text-white/40"
<div className="absolute bottom-0 left-0 right-0 h-[2px] bg-brand-primary" /> )}>
)} {info.name}
</button> </span>
))} {active && <div className="absolute bottom-0 left-0 right-0 h-[2px] bg-brand-primary" />}
</button>
)
})}
</div> </div>
</div> </div>
{/* Category Header (Soccer) */} {/* Category Header */}
<div className="bg-brand-primary px-3 py-1.5 flex items-center gap-2 border-l-[4px] border-brand-primary"> <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]"></span> <span className="text-[16px]">{SPORT_ID_MAP[sportId]?.name === "Soccer" ? "⚽" : "•"}</span>
<h2 className="text-[14px] font-black text-white uppercase tracking-tight">Soccer</h2> <h2 className="text-[14px] font-black text-white uppercase tracking-tight">
{SPORT_ID_MAP[sportId]?.name ?? "Live"}
</h2>
</div> </div>
{/* Grouped Live Matches */} {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"> <div className="flex flex-col mb-10">
{liveMatches.map((group, gIdx) => ( {Object.entries(groupedByLeague).map(([leagueName, matches]) => (
<div key={gIdx} className="flex flex-col"> <div key={leagueName} className="flex flex-col">
{/* League Header */}
<div className="bg-brand-surface px-3 py-1 border-b border-border/10 flex items-center gap-2"> <div className="bg-brand-surface px-3 py-1 border-b border-border/10 flex items-center gap-2">
<img src={group.flag} width="14" alt={group.league} className="rounded-sm opacity-60" />
<span className="text-[9.5px] font-black text-white/60 uppercase tracking-widest leading-none"> <span className="text-[9.5px] font-black text-white/60 uppercase tracking-widest leading-none">
{group.league} {leagueName}
</span> </span>
</div> </div>
{/* Matches in this league */}
<div className="flex flex-col"> <div className="flex flex-col">
{group.matches.map((match, mIdx) => ( {matches.map((match) => (
<LiveEventRow key={match.id} event={match as any} isNoOdds={match.noOdds} /> <LiveEventRow
key={match.id}
event={match}
isNoOdds={!match.markets?.length || match.markets.every((m) => m.odds <= 0)}
/>
))} ))}
</div> </div>
</div> </div>
))} ))}
</div> </div>
)}
</div> </div>
) )
} }

View File

@ -9,6 +9,8 @@ import {
type Event, type Event,
type DetailMarketSection, type DetailMarketSection,
} from "@/lib/mock-data" } from "@/lib/mock-data"
type ApiSection = { id: string; title: string; outcomes: { label: string; odds: number }[] }
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { ChevronDown, ChevronUp } from "lucide-react" import { ChevronDown, ChevronUp } from "lucide-react"
@ -77,11 +79,12 @@ function MarketSectionBlock({
<div className="px-3 pb-3 space-y-1.5"> <div className="px-3 pb-3 space-y-1.5">
{section.outcomes.length > 2 && section.outcomes.length % 2 === 0 ? ( {section.outcomes.length > 2 && section.outcomes.length % 2 === 0 ? (
<div className="grid grid-cols-2 gap-x-4 gap-y-1.5"> <div className="grid grid-cols-2 gap-x-4 gap-y-1.5">
{section.outcomes.map((outcome) => { {section.outcomes.map((outcome, i) => {
const betId = `${event.id}-${section.id}-${outcome.label.replace(/\s/g, "-").toLowerCase()}` const oddsStr = typeof outcome.odds === "number" ? outcome.odds.toFixed(2) : String(outcome.odds)
const betId = `${event.id}-${section.id}-${i}-${outcome.label.replace(/\s/g, "-").toLowerCase()}-${oddsStr}`
const isSelected = bets.some((b) => b.id === betId) const isSelected = bets.some((b) => b.id === betId)
return ( return (
<div key={outcome.label} className="flex items-center justify-between gap-2"> <div key={`${outcome.label}-${i}-${oddsStr}`} className="flex items-center justify-between gap-2">
<span className="text-[11px] text-white/90 truncate">{outcome.label}</span> <span className="text-[11px] text-white/90 truncate">{outcome.label}</span>
<button <button
type="button" type="button"
@ -107,12 +110,13 @@ function MarketSectionBlock({
})} })}
</div> </div>
) : ( ) : (
section.outcomes.map((outcome) => { section.outcomes.map((outcome, i) => {
const betId = `${event.id}-${section.id}-${outcome.label.replace(/\s/g, "-").toLowerCase()}` const oddsStr = typeof outcome.odds === "number" ? outcome.odds.toFixed(2) : String(outcome.odds)
const betId = `${event.id}-${section.id}-${i}-${outcome.label.replace(/\s/g, "-").toLowerCase()}-${oddsStr}`
const isSelected = bets.some((b) => b.id === betId) const isSelected = bets.some((b) => b.id === betId)
return ( return (
<div <div
key={outcome.label} key={`${outcome.label}-${i}-${oddsStr}`}
className="flex items-center justify-between gap-3 py-1" className="flex items-center justify-between gap-3 py-1"
> >
<span className="text-[11px] text-white/90">{outcome.label}</span> <span className="text-[11px] text-white/90">{outcome.label}</span>
@ -145,19 +149,32 @@ function MarketSectionBlock({
) )
} }
export function MatchDetailView({ event }: { event: Event }) { export function MatchDetailView({
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({ event,
apiSections,
}: {
event: Event
apiSections?: ApiSection[] | null
}) {
useBetslipStore((s) => s.bets)
const mockDetailMarkets = getEventDetailMarkets(event.id)
const cardsBookings = getCardsBookingsMarkets(event.id)
const detailMarkets: DetailMarketSection[] = (apiSections?.length
? apiSections.map((s) => ({ id: s.id, title: s.title, outcomes: s.outcomes }))
: mockDetailMarkets) as DetailMarketSection[]
const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>(() => ({
"bookings-1x2": true, "bookings-1x2": true,
"sending-off": true, "sending-off": true,
"1st-booking": true, "1st-booking": true,
"1st-half-bookings-1x2": true, "1st-half-bookings-1x2": true,
"booking-points-ou": true, "booking-points-ou": true,
"1st-half-1st-booking": true, "1st-half-1st-booking": true,
}) ...(apiSections?.length
const [activeCategory, setActiveCategory] = useState("Cards/Bookings") ? Object.fromEntries(detailMarkets.slice(0, 8).map((s) => [s.id, true]))
: {}),
const detailMarkets = getEventDetailMarkets(event.id) }))
const cardsBookings = getCardsBookingsMarkets(event.id) const [activeCategory, setActiveCategory] = useState("Main")
const toggleSection = (id: string) => { const toggleSection = (id: string) => {
setExpandedSections((prev) => ({ ...prev, [id]: !prev[id] })) setExpandedSections((prev) => ({ ...prev, [id]: !prev[id] }))
@ -166,11 +183,15 @@ export function MatchDetailView({ event }: { event: Event }) {
const breadcrumbLeague = const breadcrumbLeague =
event.league === "Premier League" event.league === "Premier League"
? "England - Premier League" ? "England - Premier League"
: `${event.country} - ${event.league}` : event.league
? `${event.country} - ${event.league}`
: "Event"
const isCardsBookings = activeCategory === "Cards/Bookings" const isCardsBookings = activeCategory === "Cards/Bookings"
const leftSections = isCardsBookings ? cardsBookings.left : detailMarkets const allSections = isCardsBookings ? [...cardsBookings.left, ...cardsBookings.right] : detailMarkets
const rightSections = isCardsBookings ? cardsBookings.right : [] const mid = Math.ceil(allSections.length / 2)
const leftSections = allSections.slice(0, mid)
const rightSections = allSections.slice(mid)
return ( return (
<div className="flex flex-col bg-brand-bg rounded overflow-hidden"> <div className="flex flex-col bg-brand-bg rounded overflow-hidden">
@ -189,38 +210,38 @@ export function MatchDetailView({ event }: { event: Event }) {
</h1> </h1>
</div> </div>
{/* Match header */} {/* Match header: team names in boxes and below */}
<div className="bg-brand-surface px-4 py-5 border-b border-border/20"> <div className="bg-brand-surface px-4 py-5 border-b border-border/20">
<div className="flex items-center justify-center gap-10"> <div className="flex items-center justify-center gap-10">
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2 min-w-0">
<div className="w-16 h-20 rounded-md bg-brand-bg border border-white/10 flex items-center justify-center"> <div className="w-20 min-h-20 rounded-md bg-brand-bg border border-white/10 flex items-center justify-center px-2 py-3 text-center">
<span className="text-[10px] font-black text-white/60 uppercase"> <span className="text-[11px] font-black text-white leading-tight line-clamp-3">
{event.homeTeam.slice(0, 2)} {event.homeTeam}
</span> </span>
</div> </div>
<span className="text-[13px] font-bold text-white">{event.homeTeam}</span> <span className="text-[13px] font-bold text-white text-center truncate max-w-[120px]">{event.homeTeam}</span>
</div> </div>
<span className="text-[12px] font-black text-white/50 uppercase">VS</span> <span className="text-[12px] font-black text-white/50 uppercase shrink-0">VS</span>
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2 min-w-0">
<div className="w-16 h-20 rounded-md bg-brand-bg border border-white/10 flex items-center justify-center"> <div className="w-20 min-h-20 rounded-md bg-brand-bg border border-white/10 flex items-center justify-center px-2 py-3 text-center">
<span className="text-[10px] font-black text-white/60 uppercase"> <span className="text-[11px] font-black text-white leading-tight line-clamp-3">
{event.awayTeam.slice(0, 2)} {event.awayTeam}
</span> </span>
</div> </div>
<span className="text-[13px] font-bold text-white">{event.awayTeam}</span> <span className="text-[13px] font-bold text-white text-center truncate max-w-[120px]">{event.awayTeam}</span>
</div> </div>
</div> </div>
</div> </div>
{/* Category tabs: horizontal scroll, selected = darker grey */} {/* Category tabs: wrap into 23 rows, not scrollable */}
<div className="flex overflow-x-auto gap-1 p-2 bg-brand-bg border-b border-border/20 scrollbar-hide"> <div className="flex flex-wrap gap-1.5 p-2 bg-brand-bg border-b border-border/20">
{MARKET_CATEGORIES.map((label) => ( {MARKET_CATEGORIES.map((label) => (
<button <button
key={label} key={label}
type="button" type="button"
onClick={() => setActiveCategory(label)} onClick={() => setActiveCategory(label)}
className={cn( className={cn(
"px-3 py-1.5 text-[10px] font-bold uppercase whitespace-nowrap rounded transition-colors shrink-0", "px-3 py-1.5 text-[10px] font-bold uppercase whitespace-nowrap rounded transition-colors",
activeCategory === label activeCategory === label
? "bg-brand-surface-light text-white border border-white/10" ? "bg-brand-surface-light text-white border border-white/10"
: "text-white/60 hover:text-white hover:bg-white/5" : "text-white/60 hover:text-white hover:bg-white/5"
@ -231,7 +252,7 @@ export function MatchDetailView({ event }: { event: Event }) {
))} ))}
</div> </div>
{/* Two-column grid of market sections */} {/* Two-column grid of market sections (split evenly so both columns are used) */}
<div className="flex-1 min-h-0 overflow-y-auto"> <div className="flex-1 min-h-0 overflow-y-auto">
<div className="grid grid-cols-1 md:grid-cols-2 gap-0 bg-brand-surface-light"> <div className="grid grid-cols-1 md:grid-cols-2 gap-0 bg-brand-surface-light">
{/* Left column */} {/* Left column */}
@ -247,21 +268,19 @@ export function MatchDetailView({ event }: { event: Event }) {
/> />
))} ))}
</div> </div>
{/* Right column (Cards/Bookings only) */} {/* Right column */}
{rightSections.length > 0 && ( <div>
<div> {rightSections.map((section) => (
{rightSections.map((section) => ( <MarketSectionBlock
<MarketSectionBlock key={section.id}
key={section.id} section={section}
section={section} event={event}
event={event} marketName={section.title}
marketName={section.title} isExpanded={expandedSections[section.id] ?? false}
isExpanded={expandedSections[section.id] ?? false} onToggle={() => toggleSection(section.id)}
onToggle={() => toggleSection(section.id)} />
/> ))}
))} </div>
</div>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -21,7 +21,7 @@ export function SportHome() {
<HomeTabs /> <HomeTabs />
</> </>
)} )}
<EventsList /> <EventsList key={`${searchParams.get("sport") ?? "all"}-${searchParams.get("league") ?? ""}`} />
</div> </div>
) )
} }

View File

@ -1,4 +1,9 @@
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" "use client"
import Link from "next/link"
import { useSearchParams } from "next/navigation"
import { TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Tabs } from "@/components/ui/tabs"
const sports = [ const sports = [
{ id: "football", name: "Football", icon: "⚽" }, { id: "football", name: "Football", icon: "⚽" },
@ -14,17 +19,23 @@ const sports = [
] ]
export function SportsNav() { export function SportsNav() {
const searchParams = useSearchParams()
const currentSport = searchParams.get("sport") ?? "football"
return ( return (
<Tabs defaultValue="football" className="w-full"> <Tabs value={currentSport} className="w-full">
<TabsList variant="hs-nav" className="min-h-14! h-auto! py-2"> <TabsList variant="hs-nav" className="min-h-14! h-auto! py-2">
{sports.map((sport) => ( {sports.map((sport) => (
<TabsTrigger <TabsTrigger
key={sport.id} key={sport.id}
value={sport.id} value={sport.id}
asChild
className="flex-col min-w-[70px] py-2 gap-1" className="flex-col min-w-[70px] py-2 gap-1"
> >
<span className="text-xl">{sport.icon}</span> <Link href={`/?sport=${sport.id}`} scroll={false} className="flex flex-col items-center gap-1">
<span className="text-[10px] font-bold uppercase">{sport.name}</span> <span className="text-xl">{sport.icon}</span>
<span className="text-[10px] font-bold uppercase">{sport.name}</span>
</Link>
</TabsTrigger> </TabsTrigger>
))} ))}
</TabsList> </TabsList>

View File

@ -1,58 +1,91 @@
"use client" "use client"
import { useState, useEffect } from "react"
import { ChevronRight } from "lucide-react" import { ChevronRight } from "lucide-react"
import { useBetslipStore } from "@/lib/store/betslip-store" import { useBetslipStore } from "@/lib/store/betslip-store"
import {
fetchEvents,
fetchOddsForEvent,
get1X2FromOddsResponse,
TOP_LEAGUES,
type ApiEvent,
} from "@/lib/betting-api"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const topMatches = [ type TopMatch = {
{ id: string
id: "tm1", league: string
league: "England - Premier League", time: string
time: "05:00 PM", homeTeam: string
homeTeam: "Nottingham Forest", awayTeam: string
awayTeam: "Liverpool", odds: { home: number; draw: number; away: number }
odds: { home: 4.09, draw: 3.93, away: 1.82 } }
},
{ const FALLBACK_MATCHES: TopMatch[] = [
id: "tm2", { 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 } },
league: "England - Premier League", { 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 } },
time: "11:00 PM", { id: "tm3", league: "England - Premier League", time: "06:00 PM", homeTeam: "Chelsea", awayTeam: "Burnley", odds: { home: 1.21, draw: 6.91, away: 11.50 } },
homeTeam: "Man City", { id: "tm4", league: "Spain - LaLiga", time: "07:30 PM", homeTeam: "Arsenal", awayTeam: "Wolves", odds: { home: 1.56, draw: 4.16, away: 5.80 } },
awayTeam: "Newcastle", { 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 } },
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() { export function TopMatches() {
const { bets, addBet } = useBetslipStore() 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 ( return (
<div className="flex gap-3 overflow-x-auto pb-2 scrollbar-hide -mx-1 px-1"> <div className="flex gap-3 overflow-x-auto pb-2 scrollbar-hide -mx-1 px-1">
{topMatches.map((match) => { {matches.map((match) => {
const eventName = `${match.homeTeam} - ${match.awayTeam}` const eventName = `${match.homeTeam} - ${match.awayTeam}`
const leagueForBet = `Football - ${match.league}` const leagueForBet = `Football - ${match.league}`
const outcomes = [ const outcomes = [
@ -61,8 +94,8 @@ export function TopMatches() {
{ key: "2", label: "2", odds: match.odds.away }, { key: "2", label: "2", odds: match.odds.away },
] as const ] as const
return ( return (
<div <div
key={match.id} key={match.id}
className="min-w-[280px] bg-brand-surface border border-border/20 rounded-sm overflow-hidden flex flex-col relative group" className="min-w-[280px] bg-brand-surface border border-border/20 rounded-sm overflow-hidden flex flex-col relative group"
> >
{/* Top Label Ribbon */} {/* Top Label Ribbon */}
@ -94,7 +127,7 @@ export function TopMatches() {
</div> </div>
</div> </div>
<div className="grid grid-cols-3 gap-[1px] bg-white/5 mx-2 mb-2 rounded-sm overflow-hidden border border-white/5"> <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 }) => { {outcomes.map(({ key, label, odds }) => {
const betId = `${match.id}-${key}` const betId = `${match.id}-${key}`
const isSelected = bets.some((b) => b.id === betId) const isSelected = bets.some((b) => b.id === betId)
@ -123,7 +156,7 @@ export function TopMatches() {
{label} {label}
</span> </span>
<span className={cn("text-[11px] font-black tabular-nums", isSelected ? "text-black" : "text-brand-primary")}> <span className={cn("text-[11px] font-black tabular-nums", isSelected ? "text-black" : "text-brand-primary")}>
{odds.toFixed(2)} {odds > 0 ? odds.toFixed(2) : "—"}
</span> </span>
</button> </button>
) )