352 lines
17 KiB
TypeScript
352 lines
17 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect, useMemo } from "react"
|
|
import Link from "next/link"
|
|
import { useSearchParams } from "next/navigation"
|
|
import { TOP_LEAGUES, fetchLeagues } from "@/lib/store/betting-api"
|
|
import { useBettingStore } from "@/lib/store/betting-store"
|
|
import type { ApiLeague } from "@/lib/store/betting-types"
|
|
import { SportEnum, type QuickFilterKey } from "@/lib/store/betting-types"
|
|
import { getCountryName } from "@/lib/countries"
|
|
import { cn } from "@/lib/utils"
|
|
import { Button } from "@/components/ui/button"
|
|
import { ChevronsLeft, ChevronDown, ChevronUp, Plus } from "lucide-react"
|
|
|
|
/** Sidebar sports: slug for URL, sport_id for /leagues?sport_id= (order and list from design) */
|
|
const SIDEBAR_SPORTS = [
|
|
{ id: "football", sport_id: SportEnum.SOCCER, name: "Football", icon: "⚽" },
|
|
{ id: "basketball", sport_id: SportEnum.BASKETBALL, name: "Basketball", icon: "🏀" },
|
|
{ id: "american-football", sport_id: SportEnum.AMERICAN_FOOTBALL, name: "American Football", icon: "🏈" },
|
|
{ id: "baseball", sport_id: SportEnum.BASEBALL, name: "Baseball", icon: "⚾" },
|
|
{ id: "cricket", sport_id: SportEnum.CRICKET, name: "Cricket", icon: "🏏" },
|
|
{ id: "futsal", sport_id: SportEnum.FUTSAL, name: "Futsal", icon: "⚽" },
|
|
{ id: "darts", sport_id: SportEnum.DARTS, name: "Darts", icon: "🎯" },
|
|
{ id: "ice-hockey", sport_id: SportEnum.ICE_HOCKEY, name: "Ice Hockey", icon: "🏒" },
|
|
{ id: "rugby-union", sport_id: SportEnum.RUGBY_UNION, name: "Rugby", icon: "🏉" },
|
|
{ id: "rugby-league", sport_id: SportEnum.RUGBY_LEAGUE, name: "Rugby League", icon: "🏉" },
|
|
{ id: "volleyball", sport_id: SportEnum.VOLLEYBALL, name: "Volleyball", icon: "🏐" },
|
|
]
|
|
|
|
function SoccerBallIcon({ className }: { className?: string }) {
|
|
return (
|
|
<svg viewBox="0 0 24 24" className={className} fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
|
<circle cx="12" cy="12" r="10" />
|
|
<path d="M12 2v20M2 12h20" />
|
|
<path d="M4.93 4.93l14.14 14.14M4.93 19.07l14.14-14.14" strokeWidth="1" />
|
|
</svg>
|
|
)
|
|
}
|
|
|
|
const QUICK_FILTER_OPTIONS: { label: string; key: QuickFilterKey }[] = [
|
|
{ label: "All", key: "all" },
|
|
{ label: "Today", key: "today" },
|
|
{ label: "3h", key: "3h" },
|
|
{ label: "6h", key: "6h" },
|
|
{ label: "9h", key: "9h" },
|
|
{ label: "12h", key: "12h" },
|
|
]
|
|
|
|
function QuickFilterSection() {
|
|
const quickFilter = useBettingStore((s) => s.quickFilter)
|
|
const setQuickFilter = useBettingStore((s) => s.setQuickFilter)
|
|
return (
|
|
<div className="bg-brand-surface p-3 border-b border-border/30">
|
|
<span className="text-brand-primary text-[10.5px] uppercase font-black block mb-2 tracking-tight">Quick Filter</span>
|
|
<div className="grid grid-cols-6 gap-px">
|
|
{QUICK_FILTER_OPTIONS.map(({ label, key }) => (
|
|
<button
|
|
key={key}
|
|
type="button"
|
|
onClick={() => setQuickFilter(key)}
|
|
className={cn(
|
|
"text-[10px] py-1.5 font-bold transition-colors",
|
|
quickFilter === key ? "bg-brand-surface-light text-white" : "bg-brand-surface text-white/50 hover:text-white"
|
|
)}
|
|
>
|
|
{label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function SportsSidebar() {
|
|
const searchParams = useSearchParams()
|
|
const sportFromUrl = searchParams.get("sport") ?? "football"
|
|
const leagueFromUrl = searchParams.get("league")
|
|
|
|
const [expandedSport, setExpandedSport] = useState<string | null>(sportFromUrl)
|
|
const [expandedCountries, setExpandedCountries] = useState<Set<string>>(new Set())
|
|
const [leaguesBySportId, setLeaguesBySportId] = useState<Record<number, ApiLeague[]>>({})
|
|
const [loadingSportId, setLoadingSportId] = useState<number | null>(null)
|
|
|
|
useEffect(() => {
|
|
setExpandedSport((prev) => (prev ?? sportFromUrl) || sportFromUrl)
|
|
}, [sportFromUrl])
|
|
|
|
const currentSport = SIDEBAR_SPORTS.find((s) => s.id === sportFromUrl)
|
|
const sportId = currentSport?.sport_id ?? SportEnum.SOCCER
|
|
|
|
useEffect(() => {
|
|
if (!expandedSport) return
|
|
const sport = SIDEBAR_SPORTS.find((s) => s.id === expandedSport)
|
|
if (!sport || sport.sport_id in leaguesBySportId) return
|
|
let cancelled = false
|
|
setLoadingSportId(sport.sport_id)
|
|
fetchLeagues(sport.sport_id)
|
|
.then((res) => {
|
|
if (!cancelled) setLeaguesBySportId((prev) => ({ ...prev, [sport.sport_id]: res.data ?? [] }))
|
|
})
|
|
.catch(() => {
|
|
if (!cancelled) setLeaguesBySportId((prev) => ({ ...prev, [sport.sport_id]: [] }))
|
|
})
|
|
.finally(() => {
|
|
if (!cancelled) setLoadingSportId(null)
|
|
})
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
}, [expandedSport, leaguesBySportId])
|
|
|
|
const toggleCountry = (cc: string) => {
|
|
setExpandedCountries((prev) => {
|
|
const next = new Set(prev)
|
|
if (next.has(cc)) next.delete(cc)
|
|
else next.add(cc)
|
|
return next
|
|
})
|
|
}
|
|
|
|
const getCountriesForSport = (sportIdNum: number) => {
|
|
const leagues = leaguesBySportId[sportIdNum] ?? []
|
|
const ccSet = new Set<string>()
|
|
leagues.forEach((l) => ccSet.add((l.cc || "").trim().toLowerCase() || "__intl__"))
|
|
return Array.from(ccSet)
|
|
.map((cc) => ({
|
|
cc: cc === "__intl__" ? "" : cc,
|
|
name: cc === "__intl__" ? "International" : getCountryName(cc),
|
|
}))
|
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
}
|
|
|
|
const getLeaguesForCountry = (sportIdNum: number, countryCc: string) => {
|
|
const leagues = leaguesBySportId[sportIdNum] ?? []
|
|
const cc = countryCc.toLowerCase()
|
|
return leagues
|
|
.filter((l) => ((l.cc || "").trim().toLowerCase() || "") === cc)
|
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
}
|
|
|
|
return (
|
|
<aside className="hidden h-full w-[280px] shrink-0 bg-brand-surface-light lg:block overflow-y-auto border-r border-border/40 scrollbar-hide">
|
|
{/* Sports Menu Header */}
|
|
<div className="bg-brand-surface px-3 py-2 text-[11px] font-black text-white uppercase tracking-tighter flex items-center justify-between border-b border-border/30 h-10">
|
|
<span>Sports Menu</span>
|
|
<button type="button" className="p-1 rounded hover:bg-white/10 text-white/70 hover:text-white transition-colors" aria-label="Collapse sidebar">
|
|
<ChevronsLeft className="size-4" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Top Leagues Header */}
|
|
<div className="bg-brand-surface-light px-3 py-2.5 text-[11px] font-black text-brand-primary uppercase text-center border-b border-border/20 flex items-center justify-center gap-2">
|
|
Top Leagues
|
|
</div>
|
|
|
|
{/* Top Leagues */}
|
|
<div className="flex flex-col">
|
|
{TOP_LEAGUES.map((league) => (
|
|
<Link
|
|
key={league.id}
|
|
href={`/?sport=${sportFromUrl}&league=${league.id}`}
|
|
scroll={false}
|
|
className="w-full flex items-center justify-between px-3 py-2 text-left text-white/90 hover:bg-brand-surface transition-colors border-b border-border/10 group h-9"
|
|
>
|
|
<div className="flex items-center gap-2 min-w-0">
|
|
<div className="size-5 shrink-0 overflow-hidden rounded-sm flex items-center justify-center bg-white/5 border border-white/10 group-hover:border-white/20 transition-colors">
|
|
<span className="text-[11px]">⚽</span>
|
|
</div>
|
|
<span className="text-white/50 text-[8px] font-bold select-none">•</span>
|
|
<span className="text-[10.5px] font-bold leading-tight truncate max-w-[140px]">{league.name}</span>
|
|
</div>
|
|
<SoccerBallIcon className="size-4 shrink-0 text-white/40 group-hover:text-brand-primary transition-colors" />
|
|
</Link>
|
|
))}
|
|
</div>
|
|
|
|
{/* In-Play Strip */}
|
|
<Button asChild className="w-full bg-[#004d40] text-[#00bfa5] hover:bg-[#003d33] border-none py-2.5 h-auto text-[11px] font-black uppercase rounded-none tracking-[2px]">
|
|
<Link href="/live">
|
|
<span className="size-2 rounded-full bg-brand-primary mr-2 live-dot shadow-[0_0_8px_var(--brand-primary)]"></span>
|
|
IN-PLAY
|
|
</Link>
|
|
</Button>
|
|
|
|
{/* Quick Filter Section: passes first_start_time (RFC3339) to events API */}
|
|
<QuickFilterSection />
|
|
|
|
{/* Search Event Section */}
|
|
<div className="bg-brand-surface p-3 border-b border-border/40">
|
|
<span className="text-brand-primary text-[10.5px] uppercase font-black block mb-1 tracking-tight">Search Event</span>
|
|
<p className="text-[9px] text-white/30 mb-2 leading-tight font-medium uppercase">Insert the events name or at least one team in the form below</p>
|
|
<div className="flex flex-col gap-1.5">
|
|
<div className="relative">
|
|
<input type="text" className="bg-brand-surface-light border-none text-white text-[11px] px-3 py-2 w-full focus:ring-0 placeholder:text-white/20" placeholder="Search" />
|
|
</div>
|
|
<Button className="bg-brand-primary text-black hover:bg-brand-primary-hover py-2 h-auto rounded-none text-[11px] font-black uppercase tracking-wider">Search</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Nested: Sport → Countries → Leagues (mapped to sport_id & leagues API cc) */}
|
|
<div className="divide-y divide-border/10 bg-brand-surface-light">
|
|
{SIDEBAR_SPORTS.map((sport) => {
|
|
const isExpanded = expandedSport === sport.id
|
|
const leagues = leaguesBySportId[sport.sport_id] ?? []
|
|
const loading = loadingSportId === sport.sport_id
|
|
const countries = getCountriesForSport(sport.sport_id)
|
|
|
|
return (
|
|
<div key={sport.id} className="border-b border-border/10">
|
|
{/* Sport row: click only expands/collapses to show countries (no navigation) */}
|
|
<button
|
|
type="button"
|
|
onClick={() => setExpandedSport(isExpanded ? null : sport.id)}
|
|
className={cn(
|
|
"w-full flex items-center gap-1 py-2 pr-2 pl-1.5 text-left transition-colors h-9",
|
|
isExpanded ? "bg-brand-surface text-brand-primary" : "text-white/80 hover:bg-brand-surface hover:text-white"
|
|
)}
|
|
>
|
|
{isExpanded ? <ChevronUp className="size-3.5 shrink-0 text-current" /> : <ChevronDown className="size-3.5 shrink-0 text-current" />}
|
|
<div className="flex items-center gap-2 min-w-0 flex-1">
|
|
<span className="text-[12px] shrink-0">{sport.icon}</span>
|
|
<span className="text-[10.5px] font-bold truncate">{sport.name}</span>
|
|
</div>
|
|
{leagues.length > 0 && (
|
|
<span className="text-[9px] font-bold text-white/40 shrink-0">{leagues.length}</span>
|
|
)}
|
|
</button>
|
|
|
|
{/* Countries (nested under sport) */}
|
|
{isExpanded && (
|
|
<div className="bg-brand-surface-light/80 pl-4">
|
|
{loading ? (
|
|
<div className="py-2 text-[10px] text-white/50">Loading…</div>
|
|
) : (
|
|
countries.map(({ cc, name }) => {
|
|
const countryExpanded = expandedCountries.has(cc || "__intl__")
|
|
const countryKey = cc || "__intl__"
|
|
const leaguesInCountry = getLeaguesForCountry(sport.sport_id, cc)
|
|
|
|
return (
|
|
<div key={countryKey} className="border-b border-border/5">
|
|
<button
|
|
type="button"
|
|
onClick={() => toggleCountry(countryKey)}
|
|
className={cn(
|
|
"w-full flex items-center justify-between gap-2 py-1.5 pr-2 text-left text-[10.5px] font-bold transition-colors",
|
|
countryExpanded ? "text-brand-primary" : "text-white/80 hover:text-white"
|
|
)}
|
|
>
|
|
<div className="flex items-center gap-2 min-w-0">
|
|
{cc ? (
|
|
<img
|
|
src={`https://flagcdn.com/w20/${cc}.png`}
|
|
alt=""
|
|
width={20}
|
|
height={14}
|
|
className="shrink-0 rounded-sm object-cover w-5 h-[14px]"
|
|
/>
|
|
) : (
|
|
<span className="size-5 shrink-0 flex items-center justify-center text-[10px] text-white/50">◆</span>
|
|
)}
|
|
<span className="truncate">{name}</span>
|
|
</div>
|
|
{countryExpanded ? <ChevronUp className="size-3 shrink-0" /> : <ChevronDown className="size-3 shrink-0" />}
|
|
</button>
|
|
|
|
{/* Leagues (nested under country) */}
|
|
{countryExpanded && (
|
|
<div className="pl-2 pb-1 max-h-48 overflow-y-auto">
|
|
{leaguesInCountry.map((league) => (
|
|
<Link
|
|
key={league.id}
|
|
href={`/?sport=${sport.id}&league=${league.id}`}
|
|
scroll={false}
|
|
className={cn(
|
|
"flex items-center justify-between gap-1 py-1.5 pr-1 text-[10px] font-bold border-b border-border/5 hover:bg-brand-surface/50 transition-colors group",
|
|
leagueFromUrl === String(league.id) ? "text-brand-primary" : "text-white/90"
|
|
)}
|
|
>
|
|
<span className="text-white/50 text-[8px] group-hover:text-brand-primary">•</span>
|
|
<span className="flex-1 truncate">{league.name}</span>
|
|
<Plus className="size-3 shrink-0 text-white/40 group-hover:text-brand-primary" />
|
|
</Link>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
})
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
{/* Bet Services */}
|
|
<div className="mt-2 text-[11px] font-bold text-brand-primary px-3 py-2 uppercase border-y border-border/20 bg-brand-surface">
|
|
Bet Services
|
|
</div>
|
|
<div className="grid grid-cols-3 gap-0 border-b border-border/10">
|
|
<Button
|
|
variant="ghost"
|
|
type="button"
|
|
className="flex flex-col items-center justify-center py-4 h-auto rounded-none border-r border-border/10 hover:bg-brand-surface group"
|
|
onClick={() => {
|
|
const w = 1200
|
|
const h = 800
|
|
const left = typeof window !== "undefined" ? Math.max(0, (window.screen.width - w) / 2) : 0
|
|
const top = typeof window !== "undefined" ? Math.max(0, (window.screen.height - h) / 2) : 0
|
|
window.open(
|
|
"https://s5.sir.sportradar.com/betinaction/en",
|
|
"LiveScore",
|
|
`noopener,noreferrer,width=${w},height=${h},left=${left},top=${top},scrollbars=yes,resizable=yes`
|
|
)
|
|
}}
|
|
>
|
|
<svg viewBox="0 0 24 24" className="size-5 mb-1.5 fill-muted-foreground group-hover:fill-brand-primary transition-colors"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zm3.3 14.71L11 12.41V7h2v4.59l3.71 3.71-1.42 1.41z"/></svg>
|
|
<span className="text-[9px] text-white font-medium">Live Score</span>
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
type="button"
|
|
className="flex flex-col items-center justify-center py-4 h-auto rounded-none border-r border-border/10 hover:bg-brand-surface group"
|
|
onClick={() => {
|
|
const w = 1200
|
|
const h = 800
|
|
const left = typeof window !== "undefined" ? Math.max(0, (window.screen.width - w) / 2) : 0
|
|
const top = typeof window !== "undefined" ? Math.max(0, (window.screen.height - h) / 2) : 0
|
|
window.open(
|
|
"https://statistics.betconstruct.com/#/en",
|
|
"Results",
|
|
`noopener,noreferrer,width=${w},height=${h},left=${left},top=${top},scrollbars=yes,resizable=yes`
|
|
)
|
|
}}
|
|
>
|
|
<svg viewBox="0 0 24 24" className="size-5 mb-1.5 fill-muted-foreground group-hover:fill-brand-primary transition-colors"><path d="M5 9.2h3V19H5zM10.6 5h2.8v14h-2.8zm5.6 8H19v6h-2.8z"/></svg>
|
|
<span className="text-[9px] text-white font-medium">Results</span>
|
|
</Button>
|
|
<Button variant="ghost" asChild className="flex flex-col items-center justify-center py-4 h-auto rounded-none hover:bg-brand-surface group">
|
|
<Link href="/print-odds" className="flex flex-col items-center justify-center">
|
|
<svg viewBox="0 0 24 24" className="size-5 mb-1.5 fill-muted-foreground group-hover:fill-brand-primary transition-colors"><path d="M19 8H5c-1.66 0-3 1.34-3 3v6h4v4h12v-4h4v-6c0-1.66-1.34-3-3-3zm-3 11H8v-5h8v5zm3-7c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1zm-1-9H6v4h12V3z"/></svg>
|
|
<span className="text-[9px] text-white font-medium">Print Odds</span>
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
|
|
</aside>
|
|
)
|
|
}
|