Compare commits

..

6 Commits

Author SHA1 Message Date
bf1efb3341 header fix 2026-03-01 14:34:23 +03:00
263cd4b65e games pages 2026-03-01 14:24:51 +03:00
463cabbdd0 layout 2026-03-01 14:24:30 +03:00
bad08b229e axios setup 2026-03-01 14:23:49 +03:00
9029fb4f6a betting and ui components 2026-03-01 14:23:23 +03:00
5587dff57c new pages added 2026-03-01 14:22:33 +03:00
37 changed files with 2245 additions and 415 deletions

View File

@ -1,2 +0,0 @@
Internal shared components for app routes.

21
app/event/[id]/page.tsx Normal file
View File

@ -0,0 +1,21 @@
import Link from "next/link"
import { getEventById } from "@/lib/mock-data"
import { MatchDetailView } from "@/components/betting/match-detail-view"
export default async function EventPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params
const event = getEventById(id)
if (!event) {
return (
<div className="flex flex-col items-center justify-center min-h-[40vh] gap-4">
<p className="text-white/80">Match not found.</p>
<Link href="/" className="text-brand-primary font-bold hover:underline">
Back to home
</Link>
</div>
)
}
return <MatchDetailView event={event} />
}

View File

@ -57,41 +57,39 @@
:root {
--radius: 0rem;
/* Brand Colors (Black, Yellow, White theme) */
--brand-primary: #ff9800;
--brand-primary-hover: #e68900;
--brand-secondary: #ff9800;
/* Usually same as primary for now */
--brand-bg: #121212;
--brand-surface: #1a1a1a;
--brand-surface-light: #2a2a2a;
--brand-accent: #852222;
/* Maroon */
--brand-live: #ff3b3b;
/* Brand Colors (White and Green theme) - dark grey, not near-black */
--brand-primary: #22c55e;
--brand-primary-hover: #16a34a;
--brand-secondary: #22c55e;
--brand-bg: #252525;
--brand-surface: #2d2d2d;
--brand-surface-light: #353535;
--brand-accent: #15803d;
--brand-live: #22c55e;
/* Shadcn default mappings */
--background: var(--brand-bg);
--foreground: #ffffff;
--card: #1e1e1e;
--card: #2a2a2a;
--card-foreground: #ffffff;
--popover: #1e1e1e;
--popover: #2a2a2a;
--popover-foreground: #ffffff;
--primary: var(--brand-primary);
--primary-foreground: #ffffff;
--secondary: #222222;
--secondary: #2d2d2d;
--secondary-foreground: #a0a0a0;
--muted: #1a1a1a;
--muted: #252525;
--muted-foreground: #808080;
--accent: var(--brand-primary);
--accent-foreground: #121212;
--destructive: #ef4444;
--border: #2a2a2a;
--input: #222222;
--border: #353535;
--input: #2d2d2d;
--ring: var(--brand-primary);
}
.dark {
--background: oklch(0.13 0.008 250);
--background: oklch(0.17 0.01 250);
--foreground: oklch(0.93 0.005 250);
--card: oklch(0.17 0.01 250);
--card-foreground: oklch(0.93 0.005 250);

View File

@ -42,13 +42,13 @@ export default function LoginPage() {
))}
</div>
{/* HARIF box */}
<div className="bg-[#852222] px-3 py-1 -skew-x-12 flex items-center h-[38px]">
<div className="bg-brand-accent px-3 py-1 -skew-x-12 flex items-center h-[38px]">
<span className="text-2xl font-black text-white italic tracking-tighter skew-x-12 inline-block leading-none">
HARIF
</span>
</div>
{/* SPORT text */}
<span className="text-2xl font-black text-[#ff9800] italic tracking-tighter ml-1 leading-none">
<span className="text-2xl font-black text-brand-primary italic tracking-tighter ml-1 leading-none">
SPORT
</span>
</div>
@ -76,7 +76,7 @@ export default function LoginPage() {
type="tel"
value={phone}
onChange={(e) => setPhone(e.target.value)}
className="flex-1 bg-white border border-gray-300 px-3 py-2 text-sm text-[#333] placeholder:text-gray-400 focus:outline-none focus:border-[#ff9800]"
className="flex-1 bg-white border border-gray-300 px-3 py-2 text-sm text-[#333] placeholder:text-gray-400 focus:outline-none focus:border-brand-primary"
/>
</div>
</div>
@ -91,7 +91,7 @@ export default function LoginPage() {
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
className="w-full bg-white border border-gray-300 px-3 py-2 text-sm text-[#333] placeholder:text-gray-400 focus:outline-none focus:border-[#ff9800]"
className="w-full bg-white border border-gray-300 px-3 py-2 text-sm text-[#333] placeholder:text-gray-400 focus:outline-none focus:border-brand-primary"
/>
</div>
@ -99,7 +99,7 @@ export default function LoginPage() {
<div className="text-right">
<Link
href="/reset-password"
className="text-[11px] text-white/50 hover:text-[#ff9800] transition-colors"
className="text-[11px] text-white/50 hover:text-brand-primary transition-colors"
>
Forgot password?
</Link>
@ -120,7 +120,7 @@ export default function LoginPage() {
{/* Register link */}
<p className="text-center text-[11px] text-white/50">
Don&apos;t have an account?{" "}
<Link href="/register" className="text-[#ff9800] hover:underline font-semibold">
<Link href="/register" className="text-brand-primary hover:underline font-semibold">
Register
</Link>
</p>

View File

@ -1,6 +1,11 @@
import { Suspense } from "react"
import { SportHome } from "@/components/betting/sport-home"
export default function Home() {
return <SportHome />
return (
<Suspense fallback={<div>Loading...</div>}>
<SportHome />
</Suspense>
)
}

385
app/print-odds/page.tsx Normal file
View File

@ -0,0 +1,385 @@
"use client"
import { useState, useMemo } from "react"
import { cn } from "@/lib/utils"
type NationId = string
type TournamentId = string
type MarketId = string
const SPORTS_WITH_NATIONS: { sport: string; sportId: string; nations: { id: NationId; name: string; flag: string }[] }[] = [
{
sport: "Football",
sportId: "football",
nations: [
{ id: "intl-clubs", name: "International Clubs", flag: "🌍" },
{ id: "england", name: "England", flag: "🏴" },
{ id: "spain", name: "Spain", flag: "🇪🇸" },
{ id: "germany", name: "Germany", flag: "🇩🇪" },
{ id: "italy", name: "Italy", flag: "🇮🇹" },
{ id: "international", name: "International", flag: "🌐" },
{ id: "france", name: "France", flag: "🇫🇷" },
{ id: "portugal", name: "Portugal", flag: "🇵🇹" },
{ id: "england-amateur", name: "England Amateur", flag: "🏴" },
{ id: "netherlands", name: "Netherlands", flag: "🇳🇱" },
{ id: "scotland", name: "Scotland", flag: "🏴" },
{ id: "belgium", name: "Belgium", flag: "🇧🇪" },
{ id: "turkiye", name: "Turkiye", flag: "🇹🇷" },
],
},
]
const TOURNAMENTS_BY_NATION: Record<NationId, { id: TournamentId; name: string; flag: string }[]> = {
"intl-clubs": [
{ id: "uefa-cl", name: "UEFA Champions League", flag: "🏆" },
{ id: "uefa-el", name: "UEFA Europa League", flag: "🏆" },
{ id: "uefa-ecl", name: "UEFA Europa Conference League", flag: "🏆" },
{ id: "copa-libertadores", name: "Copa Libertadores", flag: "🏆" },
{ id: "copa-sudamericana", name: "Copa Sudamericana", flag: "🏆" },
],
england: [
{ id: "premier-league", name: "Premier League", flag: "🏴" },
{ id: "championship", name: "Championship", flag: "🏴" },
{ id: "league-one", name: "League One", flag: "🏴" },
{ id: "league-two", name: "League Two", flag: "🏴" },
{ id: "fa-cup", name: "FA Cup", flag: "🏴" },
{ id: "efl-cup", name: "EFL Cup", flag: "🏴" },
{ id: "national-league", name: "National League", flag: "🏴" },
],
spain: [
{ id: "laliga", name: "LaLiga", flag: "🇪🇸" },
{ id: "laliga2", name: "LaLiga 2", flag: "🇪🇸" },
{ id: "copa-del-rey", name: "Copa del Rey", flag: "🇪🇸" },
],
germany: [
{ id: "bundesliga", name: "Bundesliga", flag: "🇩🇪" },
{ id: "bundesliga2", name: "2. Bundesliga", flag: "🇩🇪" },
{ id: "dfb-pokal", name: "DFB-Pokal", flag: "🇩🇪" },
],
italy: [
{ id: "serie-a", name: "Serie A", flag: "🇮🇹" },
{ id: "serie-b", name: "Serie B", flag: "🇮🇹" },
{ id: "coppa-italia", name: "Coppa Italia", flag: "🇮🇹" },
],
international: [
{ id: "world-cup", name: "FIFA World Cup", flag: "🌐" },
{ id: "euro", name: "UEFA European Championship", flag: "🌐" },
],
france: [
{ id: "ligue1", name: "Ligue 1", flag: "🇫🇷" },
{ id: "ligue2", name: "Ligue 2", flag: "🇫🇷" },
],
portugal: [
{ id: "liga-portugal", name: "Liga Portugal", flag: "🇵🇹" },
],
"england-amateur": [],
netherlands: [
{ id: "eredivisie", name: "Eredivisie", flag: "🇳🇱" },
{ id: "eerste-divisie", name: "Eerste Divisie", flag: "🇳🇱" },
],
scotland: [{ id: "scotland-prem", name: "Scottish Premiership", flag: "🏴" }],
belgium: [{ id: "pro-league", name: "Pro League", flag: "🇧🇪" }],
turkiye: [{ id: "super-lig", name: "Süper Lig", flag: "🇹🇷" }],
}
const ALL_MARKETS: { id: MarketId; code: string; name: string }[] = [
{ id: "164", code: "164", name: "1x2" },
{ id: "166", code: "166", name: "Double Chance" },
{ id: "168", code: "168", name: "Draw No Bet" },
{ id: "170", code: "170", name: "Handicap 1:0" },
{ id: "172", code: "172", name: "Asian Handicap 0.5" },
{ id: "174", code: "174", name: "Total 0.5" },
{ id: "176", code: "176", name: "Total 1.5" },
{ id: "178", code: "178", name: "Total 2.5" },
{ id: "180", code: "180", name: "Total 3.5" },
{ id: "181", code: "181", name: "Total 4.5" },
{ id: "184", code: "184", name: "Home Team Over/Under" },
{ id: "186", code: "186", name: "Home Team Over/Under" },
{ id: "188", code: "188", name: "Away Team Over/Under" },
{ id: "189", code: "189", name: "Away Team Over/Under" },
{ id: "191", code: "191", name: "Home Team Goals" },
{ id: "193", code: "193", name: "Away Team Goals" },
{ id: "194", code: "194", name: "1st Half - 1X2" },
{ id: "196", code: "196", name: "1st Half - Draw No Bet" },
]
const MAIN_MARKET_IDS: MarketId[] = ["164", "166", "168", "178", "194"]
export default function PrintOddsPage() {
const [sportChecked, setSportChecked] = useState(true)
const [selectedNations, setSelectedNations] = useState<Set<NationId>>(new Set())
const [selectedTournaments, setSelectedTournaments] = useState<Set<TournamentId>>(new Set())
const [selectedMarkets, setSelectedMarkets] = useState<Set<MarketId>>(new Set())
const [sorting, setSorting] = useState("Events")
const [printSorting, setPrintSorting] = useState("Horizontal")
const [dateHelper, setDateHelper] = useState("Today")
const [startDate, setStartDate] = useState("02/28/2026")
const [endDate, setEndDate] = useState("02/28/2026")
const [logo, setLogo] = useState("Without logo")
const availableTournaments = useMemo(() => {
if (!sportChecked || selectedNations.size === 0) return []
const list: { id: TournamentId; name: string; flag: string }[] = []
selectedNations.forEach((nationId) => {
const tournaments = TOURNAMENTS_BY_NATION[nationId as NationId]
if (tournaments) list.push(...tournaments)
})
return list
}, [sportChecked, selectedNations])
const toggleNation = (id: NationId) => {
setSelectedNations((prev) => {
const next = new Set(prev)
if (next.has(id)) next.delete(id)
else next.add(id)
return next
})
}
const toggleTournament = (id: TournamentId) => {
setSelectedTournaments((prev) => {
const next = new Set(prev)
if (next.has(id)) next.delete(id)
else next.add(id)
return next
})
}
const toggleMarket = (id: MarketId) => {
setSelectedMarkets((prev) => {
const next = new Set(prev)
if (next.has(id)) next.delete(id)
else next.add(id)
return next
})
}
const pickMainMarkets = () => {
setSelectedMarkets((prev) => {
const next = new Set(prev)
MAIN_MARKET_IDS.forEach((id) => next.add(id))
return next
})
}
const makePdf = () => {
const printWindow = window.open("", "_blank")
if (!printWindow) {
window.print()
return
}
printWindow.document.write(`
<!DOCTYPE html>
<html>
<head><title>Print Odds</title>
<style>
body { font-family: sans-serif; padding: 20px; color: #111; }
h1 { font-size: 18px; margin-bottom: 16px; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
th { background: #f5f5f5; }
</style>
</head>
<body>
<h1>Print Odds ${dateHelper}</h1>
<p><strong>Date range:</strong> ${startDate} ${endDate}</p>
<p><strong>Nations:</strong> ${Array.from(selectedNations).join(", ")}</p>
<p><strong>Tournaments:</strong> ${Array.from(selectedTournaments).join(", ")}</p>
<p><strong>Markets:</strong> ${Array.from(selectedMarkets).map((id) => ALL_MARKETS.find((m) => m.id === id)?.name ?? id).join(", ")}</p>
<p><em>Odds sheet would be generated based on selected options.</em></p>
</body>
</html>
`)
printWindow.document.close()
printWindow.focus()
printWindow.print()
printWindow.close()
}
const sport = SPORTS_WITH_NATIONS[0]
return (
<div className="min-h-screen bg-brand-bg text-white flex flex-col">
<div className="bg-brand-surface border-b border-white/10 px-4 py-2 flex flex-wrap items-center gap-4">
<select
value={sorting}
onChange={(e) => setSorting(e.target.value)}
className="bg-brand-surface-light border border-white/20 text-white text-[11px] px-2 py-1.5 rounded-none"
>
<option value="Events">Events</option>
<option value="Leagues">Leagues</option>
</select>
<select
value={printSorting}
onChange={(e) => setPrintSorting(e.target.value)}
className="bg-brand-surface-light border border-white/20 text-white text-[11px] px-2 py-1.5 rounded-none"
>
<option value="Horizontal">Horizontal</option>
<option value="Vertical">Vertical</option>
</select>
<select
value={dateHelper}
onChange={(e) => setDateHelper(e.target.value)}
className="bg-brand-surface-light border border-white/20 text-white text-[11px] px-2 py-1.5 rounded-none"
>
<option value="Today">Today</option>
<option value="Tomorrow">Tomorrow</option>
<option value="Week">Week</option>
</select>
<input
type="text"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="bg-brand-surface-light border border-white/20 text-white text-[11px] w-28 px-2 py-1.5 rounded-none"
/>
<input
type="text"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className="bg-brand-surface-light border border-white/20 text-white text-[11px] w-28 px-2 py-1.5 rounded-none"
/>
<select
value={logo}
onChange={(e) => setLogo(e.target.value)}
className="bg-brand-surface-light border border-white/20 text-white text-[11px] px-2 py-1.5 rounded-none"
>
<option value="Without logo">Without logo</option>
<option value="With logo">With logo</option>
</select>
<div className="flex-1" />
<button
type="button"
onClick={pickMainMarkets}
className="bg-brand-primary text-black px-4 py-2 text-[11px] font-bold uppercase rounded-none hover:bg-brand-primary-hover"
>
Pick Main Markets +
</button>
<button
type="button"
onClick={makePdf}
className="bg-brand-primary text-black px-4 py-2 text-[11px] font-bold uppercase rounded-none hover:bg-brand-primary-hover flex items-center gap-1"
>
Make PDF
<svg viewBox="0 0 24 24" className="size-4 fill-current"><path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0016 9.5 6.5 6.5 0 109.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
</button>
</div>
<div id="print-odds-content" className="flex-1 grid grid-cols-1 md:grid-cols-3 gap-0 p-4 min-h-0">
{/* 1 Choose Nation */}
<div className="border border-white/10 rounded-l bg-brand-surface-light overflow-hidden flex flex-col">
<div className="px-3 py-2 bg-brand-surface border-b border-white/10 text-[11px] font-black uppercase">
1 Choose Nation
</div>
<div className="flex-1 overflow-y-auto p-2 space-y-1">
<label className="flex items-center gap-2 py-1.5 px-2 hover:bg-white/5 cursor-pointer">
<input
type="checkbox"
checked={sportChecked}
onChange={(e) => setSportChecked(e.target.checked)}
className="rounded border-white/40 text-brand-primary"
/>
<span className="text-[11px] font-bold">{sport.sport}</span>
</label>
{sport.nations.map((nation) => (
<label
key={nation.id}
className={cn(
"flex items-center gap-2 py-1.5 px-2 hover:bg-white/5 cursor-pointer rounded",
selectedNations.has(nation.id) && "bg-white/10"
)}
>
<input
type="checkbox"
checked={selectedNations.has(nation.id)}
onChange={() => toggleNation(nation.id)}
className="rounded border-white/40 text-brand-primary"
/>
<span className="text-[10px] mr-1">{nation.flag}</span>
<span className="text-[11px]">{nation.name}</span>
</label>
))}
</div>
</div>
{/* 2 Choose Tournaments */}
<div className="border border-white/10 border-x-0 md:border-x bg-brand-surface-light overflow-hidden flex flex-col">
<div className="px-3 py-2 bg-brand-surface border-b border-white/10 text-[11px] font-black uppercase">
2 Choose Tournaments
</div>
<div className="flex-1 overflow-y-auto p-2 space-y-1">
{availableTournaments.length === 0 ? (
<p className="text-white/50 text-[11px] py-4 px-2">Select nations first</p>
) : (
<>
<label className="flex items-center gap-2 py-1.5 px-2 hover:bg-white/5 cursor-pointer">
<input
type="checkbox"
checked={selectedTournaments.size === availableTournaments.length}
onChange={(e) => {
if (e.target.checked) {
setSelectedTournaments(new Set(availableTournaments.map((t) => t.id)))
} else {
setSelectedTournaments(new Set())
}
}}
className="rounded border-white/40 text-brand-primary"
/>
<span className="text-[11px] font-bold">All</span>
</label>
{availableTournaments.map((t) => (
<label
key={t.id}
className={cn(
"flex items-center gap-2 py-1.5 px-2 hover:bg-white/5 cursor-pointer rounded",
selectedTournaments.has(t.id) && "bg-white/10"
)}
>
<input
type="checkbox"
checked={selectedTournaments.has(t.id)}
onChange={() => toggleTournament(t.id)}
className="rounded border-white/40 text-brand-primary"
/>
<span className="text-[10px] mr-1">{t.flag}</span>
<span className="text-[11px]">{t.name}</span>
</label>
))}
</>
)}
</div>
</div>
{/* 3 Choose Markets - shown only after nations and tournaments are chosen */}
<div className="border border-white/10 rounded-r bg-brand-surface-light overflow-hidden flex flex-col">
<div className="px-3 py-2 bg-brand-surface border-b border-white/10 text-[11px] font-black uppercase">
3 Choose Markets
</div>
<div className="flex-1 overflow-y-auto p-2 space-y-1">
{selectedNations.size === 0 || selectedTournaments.size === 0 ? (
<p className="text-white/50 text-[11px] py-4 px-2">Select nations and tournaments first</p>
) : (
ALL_MARKETS.map((m) => (
<label
key={m.id}
className={cn(
"flex items-center gap-2 py-1.5 px-2 hover:bg-white/5 cursor-pointer rounded",
selectedMarkets.has(m.id) && "bg-white/10"
)}
>
<input
type="checkbox"
checked={selectedMarkets.has(m.id)}
onChange={() => toggleMarket(m.id)}
className="rounded border-white/40 text-brand-primary"
/>
<span className="text-[10px] text-white/60 w-6">{m.code}</span>
<span className="text-[11px]">{m.name}</span>
</label>
))
)}
</div>
</div>
</div>
</div>
)
}

View File

@ -44,13 +44,13 @@ export default function RegisterPage() {
))}
</div>
{/* HARIF box */}
<div className="bg-[#852222] px-3 py-1 -skew-x-12 flex items-center h-[38px]">
<div className="bg-brand-accent px-3 py-1 -skew-x-12 flex items-center h-[38px]">
<span className="text-2xl font-black text-white italic tracking-tighter skew-x-12 inline-block leading-none">
HARIF
</span>
</div>
{/* SPORT text */}
<span className="text-2xl font-black text-[#ff9800] italic tracking-tighter ml-1 leading-none">
<span className="text-2xl font-black text-brand-primary italic tracking-tighter ml-1 leading-none">
SPORT
</span>
</div>
@ -78,7 +78,7 @@ export default function RegisterPage() {
type="tel"
value={phone}
onChange={(e) => setPhone(e.target.value)}
className="flex-1 bg-white border border-gray-300 px-3 py-2 text-sm text-[#333] placeholder:text-gray-400 focus:outline-none focus:border-[#ff9800]"
className="flex-1 bg-white border border-gray-300 px-3 py-2 text-sm text-[#333] placeholder:text-gray-400 focus:outline-none focus:border-brand-primary"
/>
</div>
</div>
@ -93,7 +93,7 @@ export default function RegisterPage() {
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
className="w-full bg-white border border-gray-300 px-3 py-2 text-sm text-[#333] placeholder:text-gray-400 focus:outline-none focus:border-[#ff9800]"
className="w-full bg-white border border-gray-300 px-3 py-2 text-sm text-[#333] placeholder:text-gray-400 focus:outline-none focus:border-brand-primary"
/>
</div>
@ -107,7 +107,7 @@ export default function RegisterPage() {
value={repeatPassword}
onChange={(e) => setRepeatPassword(e.target.value)}
placeholder="Repeat Password"
className="w-full bg-white border border-gray-300 px-3 py-2 text-sm text-[#333] placeholder:text-gray-400 focus:outline-none focus:border-[#ff9800]"
className="w-full bg-white border border-gray-300 px-3 py-2 text-sm text-[#333] placeholder:text-gray-400 focus:outline-none focus:border-brand-primary"
/>
</div>
@ -117,7 +117,7 @@ export default function RegisterPage() {
type="checkbox"
checked={ageConfirmed}
onChange={(e) => setAgeConfirmed(e.target.checked)}
className="w-4 h-4 accent-[#ff9800]"
className="w-4 h-4 accent-brand-primary"
/>
<span className="text-[12px] text-white/80">
I confirm I&apos;m over 21 years old
@ -139,7 +139,7 @@ export default function RegisterPage() {
{/* Login link */}
<p className="text-center text-[11px] text-white/50">
Already have an account?{" "}
<Link href="/login" className="text-[#ff9800] hover:underline font-semibold">
<Link href="/login" className="text-brand-primary hover:underline font-semibold">
Login
</Link>
</p>

106
app/special-games/page.tsx Normal file
View File

@ -0,0 +1,106 @@
"use client"
import { useState } from "react"
import Image from "next/image"
import { GamingSidebar } from "@/components/games/gaming-sidebar"
import { GameRow } from "@/components/games/game-row"
import { GameCard } from "@/components/games/game-card"
import { Search, Heart, Clock, Star, Zap } from "lucide-react"
// Dummy data
const DUMMY_GAMES = [
{ id: "s1", title: "Safari Hot 20", image: "https://images.unsplash.com/photo-1546182990-dffeafbe841d?w=400&h=300&fit=crop", provider: "Pragmatic Play" },
{ id: "s2", title: "Phoenix Hot 20", image: "https://images.unsplash.com/photo-1535223289827-42f1e9919769?w=400&h=300&fit=crop", provider: "Gamzix" },
{ id: "s3", title: "Habesha Fortune 5", image: "https://images.unsplash.com/photo-1511512578047-dfb367046420?w=400&h=300&fit=crop", provider: "Smartsoft" },
{ id: "s4", title: "Chicken Road 2.0", image: "https://images.unsplash.com/photo-1518717758536-85ae29035b6d?w=400&h=300&fit=crop", provider: "Smartsoft" },
{ id: "s5", title: "Hot to Burn", image: "https://images.unsplash.com/photo-1553775282-20af807d920c?w=400&h=300&fit=crop", provider: "Pragmatic Play" },
{ id: "s6", title: "HydroPlane", image: "https://images.unsplash.com/photo-1567620905732-2d1ec7bb7445?w=400&h=300&fit=crop", provider: "Smartsoft" },
{ id: "s7", title: "Maneki Win Hard", image: "https://images.unsplash.com/photo-1569154941061-e231b4725ef1?w=400&h=300&fit=crop", provider: "Scribe" },
]
const CATEGORIES = [
{ id: "search", name: "Search", icon: Search },
{ id: "popular", name: "Popular Games", icon: Star },
{ id: "favourites", name: "Favourites", icon: Heart },
{ id: "recently-played", name: "Recently Played", icon: Clock },
{ id: "evoplay", name: "Evoplay", icon: Star },
{ id: "betsoft", name: "Betsoft", icon: Star },
{ id: "inout", name: "InOUT Games", icon: Star },
{ id: "scribe", name: "Scribe Games", icon: Star },
{ id: "pragmatic", name: "Pragmatic", icon: Star },
{ id: "mancala", name: "Mancala", icon: Star },
{ id: "spinomenal", name: "Spinomenal", icon: Star },
{ id: "abracadabra", name: "Abracadabra", icon: Star },
{ id: "turbo", name: "Turbo Games", icon: Star },
]
const CATEGORIES_DATA = [
{ id: "popular", title: "Popular Games", games: [...DUMMY_GAMES, ...DUMMY_GAMES, ...DUMMY_GAMES].slice(0, 18) },
{ id: "evoplay", title: "Evoplay", games: [...DUMMY_GAMES].reverse() },
{ id: "betsoft", title: "Betsoft", games: DUMMY_GAMES.slice(0, 4) },
]
export default function SpecialGamesPage() {
const [activeCategory, setActiveCategory] = useState("all")
const activeCategoryData = CATEGORIES_DATA.find(c => c.id === activeCategory)
return (
<div className="flex h-[calc(100vh-140px)] overflow-hidden bg-brand-bg">
<GamingSidebar
title="Special Games"
subtitle="Check out our games!"
activeCategory={activeCategory}
onCategoryChange={setActiveCategory}
categories={CATEGORIES}
/>
<main className="flex-1 overflow-y-auto scrollbar-none pb-12">
<div className="relative w-full aspect-[4/1] md:aspect-[5/1] overflow-hidden">
<Image
src="/aviator-bannsser.jpg"
alt="Special Games Banner"
fill
className="object-cover"
priority
/>
</div>
<div className="mt-0">
{activeCategory === "all" ? (
<div className="flex flex-col">
{CATEGORIES_DATA.map((category) => (
<GameRow
key={category.id}
title={category.title}
games={category.games}
rows={category.id === "popular" ? 3 : 1}
/>
))}
</div>
) : (
<div className="p-4">
<div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-bold text-white uppercase border-l-4 border-brand-primary pl-4">
{activeCategoryData?.title || activeCategory.replace("-", " ")}
</h2>
<button
onClick={() => setActiveCategory("all")}
className="text-xs text-white/40 hover:text-white uppercase font-bold"
>
Back to Special Games
</button>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-6">
{(activeCategoryData?.games || DUMMY_GAMES).map((game, idx) => (
<GameCard key={idx} {...game} />
))}
</div>
</div>
)}
</div>
</main>
</div>
)
}

204
app/virtual/page.tsx Normal file
View File

@ -0,0 +1,204 @@
"use client"
import { useState, useEffect } from "react"
import Image from "next/image"
import { GamingSidebar } from "@/components/games/gaming-sidebar"
import { GameRow } from "@/components/games/game-row"
import { GameCard } from "@/components/games/game-card"
import { Search, Heart, Clock, Star, Zap, Gamepad2, AlertCircle, LayoutGrid } from "lucide-react"
import { cn } from "@/lib/utils"
import api from "@/lib/api"
interface Provider {
provider_id: string
provider_name: string
logo_dark: string
logo_light: string
enabled: boolean
}
interface ApiGame {
gameId: string
providerId: string
provider: string
name: string
category: string
deviceType: string
hasDemo: boolean
hasFreeBets: boolean
demoUrl?: string
image?: string // In case it gets added
thumbnail?: string
provider_id?: string // Fallback
}
const DEFAULT_IMAGE = "https://st.pokgaming.com/gameThumbnails/246.jpg"
export default function VirtualPage() {
const [activeCategory, setActiveCategory] = useState("all")
const [providers, setProviders] = useState<Provider[]>([])
const [games, setGames] = useState<ApiGame[]>([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
const fetchVirtualData = async () => {
try {
setIsLoading(true)
const [providersRes, gamesRes] = await Promise.all([
api.get("/virtual-game/orchestrator/providers"),
api.get("/virtual-game/orchestrator/games", { params: { limit: 2000 } })
])
const pData = providersRes.data
const gData = gamesRes.data
const providersList = pData.providers || pData.data || pData || []
const gamesList = gData.data || gData.games || gData || []
setProviders(Array.isArray(providersList) ? providersList : [])
setGames(Array.isArray(gamesList) ? gamesList : [])
} catch (err: any) {
console.error("Failed to fetch virtual games data:", err)
setError("Failed to load games data.")
} finally {
setIsLoading(false)
}
}
fetchVirtualData()
}, [])
// Create Sidebar Categories dynamically from providers
const sidebarCategories = [
{ id: "all", name: "All Games", icon: LayoutGrid },
...providers.map(p => ({
id: p.provider_id,
name: p.provider_name,
icon: p.logo_dark || p.logo_light || Gamepad2
}))
]
// Filter games based on active category
// If "all", group by provider
let displayedGames: any[] = []
let groupedGames: { title: string, games: any[] }[] = []
const mapApiGameToCard = (game: ApiGame) => ({
id: game.gameId,
title: game.name,
image: game.thumbnail || game.image || DEFAULT_IMAGE,
provider: game.provider
})
if (activeCategory === "all") {
// Group up to 12 games per provider for the rows
providers.forEach(p => {
const providerIdStr = String(p.provider_id || "").trim().toLowerCase()
const providerGames = games
.filter(g => {
const gameProvId = String(g.providerId || g.provider_id || "").trim().toLowerCase()
return gameProvId === providerIdStr
})
.slice(0, 12)
.map(mapApiGameToCard)
if (providerGames.length > 0) {
groupedGames.push({
title: p.provider_name,
games: providerGames
})
}
})
} else {
displayedGames = games
.filter(g => {
const gameProvId = String(g.providerId || g.provider_id || "").trim().toLowerCase()
const matches = gameProvId === String(activeCategory).trim().toLowerCase()
if (g.providerId?.toLowerCase().includes('pop') || g.provider_id?.toLowerCase().includes('pop')) {
}
return matches
})
.map(mapApiGameToCard)
}
const activeCategoryData = providers.find(
p => String(p.provider_id || "").trim().toLowerCase() === String(activeCategory).trim().toLowerCase()
)
return (
<div className="flex h-[calc(100vh-140px)] overflow-hidden bg-brand-bg">
{/* Sidebar */}
<GamingSidebar
title="Virtual"
subtitle="Check out our games!"
activeCategory={activeCategory}
onCategoryChange={setActiveCategory}
categories={sidebarCategories}
/>
{/* Main Content */}
<main className="flex-1 overflow-y-auto scrollbar-none pb-12">
{/* Banner */}
<div className="relative w-full aspect-[4/1] md:aspect-[5/1] overflow-hidden">
<Image
src="/aviator-bannsser.jpg"
alt="Virtual Games Banner"
fill
className="object-cover"
priority
/>
</div>
<div className="mt-0">
{isLoading ? (
<div className="p-8 text-center text-white/60 text-sm font-bold flex items-center justify-center gap-2">
<div className="size-4 rounded-full border-2 border-brand-primary border-t-transparent animate-spin" />
Loading games...
</div>
) : error ? (
<div className="p-8 text-center text-red-400 text-sm font-bold flex items-center justify-center gap-2">
<AlertCircle className="size-4" />
{error}
</div>
) : activeCategory === "all" ? (
// Show all categories
<div className="flex flex-col">
{groupedGames.map((category, index) => (
<GameRow
key={category.title}
title={category.title}
games={category.games}
rows={1}
/>
))}
</div>
) : (
// Show only selected category
<div className="p-4">
<div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-bold text-white uppercase border-l-4 border-brand-primary pl-4">
{activeCategoryData?.provider_name || 'Games'}
</h2>
<button
onClick={() => setActiveCategory("all")}
className="text-xs text-white/40 hover:text-white uppercase font-bold"
>
Back to Virtual
</button>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-6">
{displayedGames.map((game, idx) => (
<GameCard key={game.id || idx} {...game} />
))}
</div>
</div>
)}
</div>
</main>
</div>
)
}

View File

@ -2,183 +2,203 @@
import { useState } from "react"
import { useBetslipStore } from "@/lib/store/betslip-store"
import { X, ChevronDown, Trash2 } from "lucide-react"
import { X, Save, Trash2 } from "lucide-react"
import { cn } from "@/lib/utils"
const quickStakes = [5, 10, 20, 50, 100, 200]
const quickStakes = [10, 50, 100, 1000, 2000, 5000]
export function Betslip() {
const { bets, removeBet, clearBets, updateStake, getPotentialWin, getTotalOdds } = useBetslipStore()
const [activeTab, setActiveTab] = useState<"single" | "accumulator">("single")
const [globalStake, setGlobalStake] = useState("10")
const potentialWin = getPotentialWin()
const totalOdds = getTotalOdds()
const isMulti = bets.length > 1
const potentialWin = isMulti
? Number(globalStake) * totalOdds
: getPotentialWin()
return (
<div className="bg-card rounded border border-border overflow-hidden">
{/* Header */}
<div className="bg-primary px-3 py-2 flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-[11px] font-bold text-primary-foreground uppercase">Betslip</span>
{bets.length > 0 && (
<span className="bg-primary-foreground text-primary text-[10px] font-bold px-1.5 py-0.5 rounded-full min-w-[18px] text-center">
{bets.length}
</span>
)}
</div>
{bets.length > 0 && (
<button
onClick={clearBets}
className="text-primary-foreground/70 hover:text-primary-foreground transition-colors"
>
<Trash2 className="size-3.5" />
</button>
)}
</div>
{/* Tabs */}
{bets.length > 1 && (
<div className="flex border-b border-border">
{(["single", "accumulator"] as const).map((tab) => (
<button
key={tab}
onClick={() => setActiveTab(tab)}
className={cn(
"flex-1 py-1.5 text-[10px] font-semibold uppercase transition-colors",
activeTab === tab
? "bg-muted text-foreground border-b-2 border-primary"
: "text-muted-foreground hover:text-foreground"
)}
>
{tab}
</button>
))}
</div>
)}
{/* Content */}
<div className="p-2 space-y-2">
<div className="overflow-hidden">
<div className="space-y-3">
{bets.length === 0 ? (
<div className="py-6 text-center">
<div className="text-2xl mb-2">🎯</div>
<p className="text-[11px] text-muted-foreground leading-relaxed">
No bets selected. Click on odds to add selections.
<div className="py-10 px-4 text-center">
<p className="text-[11px] text-muted-foreground font-medium leading-relaxed">
No bet has been selected. To select a bet, please click on the respective odds
</p>
</div>
) : (
<>
{/* Bet items */}
{/* Bet cards */}
<div className="space-y-2">
{bets.map((bet) => (
<div key={bet.id} className="bg-secondary/50 rounded border border-border/50 p-2">
<div className="flex items-start justify-between gap-2 mb-2">
<div key={bet.id} className="bg-brand-surface-light border border-border/30 p-2.5">
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<div className="text-[11px] font-semibold text-foreground truncate">{bet.event}</div>
<div className="text-[10px] text-muted-foreground truncate">{bet.league}</div>
<div className="text-[10px] text-primary font-medium mt-0.5">
{bet.market}: <span className="text-foreground">{bet.selection}</span>
<div className="text-[11px] font-bold text-white uppercase leading-tight truncate">
{bet.event}
</div>
<div className="text-[10px] text-white/50 uppercase mt-0.5 truncate">
{bet.league}
</div>
<div className="flex items-center justify-between gap-2 mt-1.5">
<span className="text-[10px] text-white/80">
{bet.market}: <span className="font-bold text-white">{bet.selection}</span>
</span>
<span className="text-[11px] font-bold text-brand-primary shrink-0">{bet.odds.toFixed(2)}</span>
</div>
{!isMulti && (
<div className="flex justify-between text-[10px] text-white/50 mt-0.5">
<span>Odds</span>
<span className="text-brand-primary font-bold">{bet.odds.toFixed(2)}</span>
</div>
)}
</div>
<div className="flex items-center gap-1.5 shrink-0">
<span className="text-sm font-bold text-primary">{bet.odds.toFixed(2)}</span>
<button
onClick={() => removeBet(bet.id)}
className="text-muted-foreground hover:text-destructive transition-colors"
>
<X className="size-3.5" />
</button>
</div>
<button
onClick={() => removeBet(bet.id)}
className="text-white/50 hover:text-white transition-colors shrink-0 p-0.5"
aria-label="Remove bet"
>
<X className="size-3.5" />
</button>
</div>
{/* Stake input for single */}
{(activeTab === "single" || bets.length === 1) && (
<div>
<div className="text-[10px] text-muted-foreground mb-1">Stake (ETB)</div>
<div className="flex gap-1">
{/* Single bet: stake on card */}
{!isMulti && (
<div className="mt-3 pt-3 border-t border-border/20">
<div className="flex items-center gap-1 mb-2">
<button
type="button"
onClick={() => updateStake(bet.id, Math.max(1, (bet.stake ?? 10) - 1))}
className="w-7 h-7 flex items-center justify-center rounded bg-white/10 text-white text-sm font-bold hover:bg-white/20"
>
</button>
<input
type="number"
value={bet.stake ?? 10}
onChange={(e) => updateStake(bet.id, Number(e.target.value))}
className="flex-1 bg-input border border-border rounded px-2 py-1 text-xs text-foreground w-full min-w-0 focus:outline-none focus:border-primary"
onChange={(e) => updateStake(bet.id, Number(e.target.value) || 10)}
className="flex-1 h-7 bg-brand-bg border border-border/40 px-2 text-[11px] text-white text-center font-bold focus:outline-none focus:border-brand-primary"
min="1"
/>
<button
type="button"
onClick={() => updateStake(bet.id, (bet.stake ?? 10) + 1)}
className="w-7 h-7 flex items-center justify-center rounded bg-white/10 text-white text-sm font-bold hover:bg-white/20"
>
+
</button>
</div>
<div className="flex gap-1 mt-1 flex-wrap">
<div className="flex flex-wrap gap-1.5">
{quickStakes.map((s) => (
<button
key={s}
onClick={() => updateStake(bet.id, s)}
className={cn(
"text-[10px] px-1.5 py-0.5 rounded border transition-colors",
"size-7 rounded-full text-[10px] font-bold transition-colors",
(bet.stake ?? 10) === s
? "bg-primary text-primary-foreground border-primary"
: "border-border text-muted-foreground hover:border-primary hover:text-primary"
? "bg-brand-primary text-black"
: "bg-brand-primary/20 text-brand-primary hover:bg-brand-primary/30"
)}
>
{s}
</button>
))}
</div>
{/* Potential win */}
<div className="mt-1.5 flex justify-between text-[10px]">
<span className="text-muted-foreground">Potential win:</span>
<span className="text-primary font-semibold">
{((bet.stake ?? 10) * bet.odds).toFixed(2)} ETB
</span>
</div>
</div>
)}
</div>
))}
</div>
{/* Accumulator stake section */}
{activeTab === "accumulator" && bets.length > 1 && (
<div className="bg-secondary/50 rounded border border-border/50 p-2 space-y-2">
<div className="flex justify-between text-[11px]">
<span className="text-muted-foreground">Total Odds:</span>
<span className="font-bold text-primary">{totalOdds.toFixed(2)}</span>
{/* Single bet: potential winning below card */}
{!isMulti && (
<div className="flex justify-between items-center text-[11px] font-bold text-white">
<span className="text-white/70">Potential winning</span>
<span>{potentialWin.toFixed(2)} ETB</span>
</div>
)}
{/* Multiple bets: combined Odds, one stake, quick buttons, potential winning */}
{isMulti && (
<div className="bg-brand-surface-light border border-border/30 p-2.5 space-y-2.5">
<div className="flex justify-between text-[11px] font-bold text-white">
<span className="text-white/70">Odds</span>
<span className="text-brand-primary">{totalOdds.toFixed(2)}</span>
</div>
<div>
<div className="text-[10px] text-muted-foreground mb-1">Stake (ETB)</div>
<div className="flex items-center gap-1">
<button
type="button"
onClick={() => setGlobalStake(String(Math.max(1, Number(globalStake) - 1)))}
className="w-7 h-7 flex items-center justify-center rounded bg-white/10 text-white text-sm font-bold hover:bg-white/20"
>
</button>
<input
type="number"
value={globalStake}
onChange={(e) => setGlobalStake(e.target.value)}
className="w-full bg-input border border-border rounded px-2 py-1 text-xs text-foreground focus:outline-none focus:border-primary"
className="flex-1 h-7 bg-brand-bg border border-border/40 px-2 text-[11px] text-white text-center font-bold focus:outline-none focus:border-brand-primary"
min="1"
/>
<div className="flex gap-1 mt-1 flex-wrap">
{quickStakes.map((s) => (
<button
key={s}
onClick={() => setGlobalStake(String(s))}
className={cn(
"text-[10px] px-1.5 py-0.5 rounded border transition-colors",
globalStake === String(s)
? "bg-primary text-primary-foreground border-primary"
: "border-border text-muted-foreground hover:border-primary hover:text-primary"
)}
>
{s}
</button>
))}
</div>
<div className="mt-1.5 flex justify-between text-[10px]">
<span className="text-muted-foreground">Potential win:</span>
<span className="text-primary font-semibold">
{(Number(globalStake) * totalOdds).toFixed(2)} ETB
</span>
</div>
<button
type="button"
onClick={() => setGlobalStake(String(Number(globalStake) + 1))}
className="w-7 h-7 flex items-center justify-center rounded bg-white/10 text-white text-sm font-bold hover:bg-white/20"
>
+
</button>
</div>
<div className="flex flex-wrap gap-1.5">
{quickStakes.map((s) => (
<button
key={s}
onClick={() => setGlobalStake(String(s))}
className={cn(
"size-7 rounded-full text-[10px] font-bold transition-colors",
Number(globalStake) === s
? "bg-brand-primary text-black"
: "bg-brand-primary/20 text-brand-primary hover:bg-brand-primary/30"
)}
>
{s}
</button>
))}
</div>
<div className="flex justify-between text-[11px] font-bold text-white pt-0.5">
<span className="text-white/70">Potential winning</span>
<span className="text-brand-primary">{(Number(globalStake) * totalOdds).toFixed(2)} ETB</span>
</div>
</div>
)}
{/* Place bet button */}
<button className="w-full bg-primary text-primary-foreground text-[12px] font-bold py-2.5 rounded hover:opacity-90 active:scale-95 transition-all uppercase tracking-wide">
Place Bet
</button>
<div className="flex items-center gap-2 py-2 px-1 text-[10px] text-brand-primary/90">
<svg viewBox="0 0 24 24" className="size-4 shrink-0 fill-brand-primary" aria-hidden><path d="M12 2L1 21h22L12 2zm0 3.99L19.53 19H4.47L12 5.99zM11 10v4h2v-2h-2v-2zm0 6v2h2v-2h-2z"/></svg>
<span>You need to login to be able to place a bet.</span>
</div>
<div className="flex gap-2">
<button
type="button"
className="flex-1 flex items-center justify-center gap-1.5 py-2 px-3 rounded text-[11px] font-bold text-white bg-white/10 hover:bg-white/15 border border-border/40"
>
<Save className="size-3.5" />
Save
</button>
<button
type="button"
onClick={clearBets}
className="flex items-center justify-center gap-1.5 py-2 px-3 rounded text-[11px] font-bold text-white bg-red-600/80 hover:bg-red-600 border border-red-500/50"
>
<Trash2 className="size-3.5" />
Clear
</button>
<button
type="button"
className="flex-1 py-2 px-3 rounded text-[11px] font-bold text-white bg-green-600 hover:bg-green-500"
>
Bet
</button>
</div>
</>
)}
</div>

View File

@ -16,19 +16,19 @@ export function CheckYourBet() {
return (
<div className="bg-transparent border-t border-border/20 pt-4 pb-2">
<div className="px-1 text-center space-y-3">
<h3 className="text-[12px] font-bold uppercase text-[#ff9800]">Check Your Bet</h3>
<h3 className="text-[12px] font-bold uppercase text-brand-primary">Check Your Bet</h3>
<p className="text-[11px] text-white">Your bet ID</p>
<div className="flex gap-1">
<input
type="text"
value={betId}
onChange={(e) => setBetId(e.target.value)}
className="flex-1 bg-[#121212] border border-border/40 px-2 py-2 text-[11px] text-white outline-none focus:border-primary"
className="flex-1 bg-brand-bg border border-border/40 px-2 py-2 text-[11px] text-white outline-none focus:border-brand-primary"
onKeyDown={(e) => e.key === "Enter" && handleCheck()}
/>
<button
onClick={handleCheck}
className="bg-[#ff9800] text-black px-2 py-1.5 flex items-center justify-center min-w-[32px] hover:bg-[#ffa726] transition-colors"
className="bg-brand-primary text-black px-2 py-1.5 flex items-center justify-center min-w-[32px] hover:bg-brand-primary-hover transition-colors"
>
<svg viewBox="0 0 24 24" className="size-5 fill-current"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
</button>

View File

@ -1,6 +1,7 @@
"use client"
import { useState, useEffect } from "react"
import Link from "next/link"
import { useSearchParams } from "next/navigation"
import { useBetslipStore } from "@/lib/store/betslip-store"
import { mockEvents, popularLeagues, type Event } from "@/lib/mock-data"
@ -16,11 +17,11 @@ function OddsButton({ odds, onClick, isSelected }: {
<button
onClick={onClick}
className={cn(
"flex items-center justify-center py-2 px-0.5 border-r border-border/10 text-[10px] transition-all min-w-0 bg-[#262626] hover:bg-[#333] h-full",
isSelected && "bg-[#ff9800] text-black font-bold border-none"
"flex items-center justify-center py-2 px-0.5 border-r border-border/10 text-[10px] transition-all min-w-0 bg-brand-surface hover:bg-white/5 h-full",
isSelected && "bg-brand-primary text-black font-bold border-none"
)}
>
<span className={cn("font-bold tracking-tighter", isSelected ? "text-black" : "text-[#ff9800]")}>{odds.toFixed(2)}</span>
<span className={cn("font-bold tracking-tighter", isSelected ? "text-black" : "text-brand-primary")}>{odds.toFixed(2)}</span>
</button>
)
}
@ -29,12 +30,12 @@ function EventRow({ event }: { event: Event }) {
const { bets, addBet } = useBetslipStore()
return (
<div className="bg-[#1a1a1a] border-b border-border/20 hover:bg-[#222] transition-colors h-[38px] flex items-center">
<div className="bg-brand-surface border-b border-border/20 hover:bg-white/5 transition-colors h-[38px] flex items-center">
{/* Small Icons & ID Column */}
<div className="flex items-center gap-1.5 px-2 w-[80px] shrink-0 border-r border-border/10 h-full">
<BarChart2 className="size-3 text-muted-foreground hover:text-primary cursor-pointer shrink-0" />
<TrendingUp className="size-3 text-muted-foreground hover:text-primary cursor-pointer shrink-0" />
<span className="text-[9.5px] text-[#ff9800] font-bold tabular-nums italic ml-0.5">{event.id || "01682"}</span>
<span className="text-[9.5px] text-brand-primary font-bold tabular-nums italic ml-0.5">{event.id || "01682"}</span>
</div>
{/* Time & Team Column */}
@ -58,10 +59,10 @@ function EventRow({ event }: { event: Event }) {
isSelected={isSelected}
onClick={() => addBet({
id: betId,
event: `${event.homeTeam} vs ${event.awayTeam}`,
league: event.league,
event: `${event.homeTeam} - ${event.awayTeam}`,
league: `${event.sport} - ${event.country} - ${event.league}`,
market: "1X2",
selection: `${event.homeTeam} (${market.label})`,
selection: market.label,
odds: market.odds,
})}
/>
@ -69,10 +70,14 @@ function EventRow({ event }: { event: Event }) {
})}
</div>
{/* More Markets Button */}
<button className="w-10 flex items-center justify-center h-full hover:bg-white/5 transition-colors border-l border-border/10 group">
{/* More Markets 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-border/10 group"
aria-label="View all markets"
>
<Plus className="size-3 text-white group-hover:scale-110 transition-transform" />
</button>
</Link>
</div>
)
}
@ -112,26 +117,26 @@ export function EventsList({ filter = "All", sport = "all", search = "" }: {
const renderTableHeaders = () => (
<>
{/* Table Header Categories */}
<div className="bg-[#1a1a1a] border-b border-border/40 grid grid-cols-5 text-[11px] font-bold text-white uppercase text-center items-center h-9">
<div className="h-full flex items-center justify-center hover:bg-[#222] transition-colors cursor-pointer border-r border-border/20">Main</div>
<div className="h-full flex items-center justify-center hover:bg-[#222] transition-colors cursor-pointer border-r border-border/20">Goals</div>
<div className="h-full flex items-center justify-center hover:bg-[#222] transition-colors cursor-pointer border-r border-border/20">Handicap</div>
<div className="h-full flex items-center justify-center hover:bg-[#222] transition-colors cursor-pointer border-r border-border/20 text-[9px] leading-tight">Half Time / Full Time</div>
<div className="h-full flex items-center justify-center hover:bg-[#222] transition-colors cursor-pointer">Correct Score</div>
<div className="bg-brand-surface border-b border-border/40 grid grid-cols-5 text-[11px] font-bold text-white uppercase text-center items-center h-9">
<div className="h-full flex items-center justify-center hover:bg-white/5 transition-colors cursor-pointer border-r border-border/20">Main</div>
<div className="h-full flex items-center justify-center hover:bg-white/5 transition-colors cursor-pointer border-r border-border/20">Goals</div>
<div className="h-full flex items-center justify-center hover:bg-white/5 transition-colors cursor-pointer border-r border-border/20">Handicap</div>
<div className="h-full flex items-center justify-center hover:bg-white/5 transition-colors cursor-pointer border-r border-border/20 text-[9px] leading-tight">Half Time / Full Time</div>
<div className="h-full flex items-center justify-center hover:bg-white/5 transition-colors cursor-pointer">Correct Score</div>
</div>
{/* Sub Headers */}
<div className="bg-[#1a1a1a] border-b border-border/40 grid grid-cols-5 text-[10px] font-bold text-white uppercase text-center items-center h-8">
<div className="h-full flex items-center justify-center hover:bg-[#222] transition-colors cursor-pointer border-r border-border/20">1st Half</div>
<div className="h-full flex items-center justify-center hover:bg-[#222] transition-colors cursor-pointer border-r border-border/20">2nd Half</div>
<div className="h-full flex items-center justify-center bg-[#ff9800] text-black border-r border-border/10">Combo</div>
<div className="h-full flex items-center justify-center hover:bg-[#222] transition-colors cursor-pointer border-r border-border/20">Chance Mix</div>
<div className="h-full flex items-center justify-center hover:bg-[#222] transition-colors cursor-pointer">Home</div>
<div className="bg-brand-surface border-b border-border/40 grid grid-cols-5 text-[10px] font-bold text-white uppercase text-center items-center h-8">
<div className="h-full flex items-center justify-center hover:bg-white/5 transition-colors cursor-pointer border-r border-border/20">1st Half</div>
<div className="h-full flex items-center justify-center hover:bg-white/5 transition-colors cursor-pointer border-r border-border/20">2nd Half</div>
<div className="h-full flex items-center justify-center bg-brand-primary text-black border-r border-border/10">Combo</div>
<div className="h-full flex items-center justify-center hover:bg-white/5 transition-colors cursor-pointer border-r border-border/20">Chance Mix</div>
<div className="h-full flex items-center justify-center hover:bg-white/5 transition-colors cursor-pointer">Home</div>
</div>
</>
)
const renderColumnHeaders = () => (
<div className="bg-[#222] border-b border-white/5 h-8 flex items-center text-[9px] font-black text-white/40 uppercase">
<div className="bg-brand-surface border-b border-white/5 h-8 flex items-center text-[9px] font-black text-white/40 uppercase">
<div className="w-[180px] px-3 flex items-center gap-1.5 border-r border-border/10 h-full">Main</div>
<div className="w-[180px] flex items-center justify-center border-r border-border/10 h-full">Over/Under</div>
<div className="flex-1 grid grid-cols-10 text-center h-full items-center tracking-tighter">
@ -142,13 +147,13 @@ export function EventsList({ filter = "All", sport = "all", search = "" }: {
)
const renderEventItem = (event: Event) => (
<div key={event.id} className="h-[34px] group flex items-center border-b border-white/5 bg-[#121212] hover:bg-white/5 transition-colors">
<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">
{/* Stats & Icons */}
<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>
{/* ID */}
<div className="w-[45px] text-[10px] font-black text-[#ff9800] italic tabular-nums text-center border-r border-white/5 h-full flex items-center justify-center">
<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>
{/* Time */}
@ -156,10 +161,13 @@ export function EventsList({ filter = "All", sport = "all", search = "" }: {
<span>{event.time}</span>
<span className="text-[7px] text-white/30 uppercase mt-0.5">PM</span>
</div>
{/* Event Name */}
<div className="flex-1 px-4 text-[10.5px] font-black text-white truncate max-w-[200px]">
{/* 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"
>
{event.homeTeam} - {event.awayTeam}
</div>
</Link>
{/* Odds Grid */}
<div className="flex-1 grid grid-cols-10 h-full">
{event.markets.slice(0, 10).map((market) => {
@ -170,15 +178,15 @@ export function EventsList({ filter = "All", sport = "all", search = "" }: {
key={market.id}
onClick={() => addBet({
id: betId,
event: `${event.homeTeam} vs ${event.awayTeam}`,
league: event.league,
event: `${event.homeTeam} - ${event.awayTeam}`,
league: `${event.sport} - ${event.country} - ${event.league}`,
market: "1X2",
selection: `${event.homeTeam} (${market.label})`,
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-[#ff9800] text-black" : "text-[#ff9800] hover:bg-white/5"
isSelected ? "bg-brand-primary text-black" : "text-brand-primary hover:bg-white/5"
)}
>
{market.odds.toFixed(2)}
@ -186,10 +194,14 @@ export function EventsList({ filter = "All", sport = "all", search = "" }: {
)
})}
</div>
{/* More Button */}
<button className="w-10 flex items-center justify-center h-full hover:bg-white/5 transition-colors border-l border-white/5 text-white/40">
{/* 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" />
</button>
</Link>
</div>
)
@ -202,9 +214,9 @@ export function EventsList({ filter = "All", sport = "all", search = "" }: {
}, {} as Record<string, Event[]>)
return (
<div className="flex flex-col bg-[#121212] rounded overflow-hidden shadow-2xl">
<div className="flex flex-col bg-brand-bg rounded overflow-hidden shadow-2xl">
{/* League Header / Breadcrumbs */}
<div className="bg-[#1a1a1a] px-3 py-2 flex items-center justify-between border-b border-border/20">
<div className="bg-brand-surface px-3 py-2 flex items-center justify-between border-b border-border/20">
<div className="flex items-center gap-2 text-[11px] font-bold text-white/60">
<Plus className="size-3 cursor-pointer hover:text-white" />
<div className="flex items-center gap-1.5">
@ -220,7 +232,7 @@ export function EventsList({ filter = "All", sport = "all", search = "" }: {
</div>
{/* Large Market Tab Grid */}
<div className="grid grid-cols-5 bg-[#121212] border-b border-border/10">
<div className="grid grid-cols-5 bg-brand-bg border-b border-border/10">
{[
{ label: "Main", active: true }, { label: "Goals" }, { label: "Handicap" }, { label: "Half Time / Full Time" }, { label: "Correct Score" },
{ label: "1st Half" }, { label: "2nd Half" }, { label: "Asian Markets" }, { label: "Corners" }, { label: "Home" }
@ -229,7 +241,7 @@ export function EventsList({ filter = "All", sport = "all", search = "" }: {
key={i}
className={cn(
"h-8 border-r border-b border-border/10 flex items-center justify-center text-[10px] font-black uppercase transition-all",
m.active ? "bg-[#ff9800] text-black" : "text-white/60 hover:bg-[#222]"
m.active ? "bg-brand-primary text-black" : "text-white/60 hover:bg-brand-surface"
)}
>
{m.label}
@ -244,7 +256,7 @@ export function EventsList({ filter = "All", sport = "all", search = "" }: {
<div className="overflow-y-auto max-h-[700px]">
{Object.entries(groupedEvents).map(([date, dateEvents]) => (
<div key={date} className="flex flex-col">
<div className="bg-[#1a1a1a] 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}
</div>
{dateEvents.map(event => renderEventItem(event))}
@ -263,12 +275,12 @@ export function EventsList({ filter = "All", sport = "all", search = "" }: {
}, {} as Record<string, Event[]>)
return (
<div className="flex flex-col bg-[#121212] rounded overflow-hidden">
<div className="flex flex-col bg-brand-bg rounded overflow-hidden">
<div className="flex flex-col">
{Object.entries(homeEventsByLeague).map(([leagueName, leagueEvents]) => (
<div key={leagueName} className="flex flex-col border-b border-white/5 last:border-none">
{/* League Box Header */}
<div className="bg-[#2a2a2a] px-3 py-1.5 text-[10px] font-bold text-[#ff9800] uppercase border-y border-border/20 flex items-center justify-between">
<div className="bg-brand-surface-light px-3 py-1.5 text-[10px] font-bold text-brand-primary uppercase border-y border-border/20 flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-[14px]">
{popularLeagues.find(l => l.name === leagueName)?.icon ||
@ -285,7 +297,7 @@ export function EventsList({ filter = "All", sport = "all", search = "" }: {
</div>
{/* Column Headers for each league box */}
<div className="bg-[#222] px-3 py-1 flex items-center text-[8px] font-black text-white/40 uppercase border-b border-border/20">
<div className="bg-brand-surface px-3 py-1 flex items-center text-[8px] font-black text-white/40 uppercase border-b border-border/20">
<div className="w-[180px] flex gap-4">
<span className="w-5 text-center">Stats</span>
<span className="w-6">ID</span>

View File

@ -19,7 +19,7 @@ export function HeroBanner() {
}, [])
return (
<div className="w-full h-[240px] md:h-[300px] relative overflow-hidden rounded-none group shadow-lg bg-[#111]">
<div className="w-full h-[240px] md:h-[300px] relative overflow-hidden rounded-none group shadow-lg bg-brand-bg">
{images.map((src, index) => (
<img
key={src}
@ -52,7 +52,7 @@ export function HeroBanner() {
key={index}
onClick={() => setCurrentIndex(index)}
className={`w-1.5 h-1.5 rounded-full transition-all ${
index === currentIndex ? "bg-[#ff9800] scale-125" : "bg-white/40"
index === currentIndex ? "bg-brand-primary scale-125" : "bg-white/40"
}`}
/>
))}

View File

@ -1,3 +1,6 @@
"use client"
import { useState } from "react"
import { InPlayHeader } from "@/components/betting/in-play-header"
import { QuickFilterBar } from "@/components/betting/quick-filter-bar"
import { SearchEvent } from "@/components/betting/search-event"
@ -5,11 +8,14 @@ import { BetServices } from "@/components/betting/bet-services"
import { EventsList } from "@/components/betting/events-list"
export function InPlayPage() {
const [activeFilter, setActiveFilter] = useState("All")
const [searchQuery, setSearchQuery] = useState("")
return (
<div className="flex flex-col gap-3">
<InPlayHeader />
<QuickFilterBar />
<SearchEvent />
<QuickFilterBar active={activeFilter} onChange={setActiveFilter} />
<SearchEvent value={searchQuery} onChange={setSearchQuery} />
<BetServices />
<EventsList />
</div>

View File

@ -15,16 +15,16 @@ function LiveEventRow({ event, isNoOdds }: { event: Event, isNoOdds?: boolean })
const period = "H2"
return (
<div className="bg-[#121212] border-b border-white/5 hover:bg-white/5 transition-colors h-[50px] flex items-center group">
<div className="bg-brand-bg border-b border-white/5 hover:bg-white/5 transition-colors h-[50px] flex items-center group">
{/* Match Info Column (Time & Score) */}
<div className="flex items-center gap-3 px-3 w-[360px] shrink-0 h-full border-r border-white/5">
<div className="flex flex-col text-[10px] font-black leading-tight italic w-[55px] shrink-0 tabular-nums">
<span className="text-[#ff9800]">{time}</span>
<span className="text-brand-primary">{time}</span>
<span className="text-white/40">{period}</span>
</div>
<div className="flex items-center min-w-0 flex-1 gap-2">
<span className="text-[11.5px] font-black text-white truncate italic uppercase">
{event.homeTeam} <span className="text-[#ff9800] mx-1 tabular-nums">{score}</span> {event.awayTeam}
{event.homeTeam} <span className="text-brand-primary mx-1 tabular-nums">{score}</span> {event.awayTeam}
</span>
</div>
<div className="flex items-center gap-2 shrink-0 px-1 opacity-20 group-hover:opacity-100 transition-opacity">
@ -36,7 +36,7 @@ function LiveEventRow({ event, isNoOdds }: { event: Event, isNoOdds?: boolean })
{/* Odds Grid or Placeholder */}
<div className="flex-1 h-full flex items-center">
{isNoOdds ? (
<div className="flex-1 h-full bg-[#161616] flex items-center justify-center text-[10.5px] font-black text-white/20 uppercase italic tracking-tight">
<div className="flex-1 h-full bg-brand-surface-light flex items-center justify-center text-[10.5px] font-black text-white/20 uppercase italic tracking-tight">
Sorry, no odds for this match
</div>
) : (
@ -48,16 +48,16 @@ function LiveEventRow({ event, isNoOdds }: { event: Event, isNoOdds?: boolean })
key={m.id}
onClick={() => addBet({
id: `${event.id}-${m.id}`,
event: `${event.homeTeam} vs ${event.awayTeam}`,
league: event.league,
event: `${event.homeTeam} - ${event.awayTeam}`,
league: `${event.sport} - ${event.country} - ${event.league}`,
market: "1X2",
selection: `${m.label}`,
selection: m.label,
odds: m.odds,
})}
className="bg-[#1a1a1a] hover:bg-[#2a2a2a] flex items-center justify-between px-4 h-full border-r border-white/5 transition-colors group/btn"
className="bg-brand-bg hover:bg-white/5 flex items-center justify-between px-4 h-full border-r border-white/5 transition-colors group/btn"
>
<span className="text-[10px] font-black text-white/40 group-hover/btn:text-white uppercase">{labels[idx]}</span>
<span className="text-[11px] font-black text-[#ff9800] tabular-nums">{m.odds.toFixed(2)}</span>
<span className="text-[11px] font-black text-brand-primary tabular-nums">{m.odds.toFixed(2)}</span>
</button>
)
})}
@ -66,7 +66,7 @@ function LiveEventRow({ event, isNoOdds }: { event: Event, isNoOdds?: boolean })
</div>
{/* Right Indicator */}
<div className="w-[4px] bg-[#ff9800] h-full" />
<div className="w-[4px] bg-brand-primary h-full" />
</div>
)
}
@ -124,9 +124,9 @@ export function LiveEventsList() {
]
return (
<div className="flex flex-col min-h-screen bg-[#111]">
<div className="flex flex-col min-h-screen bg-brand-bg">
{/* Sport Navigation Carousel */}
<div className="bg-[#1a1a1a] 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">
{/* Favourites & Prematch */}
<button className="flex flex-col items-center justify-center px-4 h-full border-r border-white/5 min-w-[70px]">
@ -144,19 +144,19 @@ export function LiveEventsList() {
key={sport.id}
className={cn(
"flex flex-col items-center justify-center px-3 h-full border-r border-white/5 min-w-[75px] relative transition-colors",
sport.active ? "bg-black/20" : "hover:bg-white/5"
sport.active ? "bg-white/5" : "hover:bg-white/5"
)}
>
<span className="absolute top-1 right-2 text-[8.5px] font-black text-white/40">{sport.count}</span>
<span className="text-[16px]">{sport.icon}</span>
<span className={cn(
"text-[9px] font-bold uppercase mt-1 tracking-tighter whitespace-nowrap",
sport.active ? "text-[#ff9800]" : "text-white/40"
sport.active ? "text-brand-primary" : "text-white/40"
)}>
{sport.label}
</span>
{sport.active && (
<div className="absolute bottom-0 left-0 right-0 h-[2px] bg-[#ff9800]" />
<div className="absolute bottom-0 left-0 right-0 h-[2px] bg-brand-primary" />
)}
</button>
))}
@ -164,7 +164,7 @@ export function LiveEventsList() {
</div>
{/* Category Header (Soccer) */}
<div className="bg-[#009688] px-3 py-1.5 flex items-center gap-2 border-l-[4px] border-[#ff9800]">
<div className="bg-brand-primary px-3 py-1.5 flex items-center gap-2 border-l-[4px] border-brand-primary">
<span className="text-[16px]"></span>
<h2 className="text-[14px] font-black text-white uppercase tracking-tight">Soccer</h2>
</div>
@ -174,7 +174,7 @@ export function LiveEventsList() {
{liveMatches.map((group, gIdx) => (
<div key={gIdx} className="flex flex-col">
{/* League Header */}
<div className="bg-[#1a1a1a] 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">
{group.league}

View File

@ -0,0 +1,269 @@
"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"
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) => {
const betId = `${event.id}-${section.id}-${outcome.label.replace(/\s/g, "-").toLowerCase()}`
const isSelected = bets.some((b) => b.id === betId)
return (
<div key={outcome.label} 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) => {
const betId = `${event.id}-${section.id}-${outcome.label.replace(/\s/g, "-").toLowerCase()}`
const isSelected = bets.some((b) => b.id === betId)
return (
<div
key={outcome.label}
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 }: { event: Event }) {
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,
})
const [activeCategory, setActiveCategory] = useState("Cards/Bookings")
const detailMarkets = getEventDetailMarkets(event.id)
const cardsBookings = getCardsBookingsMarkets(event.id)
const toggleSection = (id: string) => {
setExpandedSections((prev) => ({ ...prev, [id]: !prev[id] }))
}
const breadcrumbLeague =
event.league === "Premier League"
? "England - Premier League"
: `${event.country} - ${event.league}`
const isCardsBookings = activeCategory === "Cards/Bookings"
const leftSections = isCardsBookings ? cardsBookings.left : detailMarkets
const rightSections = isCardsBookings ? cardsBookings.right : []
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">&lt;</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 */}
<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">
<div className="w-16 h-20 rounded-md bg-brand-bg border border-white/10 flex items-center justify-center">
<span className="text-[10px] font-black text-white/60 uppercase">
{event.homeTeam.slice(0, 2)}
</span>
</div>
<span className="text-[13px] font-bold text-white">{event.homeTeam}</span>
</div>
<span className="text-[12px] font-black text-white/50 uppercase">VS</span>
<div className="flex flex-col items-center gap-2">
<div className="w-16 h-20 rounded-md bg-brand-bg border border-white/10 flex items-center justify-center">
<span className="text-[10px] font-black text-white/60 uppercase">
{event.awayTeam.slice(0, 2)}
</span>
</div>
<span className="text-[13px] font-bold text-white">{event.awayTeam}</span>
</div>
</div>
</div>
{/* Category tabs: horizontal scroll, selected = darker grey */}
<div className="flex overflow-x-auto gap-1 p-2 bg-brand-bg border-b border-border/20 scrollbar-hide">
{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 shrink-0",
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 */}
<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 (Cards/Bookings only) */}
{rightSections.length > 0 && (
<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>
)
}

View File

@ -8,16 +8,16 @@ export function ReloadTicket() {
return (
<div className="bg-transparent border-t border-border/20 pt-4 pb-2">
<div className="px-1 text-center space-y-3">
<h3 className="text-[12px] font-bold uppercase text-[#ff9800]">Reload Ticket</h3>
<h3 className="text-[12px] font-bold uppercase text-brand-primary">Reload Ticket</h3>
<p className="text-[11px] text-white">Insert the code to load</p>
<div className="flex gap-1">
<input
type="text"
value={code}
onChange={(e) => setCode(e.target.value)}
className="flex-1 bg-[#121212] border border-border/40 px-2 py-2 text-[11px] text-white outline-none focus:border-primary"
className="flex-1 bg-brand-bg border border-border/40 px-2 py-2 text-[11px] text-white outline-none focus:border-brand-primary"
/>
<button className="bg-[#ff9800] text-black px-2 py-1.5 flex items-center justify-center min-w-[32px] hover:bg-[#ffa726] transition-colors">
<button className="bg-brand-primary text-black px-2 py-1.5 flex items-center justify-center min-w-[32px] hover:bg-brand-primary-hover transition-colors">
<svg viewBox="0 0 24 24" className="size-5 fill-current"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
</button>
</div>

View File

@ -16,7 +16,7 @@ const sports = [
export function SportsNav() {
return (
<Tabs defaultValue="football" className="w-full">
<TabsList variant="hs-nav" className="h-auto">
<TabsList variant="hs-nav" className="min-h-14! h-auto! py-2">
{sports.map((sport) => (
<TabsTrigger
key={sport.id}

View File

@ -1,5 +1,8 @@
"use client"
import { ChevronRight } from "lucide-react"
import { Button } from "@/components/ui/button"
import { useBetslipStore } from "@/lib/store/betslip-store"
import { cn } from "@/lib/utils"
const topMatches = [
{
@ -45,58 +48,90 @@ const topMatches = [
]
export function TopMatches() {
const { bets, addBet } = useBetslipStore()
return (
<div className="flex gap-3 overflow-x-auto pb-2 scrollbar-hide -mx-1 px-1">
{topMatches.map((match) => (
<div
key={match.id}
className="min-w-[280px] bg-[#222] border border-border/20 rounded-sm overflow-hidden flex flex-col relative group"
>
{/* Top Label Ribbon */}
<div className="absolute top-0 right-0 w-12 h-12 overflow-hidden z-10 pointer-events-none">
<div className="absolute top-[6px] right-[-14px] bg-[#ff9800] text-black text-[8px] font-black py-0.5 px-6 rotate-45 shadow-sm uppercase tracking-tighter">
TOP
{topMatches.map((match) => {
const eventName = `${match.homeTeam} - ${match.awayTeam}`
const leagueForBet = `Football - ${match.league}`
const outcomes = [
{ key: "1", label: "1", odds: match.odds.home },
{ key: "x", label: "X", odds: match.odds.draw },
{ key: "2", label: "2", odds: match.odds.away },
] as const
return (
<div
key={match.id}
className="min-w-[280px] bg-brand-surface border border-border/20 rounded-sm overflow-hidden flex flex-col relative group"
>
{/* Top Label Ribbon */}
<div className="absolute top-0 right-0 w-12 h-12 overflow-hidden z-10 pointer-events-none">
<div className="absolute top-[6px] right-[-14px] bg-brand-primary text-black text-[8px] font-black py-0.5 px-6 rotate-45 shadow-sm uppercase tracking-tighter">
TOP
</div>
</div>
<div className="bg-brand-bg px-3 py-1.5 flex items-center justify-between text-[10px] text-muted-foreground border-b border-border/10">
<div className="flex items-center gap-1">
<span className="font-bold text-brand-primary">{match.league}</span>
<span className="font-black text-white ml-1 italic">{match.time}</span>
</div>
<ChevronRight className="size-3 text-white/20 mr-4" />
</div>
<div className="p-3 flex flex-col gap-3">
<div className="flex items-center justify-between gap-1">
<div className="flex items-center gap-2 flex-1 min-w-0">
<div className="size-4 shrink-0 bg-white/5 rounded-sm flex items-center justify-center text-[9px] border border-white/10"></div>
<span className="text-[11px] font-black text-white truncate uppercase italic">{match.homeTeam}</span>
</div>
<span className="text-brand-primary text-[9.5px] font-black italic shrink-0 px-2 opacity-60">VS</span>
<div className="flex items-center gap-2 flex-1 min-w-0 justify-end">
<span className="text-[11px] font-black text-white truncate uppercase italic text-right">{match.awayTeam}</span>
<div className="size-4 shrink-0 bg-white/5 rounded-sm flex items-center justify-center text-[9px] border border-white/10"></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">
{outcomes.map(({ key, label, odds }) => {
const betId = `${match.id}-${key}`
const isSelected = bets.some((b) => b.id === betId)
return (
<button
key={key}
type="button"
onClick={() =>
addBet({
id: betId,
event: eventName,
league: leagueForBet,
market: "1X2",
selection: label,
odds,
})
}
className={cn(
"flex items-center justify-between px-2.5 py-1.5 transition-colors group/btn border-x border-white/5 first:border-x-0",
isSelected
? "bg-brand-primary text-black"
: "bg-brand-bg hover:bg-brand-surface-light"
)}
>
<span className={cn("text-[10px] font-black", isSelected ? "text-black" : "text-white/40 group-hover/btn:text-white")}>
{label}
</span>
<span className={cn("text-[11px] font-black tabular-nums", isSelected ? "text-black" : "text-brand-primary")}>
{odds.toFixed(2)}
</span>
</button>
)
})}
</div>
</div>
<div className="bg-[#1a1a1a] px-3 py-1.5 flex items-center justify-between text-[10px] text-muted-foreground border-b border-border/10">
<div className="flex items-center gap-1">
<span className="font-bold text-[#ff9800]">{match.league}</span>
<span className="font-black text-white ml-1 italic">{match.time}</span>
</div>
<ChevronRight className="size-3 text-white/20 mr-4" />
</div>
<div className="p-3 flex flex-col gap-3">
<div className="flex items-center justify-between gap-1">
<div className="flex items-center gap-2 flex-1 min-w-0">
<div className="size-4 shrink-0 bg-white/5 rounded-sm flex items-center justify-center text-[9px] border border-white/10"></div>
<span className="text-[11px] font-black text-white truncate uppercase italic">{match.homeTeam}</span>
</div>
<span className="text-[#ff9800] text-[9.5px] font-black italic shrink-0 px-2 opacity-60">VS</span>
<div className="flex items-center gap-2 flex-1 min-w-0 justify-end">
<span className="text-[11px] font-black text-white truncate uppercase italic text-right">{match.awayTeam}</span>
<div className="size-4 shrink-0 bg-white/5 rounded-sm flex items-center justify-center text-[9px] border border-white/10"></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">
<button className="bg-[#1a1a1a] hover:bg-[#333] flex items-center justify-between px-2.5 py-1.5 transition-colors group/btn">
<span className="text-[10px] font-black text-white/40 group-hover/btn:text-white">1</span>
<span className="text-[11px] font-black text-[#ff9800] tabular-nums">{match.odds.home.toFixed(2)}</span>
</button>
<button className="bg-[#1a1a1a] hover:bg-[#333] flex items-center justify-between px-2.5 py-1.5 transition-colors border-x border-white/5 group/btn">
<span className="text-[10px] font-black text-white/40 group-hover/btn:text-white">X</span>
<span className="text-[11px] font-black text-[#ff9800] tabular-nums">{match.odds.draw.toFixed(2)}</span>
</button>
<button className="bg-[#1a1a1a] hover:bg-[#333] flex items-center justify-between px-2.5 py-1.5 transition-colors group/btn">
<span className="text-[10px] font-black text-white/40 group-hover/btn:text-white">2</span>
<span className="text-[11px] font-black text-[#ff9800] tabular-nums">{match.odds.away.toFixed(2)}</span>
</button>
</div>
</div>
))}
)
})}
</div>
)
}

View File

@ -0,0 +1,40 @@
"use client"
import Image from "next/image"
import { cn } from "@/lib/utils"
interface GameCardProps {
id: string
title: string
image: string
provider?: string
}
export function GameCard({ id, title, image, provider }: GameCardProps) {
return (
<div className="group cursor-pointer shrink-0 w-[160px] md:w-[180px]">
<div className="relative aspect-[4/3] rounded-md overflow-hidden bg-brand-surface border border-white/5 transition-transform duration-300 group-hover:scale-[1.02] group-hover:border-brand-primary/50">
<Image
src={image}
alt={title}
fill
className="object-cover"
unoptimized
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity flex flex-col justify-end p-2">
<button className="bg-brand-primary text-black text-[10px] font-black py-1 px-3 rounded uppercase self-center hover:bg-brand-primary-hover transition-colors">
Play Now
</button>
</div>
</div>
<div className="mt-2 text-center">
<h3 className="text-[11px] font-bold text-white/90 truncate group-hover:text-brand-primary transition-colors">
{title}
</h3>
{provider && (
<p className="text-[9px] text-white/40 uppercase font-medium mt-0.5">{provider}</p>
)}
</div>
</div>
)
}

View File

@ -0,0 +1,95 @@
"use client"
import { useRef } from "react"
import { ChevronLeft, ChevronRight } from "lucide-react"
import { GameCard } from "./game-card"
import { cn } from "@/lib/utils"
interface GameRowProps {
title: string
games: any[]
showSeeMore?: boolean
rows?: 1 | 2 | 3
}
export function GameRow({ title, games, showSeeMore = true, rows = 1 }: GameRowProps) {
const scrollRef = useRef<HTMLDivElement>(null)
const scroll = (direction: "left" | "right") => {
if (scrollRef.current) {
const scrollAmount = direction === "left" ? -600 : 600
scrollRef.current.scrollBy({ left: scrollAmount, behavior: "smooth" })
}
}
// Chunk games into rows if rows > 1
const renderGames = () => {
if (rows === 1) {
return games.map((game, idx) => (
<div key={idx} style={{ scrollSnapAlign: "start" }}>
<GameCard {...game} />
</div>
))
}
// For multi-row, we can use a grid with horizontal scroll on the container
// or wrap items. The requirement "3*6 horizontally scrollable" suggests
// a grid that moves as a block or multi-row horizontal layout.
return (
<div className={cn(
"grid grid-flow-col gap-4 pb-2",
rows === 2 ? "grid-rows-2" : rows === 3 ? "grid-rows-3" : ""
)}>
{games.map((game, idx) => (
<div key={idx} style={{ scrollSnapAlign: "start" }}>
<GameCard {...game} />
</div>
))}
</div>
)
}
return (
<div className="flex flex-col gap-3 py-4">
<div className="flex items-center justify-between px-4">
<h2 className="text-sm font-bold text-white/90 border-l-4 border-brand-primary pl-3 uppercase tracking-tight">
{title}
</h2>
<div className="flex items-center gap-3">
{showSeeMore && (
<button className="text-[10px] text-white/40 hover:text-white uppercase font-bold transition-colors">
See More
</button>
)}
<div className="flex items-center gap-1">
<button
onClick={() => scroll("left")}
className="size-6 flex items-center justify-center bg-white/5 hover:bg-white/10 text-white/60 hover:text-white transition-colors"
>
<ChevronLeft className="size-4" />
</button>
<button
onClick={() => scroll("right")}
className="size-6 flex items-center justify-center bg-white/5 hover:bg-white/10 text-white/60 hover:text-white transition-colors"
>
<ChevronRight className="size-4" />
</button>
</div>
</div>
</div>
<div
ref={scrollRef}
className="flex overflow-x-auto scrollbar-none px-4 pb-2 snap-x snap-mandatory"
>
{rows === 1 ? (
<div className="flex gap-4">
{renderGames()}
</div>
) : (
renderGames()
)}
</div>
</div>
)
}

View File

@ -0,0 +1,86 @@
"use client"
import { cn } from "@/lib/utils"
import { Search, Heart, Clock, Star } from "lucide-react"
export type GameCategory = string
interface GamingSidebarProps {
title: string
subtitle?: string
activeCategory: GameCategory
onCategoryChange: (category: GameCategory) => void
categories: {
id: string
name: string
icon: any
subtitle?: string
hasChevron?: boolean
}[]
}
export function GamingSidebar({ title, subtitle, activeCategory, onCategoryChange, categories }: GamingSidebarProps) {
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">
{/* Sidebar Header */}
<div className="bg-brand-surface px-3 py-2 text-[12px] font-bold text-white uppercase flex items-center justify-between border-b border-white/10 h-12">
<div className="flex items-center gap-2">
<Star className="size-4 text-brand-primary" />
<div className="flex flex-col">
<span className="leading-tight">{title}</span>
{subtitle && <span className="text-[10px] text-white/40 font-normal lowercase">{subtitle}</span>}
</div>
</div>
<svg viewBox="0 0 24 24" className="size-3.5 fill-white/40"><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>
</div>
<div className="flex flex-col gap-[1px]">
{categories.map((cat) => {
const Icon = cat.icon
const isActive = activeCategory === cat.id
return (
<button
key={cat.id}
onClick={() => onCategoryChange(cat.id)}
className={cn(
"w-full flex flex-col px-3 py-2 text-left transition-colors border-b border-border/5",
isActive
? "bg-brand-surface border-l-4 border-l-brand-primary"
: "bg-brand-surface-light hover:bg-brand-surface"
)}
>
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-3">
{typeof Icon === 'string' ? (
(Icon.startsWith('http') || Icon.startsWith('/')) ? (
<img src={Icon} alt={cat.name} className="w-5 h-5 object-contain opacity-80" />
) : (
<span className="size-4 flex items-center justify-center text-[14px]">{Icon}</span>
)
) : (
<Icon className={cn("size-4", isActive ? "text-brand-primary" : "text-white/60")} />
)}
<div className="flex flex-col">
<span className={cn(
"text-[12px] font-bold tracking-tight",
isActive ? "text-white" : "text-white/80"
)}>
{cat.name}
</span>
{cat.subtitle && (
<span className="text-[9px] text-white/40 font-medium">{cat.subtitle}</span>
)}
</div>
</div>
{cat.hasChevron && (
<svg viewBox="0 0 24 24" className="size-3.5 fill-white/40"><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>
)}
</div>
</button>
)
})}
</div>
</aside>
)
}

View File

@ -0,0 +1,88 @@
"use client"
import { cn } from "@/lib/utils"
import { Search, Heart, Clock, Star, Zap, LayoutGrid, Gamepad2, Award, Coins, Flame, Trophy } from "lucide-react"
export type GameCategory =
| "all"
| "search"
| "favourite"
| "recently-played"
| "most-popular"
| "harif-special"
| "for-you"
| "slots"
| "crash-games"
| "higher-lower"
| "smartsoft"
| "keno-spin"
| "pragmatic-play"
| "evoplay-bonus"
interface VirtualSidebarProps {
activeCategory: GameCategory
onCategoryChange: (category: GameCategory) => void
}
const categories = [
{ id: "all", name: "Virtual", icon: Star, subtitle: "Check out our games!", hasChevron: true },
{ id: "search", name: "Search", icon: Search },
{ id: "favourite", name: "Favourite", icon: Heart },
{ id: "recently-played", name: "Recently Played", icon: Clock },
{ id: "most-popular", name: "Most Popular", icon: Star },
{ id: "harif-special", name: "Harif Special", icon: Zap },
{ id: "for-you", name: "For You", icon: Star },
{ id: "slots", name: "Slots", icon: Star },
{ id: "crash-games", name: "Crash Games", icon: Star },
{ id: "higher-lower", name: "Higher Lower", icon: Star },
{ id: "smartsoft", name: "Smartsoft", icon: Star },
{ id: "keno-spin", name: "Keno & Spin", icon: Star },
{ id: "pragmatic-play", name: "Pragmatic Play", icon: Star },
{ id: "evoplay-bonus", name: "EvoPlay BONUS", icon: Star },
]
export function VirtualSidebar({ activeCategory, onCategoryChange }: VirtualSidebarProps) {
return (
<aside className="hidden h-full w-[240px] shrink-0 bg-brand-surface-light lg:block overflow-y-auto border-r border-border/40 scrollbar-hide">
<div className="flex flex-col gap-[1px]">
{categories.map((cat) => {
const Icon = cat.icon
const isActive = activeCategory === cat.id
return (
<button
key={cat.id}
onClick={() => onCategoryChange(cat.id as GameCategory)}
className={cn(
"w-full flex flex-col px-3 py-2 text-left transition-colors border-b border-border/5",
isActive
? "bg-brand-surface border-l-4 border-l-brand-primary"
: "bg-brand-surface-light hover:bg-brand-surface"
)}
>
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-3">
<Icon className={cn("size-4", isActive ? "text-brand-primary" : "text-white/60")} />
<div className="flex flex-col">
<span className={cn(
"text-[12px] font-bold tracking-tight",
isActive ? "text-white" : "text-white/80"
)}>
{cat.name}
</span>
{cat.subtitle && (
<span className="text-[9px] text-white/40 font-medium">{cat.subtitle}</span>
)}
</div>
</div>
{cat.hasChevron && (
<svg viewBox="0 0 24 24" className="size-3.5 fill-white/40"><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>
)}
</div>
</button>
)
})}
</div>
</aside>
)
}

View File

@ -66,39 +66,41 @@ export function AuthModal({ open, defaultMode, onClose }: AuthModalProps) {
onClick={onClose}
>
<div
className="relative w-[420px] bg-brand-surface-light shadow-2xl animate-fade-in"
className="relative w-[280px] bg-brand-surface-light shadow-2xl animate-fade-in"
onClick={(e) => e.stopPropagation()}
>
{/* Close button */}
<button
onClick={onClose}
className="absolute top-2 right-3 text-white/60 hover:text-white text-xl leading-none transition-colors z-10"
className="absolute top-1 right-2 text-white/60 hover:text-white text-lg leading-none transition-colors z-10"
aria-label="Close"
>
×
</button>
{/* Logo */}
<div className="flex items-center justify-center py-5 border-b border-white/10 bg-brand-surface">
<Logo />
<div className="flex items-center justify-center py-4 border-b border-white/10 bg-brand-surface">
<div className="scale-75 origin-center">
<Logo />
</div>
</div>
{/* Title */}
<div className="text-center py-4">
<h2 className="text-sm font-black text-white uppercase tracking-widest">
<div className="text-center py-3">
<h2 className="text-[12px] font-black text-white uppercase tracking-widest">
{mode === "login" ? "LOGIN" : "REGISTER"}
</h2>
</div>
{/* Form */}
<div className="px-8 pb-6 space-y-4">
<div className="px-4 pb-6 space-y-4">
{/* Phone Number */}
<div>
<label className="block text-[11px] font-semibold text-white/80 mb-1">
Phone Number
</label>
<div className="flex">
<span className="flex items-center justify-center bg-white text-[#333] text-[12px] font-bold px-3 border border-gray-300 whitespace-nowrap select-none">
<span className="flex items-center justify-center bg-white text-[#333] text-[11px] font-bold px-1.5 border border-gray-300 whitespace-nowrap select-none">
ET +251
</span>
<input

View File

@ -8,12 +8,18 @@ import { RightPanel } from "@/components/layout/right-panel"
import { SiteFooter } from "@/components/layout/site-footer"
import { AuthModal } from "@/components/layout/auth-modal"
import { MobileBottomNav } from "@/components/layout/mobile-bottom-nav"
import { cn } from "@/lib/utils"
type AuthMode = "login" | "register"
export default function LayoutClientWrapper({ children }: { children: React.ReactNode }) {
const pathname = usePathname()
const isLivePage = pathname === "/live"
const isVirtualPage = pathname === "/virtual"
const isSpecialGamesPage = pathname === "/special-games"
const hideLeftSidebar = isLivePage || isVirtualPage || isSpecialGamesPage
const hideRightSidebar = isVirtualPage || isSpecialGamesPage // Show RightPanel on Live page
const [authOpen, setAuthOpen] = useState(false)
const [authMode, setAuthMode] = useState<AuthMode>("login")
@ -24,23 +30,25 @@ export default function LayoutClientWrapper({ children }: { children: React.Reac
}, [])
return (
<div className="flex min-h-screen flex-col pb-14 md:pb-0">
<div className="flex min-h-screen flex-col pb-14 md:pb-0 overflow-x-hidden">
<SiteHeader
onLoginClick={() => openAuth("login")}
onRegisterClick={() => openAuth("register")}
/>
<div className="flex w-full flex-1 gap-0">
{/* Sidebar: hidden on mobile */}
{!isLivePage && (
<div className="hidden md:block">
<div className="flex w-full flex-1 gap-0 min-w-0">
{/* Sidebar: hidden on mobile and hidden on specific pages */}
{!hideLeftSidebar && (
<div className="hidden md:block shrink-0">
<SportsSidebar />
</div>
)}
<main className="flex-1 min-w-0 px-2 py-3">{children}</main>
{/* Right panel: hidden on mobile */}
<div className="hidden md:block">
<RightPanel />
</div>
<main className={cn("flex-1 min-w-0 overflow-x-hidden", !hideLeftSidebar && "px-2 py-3")}>{children}</main>
{/* Right panel: completely off-flow on mobile/tablet; only from lg to match RightPanel */}
{!hideRightSidebar && (
<div className="hidden lg:block shrink-0 overflow-hidden">
<RightPanel />
</div>
)}
</div>
<SiteFooter />

View File

@ -3,6 +3,7 @@
import { Betslip } from "@/components/betting/betslip"
import { ReloadTicket } from "@/components/betting/reload-ticket"
import { CheckYourBet } from "@/components/betting/check-your-bet"
import { useBetslipStore } from "@/lib/store/betslip-store"
import { useState } from "react"
import { Button } from "@/components/ui/button"
@ -10,14 +11,15 @@ import { Input } from "@/components/ui/input"
export function RightPanel() {
const [activeTab, setActiveTab] = useState<"betslip" | "myBets">("betslip")
const bets = useBetslipStore((s) => s.bets)
return (
<aside className="hidden lg:flex w-[280px] shrink-0 flex-col gap-0 border-l border-border/30 bg-[#222]">
<aside className="hidden lg:flex w-[280px] shrink-0 flex-col gap-0 border-l border-border/30 bg-brand-surface">
{/* Search Header */}
<div className="p-3 bg-[#1a1a1a] flex items-center justify-between border-b border-border/20">
<div className="p-3 bg-brand-bg flex items-center justify-between border-b border-border/20">
<div className="flex items-center gap-2 text-[12px] font-bold text-white uppercase">
Fast Bet <span className="text-[#ff9800]">QBET</span>
<svg viewBox="0 0 24 24" className="size-4 fill-[#ff9800] ml-1"><path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
Fast Bet <span className="text-brand-primary">QBET</span>
<svg viewBox="0 0 24 24" className="size-4 fill-brand-primary ml-1"><path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
</div>
</div>
@ -26,32 +28,28 @@ export function RightPanel() {
<Input
type="text"
placeholder="Event Code"
className="bg-[#333] border border-border/40 px-2 py-1.5 h-auto text-[11px] outline-none text-white focus:border-[#ff9800] rounded-none shadow-none"
className="bg-brand-surface-light border border-border/40 px-2 py-1.5 h-auto text-[11px] outline-none text-white focus:border-brand-primary rounded-none shadow-none"
/>
<div className="bg-[#333] border border-border/40 px-2 py-1.5 text-[11px] text-muted-foreground flex items-center justify-center">...</div>
<div className="bg-brand-surface-light border border-border/40 px-2 py-1.5 text-[11px] text-muted-foreground flex items-center justify-center">...</div>
</div>
{/* Tab switcher */}
<div className="flex items-center justify-between text-[11px] font-bold py-2.5 px-1 border-b border-border/10">
<div className="flex items-center gap-2">
<span className="text-[#ff9800] uppercase cursor-pointer">Betslip</span>
<span className="bg-[#ff9800] text-black px-1.5 rounded-full text-[10px] font-bold">0</span>
<span className="text-brand-primary uppercase cursor-pointer">Betslip</span>
<span className="bg-brand-primary text-black px-1.5 rounded-full text-[10px] font-bold min-w-[18px] text-center">{bets.length}</span>
<Button
variant="ghost"
className="text-white uppercase flex items-center gap-1 ml-2 opacity-80 hover:opacity-100 p-0 h-auto font-bold text-[11px]"
>
<svg viewBox="0 0 24 24" className="size-3 fill-[#ff9800]"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>
<svg viewBox="0 0 24 24" className="size-3 fill-brand-primary"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>
Settings
</Button>
</div>
<span className="text-white uppercase cursor-pointer opacity-80 hover:opacity-100">Decimal</span>
</div>
<div className="py-10 px-4 text-center">
<p className="text-[11px] text-muted-foreground font-medium leading-relaxed">
No bet has been selected. To select a bet, please click on the respective odds
</p>
</div>
<Betslip />
<ReloadTicket />
<CheckYourBet />

View File

@ -4,7 +4,7 @@ import Link from "next/link"
export function SiteFooter() {
return (
<footer className="bg-[#2a2a2a] text-white pt-12">
<footer className="bg-brand-surface text-white pt-12">
<div className="container mx-auto px-6 grid grid-cols-1 md:grid-cols-4 gap-12 text-center md:text-left">
{/* ABOUT */}
<div>
@ -51,12 +51,12 @@ export function SiteFooter() {
</div>
{/* Logo Section */}
<div className="flex flex-col items-center justify-center py-16 border-t border-white/5 mt-12 bg-[#222]">
<div className="flex items-center bg-[#1a1a1a] px-5 py-2">
<div className="bg-[#852222] px-3 py-1 -skew-x-12">
<div className="flex flex-col items-center justify-center py-16 border-t border-white/5 mt-12 bg-brand-surface-light">
<div className="flex items-center bg-brand-surface px-5 py-2">
<div className="bg-brand-accent px-3 py-1 -skew-x-12">
<span className="text-3xl font-black text-white italic tracking-tighter skew-x-12 inline-block">HARIF</span>
</div>
<span className="text-3xl font-black text-[#ff9800] italic tracking-tighter ml-1">SPORT</span>
<span className="text-3xl font-black text-brand-primary italic tracking-tighter ml-1">SPORT</span>
</div>
{/* Footer Links */}
@ -70,7 +70,7 @@ export function SiteFooter() {
</div>
{/* Cookie Text */}
<div className="bg-[#1a1a1a] py-10 px-6 text-center">
<div className="bg-brand-bg py-10 px-6 text-center">
<div className="container mx-auto max-w-5xl">
<p className="text-[10px] text-white/40 leading-relaxed font-medium uppercase tracking-tight">
By accessing, or continuing to use or browse this site, you consent to our use of certain cookies to improve your experience with us. We only use cookies that will enhance your experience and will not interfere with your privacy. Please look at our Cookie Policy for further informations on our use of the cookie and how you can disable it or manage it if you so choose.

View File

@ -6,13 +6,13 @@ import { useState, useEffect } from "react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import Image from "next/image"
const allNavItems = [
{ href: "/", label: "SPORTS" },
{ href: "/today", label: "TODAY" },
{ href: "/", label: "ALL SPORTS" },
{ href: "/live", label: "LIVE" },
{ href: "/virtual", label: "VIRTUAL" },
{ href: "/special-games", label: "SPECIAL G." },
{ href: "/special-games", label: "SPECIAL Games" },
{ href: "/multi-hot-5", label: "MULTI HOT 5" },
{ href: "/poker", label: "POKER", isNew: true },
{ href: "/race", label: "RACE", isNew: true },
@ -68,7 +68,14 @@ export function SiteHeader({ onLoginClick, onRegisterClick }: SiteHeaderProps) {
<div className="hidden md:flex bg-brand-surface px-3 py-1 items-center justify-between text-[11px] text-white">
<div className="flex items-center gap-4">
<button className="flex items-center gap-1.5 hover:text-primary transition-colors">
<img src="https://flagcdn.com/w20/gb.png" width="16" alt="English" className="rounded-sm" />
<Image
src="https://flagcdn.com/w20/gb.png"
alt="English"
width={20}
height={15}
className="rounded-sm"
unoptimized // Since it's an external image from a CDN
/>
<span className="font-bold flex items-center gap-1">en <span className="text-[8px]"></span></span>
</button>
<div className="flex items-center gap-2 font-bold">
@ -78,14 +85,16 @@ export function SiteHeader({ onLoginClick, onRegisterClick }: SiteHeaderProps) {
</div>
<div className="flex items-center gap-6">
<div className="flex items-center gap-3">
<span className="text-white font-bold tracking-tight">+251 (0) Number</span>
<span className="text-white font-bold tracking-tight">+251 (0)
<Input type="tel" placeholder="Phone Number" className="bg-brand-surface-light border-none px-2 py-0.5 w-32 text-[11px] h-7 rounded-none shadow-none text-white placeholder:text-gray-500" />
</span>
<Input type="password" placeholder="Password" className="bg-brand-surface-light border-none px-2 py-0.5 w-32 text-[11px] h-7 rounded-none shadow-none text-white placeholder:text-gray-500" />
</div>
<div className="flex items-center gap-1">
<Button onClick={onLoginClick} className="bg-[#e67e22] text-white hover:bg-[#d35400] px-5 h-7 text-[11px] font-bold rounded-none uppercase">
<Button onClick={onLoginClick} className="bg-brand-primary text-white hover:bg-brand-primary-hover px-5 h-7 text-[11px] font-bold rounded-none uppercase">
Login
</Button>
<Button onClick={onRegisterClick} className="bg-[#d35400] text-white hover:bg-[#c0392b] px-5 h-7 text-[11px] font-bold rounded-none uppercase">
<Button onClick={onRegisterClick} className="bg-brand-primary text-white hover:bg-brand-primary-hover px-5 h-7 text-[11px] font-bold rounded-none uppercase">
Sign Up
</Button>
</div>
@ -119,13 +128,13 @@ export function SiteHeader({ onLoginClick, onRegisterClick }: SiteHeaderProps) {
<div className="flex items-center gap-1 shrink-0">
<button
onClick={onLoginClick}
className="bg-[#e67e22] text-white px-3 h-8 text-[11px] font-bold uppercase"
className="bg-brand-primary text-white px-3 h-8 text-[11px] font-bold uppercase"
>
LOGIN
</button>
<button
onClick={onRegisterClick}
className="bg-[#d35400] text-white px-3 h-8 text-[11px] font-bold uppercase"
className="bg-brand-primary text-white px-3 h-8 text-[11px] font-bold uppercase"
>
SIGN UP
</button>
@ -133,30 +142,38 @@ export function SiteHeader({ onLoginClick, onRegisterClick }: SiteHeaderProps) {
</div>
{/* ===== DESKTOP: Main header bar ===== */}
<div className="hidden md:flex items-center px-0 bg-[#333] h-[60px]">
<div className="hidden md:flex items-center px-0 bg-brand-bg h-[60px]">
<Link href="/" className="flex items-center shrink-0">
<div className="flex items-center bg-brand-surface h-[60px] px-4">
<div className="flex items-center bg-brand-surface h-[60px] px-4 w-[280px] shrink-0 border-r border-white/5">
<div className="bg-brand-accent px-3 py-1 -skew-x-12 flex items-center h-[34px]">
<span className="text-2xl font-black text-white italic tracking-tighter skew-x-12 inline-block leading-none">HARIF</span>
</div>
<span className="text-2xl font-black text-brand-primary italic tracking-tighter ml-1 leading-none">SPORT</span>
</div>
</Link>
<div className="flex items-center flex-1 justify-end px-4 h-full gap-0">
<Link href="/" className="flex items-center justify-center bg-brand-primary h-[60px] w-[50px] shrink-0 hover:bg-brand-primary-hover transition-colors">
<svg viewBox="0 0 24 24" className="size-6 fill-black"><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/></svg>
<div className="flex items-center flex-1 justify-end px-4 h-full gap-0 bg-brand-surface">
<Link
href="/"
className={cn(
"flex items-center justify-center h-[60px] w-[50px] shrink-0 transition-colors",
pathname === "/"
? "bg-brand-primary hover:bg-brand-primary-hover"
: "text-white hover:bg-white/10"
)}
>
<svg viewBox="0 0 24 24" className={cn("size-6", pathname === "/" ? "fill-black" : "fill-white")}><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/></svg>
</Link>
<nav className="flex items-center text-[10.5px] font-bold h-full">
{allNavItems.slice(0, 5).map((item) => {
const isActive = pathname === item.href
const isActive = pathname === item.href && item.href !== "/"
return (
<Link key={item.href} href={item.href}
className={cn(
"px-4 flex items-center h-full transition-colors uppercase",
isActive ? (item.label === "LIVE" ? "bg-brand-primary text-black" : "text-primary bg-black/10") : "text-white hover:text-primary"
"px-4 flex items-center h-full transition-colors uppercase relative",
isActive ? "text-white bg-brand-primary" : "text-white hover:bg-white/5"
)}
>
{item.label}
<span>{item.label}</span>
</Link>
)
})}
@ -168,11 +185,11 @@ export function SiteHeader({ onLoginClick, onRegisterClick }: SiteHeaderProps) {
<Link key={item.href} href={item.href}
className={cn(
"px-4 flex flex-col items-center justify-center h-full transition-colors relative uppercase",
isActive ? "text-primary bg-black/10" : "text-white hover:text-primary"
isActive ? "text-white bg-brand-primary" : "text-white hover:bg-white/5"
)}
>
{item.isNew && (
<span className="absolute top-3 text-[7px] text-primary font-black tracking-tighter leading-none">NEW</span>
<span className="absolute top-3 text-[7px] text-white/70 font-black tracking-tighter leading-none">NEW</span>
)}
<span className={cn(item.isNew && "mt-1.5")}>{item.label}</span>
</Link>
@ -184,8 +201,14 @@ export function SiteHeader({ onLoginClick, onRegisterClick }: SiteHeaderProps) {
{/* ===== MOBILE: Horizontally scrollable nav tabs ===== */}
<div className="flex md:hidden overflow-x-auto scrollbar-none bg-brand-surface-light border-t border-white/5">
<Link href="/" className="flex-none px-4 h-9 flex items-center bg-brand-primary">
<svg viewBox="0 0 24 24" className="size-5 fill-black"><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/></svg>
<Link
href="/"
className={cn(
"flex-none px-4 h-9 flex items-center transition-colors",
pathname === "/" ? "bg-brand-primary" : "text-white"
)}
>
<svg viewBox="0 0 24 24" className={cn("size-5", pathname === "/" ? "fill-black" : "fill-white")}><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/></svg>
</Link>
{allNavItems.map((item) => {
const isActive = pathname === item.href
@ -210,7 +233,7 @@ export function SiteHeader({ onLoginClick, onRegisterClick }: SiteHeaderProps) {
</div>
{/* ===== MOBILE: Sport Category Icons Row ===== */}
<div className="flex md:hidden overflow-x-auto scrollbar-none bg-[#333] border-t border-white/5 py-2 px-2 gap-4">
<div className="flex md:hidden overflow-x-auto scrollbar-none bg-brand-surface-light border-t border-white/5 py-2 px-2 gap-4">
{[
{ label: "Check Bet", icon: (active: boolean) => <svg viewBox="0 0 24 24" className="size-5 fill-white/80"><path d="M20 4H4c-1.11 0-2 .89-2 2v12c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V6c0-1.11-.89-2-2-2zm0 14H4v-6h16v6zm0-10H4V6h16v2z"/></svg>, count: 99 },
{ label: "Live", icon: (active: boolean) => <div className="size-2 bg-brand-live rounded-full animate-pulse" />, count: 1247 },
@ -229,28 +252,29 @@ export function SiteHeader({ onLoginClick, onRegisterClick }: SiteHeaderProps) {
</div>
{/* ===== DESKTOP: Secondary sub-header ===== */}
<div className="hidden md:flex bg-brand-surface-light border-t border-white/5 h-8 px-3 items-center gap-6 text-[11px]">
{[
{ label: "Sport Home", href: "/" },
{ label: "General View", href: "/live", forceActive: isLivePage },
{ label: "Event View", href: "/live/event" },
].map((tab) => {
const isActive = tab.forceActive || pathname === tab.href
return (
<Link key={tab.label} href={tab.href}
className={cn(
"relative h-full flex items-center text-[10px] font-bold uppercase transition-colors tracking-tight px-1",
isActive ? "text-brand-primary" : "text-white/60 hover:text-white"
)}
>
{tab.label}
{isActive && tab.label !== "Sport Home" && (
<div className="absolute bottom-0 left-0 right-0 h-[2px] bg-brand-primary" />
)}
</Link>
)
})}
</div>
{pathname !== "/virtual" && pathname !== "/special-games" && (
<div className="hidden md:flex bg-brand-surface-light border-t border-white/5 h-8 px-3 items-center gap-6 text-[11px]">
{[
{ label: "Sport Home", href: "/" },
{ label: "Live View", href: "/live", forceActive: isLivePage },
].map((tab) => {
const isActive = tab.forceActive || pathname === tab.href
return (
<Link key={tab.label} href={tab.href}
className={cn(
"relative h-full flex items-center text-[10px] font-bold uppercase transition-colors tracking-tight px-1",
isActive ? "text-brand-primary" : "text-white/60 hover:text-white"
)}
>
{tab.label}
{isActive && tab.label !== "Sport Home" && (
<div className="absolute bottom-0 left-0 right-0 h-[2px] bg-brand-primary" />
)}
</Link>
)
})}
</div>
)}
</header>
{/* ===== MOBILE Drawer ===== */}

View File

@ -5,6 +5,18 @@ import Link from "next/link"
import { popularLeagues } from "@/lib/mock-data"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { ChevronsLeft } from "lucide-react"
/** Soccer ball icon - outline style for white/green theme */
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 sportCategories = [
{ id: "football", name: "Football", icon: "⚽", count: 1412 },
@ -25,11 +37,13 @@ export function SportsSidebar() {
const [activeSport, setActiveSport] = useState("football")
return (
<aside className="hidden h-full w-[240px] shrink-0 bg-brand-surface-light lg:block overflow-y-auto border-r border-border/40 scrollbar-hide">
<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-brand-primary uppercase tracking-tighter flex items-center justify-between border-b border-border/30">
Sports Menu
<svg viewBox="0 0 24 24" className="size-3.5 fill-current opacity-60"><path d="M15.41 16.59L10.83 12l4.58-4.59L14 6l-6 6 6 6 1.41-1.41z"/></svg>
<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 */}
@ -45,27 +59,28 @@ export function SportsSidebar() {
href={`/?league=${league.id}`}
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.5 min-w-0">
<div className="size-4 shrink-0 overflow-hidden rounded-sm flex items-center justify-center bg-white/10 group-hover:bg-white/20 transition-colors">
<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">
{league.logo ? (
<img src={league.logo} alt="" className="size-full object-contain" />
) : (
<span className="text-[10px] leading-none invert"></span>
<span className="text-[11px]"></span>
)}
</div>
<span className="text-[10.5px] font-bold leading-tight truncate max-w-[150px]">{league.name}</span>
</div>
<div className="size-3.5 rounded-full border border-white/20 flex items-center justify-center group-hover:border-brand-primary transition-colors shrink-0">
<div className="size-1.5 bg-white/40 rounded-full group-hover:bg-brand-primary" />
<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 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]">
<span className="size-2 rounded-full bg-[#ff9800] mr-2 live-dot shadow-[0_0_8px_#ff9800]"></span>
<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 */}
@ -75,7 +90,7 @@ export function SportsSidebar() {
{["All", "Today", "3h", "6h", "9h", "12h"].map((t) => (
<button key={t} className={cn(
"text-[10px] py-1.5 font-bold transition-colors",
t === "All" ? "bg-[#333] text-white" : "bg-[#2a2a2a] text-white/50 hover:text-white"
t === "All" ? "bg-brand-surface-light text-white" : "bg-brand-surface text-white/50 hover:text-white"
)}>{t}</button>
))}
</div>
@ -112,7 +127,7 @@ export function SportsSidebar() {
</div>
<div className="flex items-center gap-2">
<span className="text-[10px] font-bold text-white/40">{sport.count}</span>
<span className="text-[11px] text-white/20"></span>
<SoccerBallIcon className="size-3.5 text-white/30 shrink-0" />
</div>
</button>
))}
@ -123,17 +138,49 @@ export function SportsSidebar() {
Bet Services
</div>
<div className="grid grid-cols-3 gap-0 border-b border-border/10">
<Button variant="ghost" className="flex flex-col items-center justify-center py-4 h-auto rounded-none border-r border-border/10 hover:bg-brand-surface group">
<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" className="flex flex-col items-center justify-center py-4 h-auto rounded-none border-r border-border/10 hover:bg-brand-surface group">
<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" className="flex flex-col items-center justify-center py-4 h-auto rounded-none hover:bg-brand-surface group">
<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>
<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>

View File

@ -19,11 +19,11 @@ const buttonVariants = cva(
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
"hs-primary": "bg-[#ff9800] text-black font-bold hover:bg-[#ffa726] rounded-none",
"hs-secondary": "bg-[#333] text-white hover:bg-[#444] border border-border/20 rounded-none",
"hs-maroon": "bg-[#852222] text-white font-bold hover:bg-[#962d2d] rounded-none",
"hs-inplay": "bg-[#004242] text-[#ff9800] font-bold hover:bg-[#005252] border-y border-border/10 rounded-none",
"hs-nav": "bg-[#1a1a1a] text-[#ff9800] font-bold hover:bg-[#222] border-r border-border/10 rounded-none",
"hs-primary": "bg-brand-primary text-black font-bold hover:bg-brand-primary-hover rounded-none",
"hs-secondary": "bg-brand-surface-light text-white hover:bg-[#404040] border border-border/20 rounded-none",
"hs-maroon": "bg-brand-accent text-white font-bold hover:opacity-90 rounded-none",
"hs-inplay": "bg-[#004242] text-brand-primary font-bold hover:bg-[#005252] border-y border-border/10 rounded-none",
"hs-nav": "bg-brand-bg text-brand-primary font-bold hover:bg-brand-surface border-r border-border/10 rounded-none",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",

View File

@ -1,10 +1,10 @@
"use client"
"use client";
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Tabs as TabsPrimitive } from "radix-ui"
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { Tabs as TabsPrimitive } from "radix-ui";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
function Tabs({
className,
@ -18,29 +18,30 @@ function Tabs({
orientation={orientation}
className={cn(
"group/tabs flex gap-2 data-[orientation=horizontal]:flex-col",
className
className,
)}
{...props}
/>
)
);
}
const tabsListVariants = cva(
"rounded-lg p-[3px] group-data-[orientation=horizontal]/tabs:h-9 data-[variant=line]:rounded-none data-[variant=hs-home]:rounded-none data-[variant=hs-home]:bg-[#1a1a1a] data-[variant=hs-nav]:rounded-none data-[variant=hs-nav]:bg-[#1a1a1a] group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col",
"rounded-lg p-[3px] group-data-[orientation=horizontal]/tabs:h-9 data-[variant=line]:rounded-none data-[variant=hs-home]:rounded-none data-[variant=hs-home]:bg-brand-bg data-[variant=hs-nav]:rounded-none data-[variant=hs-nav]:bg-brand-bg group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col",
{
variants: {
variant: {
default: "bg-muted",
line: "gap-1 bg-transparent",
"hs-home": "w-full gap-0 border border-border/20",
"hs-nav": "w-full gap-0 border border-border/20 overflow-x-auto justify-start",
"hs-nav":
"w-full gap-0 border border-border/20 flex-wrap justify-start overflow-hidden min-h-14 h-auto py-1",
},
},
defaultVariants: {
variant: "default",
},
}
)
},
);
function TabsList({
className,
@ -55,7 +56,7 @@ function TabsList({
className={cn(tabsListVariants({ variant }), className)}
{...props}
/>
)
);
}
function TabsTrigger({
@ -69,14 +70,14 @@ function TabsTrigger({
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent",
"group-data-[variant=hs-home]/tabs-list:rounded-none group-data-[variant=hs-home]/tabs-list:h-12 group-data-[variant=hs-home]/tabs-list:text-[13px] group-data-[variant=hs-home]/tabs-list:font-extrabold group-data-[variant=hs-home]/tabs-list:uppercase group-data-[variant=hs-home]/tabs-list:data-[state=active]:bg-transparent group-data-[variant=hs-home]/tabs-list:data-[state=active]:text-white",
"group-data-[variant=hs-nav]/tabs-list:rounded-none group-data-[variant=hs-nav]/tabs-list:flex-col group-data-[variant=hs-nav]/tabs-list:min-w-[70px] group-data-[variant=hs-nav]/tabs-list:py-2 group-data-[variant=hs-nav]/tabs-list:gap-1 group-data-[variant=hs-nav]/tabs-list:border-r group-data-[variant=hs-nav]/tabs-list:border-border/10 group-data-[variant=hs-nav]/tabs-list:data-[state=active]:bg-[#222]",
"group-data-[variant=hs-nav]/tabs-list:rounded-none group-data-[variant=hs-nav]/tabs-list:flex-col group-data-[variant=hs-nav]/tabs-list:min-w-[70px] group-data-[variant=hs-nav]/tabs-list:py-2 group-data-[variant=hs-nav]/tabs-list:gap-1 group-data-[variant=hs-nav]/tabs-list:border-r group-data-[variant=hs-nav]/tabs-list:border-border/10 group-data-[variant=hs-nav]/tabs-list:data-[state=active]:bg-brand-surface",
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 data-[state=active]:text-foreground",
"after:bg-[#ff9800] after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-0 group-data-[orientation=horizontal]/tabs:after:h-[3px] group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100 group-data-[variant=hs-home]/tabs-list:data-[state=active]:after:opacity-100",
className
"after:bg-brand-primary after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-0 group-data-[orientation=horizontal]/tabs:after:h-[3px] group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100 group-data-[variant=hs-home]/tabs-list:data-[state=active]:after:opacity-100",
className,
)}
{...props}
/>
)
);
}
function TabsContent({
@ -89,7 +90,7 @@ function TabsContent({
className={cn("flex-1 outline-none", className)}
{...props}
/>
)
);
}
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }
export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };

47
lib/api.ts Normal file
View File

@ -0,0 +1,47 @@
import axios from 'axios';
// Create a configured Axios instance
const api = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8080/api/v1',
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor for adding the auth token
api.interceptors.request.use(
(config) => {
// Only access localStorage if we are running in the browser
if (typeof window !== 'undefined') {
// const token = localStorage.getItem('token');
const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJmb3J0dW5lLWJldCIsImF1ZCI6WyJhcGkuZm9ydHVuZWJldHMubmV0Il0sImV4cCI6MTc3MjI3NzQxNSwibmJmIjoxNzcyMjc2ODE1LCJpYXQiOjE3NzIyNzY4MTUsIlVzZXJJZCI6NCwiUm9sZSI6InN1cGVyX2FkbWluIiwiQ29tcGFueUlEIjp7IlZhbHVlIjowLCJWYWxpZCI6ZmFsc2V9fQ.QJJ1KAFkWWCMmxxBi8rQc9C5aChN2XmTys-RCufV_Zo";
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response interceptor for handling common errors (like 401 Unauthorized)
api.interceptors.response.use(
(response) => {
return response;
},
(error) => {
if (error.response?.status === 401) {
// Handle unauthorized errors, e.g., redirecting to login or clearing the token
if (typeof window !== 'undefined') {
localStorage.removeItem('token');
// Uncomment the line below to redirect automatically
// window.location.href = '/login';
}
}
return Promise.reject(error);
}
);
export default api;

View File

@ -220,6 +220,31 @@ export const mockEvents: Event[] = [
],
totalMarkets: 110,
},
{
id: "01890",
sport: "Soccer",
sportIcon: "⚽",
league: "Premier League",
country: "England",
homeTeam: "Burnley",
awayTeam: "Brentford",
time: "05:00",
date: "Feb 28, 26",
isLive: false,
markets: [
{ id: "1", label: "1", odds: 4.58 },
{ id: "x", label: "x", odds: 4.00 },
{ id: "2", label: "2", odds: 1.78 },
{ id: "o25", label: "Over (2.5)", odds: 1.80 },
{ id: "u25", label: "Under (2.5)", odds: 2.05 },
{ id: "1x", label: "1X", odds: 2.10 },
{ id: "12", label: "12", odds: 1.25 },
{ id: "x2", label: "X2", odds: 1.20 },
{ id: "yes", label: "Yes", odds: 1.71 },
{ id: "no", label: "No", odds: 2.15 },
],
totalMarkets: 112,
},
{
id: "01124",
sport: "Soccer",
@ -247,6 +272,125 @@ export const mockEvents: Event[] = [
},
];
export function getEventById(id: string): Event | undefined {
return mockEvents.find((e) => e.id === id)
}
export type DetailMarketOutcome = { label: string; odds: number }
export type DetailMarketSection = {
id: string
title: string
outcomes: DetailMarketOutcome[]
}
export function getEventDetailMarkets(eventId: string): DetailMarketSection[] {
const event = getEventById(eventId)
if (!event) return []
return [
{
id: "1x2",
title: "1x2",
outcomes: [
{ label: "Home", odds: 4.58 },
{ label: "Draw", odds: 4.0 },
{ label: "Away", odds: 1.78 },
],
},
{
id: "total",
title: "Total",
outcomes: [
{ label: "Over (2.5)", odds: 1.8 },
{ label: "Under (2.5)", odds: 2.05 },
],
},
{
id: "draw-no-bet",
title: "Draw No Bet",
outcomes: [
{ label: "Home", odds: 3.25 },
{ label: "Away", odds: 1.35 },
],
},
{
id: "asian-handicap",
title: "Asian Handicap 0.5",
outcomes: [
{ label: "Home (+0.5)", odds: 2.1 },
{ label: "Away (-0.5)", odds: 1.76 },
],
},
{
id: "double-chance",
title: "Double Chance",
outcomes: [
{ label: "Home or Draw", odds: 1.96 },
{ label: "Home or Away", odds: 1.25 },
{ label: "Draw or Away", odds: 1.2 },
],
},
{
id: "btts",
title: "Both teams to score",
outcomes: [
{ label: "Yes", odds: 1.71 },
{ label: "No", odds: 2.15 },
],
},
{
id: "1st-half-1x2",
title: "1st Half - 1X2",
outcomes: [
{ label: "Home", odds: 4.8 },
{ label: "Draw", odds: 2.4 },
{ label: "Away", odds: 2.3 },
],
},
]
}
/** Cards/Bookings markets: left column and right column of sections */
export function getCardsBookingsMarkets(eventId: string): {
left: DetailMarketSection[]
right: DetailMarketSection[]
} {
getEventById(eventId) // ensure event exists
return {
left: [
{ id: "bookings-1x2", title: "Bookings 1X2", outcomes: [{ label: "Home", odds: 2.2 }, { label: "Draw", odds: 3.7 }, { label: "Away", odds: 2.5 }] },
{ id: "bookings-ou", title: "Bookings - Over/Under", outcomes: [] },
{ id: "sending-off", title: "Sending Off", outcomes: [{ label: "Yes", odds: 5.4 }, { label: "No", odds: 1.12 }] },
{ id: "1st-half-bookings-ou", title: "1st Half Bookings - Over/Under", outcomes: [] },
{ id: "1st-booking", title: "1st Booking", outcomes: [{ label: "Home", odds: 1.79 }, { label: "None", odds: 28.11 }, { label: "Away", odds: 1.9 }] },
{ id: "home-exact-bookings", title: "Home Exact Bookings", outcomes: [] },
{ id: "1st-half-bookings-1x2", title: "1st Half - Bookings 1X2", outcomes: [{ label: "Home", odds: 2.95 }, { label: "Draw", odds: 2.15 }, { label: "Away", odds: 3.2 }] },
{ id: "1st-half-away-exact", title: "1st Half - Away Exact Bookings", outcomes: [] },
{ id: "away-sending-off", title: "Away Sending Off", outcomes: [] },
{ id: "1st-half-home-sending", title: "1st Half - Home Sending Off", outcomes: [] },
],
right: [
{
id: "booking-points-ou",
title: "Booking Points-Over/Under",
outcomes: [
{ label: "Over 35.5", odds: 1.85 }, { label: "Under 35.5", odds: 1.85 },
{ label: "Over 45.5", odds: 2.85 }, { label: "Under 45.5", odds: 1.38 },
{ label: "Over 55.5", odds: 4.6 }, { label: "Under 55.5", odds: 1.16 },
{ label: "Over 15.5", odds: 1.1 }, { label: "Under 15.5", odds: 5.8 },
{ label: "Over 25.5", odds: 1.35 }, { label: "Under 25.5", odds: 2.95 },
],
},
{ id: "exact-bookings", title: "Exact Bookings", outcomes: [] },
{ id: "1st-half-total-points", title: "1st Half - Total Booking Points", outcomes: [] },
{ id: "1st-half-exact-bookings", title: "1st Half - Exact Bookings", outcomes: [] },
{ id: "1st-half-1st-booking", title: "1st Half - 1st Booking", outcomes: [{ label: "Home", odds: 2.5 }, { label: "None", odds: 2.95 }, { label: "Away", odds: 2.65 }] },
{ id: "away-exact-bookings", title: "Away Exact Bookings", outcomes: [] },
{ id: "1st-half-home-exact", title: "1st Half - Home Exact Bookings", outcomes: [] },
{ id: "home-sending-off", title: "Home Sending Off", outcomes: [] },
],
}
}
export const popularLeagues = [
{ id: "ucl", name: "UEFA Champions League", logo: "https://upload.wikimedia.org/wikipedia/en/thumb/b/bf/UEFA_Champions_League_logo_2.svg/300px-UEFA_Champions_League_logo_2.svg.png" },
{ id: "uel", name: "UEFA Europa League", logo: "https://upload.wikimedia.org/wikipedia/en/thumb/0/0c/UEFA_Europa_League_logo_%282021%29.svg/300px-UEFA_Europa_League_logo_%282021%29.svg.png" },

107
package-lock.json generated
View File

@ -8,6 +8,7 @@
"name": "fortune-play",
"version": "0.1.0",
"dependencies": {
"axios": "^1.13.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.574.0",
@ -4827,6 +4828,11 @@
"node": ">= 0.4"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@ -4853,6 +4859,16 @@
"node": ">=4"
}
},
"node_modules/axios": {
"version": "1.13.6",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axobject-query": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@ -5011,7 +5027,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@ -5256,6 +5271,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
@ -5604,6 +5630,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@ -5670,7 +5704,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
@ -5837,7 +5870,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -5847,7 +5879,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -5885,7 +5916,6 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
@ -5898,7 +5928,6 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
@ -6742,6 +6771,25 @@
"dev": true,
"license": "ISC"
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@ -6758,6 +6806,40 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/form-data/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/form-data/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
@ -6810,7 +6892,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@ -6908,7 +6989,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
@ -6955,7 +7035,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
@ -7060,7 +7139,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -7142,7 +7220,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -7155,7 +7232,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
@ -7171,7 +7247,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@ -8539,7 +8614,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
@ -9555,6 +9629,11 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",

View File

@ -9,6 +9,7 @@
"lint": "eslint"
},
"dependencies": {
"axios": "^1.13.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.574.0",

BIN
public/aviator-bannsser.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

111
tenant.config.js Normal file
View File

@ -0,0 +1,111 @@
module.exports = {
tenant: "fortunebets",
tenant_name: "FortuneBets",
tenant_email: "customer@fortunebets.net",
tenant_phone_number: "",
tenant_location: "Around Lem Hotel , Addis Ababa Ethiopia",
tenant_map_link: "https://maps.app.goo.gl/zvhxt8sh7JbsVpbc8",
tenant_telegram_link: "https://t.me/fortunebetts",
tenant_facebook_link: "https://www.facebook.com/profile.php?id=61565869044955",
tenant_bot_link: "",
app_title: "FortuneBets",
theme_color: "#ffffff",
robots_sitemap: "https://fortunebets.net/sitemap.xml",
root_url: "https://fortunebets.net",
tile_color: "#00a300",
theme: "/tenant/fortunebets/theme.json",
logo: "/tenant/fortunebets/Logo.webp",
favicon: "/tenant/fortunebets/favicon.ico",
favicon96: "/tenant/fortunebets/favicon-96x96.png",
favicon_svg: "/tenant/fortunebets/favicon.svg",
apple_touch_icon: "/tenant/fortunebets/apple-touch-icon.png",
logo_small: "/tenant/fortunebets/logo-small.webp",
logo_white: "/tenant/fortunebets/logo-white.webp",
site_manifest: "/tenant/fortunebets/site.webmanifest",
slider_1: "/tenant/fortunebets/fortunebet-image-1.webp",
slider_2: "/tenant/fortunebets/fortunebet-image-2.webp",
slider_3: "/tenant/fortunebets/fortunebet-image-3.webp",
home_page_canonical: "https://fortunebets.net",
homepage_title:
"FortuneBets - Premier Ethiopia Online Casino and Sports Betting",
homepage_desc:
"Discover Ethiopia's premier betting destination. Premier League, Champions League, MMA, Dog and Horse Racing and Virtual Games Betting!",
homepage_keywords:
"Casino, Sports Betting, Ethiopia, MMA, Keno, Spin to Win, Fire Crash, Aviator, Football, GoldenRace, Premier League, Champions League, Chapa, Telebirr, CBEBirr, Betika, Betking, QwickBirr, Anbessabet, Vamos bet, Habeshabet, Hulusport, ZuraPlay",
homepage_openGraph: {
images: [
"https://fortunebets.net/Logo.webp",
"https://demo.fortunebets.net/Logo.webp",
],
},
jsonD: {
"@context": "https://schema.org",
"@type": "Organization",
name: "Fortunebets",
image: "https://fortunebets.net/Logo.webp",
"@id": "",
url: "https://fortunebets.net",
telephone: "",
address: {
"@type": "PostalAddress",
streetAddress: "Lem Hotel, Megenagna",
addressLocality: "Addis Ababa",
postalCode: "",
addressCountry: "ET",
},
geo: {
"@type": "GeoCoordinates",
latitude: 9.018046093660232,
longitude: 38.79564644773323,
},
sameAs: ["https://www.facebook.com/profile.php?id=61565869044955"],
},
deposit_title: "Deposit - FortuneBets",
deposit_description:
"Deposit cash into your FortuneBets Account so that you can start playing games for cash",
deposit_keyword:
"withdraw, Birr, Chapa, CBEBirr, TeleBirr, Deposit, FortuneBets, Ethiopia, Sports Betting",
deposit_alt_canonical: "https://fortunebets.net/deposit",
games_title: "FortuneBetss - Ethiopia Online Casino and Sports Betting",
games_desc:
"FortuneBetss is Ethiopia's premier betting destination. Win cash prizes by playing Number-Games like Spin To Win, roulette, online slots and Keno, as well as adrenaline-pumping Crash-Games including Fire Crash, Bank Run, and Meteoroid. You can preview or play the game. Join now and win your fortune!",
games_keywords:
"FortuneBetss, Casino, Sports Betting, Ethiopia, Addis Ababa, Hawassa, Fire Crash, Bank Run, Meteoroid, Keno, MMA, Spin to Win , Aviator , Virtual Football,Football, GoldenRace, Champions League, QwickBirr, Anbessabet, Vamos bet, Vamosbet, Habeshabet, Hulusport, ZuraPlay",
games_openGraph: {
images: ["https://fortunebets.net/Logo.webp"],
},
games_alt_canonical: "https://fortunebets.net/games",
login_title: "Login - FortuneBets",
login_desc:
"Login to FortuneBets Account so that you can start playing games for cash",
login_keywords:
"Sign Up, Register, Reset Password, Login, FortuneBets, Ethiopia, Sports Betting",
login_alt_canonical: "https://fortunebets.net/login",
register_title: "Register - FortuneBets",
register_desc:
"Create a new FortuneBets Account so that you can start playing games for cash",
register_keywords:
"Sign Up, Register, Reset Password, Login, FortuneBets, Ethiopia, Sports Betting",
register_alt_canonical: "https://fortunebets.net/register",
reset_title: "Reset Password - FortuneBets",
reset_desc:
"Did you forget your FortuneBets Password? Send a code to your account to create a new password",
reset_keywords:
"Sign Up, Register, Reset Password, Login, FortuneBets, Ethiopia, Sports Betting",
reset_alt_canonical: "https://fortunebets.net/reset-password",
rules_title: "Rules - FortuneBets",
rules_desc:
"Terms and Conditions, Responsible Gambling, FortuneBets is committed to providing a safe and responsible platform where Players can use the Services",
rules_keywords:
"FortuneBets, Ethiopia, Sports Betting, Terms and Conditions, Responsible Gambling, Account Suspension,",
rules_alt_canonical: "https://fortunebets.net/rules",
virtual_sports_title: "Virtual Sports - FortuneBets",
virtual_sports_desc:
"Win Cash Prizes by betting on FortuneBets Virtual Sports! Football tournaments like Premier League, Bundesliga, La Liga, Ligue 1, Series A, Champions League, World Cup. Mixed Martial Arts Betting. Racing sports like Motorcycle, Horse, Dog, Speedway Racing.",
virtual_sports_keywords:
"Virtual Sports Betting, Football Betting, QwickBirr Premier League, Bundesliga, La Liga, Ligue 1, Serie A, Champions League, World Cup, MMA, Motoracing, Horse Racing, Dog Racing, Basketball, Speedway Racing, Betika",
virtual_sports_alt_canonical: "https://fortunebets.net/virtual",
};