From 86bd615b9efbd42bafa562173ad31707c1b465f2 Mon Sep 17 00:00:00 2001 From: brooktewabe Date: Fri, 20 Feb 2026 12:20:07 +0300 Subject: [PATCH] feat: Introduce initial sports betting application with core pages, UI components, and state management. --- app/globals.css | 154 +++++++++++++--------- app/layout.tsx | 20 +-- app/page.tsx | 4 +- components/ui/button.tsx | 5 + components/ui/tabs.tsx | 8 +- lib/mock-data.ts | 261 +++++++++++++++++++++++++++++++++++++ lib/store/betslip-store.ts | 49 ++++++- package-lock.json | 4 +- 8 files changed, 413 insertions(+), 92 deletions(-) create mode 100644 lib/mock-data.ts diff --git a/app/globals.css b/app/globals.css index 1c2b16d..5621fc0 100644 --- a/app/globals.css +++ b/app/globals.css @@ -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; } \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index e89cbde..f2e2fe6 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -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({ -
- -
- -
{children}
- -
-
+ {children} ) -} - +} \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index 1db43bd..2c640c2 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,6 +1,6 @@ -import { InPlayPage } from "@/components/betting/in-play-page" +import { SportHome } from "@/components/betting/sport-home" export default function Home() { - return + return } diff --git a/components/ui/button.tsx b/components/ui/button.tsx index b5ea4ab..b678112 100644 --- a/components/ui/button.tsx +++ b/components/ui/button.tsx @@ -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", diff --git a/components/ui/tabs.tsx b/components/ui/tabs.tsx index 7f73dcd..8c4bf4e 100644 --- a/components/ui/tabs.tsx +++ b/components/ui/tabs.tsx @@ -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} diff --git a/lib/mock-data.ts b/lib/mock-data.ts new file mode 100644 index 0000000..0f30267 --- /dev/null +++ b/lib/mock-data.ts @@ -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: "��", 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: "��", logo: "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0f/Eredivisie_logo_2017.svg/300px-Eredivisie_logo_2017.svg.png" }, +]; \ No newline at end of file diff --git a/lib/store/betslip-store.ts b/lib/store/betslip-store.ts index 41fea36..b3a5fa5 100644 --- a/lib/store/betslip-store.ts +++ b/lib/store/betslip-store.ts @@ -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((set) => ({ +export const useBetslipStore = create((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; + }, +})); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b6c9144..024e3b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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",