48 |
setIsOpen(false)}
51 | />
52 |
92 |
93 | )}
94 |
95 | );
96 | }
97 |
98 | export default Nav;
99 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { Bookmaker, Market, Outcome } from "../lib/api";
2 |
3 | interface BestOdds {
4 | moneyline: { price: number; bookmaker: string } | null;
5 | spread: { price: number; bookmaker: string } | null;
6 | over: { price: number; bookmaker: string } | null;
7 | under: { price: number; bookmaker: string } | null;
8 | }
9 |
10 | export function findBestOdds(bookmakers: Bookmaker[], team: string): BestOdds {
11 | const bestOdds: BestOdds = {
12 | moneyline: null,
13 | spread: null,
14 | over: null,
15 | under: null,
16 | };
17 |
18 | // Single pass through all bookmakers
19 | for (const bookmaker of bookmakers) {
20 | // Single pass through all markets
21 | for (const market of bookmaker.markets) {
22 | switch (market.key) {
23 | case "h2h":
24 | // Moneyline odds
25 | for (const outcome of market.outcomes) {
26 | if (outcome.name === team) {
27 | if (
28 | !bestOdds.moneyline ||
29 | outcome.price > bestOdds.moneyline.price
30 | ) {
31 | bestOdds.moneyline = {
32 | price: outcome.price,
33 | bookmaker: bookmaker.title,
34 | };
35 | }
36 | }
37 | }
38 | break;
39 |
40 | case "spreads":
41 | // Spread odds
42 | for (const outcome of market.outcomes) {
43 | if (outcome.name === team) {
44 | if (!bestOdds.spread || outcome.price > bestOdds.spread.price) {
45 | bestOdds.spread = {
46 | price: outcome.price,
47 | bookmaker: bookmaker.title,
48 | };
49 | }
50 | }
51 | }
52 | break;
53 |
54 | case "totals":
55 | // Over/Under odds
56 | for (const outcome of market.outcomes) {
57 | if (outcome.name === "Over") {
58 | if (!bestOdds.over || outcome.price > bestOdds.over.price) {
59 | bestOdds.over = {
60 | price: outcome.price,
61 | bookmaker: bookmaker.title,
62 | };
63 | }
64 | } else if (outcome.name === "Under") {
65 | if (!bestOdds.under || outcome.price > bestOdds.under.price) {
66 | bestOdds.under = {
67 | price: outcome.price,
68 | bookmaker: bookmaker.title,
69 | };
70 | }
71 | }
72 | }
73 | break;
74 | }
75 | }
76 | }
77 |
78 | return bestOdds;
79 | }
80 |
81 | // Individual helper functions for backward compatibility
82 | export function findBestMoneylineOdds(bookmakers: Bookmaker[], team: string) {
83 | return findBestOdds(bookmakers, team).moneyline;
84 | }
85 |
86 | export function findBestSpreadOdds(bookmakers: Bookmaker[], team: string) {
87 | return findBestOdds(bookmakers, team).spread;
88 | }
89 |
90 | export function findBestOverUnderOdds(bookmakers: Bookmaker[]) {
91 | const bestOdds = findBestOdds(bookmakers, "");
92 | return { bestOver: bestOdds.over, bestUnder: bestOdds.under };
93 | }
94 |
95 | // Optimized comparison functions
96 | export function isBestMoneylineOdds(
97 | outcome: Outcome,
98 | team: string,
99 | bookmakerTitle: string,
100 | bestMoneyline: { price: number; bookmaker: string } | null
101 | ): boolean {
102 | return (
103 | outcome.name === team &&
104 | bestMoneyline !== null &&
105 | outcome.price === bestMoneyline.price &&
106 | bookmakerTitle === bestMoneyline.bookmaker
107 | );
108 | }
109 |
110 | export function isBestSpreadOdds(
111 | outcome: Outcome,
112 | team: string,
113 | bookmakerTitle: string,
114 | bestSpread: { price: number; bookmaker: string } | null
115 | ): boolean {
116 | return (
117 | outcome.name === team &&
118 | bestSpread !== null &&
119 | outcome.price === bestSpread.price &&
120 | bookmakerTitle === bestSpread.bookmaker
121 | );
122 | }
123 |
124 | export function isBestOverUnderOdds(
125 | outcome: Outcome,
126 | bookmakerTitle: string,
127 | bestOver: { price: number; bookmaker: string } | null,
128 | bestUnder: { price: number; bookmaker: string } | null
129 | ): boolean {
130 | if (outcome.name === "Over" && bestOver) {
131 | return (
132 | outcome.price === bestOver.price && bookmakerTitle === bestOver.bookmaker
133 | );
134 | }
135 | if (outcome.name === "Under" && bestUnder) {
136 | return (
137 | outcome.price === bestUnder.price &&
138 | bookmakerTitle === bestUnder.bookmaker
139 | );
140 | }
141 | return false;
142 | }
143 |
--------------------------------------------------------------------------------
/lib/api.ts:
--------------------------------------------------------------------------------
1 | import * as Sentry from '@sentry/nextjs';
2 | const baseURL = "https://api.the-odds-api.com";
3 | const apiKey = process.env.NEXT_PUBLIC_API_ODDS_KEY;
4 |
5 | export interface Outcome {
6 | name: string;
7 | price: number;
8 | point?: number;
9 | }
10 | export interface Market {
11 | key: string;
12 | last_update: string;
13 | outcomes: Outcome[];
14 | }
15 |
16 | export interface Bookmaker {
17 | key: string;
18 | title: string;
19 | last_update: string;
20 | markets: Market[];
21 | }
22 |
23 | export interface Odds {
24 | id: string;
25 | sport_key: string;
26 | sport_title: string;
27 | commence_time: string;
28 | home_team: string;
29 | away_team: string;
30 | bookmakers: Bookmaker[];
31 | }
32 |
33 | export interface Sport {
34 | key: string;
35 | group: string;
36 | title: string;
37 | description: string;
38 | active: boolean;
39 | has_outrights: boolean;
40 | }
41 |
42 | const fallbackSports: Sport[] = [
43 | { key: "mma_mixed_martial_arts", group: "Fighting", title: "MMA", description: "MMA", active: true, has_outrights: false },
44 | { key: "boxing_boxing", group: "Fighting", title: "Boxing", description: "Boxing", active: true, has_outrights: false },
45 | { key: "basketball_nba", group: "Basketball", title: "NBA", description: "NBA", active: true, has_outrights: false },
46 | { key: "baseball_mlb", group: "Baseball", title: "MLB", description: "MLB", active: true, has_outrights: false },
47 | { key: "icehockey_nhl", group: "Hockey", title: "NHL", description: "NHL", active: true, has_outrights: false },
48 | ];
49 |
50 | function isCiEnv() {
51 | return process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
52 | }
53 |
54 | export async function getOdds(sport = "upcoming", type = "spreads,totals,h2h") {
55 | try {
56 | if (!apiKey) {
57 | return [];
58 | }
59 | const res = await fetch(
60 | `${baseURL}/v4/sports/${sport}/odds/?apiKey=${apiKey}®ions=us&markets=${type}&oddsFormat=american`,
61 | {
62 | cache: "no-cache",
63 | }
64 | );
65 | const data: Odds[] = await res.json();
66 | return data;
67 | } catch (error) {
68 | Sentry.captureException(error, { tags: { sport, function: "getOdds", from: 'server' } });
69 | console.error(error);
70 | return [];
71 | }
72 | }
73 |
74 | export async function getInSeasonSports() {
75 | //get sports that are currently in season
76 | try {
77 | if (!apiKey) {
78 | return fallbackSports;
79 | }
80 | const res = await fetch(
81 | `${baseURL}/v4/sports/?apiKey=${apiKey}&all=false`,
82 | {
83 | cache: "no-cache",
84 | }
85 | );
86 | const data = await res.json();
87 | return Array.isArray(data) && data.length ? data : fallbackSports;
88 | } catch (error) {
89 | Sentry.captureException(error, { tags: { sport: "all", function: "getInSeasonSports", from: 'server' } });
90 | console.error(error);
91 | return fallbackSports;
92 | }
93 | }
94 |
95 | export async function getMoneyLineOdds(sport = "upcoming") {
96 | try {
97 | const odds = await getOdds(sport, "h2h");
98 | if (odds) {
99 | return odds;
100 | }
101 | return [];
102 | } catch (error) {
103 | Sentry.captureException(error, { tags: { sport, function: "getMoneyLineOdds", from: 'server' } });
104 | console.error(error);
105 | return [];
106 | }
107 | }
108 |
109 | export async function getSpreadOdds(sport = "upcoming") {
110 | try {
111 | const odds = await getOdds(sport, "spreads");
112 | if (odds) {
113 | return odds;
114 | }
115 | return [];
116 | } catch (error) {
117 | Sentry.captureException(error, { tags: { sport, function: "getSpreadOdds", from: 'server' } });
118 | console.error(error);
119 | return [];
120 | }
121 | }
122 |
123 | export async function getPointOdds(sport = "upcoming") {
124 | try {
125 | const odds = await getOdds(sport, "totals");
126 | if (odds) {
127 | return odds;
128 | }
129 | return [];
130 | } catch (error) {
131 | Sentry.captureException(error, { tags: { sport, function: "getPointOdds", from: 'server' } });
132 | console.error(error);
133 | return [];
134 | }
135 | }
136 |
137 | //Playerprops
138 |
139 | export async function getPlayerProps(
140 | sport: string,
141 | eventid: string,
142 | markets: string
143 | ) {
144 | "use server";
145 | try {
146 | if (!apiKey) {
147 | const { dummyPlayerProps } = await import("./dummyPlayerProps");
148 | return dummyPlayerProps;
149 | }
150 | const odds = await fetch(
151 | `${baseURL}/v4/sports/${sport}/events/${eventid}/odds/?apiKey=${apiKey}®ions=us&markets=${markets}&oddsFormat=american`,
152 | { cache: "force-cache" }
153 | );
154 |
155 | const data = odds.json();
156 | if (data) {
157 | return data ?? [];
158 | }
159 | return [];
160 | } catch (error) {
161 | Sentry.captureException(error, { tags: { sport, eventid, markets, function: "getPlayerProps", from: 'server' } });
162 | console.error(error);
163 | return [];
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/src/components/ui/command.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { type DialogProps } from "@radix-ui/react-dialog"
5 | import { MagnifyingGlassIcon } from "@radix-ui/react-icons"
6 | import { Command as CommandPrimitive } from "cmdk"
7 |
8 | import { cn } from "@/lib/utils"
9 | import { Dialog, DialogContent } from "@/components/ui/dialog"
10 |
11 | const Command = React.forwardRef<
12 | React.ElementRef
,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
23 | ))
24 | Command.displayName = CommandPrimitive.displayName
25 |
26 | interface CommandDialogProps extends DialogProps {}
27 |
28 | const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
29 | return (
30 |
37 | )
38 | }
39 |
40 | const CommandInput = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
45 |
46 |
54 |
55 | ))
56 |
57 | CommandInput.displayName = CommandPrimitive.Input.displayName
58 |
59 | const CommandList = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, ...props }, ref) => (
63 |
68 | ))
69 |
70 | CommandList.displayName = CommandPrimitive.List.displayName
71 |
72 | const CommandEmpty = React.forwardRef<
73 | React.ElementRef,
74 | React.ComponentPropsWithoutRef
75 | >((props, ref) => (
76 |
81 | ))
82 |
83 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName
84 |
85 | const CommandGroup = React.forwardRef<
86 | React.ElementRef,
87 | React.ComponentPropsWithoutRef
88 | >(({ className, ...props }, ref) => (
89 |
97 | ))
98 |
99 | CommandGroup.displayName = CommandPrimitive.Group.displayName
100 |
101 | const CommandSeparator = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName
112 |
113 | const CommandItem = React.forwardRef<
114 | React.ElementRef,
115 | React.ComponentPropsWithoutRef
116 | >(({ className, ...props }, ref) => (
117 |
125 | ))
126 |
127 | CommandItem.displayName = CommandPrimitive.Item.displayName
128 |
129 | const CommandShortcut = ({
130 | className,
131 | ...props
132 | }: React.HTMLAttributes) => {
133 | return (
134 |
141 | )
142 | }
143 | CommandShortcut.displayName = "CommandShortcut"
144 |
145 | export {
146 | Command,
147 | CommandDialog,
148 | CommandInput,
149 | CommandList,
150 | CommandEmpty,
151 | CommandGroup,
152 | CommandItem,
153 | CommandShortcut,
154 | CommandSeparator,
155 | }
156 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 🏈 Sportsbook Odds Comparer
2 |
3 | A modern, responsive web application for comparing sports betting odds across different sportsbooks. Built with cutting-edge technologies and comprehensive testing.
4 |
5 | [](https://sportsbook-odds-comparer.vercel.app/)
6 | [](https://github.com/features/actions)
7 | [](https://www.cypress.io/)
8 |
9 | ## ✨ Features
10 |
11 | ### 🎯 Core Functionality
12 |
13 | - **Multi-Sport Support**: NBA, NFL, NHL, MLB, MMA, and more
14 | - **Odds Comparison**: View and compare odds across multiple sportsbooks
15 | - **Betting Markets**: Moneyline, Spread, Points, and Player Props
16 | - **Real-time Data**: Live odds updates from sportsbook APIs
17 |
18 | ### 🎮 Player Props (Advanced Feature)
19 |
20 | - **Dynamic Market Selection**: Choose from various player prop markets
21 | - **Real-time Odds**: Live player prop odds from multiple bookmakers
22 | - **Interactive UI**: Modern combobox interface for easy navigation
23 | - **Comprehensive Coverage**: Batter hits, passing yards, and more
24 |
25 | ### 🧭 Navigation & UX
26 |
27 | - **Responsive Design**: Mobile-first approach with Tailwind CSS
28 | - **Dark/Light Theme**: Toggle between themes for user preference
29 | - **Intuitive Navigation**: Easy switching between sports and markets
30 | - **Accessibility**: ARIA labels and keyboard navigation support
31 |
32 | ## 🚀 Tech Stack
33 |
34 | ### Frontend
35 |
36 | - **Next.js 14** - React framework with App Router
37 | - **React 18** - Latest React with concurrent features
38 | - **TypeScript** - Type-safe development
39 | - **Tailwind CSS** - Utility-first CSS framework
40 | - **Radix UI** - Accessible component primitives
41 |
42 | ### State Management & Data
43 |
44 | - **SWR** - Data fetching and caching
45 | - **UUID** - Unique identifier generation
46 | - **Class Variance Authority** - Component variant management
47 |
48 | ### Testing & Quality
49 |
50 | - **Cypress 14** - End-to-end testing
51 | - **ESLint** - Code quality and consistency
52 | - **TypeScript** - Static type checking
53 |
54 | ### Development Tools
55 |
56 | - **pnpm** - Fast, disk space efficient package manager
57 | - **PostCSS** - CSS processing
58 | - **Autoprefixer** - CSS vendor prefixing
59 |
60 | ## 🧪 Testing
61 |
62 | ### E2E Tests with Cypress
63 |
64 | Comprehensive test coverage including:
65 |
66 | ```typescript
67 | // Example test structure
68 | describe("Sportsbook Odds", () => {
69 | it("loads and displays odds", () => {
70 | cy.visit("/");
71 | cy.get('[data-cy="odds-ml-item"]').should("be.visible");
72 | });
73 |
74 | it("navigates between sports correctly", () => {
75 | // Navigation tests
76 | });
77 | });
78 |
79 | describe("Player Props", () => {
80 | it("displays player props for selected markets", () => {
81 | // Player props functionality tests
82 | });
83 | });
84 | ```
85 |
86 | ### Test Coverage
87 |
88 | - ✅ **Page Loading**: Homepage and odds pages
89 | - ✅ **Navigation**: Sport switching and market navigation
90 | - ✅ **Player Props**: Market selection and data display
91 | - ✅ **Responsive Design**: Mobile and desktop interactions
92 | - ✅ **Data Integration**: API responses and error handling
93 |
94 | ## 🚀 Getting Started
95 |
96 | ### Prerequisites
97 |
98 | - Node.js 18+
99 | - pnpm (recommended) or npm
100 |
101 | ### Installation
102 |
103 | ```bash
104 | # Clone the repository
105 | git clone https://github.com/yourusername/sportsbook-odds-comparer.git
106 | cd sportsbook-odds-comparer
107 |
108 | # Install dependencies
109 | pnpm install
110 |
111 | # Start development server
112 | pnpm dev
113 | ```
114 |
115 | ### Available Scripts
116 |
117 | ```bash
118 | pnpm dev # Start development server
119 | pnpm build # Build for production
120 | pnpm start # Start production server
121 | pnpm lint # Run ESLint
122 | pnpm test # Run Cypress tests
123 | pnpm cypress:open # Open Cypress test runner
124 | ```
125 |
126 | ## 🏗️ Project Structure
127 |
128 | ```
129 | sportsbook-odds-comparer/
130 | ├── src/
131 | │ ├── app/ # Next.js App Router
132 | │ │ ├── odds/ # Odds comparison pages
133 | │ │ │ ├── [sport]/ # Dynamic sport routes
134 | │ │ │ └── playerProps/ # Player props functionality
135 | │ │ └── layout.tsx # Root layout
136 | │ ├── components/ # Reusable components
137 | │ │ ├── ui/ # Base UI components
138 | │ │ ├── OddsTable.tsx # Main odds display
139 | │ │ ├── PlayerProp.tsx # Player props component
140 | │ │ └── ThemeToggle.tsx # Theme switching
141 | │ └── lib/ # Utilities and API
142 | ├── cypress/ # E2E testing
143 | ├── public/ # Static assets
144 | └── .github/workflows/ # CI/CD pipelines
145 | ```
146 |
147 | ## 🔄 CI/CD Pipeline
148 |
149 | ### GitHub Actions Workflow
150 |
151 | - **Automated Testing**: Runs Cypress tests on every deployment
152 | - **Quality Checks**: ESLint and TypeScript validation
153 | - **Deployment Integration**: Tests against live deployed applications
154 | - **Smart Caching**: Optimized dependency and build caching
155 |
156 | ### Workflow Features
157 |
158 | - **Event-Driven**: Triggers on successful deployments
159 | - **E2E Testing**: Comprehensive testing against live applications
160 | - **Fail-Fast**: Immediate feedback on deployment issues
161 | - **Resource Efficient**: Optimized for speed and cost
162 |
163 | ## 🌟 What's Next
164 |
165 | ### Planned Features
166 |
167 | - **Personal Betting Tracker**: Track your bets and performance
168 | - **Daily Picks**: Curated betting recommendations
169 | - **Sports Bettor Profiles**: Featured handicapper picks
170 | - **Advanced Analytics**: Betting performance insights
171 | - **Mobile App**: Native mobile experience
172 |
173 | ### Technical Improvements
174 |
175 | - **Performance Optimization**: Core Web Vitals improvements
176 | - **SEO Enhancement**: Better search engine optimization
177 | - **PWA Features**: Progressive web app capabilities
178 | - **Real-time Updates**: WebSocket integration for live odds
179 |
180 | ## 🤝 Contributing
181 |
182 | This is a personal project, but contributions are welcome!
183 |
184 | ### Development Guidelines
185 |
186 | - Follow TypeScript best practices
187 | - Write comprehensive tests for new features
188 | - Maintain accessibility standards
189 | - Use conventional commit messages
190 |
191 | ## 📱 Live Demo
192 |
193 | **Visit the live application**: [https://sportsbook-odds-comparer.vercel.app/](https://sportsbook-odds-comparer.vercel.app/)
194 |
195 | ## 📄 License
196 |
197 | This project is for personal use and educational purposes.
198 |
199 | ---
200 |
201 | **Built with ❤️ using Next.js, React, and modern web technologies**
202 |
--------------------------------------------------------------------------------
/src/components/PlayerPropContainer.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useSearchParams } from "next/navigation";
3 | import { useEffect, useState, useCallback } from "react";
4 | import GameHeader from "./GameHeader";
5 | import { StarFilledIcon } from "@radix-ui/react-icons";
6 |
7 | function PlayerPropContainer({ getPlayerProps }: PlayerPropContainerProps) {
8 | const searchParams = useSearchParams();
9 | const sport = searchParams?.get("sport") as string;
10 | const eventId = searchParams?.get("event") as string;
11 | const markets = searchParams?.get("markets") as string;
12 | const [playerProps, setPlayerProps] = useState(
13 | null
14 | );
15 |
16 | const [propMarket, setPropMarket] = useState(null);
17 | const [gameInfo, setGameInfo] = useState<{
18 | homeTeam: string;
19 | awayTeam: string;
20 | commenceTime: string;
21 | } | null>(null);
22 |
23 | const updatePlayerProps = useCallback(async () => {
24 | const data = await getPlayerProps(sport, eventId, markets);
25 | if (data?.bookmakers) {
26 | setPropMarket(data.bookmakers[0]?.markets[0].key.replaceAll("_", " "));
27 | setGameInfo({
28 | homeTeam: data.home_team,
29 | awayTeam: data.away_team,
30 | commenceTime: data.commence_time,
31 | });
32 | const reformatted = reformatPlayerProps(data);
33 | setPlayerProps(reformatted);
34 | }
35 | }, [sport, eventId, markets, getPlayerProps]);
36 |
37 | useEffect(() => {
38 | updatePlayerProps();
39 | }, [sport, eventId, markets, updatePlayerProps]);
40 |
41 | if (!playerProps) {
42 | return Please select player props
;
43 | }
44 |
45 | return (
46 |
47 |
48 | {propMarket}
49 |
50 | {gameInfo && (
51 |
56 | )}
57 | {playerProps &&
58 | Object.entries(playerProps).map((details) => {
59 | const [player, odds] = details;
60 |
61 | // Find best odds and the first bookmaker offering them for each outcome type
62 | const bestOdds: { [key: string]: { price: number; book: string } } = {};
63 | odds.forEach((odd) => {
64 | if (!bestOdds[odd.name] || odd.price > bestOdds[odd.name].price) {
65 | bestOdds[odd.name] = { price: odd.price, book: odd.book };
66 | }
67 | });
68 |
69 | return (
70 |
75 |
76 |
77 | {player}
78 |
79 |
80 |
81 | {odds.map((odd, idx) => {
82 | const isBest = odd.price === bestOdds[odd.name].price && odd.book === bestOdds[odd.name].book;
83 |
84 | return (
85 |
90 | {isBest && (
91 |
96 | )}
97 | {odd.price > 0 ? (
98 |
102 | +{odd.price}
103 |
104 | ) : (
105 |
109 | {odd.price}
110 |
111 | )}
112 |
116 | {odd.name} {odd?.point}
117 |
118 |
119 | {odd.book}
120 |
121 |
122 | );
123 | })}
124 |
125 |
126 | );
127 | })}
128 |
129 | );
130 | }
131 |
132 | export default PlayerPropContainer;
133 |
134 | export interface PlayerPropsData {
135 | away_team: string;
136 | bookmakers: Bookmaker[];
137 | commence_time: string;
138 | home_team: string;
139 | id: string;
140 | sport_key: string;
141 | sport_title: string;
142 | }
143 |
144 | export interface Bookmaker {
145 | key: string;
146 | markets: Market[];
147 | title: string;
148 | }
149 |
150 | export interface Market {
151 | key: string;
152 | last_update: string;
153 | outcomes: Outcome[];
154 | }
155 |
156 | interface Outcome {
157 | description: string;
158 | name: string;
159 | point: number;
160 | price: number;
161 | }
162 |
163 | interface PlayerPropContainerProps {
164 | getPlayerProps: (
165 | sport: string,
166 | eventId: string,
167 | markets: string
168 | ) => Promise;
169 | }
170 |
171 | interface ReformattedPlayerProps {
172 | [key: string]: {
173 | name: string;
174 | book: string;
175 | price: number;
176 | point?: number;
177 | }[];
178 | }
179 |
180 | function reformatPlayerProps(playerProps: PlayerPropsData) {
181 | //make data shape easier to work with and map through. I will thank myself later.
182 | const result: ReformattedPlayerProps = {};
183 |
184 | playerProps.bookmakers.forEach((bookmaker) => {
185 | bookmaker.markets.forEach((market) => {
186 | market.outcomes.forEach((outcome) => {
187 | const playerName = outcome.description;
188 |
189 | if (!result[playerName]) {
190 | result[playerName] = [];
191 | }
192 |
193 | result[playerName].push({
194 | name: outcome.name,
195 | book: bookmaker.title,
196 | price: outcome.price,
197 | point: outcome.point,
198 | });
199 | });
200 | });
201 | });
202 |
203 | return result;
204 | }
205 |
--------------------------------------------------------------------------------
/lib/dummyPlayerProps.ts:
--------------------------------------------------------------------------------
1 | export const dummyPlayerProps = {
2 | id: "0a5a9e810f06ed0adb33581b2163f143",
3 | sport_key: "basketball_nba",
4 | sport_title: "NBA",
5 | commence_time: "2025-01-02T00:10:00Z",
6 | home_team: "Washington Wizards",
7 | away_team: "Chicago Bulls",
8 | bookmakers: [
9 | {
10 | key: "fanduel",
11 | title: "FanDuel",
12 | markets: [
13 | {
14 | key: "player_assists",
15 | last_update: "2025-01-01T21:15:55Z",
16 | outcomes: [
17 | {
18 | name: "Over",
19 | description: "Bilal Coulibaly",
20 | price: 128,
21 | point: 4.5,
22 | },
23 | {
24 | name: "Under",
25 | description: "Bilal Coulibaly",
26 | price: -158,
27 | point: 4.5,
28 | },
29 | {
30 | name: "Over",
31 | description: "Jordan Poole",
32 | price: -140,
33 | point: 4.5,
34 | },
35 | {
36 | name: "Under",
37 | description: "Jordan Poole",
38 | price: 114,
39 | point: 4.5,
40 | },
41 | {
42 | name: "Over",
43 | description: "Josh Giddey",
44 | price: 106,
45 | point: 7.5,
46 | },
47 | {
48 | name: "Under",
49 | description: "Josh Giddey",
50 | price: -130,
51 | point: 7.5,
52 | },
53 | {
54 | name: "Over",
55 | description: "Zach LaVine",
56 | price: 122,
57 | point: 4.5,
58 | },
59 | {
60 | name: "Under",
61 | description: "Zach LaVine",
62 | price: -150,
63 | point: 4.5,
64 | },
65 | {
66 | name: "Over",
67 | description: "Coby White",
68 | price: 106,
69 | point: 4.5,
70 | },
71 | {
72 | name: "Under",
73 | description: "Coby White",
74 | price: -130,
75 | point: 4.5,
76 | },
77 | {
78 | name: "Over",
79 | description: "Nikola Vucevic",
80 | price: 132,
81 | point: 3.5,
82 | },
83 | {
84 | name: "Under",
85 | description: "Nikola Vucevic",
86 | price: -162,
87 | point: 3.5,
88 | },
89 | ],
90 | },
91 | ],
92 | },
93 | {
94 | key: "draftkings",
95 | title: "DraftKings",
96 | markets: [
97 | {
98 | key: "player_assists",
99 | last_update: "2025-01-01T21:16:43Z",
100 | outcomes: [
101 | {
102 | name: "Under",
103 | description: "Josh Giddey",
104 | price: -130,
105 | point: 7.5,
106 | },
107 | {
108 | name: "Over",
109 | description: "Josh Giddey",
110 | price: 100,
111 | point: 7.5,
112 | },
113 | {
114 | name: "Under",
115 | description: "Jordan Poole",
116 | price: 105,
117 | point: 4.5,
118 | },
119 | {
120 | name: "Over",
121 | description: "Jordan Poole",
122 | price: -135,
123 | point: 4.5,
124 | },
125 | {
126 | name: "Over",
127 | description: "Coby White",
128 | price: -105,
129 | point: 4.5,
130 | },
131 | {
132 | name: "Under",
133 | description: "Coby White",
134 | price: -125,
135 | point: 4.5,
136 | },
137 | {
138 | name: "Over",
139 | description: "Bilal Coulibaly",
140 | price: 120,
141 | point: 4.5,
142 | },
143 | {
144 | name: "Under",
145 | description: "Bilal Coulibaly",
146 | price: -154,
147 | point: 4.5,
148 | },
149 | {
150 | name: "Over",
151 | description: "Zach LaVine",
152 | price: 124,
153 | point: 4.5,
154 | },
155 | {
156 | name: "Under",
157 | description: "Zach LaVine",
158 | price: -160,
159 | point: 4.5,
160 | },
161 | {
162 | name: "Under",
163 | description: "Nikola Vucevic",
164 | price: -175,
165 | point: 3.5,
166 | },
167 | {
168 | name: "Over",
169 | description: "Nikola Vucevic",
170 | price: 135,
171 | point: 3.5,
172 | },
173 | {
174 | name: "Under",
175 | description: "Kyle Kuzma",
176 | price: -130,
177 | point: 2.5,
178 | },
179 | {
180 | name: "Over",
181 | description: "Kyle Kuzma",
182 | price: 100,
183 | point: 2.5,
184 | },
185 | {
186 | name: "Over",
187 | description: "Alex Sarr",
188 | price: 110,
189 | point: 2.5,
190 | },
191 | {
192 | name: "Under",
193 | description: "Alex Sarr",
194 | price: -140,
195 | point: 2.5,
196 | },
197 | {
198 | name: "Under",
199 | description: "Patrick Williams",
200 | price: 145,
201 | point: 1.5,
202 | },
203 | {
204 | name: "Over",
205 | description: "Patrick Williams",
206 | price: -188,
207 | point: 1.5,
208 | },
209 | ],
210 | },
211 | ],
212 | },
213 | {
214 | key: "betrivers",
215 | title: "BetRivers",
216 | markets: [
217 | {
218 | key: "player_assists",
219 | last_update: "2025-01-01T21:16:47Z",
220 | outcomes: [
221 | {
222 | name: "Over",
223 | description: "Nikola Vucevic",
224 | price: 123,
225 | point: 3.5,
226 | },
227 | {
228 | name: "Under",
229 | description: "Nikola Vucevic",
230 | price: -165,
231 | point: 3.5,
232 | },
233 | {
234 | name: "Over",
235 | description: "Coby White",
236 | price: 100,
237 | point: 4.5,
238 | },
239 | {
240 | name: "Under",
241 | description: "Coby White",
242 | price: -132,
243 | point: 4.5,
244 | },
245 | {
246 | name: "Over",
247 | description: "Josh Giddey",
248 | price: -105,
249 | point: 7.5,
250 | },
251 | {
252 | name: "Under",
253 | description: "Josh Giddey",
254 | price: -127,
255 | point: 7.5,
256 | },
257 | {
258 | name: "Over",
259 | description: "Zach LaVine",
260 | price: 123,
261 | point: 4.5,
262 | },
263 | {
264 | name: "Under",
265 | description: "Zach LaVine",
266 | price: -165,
267 | point: 4.5,
268 | },
269 | {
270 | name: "Over",
271 | description: "Bilal Coulibaly",
272 | price: 102,
273 | point: 4.5,
274 | },
275 | {
276 | name: "Under",
277 | description: "Bilal Coulibaly",
278 | price: -134,
279 | point: 4.5,
280 | },
281 | {
282 | name: "Over",
283 | description: "Jordan Poole",
284 | price: -132,
285 | point: 4.5,
286 | },
287 | {
288 | name: "Under",
289 | description: "Jordan Poole",
290 | price: 100,
291 | point: 4.5,
292 | },
293 | {
294 | name: "Over",
295 | description: "Kyle Kuzma",
296 | price: 108,
297 | point: 2.5,
298 | },
299 | {
300 | name: "Under",
301 | description: "Kyle Kuzma",
302 | price: -143,
303 | point: 2.5,
304 | },
305 | ],
306 | },
307 | ],
308 | },
309 | {
310 | key: "betmgm",
311 | title: "BetMGM",
312 | markets: [
313 | {
314 | key: "player_assists",
315 | last_update: "2025-01-01T21:16:03Z",
316 | outcomes: [
317 | {
318 | name: "Over",
319 | description: "Josh Giddey",
320 | price: 100,
321 | point: 7.5,
322 | },
323 | {
324 | name: "Under",
325 | description: "Josh Giddey",
326 | price: -130,
327 | point: 7.5,
328 | },
329 | {
330 | name: "Over",
331 | description: "Bilal Coulibaly",
332 | price: 105,
333 | point: 4.5,
334 | },
335 | {
336 | name: "Under",
337 | description: "Bilal Coulibaly",
338 | price: -140,
339 | point: 4.5,
340 | },
341 | {
342 | name: "Over",
343 | description: "Jordan Poole",
344 | price: -130,
345 | point: 4.5,
346 | },
347 | {
348 | name: "Under",
349 | description: "Jordan Poole",
350 | price: 100,
351 | point: 4.5,
352 | },
353 | {
354 | name: "Over",
355 | description: "Alex Sarr",
356 | price: 120,
357 | point: 2.5,
358 | },
359 | {
360 | name: "Under",
361 | description: "Alex Sarr",
362 | price: -160,
363 | point: 2.5,
364 | },
365 | {
366 | name: "Over",
367 | description: "Coby White",
368 | price: -105,
369 | point: 4.5,
370 | },
371 | {
372 | name: "Under",
373 | description: "Coby White",
374 | price: -130,
375 | point: 4.5,
376 | },
377 | {
378 | name: "Over",
379 | description: "Nikola Vucevic",
380 | price: 130,
381 | point: 3.5,
382 | },
383 | {
384 | name: "Under",
385 | description: "Nikola Vucevic",
386 | price: -175,
387 | point: 3.5,
388 | },
389 | {
390 | name: "Over",
391 | description: "Zach LaVine",
392 | price: 120,
393 | point: 4.5,
394 | },
395 | {
396 | name: "Under",
397 | description: "Zach LaVine",
398 | price: -160,
399 | point: 4.5,
400 | },
401 | {
402 | name: "Over",
403 | description: "Patrick Williams",
404 | price: -200,
405 | point: 1.5,
406 | },
407 | {
408 | name: "Under",
409 | description: "Patrick Williams",
410 | price: 150,
411 | point: 1.5,
412 | },
413 | {
414 | name: "Over",
415 | description: "Kyle Kuzma",
416 | price: 105,
417 | point: 2.5,
418 | },
419 | {
420 | name: "Under",
421 | description: "Kyle Kuzma",
422 | price: -140,
423 | point: 2.5,
424 | },
425 | {
426 | name: "Over",
427 | description: "Jalen Smith",
428 | price: -150,
429 | point: 0.5,
430 | },
431 | {
432 | name: "Under",
433 | description: "Jalen Smith",
434 | price: 110,
435 | point: 0.5,
436 | },
437 | {
438 | name: "Over",
439 | description: "Corey Kispert",
440 | price: 105,
441 | point: 1.5,
442 | },
443 | {
444 | name: "Under",
445 | description: "Corey Kispert",
446 | price: -145,
447 | point: 1.5,
448 | },
449 | ],
450 | },
451 | ],
452 | },
453 | {
454 | key: "bovada",
455 | title: "Bovada",
456 | markets: [
457 | {
458 | key: "player_assists",
459 | last_update: "2025-01-01T21:16:15Z",
460 | outcomes: [
461 | {
462 | name: "Over",
463 | description: "Bilal Coulibaly",
464 | price: 115,
465 | point: 4.5,
466 | },
467 | {
468 | name: "Under",
469 | description: "Bilal Coulibaly",
470 | price: -150,
471 | point: 4.5,
472 | },
473 | {
474 | name: "Over",
475 | description: "Coby White",
476 | price: 120,
477 | point: 4.5,
478 | },
479 | {
480 | name: "Under",
481 | description: "Coby White",
482 | price: -160,
483 | point: 4.5,
484 | },
485 | {
486 | name: "Over",
487 | description: "Jordan Poole",
488 | price: -145,
489 | point: 4.5,
490 | },
491 | {
492 | name: "Under",
493 | description: "Jordan Poole",
494 | price: 110,
495 | point: 4.5,
496 | },
497 | {
498 | name: "Over",
499 | description: "Josh Giddey",
500 | price: -105,
501 | point: 7.5,
502 | },
503 | {
504 | name: "Under",
505 | description: "Josh Giddey",
506 | price: -125,
507 | point: 7.5,
508 | },
509 | {
510 | name: "Over",
511 | description: "Nikola Vucevic",
512 | price: 130,
513 | point: 3.5,
514 | },
515 | {
516 | name: "Under",
517 | description: "Nikola Vucevic",
518 | price: -170,
519 | point: 3.5,
520 | },
521 | {
522 | name: "Over",
523 | description: "Zach LaVine",
524 | price: 130,
525 | point: 4.5,
526 | },
527 | {
528 | name: "Under",
529 | description: "Zach LaVine",
530 | price: -170,
531 | point: 4.5,
532 | },
533 | ],
534 | },
535 | ],
536 | },
537 | {
538 | key: "betonlineag",
539 | title: "BetOnline.ag",
540 | markets: [
541 | {
542 | key: "player_assists",
543 | last_update: "2025-01-01T21:16:19Z",
544 | outcomes: [
545 | {
546 | name: "Over",
547 | description: "Coby White",
548 | price: 107,
549 | point: 4.5,
550 | },
551 | {
552 | name: "Under",
553 | description: "Coby White",
554 | price: -139,
555 | point: 4.5,
556 | },
557 | {
558 | name: "Over",
559 | description: "Zach LaVine",
560 | price: 120,
561 | point: 4.5,
562 | },
563 | {
564 | name: "Under",
565 | description: "Zach LaVine",
566 | price: -156,
567 | point: 4.5,
568 | },
569 | {
570 | name: "Over",
571 | description: "Nikola Vucevic",
572 | price: 134,
573 | point: 3.5,
574 | },
575 | {
576 | name: "Under",
577 | description: "Nikola Vucevic",
578 | price: -175,
579 | point: 3.5,
580 | },
581 | {
582 | name: "Under",
583 | description: "Jordan Poole",
584 | price: 107,
585 | point: 4.5,
586 | },
587 | {
588 | name: "Over",
589 | description: "Jordan Poole",
590 | price: -139,
591 | point: 4.5,
592 | },
593 | {
594 | name: "Over",
595 | description: "Josh Giddey",
596 | price: 100,
597 | point: 7.5,
598 | },
599 | {
600 | name: "Under",
601 | description: "Josh Giddey",
602 | price: -130,
603 | point: 7.5,
604 | },
605 | {
606 | name: "Over",
607 | description: "Bilal Coulibaly",
608 | price: 112,
609 | point: 4.5,
610 | },
611 | {
612 | name: "Under",
613 | description: "Bilal Coulibaly",
614 | price: -145,
615 | point: 4.5,
616 | },
617 | {
618 | name: "Over",
619 | description: "Alex Sarr",
620 | price: 117,
621 | point: 2.5,
622 | },
623 | {
624 | name: "Under",
625 | description: "Alex Sarr",
626 | price: -152,
627 | point: 2.5,
628 | },
629 | {
630 | name: "Over",
631 | description: "Kyle Kuzma",
632 | price: 103,
633 | point: 2.5,
634 | },
635 | {
636 | name: "Under",
637 | description: "Kyle Kuzma",
638 | price: -133,
639 | point: 2.5,
640 | },
641 | {
642 | name: "Over",
643 | description: "Patrick Williams",
644 | price: -192,
645 | point: 1.5,
646 | },
647 | {
648 | name: "Under",
649 | description: "Patrick Williams",
650 | price: 147,
651 | point: 1.5,
652 | },
653 | ],
654 | },
655 | ],
656 | },
657 | ],
658 | };
659 |
--------------------------------------------------------------------------------