feat: Introduce initial sports betting application with core pages, UI components, and state management.

This commit is contained in:
brooktewabe 2026-02-20 12:20:07 +03:00
parent 6fa5ba58d7
commit 86bd615b9e
8 changed files with 413 additions and 92 deletions

View File

@ -42,78 +42,67 @@
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.21 0.006 285.885);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.015 286.067);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.21 0.006 285.885);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.705 0.015 286.067);
--radius: 0rem;
/* HarifSport orange theme colors */
--background: #121212;
--foreground: #ffffff;
--card: #1e1e1e;
--card-foreground: #ffffff;
--popover: #1e1e1e;
--popover-foreground: #ffffff;
--primary: #ff9800;
--primary-foreground: #ffffff;
--secondary: #222222;
--secondary-foreground: #a0a0a0;
--muted: #1a1a1a;
--muted-foreground: #808080;
--accent: #ff9800;
--accent-foreground: #121212;
--destructive: #ef4444;
--border: #2a2a2a;
--input: #222222;
--ring: #ff9800;
/* HarifSport specific */
--hs-orange: #ff9800;
--hs-maroon: #852222;
--hs-dark-grey: #1a1a1a;
--hs-odds-bg: #2a2a2a;
--hs-odds-text: #ff9800;
--hs-header-bg: #121212;
--hs-live-red: #ff3b3b;
}
.dark {
--background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.92 0.004 286.32);
--primary-foreground: oklch(0.21 0.006 285.885);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0);
--background: oklch(0.13 0.008 250);
--foreground: oklch(0.93 0.005 250);
--card: oklch(0.17 0.01 250);
--card-foreground: oklch(0.93 0.005 250);
--popover: oklch(0.17 0.01 250);
--popover-foreground: oklch(0.93 0.005 250);
--primary: oklch(0.55 0.18 145);
--primary-foreground: oklch(0.98 0 0);
--secondary: oklch(0.22 0.01 250);
--secondary-foreground: oklch(0.85 0.005 250);
--muted: oklch(0.2 0.008 250);
--muted-foreground: oklch(0.58 0.01 250);
--accent: oklch(0.55 0.18 145);
--accent-foreground: oklch(0.98 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.552 0.016 285.938);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.552 0.016 285.938);
--border: oklch(0.25 0.01 250);
--input: oklch(0.22 0.01 250);
--ring: oklch(0.55 0.18 145);
--sidebar: oklch(0.15 0.01 250);
--sidebar-foreground: oklch(0.9 0.005 250);
--sidebar-primary: oklch(0.55 0.18 145);
--sidebar-primary-foreground: oklch(0.98 0 0);
--sidebar-accent: oklch(0.22 0.01 250);
--sidebar-accent-foreground: oklch(0.93 0.005 250);
--sidebar-border: oklch(0.25 0.01 250);
--sidebar-ring: oklch(0.55 0.18 145);
}
@layer base {
@ -122,5 +111,40 @@
}
body {
@apply bg-background text-foreground;
font-size: 13px;
}
}
/* HarifSport odds button animation */
@keyframes odds-flash {
0% { background-color: oklch(0.55 0.18 145); }
50% { background-color: oklch(0.7 0.22 80); }
100% { background-color: oklch(0.55 0.18 145); }
}
.odds-selected {
animation: odds-flash 0.4s ease;
}
/* Live pulse */
@keyframes live-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.live-dot {
animation: live-pulse 1.2s ease-in-out infinite;
}
/* Scrollbar */
::-webkit-scrollbar {
width: 4px;
height: 4px;
}
::-webkit-scrollbar-track {
background: var(--background);
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 2px;
}

View File

