- 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.
289 lines
10 KiB
TypeScript
289 lines
10 KiB
TypeScript
"use client"
|
||
|
||
import { useState } from "react"
|
||
import Link from "next/link"
|
||
import { useBetslipStore } from "@/lib/store/betslip-store"
|
||
import {
|
||
getEventDetailMarkets,
|
||
getCardsBookingsMarkets,
|
||
type Event,
|
||
type DetailMarketSection,
|
||
} from "@/lib/mock-data"
|
||
|
||
type ApiSection = { id: string; title: string; outcomes: { label: string; odds: number }[] }
|
||
import { cn } from "@/lib/utils"
|
||
import { ChevronDown, ChevronUp } from "lucide-react"
|
||
|
||
const MARKET_CATEGORIES = [
|
||
"Betbuilder",
|
||
"All",
|
||
"Main",
|
||
"Goals",
|
||
"Handicap",
|
||
"1st Half",
|
||
"2nd Half",
|
||
"Combo",
|
||
"Chance Mix",
|
||
"Home",
|
||
"Half Time / Full Time",
|
||
"Away",
|
||
"Correct Score",
|
||
"Asian Markets",
|
||
"Corners",
|
||
"Minutes",
|
||
"Cards/Bookings",
|
||
"Points Handicap",
|
||
"Total Points",
|
||
"Team 1",
|
||
"Team 2",
|
||
"Other",
|
||
"Handicap Goals",
|
||
"Total Goals",
|
||
"Combo",
|
||
"Specials",
|
||
]
|
||
|
||
function MarketSectionBlock({
|
||
section,
|
||
event,
|
||
marketName,
|
||
isExpanded,
|
||
onToggle,
|
||
}: {
|
||
section: DetailMarketSection
|
||
event: Event
|
||
marketName: string
|
||
isExpanded: boolean
|
||
onToggle: () => void
|
||
}) {
|
||
const { bets, addBet } = useBetslipStore()
|
||
const hasOutcomes = section.outcomes.length > 0
|
||
|
||
return (
|
||
<div className="bg-brand-surface-light border-b border-white/5">
|
||
<button
|
||
type="button"
|
||
onClick={onToggle}
|
||
className="w-full flex items-center justify-between px-3 py-2.5 text-left hover:bg-white/5 transition-colors"
|
||
>
|
||
<span className="text-[11px] font-bold text-white uppercase">
|
||
{section.title}
|
||
</span>
|
||
{isExpanded ? (
|
||
<ChevronUp className="size-4 text-white/60" />
|
||
) : (
|
||
<ChevronDown className="size-4 text-white/60" />
|
||
)}
|
||
</button>
|
||
{isExpanded && hasOutcomes && (
|
||
<div className="px-3 pb-3 space-y-1.5">
|
||
{section.outcomes.length > 2 && section.outcomes.length % 2 === 0 ? (
|
||
<div className="grid grid-cols-2 gap-x-4 gap-y-1.5">
|
||
{section.outcomes.map((outcome, i) => {
|
||
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)
|
||
return (
|
||
<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>
|
||
<button
|
||
type="button"
|
||
onClick={() =>
|
||
addBet({
|
||
id: betId,
|
||
event: `${event.homeTeam} - ${event.awayTeam}`,
|
||
league: `${event.sport} - ${event.country} - ${event.league}`,
|
||
market: marketName,
|
||
selection: outcome.label,
|
||
odds: outcome.odds,
|
||
})
|
||
}
|
||
className={cn(
|
||
"min-w-[52px] px-2 py-1 rounded text-[11px] font-bold tabular-nums text-center transition-all shrink-0",
|
||
isSelected ? "bg-brand-primary text-black" : "text-brand-primary hover:bg-white/5"
|
||
)}
|
||
>
|
||
{outcome.odds.toFixed(2)}
|
||
</button>
|
||
</div>
|
||
)
|
||
})}
|
||
</div>
|
||
) : (
|
||
section.outcomes.map((outcome, i) => {
|
||
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)
|
||
return (
|
||
<div
|
||
key={`${outcome.label}-${i}-${oddsStr}`}
|
||
className="flex items-center justify-between gap-3 py-1"
|
||
>
|
||
<span className="text-[11px] text-white/90">{outcome.label}</span>
|
||
<button
|
||
type="button"
|
||
onClick={() =>
|
||
addBet({
|
||
id: betId,
|
||
event: `${event.homeTeam} - ${event.awayTeam}`,
|
||
league: `${event.sport} - ${event.country} - ${event.league}`,
|
||
market: marketName,
|
||
selection: outcome.label,
|
||
odds: outcome.odds,
|
||
})
|
||
}
|
||
className={cn(
|
||
"min-w-[52px] px-2 py-1 rounded text-[11px] font-bold tabular-nums text-center transition-all shrink-0",
|
||
isSelected ? "bg-brand-primary text-black" : "text-brand-primary hover:bg-white/5"
|
||
)}
|
||
>
|
||
{outcome.odds.toFixed(2)}
|
||
</button>
|
||
</div>
|
||
)
|
||
})
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export function MatchDetailView({
|
||
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,
|
||
"sending-off": true,
|
||
"1st-booking": true,
|
||
"1st-half-bookings-1x2": true,
|
||
"booking-points-ou": true,
|
||
"1st-half-1st-booking": true,
|
||
...(apiSections?.length
|
||
? Object.fromEntries(detailMarkets.slice(0, 8).map((s) => [s.id, true]))
|
||
: {}),
|
||
}))
|
||
const [activeCategory, setActiveCategory] = useState("Main")
|
||
|
||
const toggleSection = (id: string) => {
|
||
setExpandedSections((prev) => ({ ...prev, [id]: !prev[id] }))
|
||
}
|
||
|
||
const breadcrumbLeague =
|
||
event.league === "Premier League"
|
||
? "England - Premier League"
|
||
: event.league
|
||
? `${event.country} - ${event.league}`
|
||
: "Event"
|
||
|
||
const isCardsBookings = activeCategory === "Cards/Bookings"
|
||
const allSections = isCardsBookings ? [...cardsBookings.left, ...cardsBookings.right] : detailMarkets
|
||
const mid = Math.ceil(allSections.length / 2)
|
||
const leftSections = allSections.slice(0, mid)
|
||
const rightSections = allSections.slice(mid)
|
||
|
||
return (
|
||
<div className="flex flex-col bg-brand-bg rounded overflow-hidden">
|
||
{/* Breadcrumb: back arrow, ellipsis, path */}
|
||
<div className="bg-brand-surface px-3 py-2 border-b border-border/20">
|
||
<Link
|
||
href="/"
|
||
className="flex items-center gap-2 text-[11px] font-bold text-white/70 hover:text-brand-primary transition-colors"
|
||
>
|
||
<span className="text-white/80"><</span>
|
||
<span>...</span>
|
||
<span>Football {breadcrumbLeague} / {event.homeTeam} vs. {event.awayTeam}</span>
|
||
</Link>
|
||
<h1 className="text-[15px] font-bold text-white mt-2">
|
||
{breadcrumbLeague}
|
||
</h1>
|
||
</div>
|
||
|
||
{/* Match header: team names in boxes and below */}
|
||
<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 flex-col items-center gap-2 min-w-0">
|
||
<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-[11px] font-black text-white leading-tight line-clamp-3">
|
||
{event.homeTeam}
|
||
</span>
|
||
</div>
|
||
<span className="text-[13px] font-bold text-white text-center truncate max-w-[120px]">{event.homeTeam}</span>
|
||
</div>
|
||
<span className="text-[12px] font-black text-white/50 uppercase shrink-0">VS</span>
|
||
<div className="flex flex-col items-center gap-2 min-w-0">
|
||
<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-[11px] font-black text-white leading-tight line-clamp-3">
|
||
{event.awayTeam}
|
||
</span>
|
||
</div>
|
||
<span className="text-[13px] font-bold text-white text-center truncate max-w-[120px]">{event.awayTeam}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Category tabs: wrap into 2–3 rows, not scrollable */}
|
||
<div className="flex flex-wrap gap-1.5 p-2 bg-brand-bg border-b border-border/20">
|
||
{MARKET_CATEGORIES.map((label) => (
|
||
<button
|
||
key={label}
|
||
type="button"
|
||
onClick={() => setActiveCategory(label)}
|
||
className={cn(
|
||
"px-3 py-1.5 text-[10px] font-bold uppercase whitespace-nowrap rounded transition-colors",
|
||
activeCategory === label
|
||
? "bg-brand-surface-light text-white border border-white/10"
|
||
: "text-white/60 hover:text-white hover:bg-white/5"
|
||
)}
|
||
>
|
||
{label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* 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="grid grid-cols-1 md:grid-cols-2 gap-0 bg-brand-surface-light">
|
||
{/* Left column */}
|
||
<div className="border-r border-white/5">
|
||
{leftSections.map((section) => (
|
||
<MarketSectionBlock
|
||
key={section.id}
|
||
section={section}
|
||
event={event}
|
||
marketName={section.title}
|
||
isExpanded={expandedSections[section.id] ?? false}
|
||
onToggle={() => toggleSection(section.id)}
|
||
/>
|
||
))}
|
||
</div>
|
||
{/* Right column */}
|
||
<div>
|
||
{rightSections.map((section) => (
|
||
<MarketSectionBlock
|
||
key={section.id}
|
||
section={section}
|
||
event={event}
|
||
marketName={section.title}
|
||
isExpanded={expandedSections[section.id] ?? false}
|
||
onToggle={() => toggleSection(section.id)}
|
||
/>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|