feat: Introduce initial sports betting application with core pages, UI components, and state management.
This commit is contained in:
parent
6fa5ba58d7
commit
86bd615b9e
154
app/globals.css
154
app/globals.css
|
|
@ -42,78 +42,67 @@
|
||||||
--radius-md: calc(var(--radius) - 2px);
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
--radius-lg: var(--radius);
|
--radius-lg: var(--radius);
|
||||||
--radius-xl: calc(var(--radius) + 4px);
|
--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 {
|
:root {
|
||||||
--radius: 0.625rem;
|
--radius: 0rem;
|
||||||
--background: oklch(1 0 0);
|
/* HarifSport orange theme colors */
|
||||||
--foreground: oklch(0.141 0.005 285.823);
|
--background: #121212;
|
||||||
--card: oklch(1 0 0);
|
--foreground: #ffffff;
|
||||||
--card-foreground: oklch(0.141 0.005 285.823);
|
--card: #1e1e1e;
|
||||||
--popover: oklch(1 0 0);
|
--card-foreground: #ffffff;
|
||||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
--popover: #1e1e1e;
|
||||||
--primary: oklch(0.21 0.006 285.885);
|
--popover-foreground: #ffffff;
|
||||||
--primary-foreground: oklch(0.985 0 0);
|
--primary: #ff9800;
|
||||||
--secondary: oklch(0.967 0.001 286.375);
|
--primary-foreground: #ffffff;
|
||||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
--secondary: #222222;
|
||||||
--muted: oklch(0.967 0.001 286.375);
|
--secondary-foreground: #a0a0a0;
|
||||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
--muted: #1a1a1a;
|
||||||
--accent: oklch(0.967 0.001 286.375);
|
--muted-foreground: #808080;
|
||||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
--accent: #ff9800;
|
||||||
--destructive: oklch(0.577 0.245 27.325);
|
--accent-foreground: #121212;
|
||||||
--border: oklch(0.92 0.004 286.32);
|
--destructive: #ef4444;
|
||||||
--input: oklch(0.92 0.004 286.32);
|
--border: #2a2a2a;
|
||||||
--ring: oklch(0.705 0.015 286.067);
|
--input: #222222;
|
||||||
--chart-1: oklch(0.646 0.222 41.116);
|
--ring: #ff9800;
|
||||||
--chart-2: oklch(0.6 0.118 184.704);
|
|
||||||
--chart-3: oklch(0.398 0.07 227.392);
|
/* HarifSport specific */
|
||||||
--chart-4: oklch(0.828 0.189 84.429);
|
--hs-orange: #ff9800;
|
||||||
--chart-5: oklch(0.769 0.188 70.08);
|
--hs-maroon: #852222;
|
||||||
--sidebar: oklch(0.985 0 0);
|
--hs-dark-grey: #1a1a1a;
|
||||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
--hs-odds-bg: #2a2a2a;
|
||||||
--sidebar-primary: oklch(0.21 0.006 285.885);
|
--hs-odds-text: #ff9800;
|
||||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
--hs-header-bg: #121212;
|
||||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
--hs-live-red: #ff3b3b;
|
||||||
--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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: oklch(0.141 0.005 285.823);
|
--background: oklch(0.13 0.008 250);
|
||||||
--foreground: oklch(0.985 0 0);
|
--foreground: oklch(0.93 0.005 250);
|
||||||
--card: oklch(0.21 0.006 285.885);
|
--card: oklch(0.17 0.01 250);
|
||||||
--card-foreground: oklch(0.985 0 0);
|
--card-foreground: oklch(0.93 0.005 250);
|
||||||
--popover: oklch(0.21 0.006 285.885);
|
--popover: oklch(0.17 0.01 250);
|
||||||
--popover-foreground: oklch(0.985 0 0);
|
--popover-foreground: oklch(0.93 0.005 250);
|
||||||
--primary: oklch(0.92 0.004 286.32);
|
--primary: oklch(0.55 0.18 145);
|
||||||
--primary-foreground: oklch(0.21 0.006 285.885);
|
--primary-foreground: oklch(0.98 0 0);
|
||||||
--secondary: oklch(0.274 0.006 286.033);
|
--secondary: oklch(0.22 0.01 250);
|
||||||
--secondary-foreground: oklch(0.985 0 0);
|
--secondary-foreground: oklch(0.85 0.005 250);
|
||||||
--muted: oklch(0.274 0.006 286.033);
|
--muted: oklch(0.2 0.008 250);
|
||||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
--muted-foreground: oklch(0.58 0.01 250);
|
||||||
--accent: oklch(0.274 0.006 286.033);
|
--accent: oklch(0.55 0.18 145);
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
--accent-foreground: oklch(0.98 0 0);
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
--border: oklch(1 0 0 / 10%);
|
--border: oklch(0.25 0.01 250);
|
||||||
--input: oklch(1 0 0 / 15%);
|
--input: oklch(0.22 0.01 250);
|
||||||
--ring: oklch(0.552 0.016 285.938);
|
--ring: oklch(0.55 0.18 145);
|
||||||
--chart-1: oklch(0.488 0.243 264.376);
|
--sidebar: oklch(0.15 0.01 250);
|
||||||
--chart-2: oklch(0.696 0.17 162.48);
|
--sidebar-foreground: oklch(0.9 0.005 250);
|
||||||
--chart-3: oklch(0.769 0.188 70.08);
|
--sidebar-primary: oklch(0.55 0.18 145);
|
||||||
--chart-4: oklch(0.627 0.265 303.9);
|
--sidebar-primary-foreground: oklch(0.98 0 0);
|
||||||
--chart-5: oklch(0.645 0.246 16.439);
|
--sidebar-accent: oklch(0.22 0.01 250);
|
||||||
--sidebar: oklch(0.21 0.006 285.885);
|
--sidebar-accent-foreground: oklch(0.93 0.005 250);
|
||||||
--sidebar-foreground: oklch(0.985 0 0);
|
--sidebar-border: oklch(0.25 0.01 250);
|
||||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
--sidebar-ring: oklch(0.55 0.18 145);
|
||||||
--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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
|
|
@ -122,5 +111,40 @@
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@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;
|
||||||
}
|
}
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
import type { Metadata } from "next"
|
import type { Metadata } from "next"
|
||||||
import { Geist, Geist_Mono } from "next/font/google"
|
import { Geist, Geist_Mono } from "next/font/google"
|
||||||
import "./globals.css"
|
import "./globals.css"
|
||||||
import { SiteHeader } from "@/components/layout/site-header"
|
import LayoutClientWrapper from "@/components/layout/layout-client-wrapper"
|
||||||
import { SportsSidebar } from "@/components/layout/sports-sidebar"
|
|
||||||
import { RightPanel } from "@/components/layout/right-panel"
|
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
|
|
@ -16,8 +14,8 @@ const geistMono = Geist_Mono({
|
||||||
})
|
})
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Harifsport",
|
title: "Harifsport - Sports Betting",
|
||||||
description: "Harifsport-style sportsbook interface built with Next.js",
|
description: "Harifsport sportsbook - Live betting, in-play events, and more",
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
|
|
@ -30,16 +28,8 @@ export default function RootLayout({
|
||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} bg-background text-foreground antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} bg-background text-foreground antialiased`}
|
||||||
>
|
>
|
||||||
<div className="flex min-h-screen flex-col bg-background">
|
<LayoutClientWrapper>{children}</LayoutClientWrapper>
|
||||||
<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>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { InPlayPage } from "@/components/betting/in-play-page"
|
import { SportHome } from "@/components/betting/sport-home"
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return <InPlayPage />
|
return <SportHome />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,11 @@ const buttonVariants = cva(
|
||||||
ghost:
|
ghost:
|
||||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
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: {
|
size: {
|
||||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,14 @@ function Tabs({
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabsListVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-muted",
|
default: "bg-muted",
|
||||||
line: "gap-1 bg-transparent",
|
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: {
|
defaultVariants: {
|
||||||
|
|
@ -66,8 +68,10 @@ function TabsTrigger({
|
||||||
className={cn(
|
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",
|
"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=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",
|
"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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
||||||
261
lib/mock-data.ts
Normal file
261
lib/mock-data.ts
Normal 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" },
|
||||||
|
];
|
||||||
|
|
@ -1,38 +1,75 @@
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
|
||||||
export type OddsFormat = "decimal";
|
export type OddsFormat = "decimal" | "fractional" | "american";
|
||||||
|
|
||||||
export type Bet = {
|
export type Bet = {
|
||||||
id: string;
|
id: string;
|
||||||
event: string;
|
event: string;
|
||||||
|
league: string;
|
||||||
market: string;
|
market: string;
|
||||||
selection: string;
|
selection: string;
|
||||||
odds: number;
|
odds: number;
|
||||||
|
stake?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type BetslipState = {
|
type BetslipState = {
|
||||||
bets: Bet[];
|
bets: Bet[];
|
||||||
oddsFormat: OddsFormat;
|
oddsFormat: OddsFormat;
|
||||||
|
defaultStake: number;
|
||||||
addBet: (bet: Bet) => void;
|
addBet: (bet: Bet) => void;
|
||||||
removeBet: (id: string) => void;
|
removeBet: (id: string) => void;
|
||||||
clearBets: () => 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: [],
|
bets: [],
|
||||||
oddsFormat: "decimal",
|
oddsFormat: "decimal",
|
||||||
|
defaultStake: 10,
|
||||||
addBet: (bet) =>
|
addBet: (bet) =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const exists = state.bets.some((b) => b.id === bet.id);
|
const exists = state.bets.some((b) => b.id === bet.id);
|
||||||
if (exists) {
|
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) =>
|
removeBet: (id) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
bets: state.bets.filter((bet) => bet.id !== id),
|
bets: state.bets.filter((bet) => bet.id !== id),
|
||||||
})),
|
})),
|
||||||
clearBets: () => set({ bets: [] }),
|
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
4
package-lock.json
generated
|
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
"name": "harifsport-ui",
|
"name": "fortune-play",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "harifsport-ui",
|
"name": "fortune-play",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user