├── .yarnrc.yml ├── .eslintrc.json ├── src ├── app │ ├── favicon.ico │ ├── odds │ │ ├── not-found.tsx │ │ ├── [sport] │ │ │ ├── loading.tsx │ │ │ ├── options.ts │ │ │ ├── page.tsx │ │ │ ├── points │ │ │ │ └── page.tsx │ │ │ ├── spread │ │ │ │ └── page.tsx │ │ │ └── moneyline │ │ │ │ └── page.tsx │ │ ├── playerProps │ │ │ ├── loading.tsx │ │ │ ├── ComboBoxClient.tsx │ │ │ └── page.tsx │ │ └── page.tsx │ ├── loading.tsx │ ├── page.tsx │ ├── about │ │ └── page.tsx │ ├── global-error.tsx │ ├── layout.tsx │ ├── globals.css │ └── nav.tsx ├── lib │ └── utils.ts ├── instrumentation.ts ├── components │ ├── LoadingSpinner.tsx │ ├── Snackbar.tsx │ ├── GameHeader.tsx │ ├── PlayerProp.tsx │ ├── OddsContainer.tsx │ ├── ui │ │ ├── popover.tsx │ │ ├── button.tsx │ │ ├── dialog.tsx │ │ └── command.tsx │ ├── OddsMenu.tsx │ ├── ThemeToggle.tsx │ ├── MiniNav.tsx │ ├── Points.tsx │ ├── ComboBox.tsx │ ├── Moneyline.tsx │ ├── Spread.tsx │ ├── OddsTable.tsx │ └── PlayerPropContainer.tsx ├── pages │ ├── api │ │ └── sports.ts │ └── _error.tsx └── instrumentation-client.ts ├── postcss.config.js ├── netlify.toml ├── components.json ├── public ├── vercel.svg ├── thirteen.svg └── next.svg ├── sentry.server.config.ts ├── .gitignore ├── .github └── workflows │ └── ci.yml ├── sentry.edge.config.ts ├── cypress.config.ts ├── tsconfig.json ├── package.json ├── next.config.js ├── cypress └── e2e │ └── sportsbookodds.cy.ts ├── lib ├── playerPropMarkets.json ├── utils.ts ├── api.ts └── dummyPlayerProps.ts └── README.md /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avalos010/sportsbook-odds-comparer/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | "@tailwindcss/postcss": {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /src/app/odds/not-found.tsx: -------------------------------------------------------------------------------- 1 | export default function NotFound() { 2 | return

Uh oh something went wrong! This could not be found.

; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/loading.tsx: -------------------------------------------------------------------------------- 1 | import LoadingSpinner from "@/components/LoadingSpinner"; 2 | 3 | export default function Loading() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/odds/[sport]/loading.tsx: -------------------------------------------------------------------------------- 1 | import LoadingSpinner from "@/components/LoadingSpinner"; 2 | 3 | const spinner = () => ; 4 | export default spinner; 5 | -------------------------------------------------------------------------------- /src/app/odds/playerProps/loading.tsx: -------------------------------------------------------------------------------- 1 | import LoadingSpinner from "@/components/LoadingSpinner"; 2 | 3 | const spinner = () => ; 4 | export default spinner; 5 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "next build" 3 | base = "." 4 | publish = ".next" 5 | 6 | [build.environment] 7 | NEXT_USE_NETLIFY_EDGE = "true" 8 | 9 | [[plugins]] 10 | package = "@netlify/plugin-nextjs" -------------------------------------------------------------------------------- /src/app/odds/[sport]/options.ts: -------------------------------------------------------------------------------- 1 | export const options: Option[] = [ 2 | { key: "MoneyLine", path: "/" }, 3 | { key: "Spread", path: "/spread" }, 4 | { key: "Points", path: "/points" }, 5 | ]; 6 | 7 | interface Option { 8 | key: string; 9 | path: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Inter } from "next/font/google"; 2 | import Odds from "./odds/page"; 3 | 4 | const inter = Inter({ subsets: ["latin"] }); 5 | 6 | export const dynamic = 'force-dynamic'; 7 | 8 | export default function Home() { 9 | return ( 10 | <> 11 | {/* @ts-ignore */} 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/app/odds/[sport]/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | 3 | const Page = async ({ params }: { params: Promise<{ sport: string }> }) => { 4 | const resolvedParams = await params; 5 | const { sport } = resolvedParams; 6 | 7 | return redirect(`odds/${sport}/moneyline`); //redirect to the moneyline page by default 8 | }; 9 | 10 | export default Page; 11 | -------------------------------------------------------------------------------- /src/instrumentation.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/nextjs'; 2 | 3 | export async function register() { 4 | if (process.env.NEXT_RUNTIME === 'nodejs') { 5 | await import('../sentry.server.config'); 6 | } 7 | 8 | if (process.env.NEXT_RUNTIME === 'edge') { 9 | await import('../sentry.edge.config'); 10 | } 11 | } 12 | 13 | export const onRequestError = Sentry.captureRequestError; 14 | -------------------------------------------------------------------------------- /src/app/about/page.tsx: -------------------------------------------------------------------------------- 1 | function About() { 2 | return ( 3 |
4 |

About

5 |

6 | This website shows current sportsbook odds across platforms for many sports. 7 |

8 |
9 | ); 10 | } 11 | 12 | export default About; 13 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /src/components/LoadingSpinner.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | function LoadingSpinner({ className }: { className?: string }) { 4 | return ( 5 |
6 | 10 | Loading 11 |
12 | ); 13 | } 14 | 15 | export default LoadingSpinner; 16 | -------------------------------------------------------------------------------- /src/pages/api/sports.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | import { getInSeasonSports } from "../../../lib/api"; 3 | 4 | export default async function handler( 5 | req: NextApiRequest, 6 | res: NextApiResponse 7 | ) { 8 | if (req.method === "GET") { 9 | // Handle any GET requests 10 | try { 11 | const sports = await getInSeasonSports(); 12 | res.status(200).json(sports); 13 | } catch (error) { 14 | console.error(error); 15 | res.status(500).json({ error: "Something went wrong" }); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Snackbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | function Snackbar({ message, type, className }: SnackbarProps) { 3 | return ( 4 |
12 |

{message}

13 |
14 | ); 15 | } 16 | 17 | interface SnackbarProps { 18 | message: string; 19 | type: "success" | "error"; 20 | className?: string; 21 | } 22 | 23 | export default Snackbar; 24 | -------------------------------------------------------------------------------- /sentry.server.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the server. 2 | // The config you add here will be used whenever the server handles a request. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from "@sentry/nextjs"; 6 | 7 | Sentry.init({ 8 | dsn: "https://34bb1ddda21cb71a68251f77d41cab2d@o4510298488963072.ingest.us.sentry.io/4510298491977728", 9 | 10 | // Enable logs to be sent to Sentry 11 | enableLogs: true, 12 | 13 | // Enable sending user PII (Personally Identifiable Information) 14 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/options/#sendDefaultPii 15 | sendDefaultPii: true, 16 | }); 17 | -------------------------------------------------------------------------------- /src/components/GameHeader.tsx: -------------------------------------------------------------------------------- 1 | export default function GameHeader({ 2 | homeTeam, 3 | awayTeam, 4 | commenceTime, 5 | className = "", 6 | }: GameHeaderProps) { 7 | return ( 8 |
9 |

10 | {awayTeam} vs {homeTeam} 11 |

12 |

13 | {new Date(commenceTime).toLocaleString()} 14 |

15 |
16 | ); 17 | } 18 | 19 | interface GameHeaderProps { 20 | homeTeam: string; 21 | awayTeam: string; 22 | commenceTime: string; 23 | className?: string; 24 | } 25 | -------------------------------------------------------------------------------- /src/pages/_error.tsx: -------------------------------------------------------------------------------- 1 | import * as Sentry from "@sentry/nextjs"; 2 | import Error from "next/error"; 3 | import type { NextPageContext } from "next"; 4 | 5 | const CustomErrorComponent = (props: { statusCode: number }) => { 6 | return ; 7 | }; 8 | 9 | CustomErrorComponent.getInitialProps = async (contextData: NextPageContext) => { 10 | // In case this is running in a serverless function, await this in order to give Sentry 11 | // time to send the error before the lambda exits 12 | await Sentry.captureUnderscoreErrorException(contextData); 13 | 14 | // This will contain the status code of the response 15 | return Error.getInitialProps(contextData); 16 | }; 17 | 18 | export default CustomErrorComponent; 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | next-env.d.ts 39 | 40 | .vscode 41 | 42 | #cypress 43 | cypress/videos 44 | cypress/screenshots 45 | cypress/downloads 46 | 47 | #yarn 48 | .yarn 49 | # Sentry Config File 50 | .env.sentry-build-plugin 51 | -------------------------------------------------------------------------------- /src/app/global-error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as Sentry from "@sentry/nextjs"; 4 | import NextError from "next/error"; 5 | import { useEffect } from "react"; 6 | 7 | export default function GlobalError({ error }: { error: Error & { digest?: string } }) { 8 | useEffect(() => { 9 | Sentry.captureException(error); 10 | }, [error]); 11 | 12 | return ( 13 | 14 | 15 | {/* `NextError` is the default Next.js error page component. Its type 16 | definition requires a `statusCode` prop. However, since the App Router 17 | does not expose status codes for errors, we simply pass 0 to render a 18 | generic error message. */} 19 | 20 | 21 | 22 | ); 23 | } -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | deployment_status: 5 | 6 | jobs: 7 | e2e: 8 | if: github.event_name == 'deployment_status' && github.event.deployment_status.state == 'success' 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Dump GitHub context 13 | env: 14 | GITHUB_CONTEXT: ${{ toJson(github) }} 15 | run: | 16 | echo "$GITHUB_CONTEXT" 17 | 18 | - name: Checkout 19 | uses: actions/checkout@v3 20 | 21 | - name: Update Lockfile and Install Dependencies 22 | run: yarn install --check-files 23 | 24 | - name: Run Cypress 🌲 25 | uses: cypress-io/github-action@v5 26 | env: 27 | CYPRESS_BASE_URL: ${{ github.event.deployment_status.target_url }} 28 | -------------------------------------------------------------------------------- /src/app/odds/playerProps/ComboBoxClient.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import ComboBox from "@/components/ComboBox"; 4 | import { useSearchParams } from "next/navigation"; 5 | 6 | interface ComboBoxClientProps { 7 | marketsList: { 8 | label: string; 9 | value: string; 10 | }[]; 11 | } 12 | 13 | function ComboBoxClient({ marketsList }: ComboBoxClientProps) { 14 | const searchParams = useSearchParams(); 15 | 16 | const handlePropSelect = (key: string, label: string) => { 17 | const params = new URLSearchParams(searchParams?.toString()); 18 | params.set("markets", key); 19 | params.set("marketsLabel", label); 20 | window.history.pushState(null, "", `?${params.toString()}`); 21 | }; 22 | 23 | return ; 24 | } 25 | 26 | export default ComboBoxClient; 27 | -------------------------------------------------------------------------------- /sentry.edge.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on). 2 | // The config you add here will be used whenever one of the edge features is loaded. 3 | // Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally. 4 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 5 | 6 | import * as Sentry from "@sentry/nextjs"; 7 | 8 | Sentry.init({ 9 | dsn: "https://34bb1ddda21cb71a68251f77d41cab2d@o4510298488963072.ingest.us.sentry.io/4510298491977728", 10 | 11 | // Enable logs to be sent to Sentry 12 | enableLogs: true, 13 | 14 | // Enable sending user PII (Personally Identifiable Information) 15 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/options/#sendDefaultPii 16 | sendDefaultPii: true, 17 | }); 18 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import Nav from "./nav"; 2 | import { Analytics } from "@vercel/analytics/react"; 3 | import "./globals.css"; 4 | import MiniNav from "@/components/MiniNav"; 5 | import { Inter } from "next/font/google"; 6 | 7 | const inter = Inter({ subsets: ["latin"], display: "swap" }); 8 | 9 | export const metadata = { 10 | title: "SportsBook Odds Comparer", 11 | description: "compare odds across all sportsbooks", 12 | }; 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: { 17 | children: React.ReactNode; 18 | }) { 19 | return ( 20 | 21 | 22 | Skip to main content 23 |