@ -1,9 +1,7 @@
import type { Metadata } from "next"
import { Geist, Geist_Mono } from "next/font/google"
import "./globals.css"
import { SiteHeader } from "@/components/layout/site-header"
import { SportsSidebar } from "@/components/layout/sports-sidebar"
import { RightPanel } from "@/components/layout/right-panel"
import LayoutClientWrapper from "@/components/layout/layout-client-wrapper"
const geistSans = Geist({
variable: "--font-geist-sans",
@ -16,8 +14,8 @@ const geistMono = Geist_Mono({
})
export const metadata: Metadata = {
title: "Harifsport",
description: "Harifsport-style sportsbook interface built with Next.js",
title: "Harifsport - Sports Betting",
description: "Harifsport sportsbook - Live betting, in-play events, and more",
}
export default function RootLayout({
@ -30,16 +28,8 @@ export default function RootLayout({
<body
className={`${geistSans.variable} ${geistMono.variable} bg-background text-foreground antialiased`}
>
<div className="flex min-h-screen flex-col bg-background">
<SiteHeader />
<div className="mx-auto flex w-full max-w-6xl flex-1 gap-0 px-2 py-3 lg:px-4">
<SportsSidebar />
<main className="flex-1 px-1 lg:px-3">{children}</main>
<RightPanel />
</div>
</div>
<LayoutClientWrapper>{children}</LayoutClientWrapper>
</body>
</html>
)
}

View File

@ -1,6 +1,6 @@
import { InPlayPage } from "@/components/betting/in-play-page"
import { SportHome } from "@/components/betting/sport-home"
export default function Home() {
return <InPlayPage />
return <SportHome />
}

View File

@ -19,6 +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",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",

View File

@ -26,12 +26,14 @@ function Tabs({
}
const tabsListVariants = cva(
"rounded-lg p-[3px] group-data-[orientation=horizontal]/tabs:h-9 data-[variant=line]:rounded-none 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-[#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",
{
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",
},
},
defaultVariants: {
@ -66,8 +68,10 @@ function TabsTrigger({
className={cn(
"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]",
"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-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 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",
"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
)}
{...props}

261
lib/mock-data.ts Normal file
View File

@ -0,0 +1,261 @@
export type Market = {
id: string;
label: string;
odds: number;
};
export type Event = {
id: string;
sport: string;
sportIcon: string;
league: string;
country: string;
homeTeam: string;
awayTeam: string;
time: string;
date: string; // Added date for grouping
isLive: boolean;
liveMinute?: number;
homeScore?: number;
awayScore?: number;
markets: Market[];
totalMarkets: number;
};
export type SportCategory = {
id: string;
name: string;
icon: string;
count: number;
};
export const sportCategories: SportCategory[] = [
{ id: "soccer", name: "Soccer", icon: "⚽", count: 142 },
{ id: "basketball", name: "Basketball", icon: "🏀", count: 38 },
{ id: "tennis", name: "Tennis", icon: "🎾", count: 26 },
{ id: "ice-hockey", name: "Ice Hockey", icon: "🏒", count: 18 },
{ id: "volleyball", name: "Volleyball", icon: "🏐", count: 14 },
{ id: "handball", name: "Handball", icon: "🤾", count: 12 },
{ id: "baseball", name: "Baseball", icon: "⚾", count: 8 },
{ id: "american-football", name: "American Football", icon: "🏈", count: 6 },
{ id: "cricket", name: "Cricket", icon: "🏏", count: 4 },
{ id: "rugby", name: "Rugby", icon: "🏉", count: 3 },
{ id: "boxing", name: "Boxing", icon: "🥊", count: 2 },
{ id: "esports", name: "E-Sports", icon: "🎮", count: 22 },
];
export const mockEvents: Event[] = [
{
id: "01682",
sport: "Soccer",
sportIcon: "⚽",
league: "LaLiga",
country: "Spain",
homeTeam: "Athletic Bilbao",
awayTeam: "Elche CF",
time: "11:00",
date: "Feb 20, 26",
isLive: false,
markets: [
{ id: "1", label: "1", odds: 1.64 },
{ id: "x", label: "x", odds: 3.91 },
{ id: "2", label: "2", odds: 5.45 },
{ id: "o25", label: "Over (2.5)", odds: 1.93 },
{ id: "u25", label: "Under (2.5)", odds: 1.88 },
{ id: "1x", label: "1X", odds: 1.15 },
{ id: "12", label: "12", odds: 1.24 },
{ id: "x2", label: "X2", odds: 2.13 },
{ id: "yes", label: "Yes", odds: 1.96 },
{ id: "no", label: "No", odds: 1.86 },
],
totalMarkets: 112,
},
{
id: "01570",
sport: "Soccer",
sportIcon: "⚽",
league: "LaLiga",
country: "Spain",
homeTeam: "Real Sociedad",
awayTeam: "Real Oviedo",
time: "04:00",
date: "Feb 21, 26",
isLive: false,
markets: [
{ id: "1", label: "1", odds: 1.52 },
{ id: "x", label: "x", odds: 4.41 },
{ id: "2", label: "2", odds: 6.91 },
{ id: "o25", label: "Over (2.5)", odds: 1.98 },
{ id: "u25", label: "Under (2.5)", odds: 1.87 },
{ id: "1x", label: "1X", odds: 1.11 },
{ id: "12", label: "12", odds: 1.21 },
{ id: "x2", label: "X2", odds: 2.43 },
{ id: "yes", label: "Yes", odds: 2.15 },
{ id: "no", label: "No", odds: 1.72 },
],
totalMarkets: 98,
},
{
id: "01541",
sport: "Soccer",
sportIcon: "⚽",
league: "LaLiga",
country: "Spain",
homeTeam: "Betis",
awayTeam: "Rayo Vallecano",
time: "06:15",
date: "Feb 21, 26",
isLive: false,
markets: [
{ id: "1", label: "1", odds: 1.92 },
{ id: "x", label: "x", odds: 3.55 },
{ id: "2", label: "2", odds: 4.10 },
{ id: "o25", label: "Over (2.5)", odds: 1.92 },
{ id: "u25", label: "Under (2.5)", odds: 1.88 },
{ id: "1x", label: "1X", odds: 1.23 },
{ id: "12", label: "12", odds: 1.28 },
{ id: "x2", label: "X2", odds: 1.80 },
{ id: "yes", label: "Yes", odds: 1.81 },
{ id: "no", label: "No", odds: 2.00 },
],
totalMarkets: 104,
},
{
id: "01605",
sport: "Soccer",
sportIcon: "⚽",
league: "LaLiga",
country: "Spain",
homeTeam: "Osasuna",
awayTeam: "Real Madrid",
time: "08:30",
date: "Feb 21, 26",
isLive: false,
markets: [
{ id: "1", label: "1", odds: 4.73 },
{ id: "x", label: "x", odds: 4.05 },
{ id: "2", label: "2", odds: 1.69 },
{ id: "o25", label: "Over (2.5)", odds: 1.67 },
{ id: "u25", label: "Under (2.5)", odds: 2.20 },
{ id: "1x", label: "1X", odds: 2.03 },
{ id: "12", label: "12", odds: 1.23 },
{ id: "x2", label: "X2", odds: 1.18 },
{ id: "yes", label: "Yes", odds: 1.70 },
{ id: "no", label: "No", odds: 2.15 },
],
totalMarkets: 128,
},
{
id: "01604",
sport: "Soccer",
sportIcon: "⚽",
league: "LaLiga",
country: "Spain",
homeTeam: "Atletico Madrid",
awayTeam: "Espanyol",
time: "11:00",
date: "Feb 21, 26",
isLive: false,
markets: [
{ id: "1", label: "1", odds: 1.51 },
{ id: "x", label: "x", odds: 4.57 },
{ id: "2", label: "2", odds: 6.81 },
{ id: "o25", label: "Over (2.5)", odds: 1.88 },
{ id: "u25", label: "Under (2.5)", odds: 1.96 },
{ id: "1x", label: "1X", odds: 1.11 },
{ id: "12", label: "12", odds: 1.20 },
{ id: "x2", label: "X2", odds: 2.43 },
{ id: "yes", label: "Yes", odds: 2.05 },
{ id: "no", label: "No", odds: 1.78 },
],
totalMarkets: 156,
},
{
id: "01672",
sport: "Soccer",
sportIcon: "⚽",
league: "Ligue 1",
country: "France",
homeTeam: "Brest",
awayTeam: "Marseille",
time: "10:45",
date: "Feb 20, 26",
isLive: false,
markets: [
{ id: "1", label: "1", odds: 3.91 },
{ id: "x", label: "x", odds: 3.80 },
{ id: "2", label: "2", odds: 1.97 },
{ id: "o25", label: "Over (2.5)", odds: 1.75 },
{ id: "u25", label: "Under (2.5)", odds: 2.15 },
{ id: "1x", label: "1X", odds: 1.80 },
{ id: "12", label: "12", odds: 1.27 },
{ id: "x2", label: "X2", odds: 1.26 },
{ id: "yes", label: "Yes", odds: 1.67 },
{ id: "no", label: "No", odds: 2.20 },
],
totalMarkets: 142,
},
{
id: "00241",
sport: "Soccer",
sportIcon: "⚽",
league: "LaLiga 2",
country: "Spain",
homeTeam: "AD Ceuta",
awayTeam: "Granada",
time: "10:30",
date: "Feb 20, 26",
isLive: false,
markets: [
{ id: "1", label: "1", odds: 2.55 },
{ id: "x", label: "x", odds: 3.20 },
{ id: "2", label: "2", odds: 2.75 },
{ id: "o25", label: "Over (2.5)", odds: 2.15 },
{ id: "u25", label: "Under (2.5)", odds: 1.66 },
{ id: "1x", label: "1X", odds: 1.40 },
{ id: "12", label: "12", odds: 1.32 },
{ id: "x2", label: "X2", odds: 1.45 },
{ id: "yes", label: "Yes", odds: 1.85 },
{ id: "no", label: "No", odds: 1.85 },
],
totalMarkets: 110,
},
{
id: "01124",
sport: "Soccer",
sportIcon: "⚽",
league: "Bundesliga",
country: "Germany",
homeTeam: "Mainz",
awayTeam: "Hamburger SV",
time: "10:30",
date: "Feb 20, 26",
isLive: false,
markets: [
{ id: "1", label: "1", odds: 2.11 },
{ id: "x", label: "x", odds: 3.40 },
{ id: "2", label: "2", odds: 3.58 },
{ id: "o25", label: "Over (2.5)", odds: 1.85 },
{ id: "u25", label: "Under (2.5)", odds: 1.96 },
{ id: "1x", label: "1X", odds: 1.28 },
{ id: "12", label: "12", odds: 1.30 },
{ id: "x2", label: "X2", odds: 1.66 },
{ id: "yes", label: "Yes", odds: 1.71 },
{ id: "no", label: "No", odds: 2.15 },
],
totalMarkets: 125,
},
];
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" },
{ id: "epl", name: "Premier League", country: "England", icon: "🏴󠁧󠁢󠁥󠁮󠁧󠁿", logo: "https://upload.wikimedia.org/wikipedia/en/thumb/f/f2/Premier_League_Logo.svg/300px-Premier_League_Logo.svg.png" },
{ id: "laliga", name: "La Liga", country: "Spain", icon: "🇪<>", logo: "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0f/LaLiga_logo_2023.svg/300px-LaLiga_logo_2023.svg.png" },
{ id: "laliga2", name: "LaLiga 2", country: "Spain", icon: "🇪🇸", logo: "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0f/LaLiga_logo_2023.svg/300px-LaLiga_logo_2023.svg.png" },
{ id: "bundesliga", name: "Bundesliga", country: "Germany", icon: "🇩🇪", logo: "https://upload.wikimedia.org/wikipedia/en/thumb/d/df/Bundesliga_logo_%282017%29.svg/300px-Bundesliga_logo_%282017%29.svg.png" },
{ id: "seriea", name: "Serie A", country: "Italy", icon: "🇮🇹", logo: "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e1/Serie_A_logo_%282019%29.svg/300px-Serie_A_logo_%282019%29.svg.png" },
{ id: "ligue1", name: "Ligue 1", country: "France", icon: "🇫🇷", logo: "https://upload.wikimedia.org/wikipedia/en/thumb/d/d4/Ligue_1_Uber_Eats_logo.svg/300px-Ligue_1_Uber_Eats_logo.svg.png" },
{ id: "ligue2", name: "Ligue 2", country: "France", icon: "<22><>", logo: "https://upload.wikimedia.org/wikipedia/en/thumb/9/91/Ligue_2_logo_2020.svg/300px-Ligue_2_logo_2020.svg.png" },
{ id: "eredivisie", name: "Eredivisie", country: "Netherlands", icon: "<22><>", logo: "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0f/Eredivisie_logo_2017.svg/300px-Eredivisie_logo_2017.svg.png" },
];

View File

@ -1,38 +1,75 @@
import { create } from "zustand";
export type OddsFormat = "decimal";
export type OddsFormat = "decimal" | "fractional" | "american";
export type Bet = {
id: string;
event: string;
league: string;
market: string;
selection: string;
odds: number;
stake?: number;
};
type BetslipState = {
bets: Bet[];
oddsFormat: OddsFormat;
defaultStake: number;
addBet: (bet: Bet) => void;
removeBet: (id: string) => void;
clearBets: () => void;
updateStake: (id: string, stake: number) => void;
setOddsFormat: (format: OddsFormat) => void;
setDefaultStake: (stake: number) => void;
getTotalOdds: () => number;
getTotalStake: () => number;
getPotentialWin: () => number;
};
export const useBetslipStore = create<BetslipState>((set) => ({
export const useBetslipStore = create<BetslipState>((set, get) => ({
bets: [],
oddsFormat: "decimal",
defaultStake: 10,
addBet: (bet) =>
set((state) => {
const exists = state.bets.some((b) => b.id === bet.id);
if (exists) {
return state;
// Toggle off if already selected
return { bets: state.bets.filter((b) => b.id !== bet.id) };
}
return { bets: [...state.bets, bet] };
return { bets: [...state.bets, { ...bet, stake: state.defaultStake }] };
}),
removeBet: (id) =>
set((state) => ({
bets: state.bets.filter((bet) => bet.id !== id),
})),
clearBets: () => set({ bets: [] }),
updateStake: (id, stake) =>
set((state) => ({
bets: state.bets.map((b) => (b.id === id ? { ...b, stake } : b)),
})),
setOddsFormat: (format) => set({ oddsFormat: format }),
setDefaultStake: (stake) => set({ defaultStake: stake }),
getTotalOdds: () => {
const bets = get().bets;
if (bets.length === 0) return 0;
return bets.reduce((acc, b) => acc * b.odds, 1);
},
getTotalStake: () => {
const bets = get().bets;
return bets.reduce((acc, b) => acc + (b.stake ?? 0), 0);
},
getPotentialWin: () => {
const bets = get().bets;
if (bets.length === 0) return 0;
if (bets.length === 1) {
const b = bets[0];
return (b.stake ?? 0) * b.odds;
}
// Accumulator: use first bet's stake
const stake = bets[0].stake ?? 0;
const totalOdds = bets.reduce((acc, b) => acc * b.odds, 1);
return stake * totalOdds;
},
}));

4
package-lock.json generated
View File

@ -1,11 +1,11 @@
{
"name": "harifsport-ui",
"name": "fortune-play",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "harifsport-ui",
"name": "fortune-play",
"version": "0.1.0",
"dependencies": {
"class-variance-authority": "^0.7.1",