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:
parent
86ffd88e46
commit
8941c45555
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 2–3 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>
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ export function SportHome() {
|
||||||
<HomeTabs />
|
<HomeTabs />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<EventsList />
|
<EventsList key={`${searchParams.get("sport") ?? "all"}-${searchParams.get("league") ?? ""}`} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user