├── .eslintrc.json
├── .gitignore
├── .idea
├── .gitignore
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── misc.xml
├── modules.xml
├── prettier.xml
├── ultrabet-ui.iml
└── vcs.xml
├── README.md
├── codegen.ts
├── graphql.config.yml
├── next.config.js
├── package.json
├── pnpm-lock.yaml
├── public
├── next.svg
├── ui.js
└── vercel.svg
├── remote-schema.graphql
├── src
├── app
│ ├── (events)
│ │ ├── [group]
│ │ │ ├── [market]
│ │ │ │ └── page.tsx
│ │ │ └── page.tsx
│ │ ├── apollo-wrapper.tsx
│ │ ├── layout.tsx
│ │ ├── page.module.css
│ │ └── page.tsx
│ ├── api
│ │ ├── auth
│ │ │ └── [auth0]
│ │ │ │ └── route.ts
│ │ ├── slip-options
│ │ │ ├── [id]
│ │ │ │ └── route.ts
│ │ │ └── route.ts
│ │ └── slip
│ │ │ └── route.ts
│ ├── bets
│ │ ├── loading.tsx
│ │ ├── page.module.css
│ │ └── page.tsx
│ ├── design-tokens.css
│ ├── favicon.ico
│ ├── global-layout.css
│ ├── layout.tsx
│ └── utilities.css
├── gql
│ ├── documents.generated.ts
│ ├── documents.graphql
│ └── types.generated.ts
├── lib
│ ├── apollo-link.ts
│ ├── client.ts
│ ├── slip-context.tsx
│ ├── useSlip.ts
│ └── util.ts
└── ui
│ ├── bet-slip
│ ├── bet-slip.module.css
│ ├── bet-slip.tsx
│ ├── place-bet-form.tsx
│ └── remove-slip-option-form.tsx
│ ├── card
│ ├── card-actions.module.css
│ ├── card-actions.tsx
│ ├── card-content.module.css
│ ├── card-content.tsx
│ ├── card-header.module.css
│ ├── card-header.tsx
│ ├── card-media.module.css
│ ├── card-media.tsx
│ ├── card.module.css
│ └── card.tsx
│ ├── date-util.ts
│ ├── event-list
│ ├── add-slip-option-form.tsx
│ ├── elapsed-time.tsx
│ ├── event-list.module.css
│ ├── event-list.tsx
│ ├── live-event-list.tsx
│ └── market.tsx
│ ├── event-util.ts
│ ├── globals.module.css
│ ├── page-nav.module.css
│ ├── page-nav.tsx
│ ├── side-menu
│ └── side-menu.tsx
│ ├── sport-list
│ ├── sport-list.module.css
│ └── sport-list.tsx
│ └── top-bar
│ ├── top-bar.module.css
│ └── top-bar.tsx
└── tsconfig.json
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.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 |
27 | # local env files
28 | .env*.local
29 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 | # Datasource local storage ignored files
7 | /dataSources/
8 | /dataSources.local.xml
9 | # GitHub Copilot persisted chat sessions
10 | /copilot/chatSessions
11 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/prettier.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/ultrabet-ui.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # A sports betting webapp
2 |
3 | Demo betting frontend: https://www.parabolicbet.com/
4 |
5 | * Shows in-play (live) events and upcoming events in different tabs
6 | * Live event odds and scores are update as they happen
7 | * The backend is [in my repository called `ultrabet`](https://github.com/anssip/ultrabet)
8 |
9 | [See it live here](https://www.parabolicbet.com/).
10 |
11 |
12 |
13 | ## Development
14 |
15 | This is a [Next.js](https://nextjs.org/) project bootstrapped
16 | with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
17 |
18 | First, run the development server:
19 |
20 | ```bash
21 | npm run dev
22 | # or
23 | yarn dev
24 | # or
25 | pnpm dev
26 | ```
27 |
28 | ## TODO
29 |
30 | See the [complete roadmap here](https://github.com/anssip/ultrabet?tab=readme-ov-file#roadmap)
31 |
32 | - [ ] Add loading state for add to slip action
33 | - [ ] Redesign the bet slip
34 | - [ ] Redesign the bets page
35 | - [ ] Spreads (handicap) market
36 | - [ ] Event view that shows all markets and options for an event
37 | - [ ] Show gravatars (url is in Auth0)
38 | - [ ] Update live scores in bets view
39 | - [ ] Fix global styling
40 | - [x] Show scores in bets view
41 | - [x] Bug: does not accept a long bet only without any singles
42 | - [x] Show some loading indicator when placing a bet
43 | - [x[ Add a loading skeleton for the bets view
44 | - [x] Fix the background color of the bets page
45 | - [x] Bet slip UI
46 |
--------------------------------------------------------------------------------
/codegen.ts:
--------------------------------------------------------------------------------
1 | import type { CodegenConfig } from '@graphql-codegen/cli'
2 |
3 | const config: CodegenConfig = {
4 | overwrite: true,
5 | schema: 'https://parabolic-snowy-cloud-5094.fly.dev/graphql',
6 | documents: ['src/**/*.graphql'],
7 | generates: {
8 | 'src/gql/types.generated.ts': { plugins: ['typescript'] },
9 | 'src/gql/': {
10 | preset: 'near-operation-file',
11 | presetConfig: {
12 | extension: '.generated.ts',
13 | baseTypesPath: 'types.generated.ts',
14 | },
15 | plugins: ['typescript-operations', 'typed-document-node'],
16 | },
17 | },
18 | }
19 |
20 | export default config
21 |
--------------------------------------------------------------------------------
/graphql.config.yml:
--------------------------------------------------------------------------------
1 | schema: remote-schema.graphql
2 | extensions:
3 | endpoints:
4 | Parabolic Betting API:
5 | url: https://parabolic-snowy-cloud-5094.fly.dev/graphql
6 | headers:
7 | user-agent: JS GraphQL
8 | introspect: true
9 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {}
3 |
4 | module.exports = nextConfig
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ultrabet-ui",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "prebuild": "graphql-codegen --config codegen.ts",
8 | "build": "next build",
9 | "start": "next start",
10 | "lint": "next lint"
11 | },
12 | "dependencies": {
13 | "@apollo/client": "3.8.6",
14 | "@apollo/experimental-nextjs-app-support": "^0.5.0",
15 | "@auth0/nextjs-auth0": "^3.2.0",
16 | "@graphql-typed-document-node/core": "^3.2.0",
17 | "@mantine/core": "^6.0.10",
18 | "@mantine/hooks": "^6.0.10",
19 | "@vercel/analytics": "^1.1.1",
20 | "@vercel/kv": "^0.2.3",
21 | "classnames": "^2.5.1",
22 | "eslint": "8.40.0",
23 | "eslint-config-next": "13.4.1",
24 | "graphql": "^16.6.0",
25 | "graphql-ws": "^5.13.1",
26 | "luxon": "^3.4.3",
27 | "next": "14.0.1",
28 | "ramda": "^0.29.0",
29 | "react": "18.3.0-canary-0c6348758-20231030",
30 | "react-dom": "18.3.0-canary-0c6348758-20231030",
31 | "react-transition-group": "^4.4.5"
32 | },
33 | "devDependencies": {
34 | "@graphql-codegen/cli": "^3.3.1",
35 | "@graphql-codegen/client-preset": "^3.0.1",
36 | "@graphql-codegen/near-operation-file-preset": "^2.5.0",
37 | "@types/luxon": "^3.3.3",
38 | "@types/node": "20.1.0",
39 | "@types/ramda": "^0.29.2",
40 | "@types/react": "18.2.8",
41 | "@types/react-dom": "18.2.4",
42 | "@types/react-transition-group": "^4.4.6",
43 | "prettier": "^2.8.8",
44 | "ts-node": "^10.9.1",
45 | "typescript": "5.1.3"
46 | },
47 | "prettier": {
48 | "semi": false,
49 | "singleQuote": true,
50 | "trailingComma": "es5",
51 | "printWidth": 100
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/ui.js:
--------------------------------------------------------------------------------
1 | ;(function (window, document) {
2 | // we fetch the elements each time because docusaurus removes the previous
3 | // element references on page navigation
4 | function getElements() {
5 | return {
6 | layout: document.getElementById('layout'),
7 | menu: document.getElementById('menu'),
8 | menuLink: document.getElementById('menuLink'),
9 | }
10 | }
11 |
12 | function toggleClass(element, className) {
13 | var classes = element.className.split(/\s+/)
14 | var length = classes.length
15 | var i = 0
16 |
17 | for (; i < length; i++) {
18 | if (classes[i] === className) {
19 | classes.splice(i, 1)
20 | break
21 | }
22 | }
23 | // The className is not found
24 | if (length === classes.length) {
25 | classes.push(className)
26 | }
27 |
28 | element.className = classes.join(' ')
29 | }
30 |
31 | function toggleAll() {
32 | var active = 'active'
33 | var elements = getElements()
34 | console.log('elements', elements)
35 |
36 | toggleClass(elements.layout, active)
37 | toggleClass(elements.menu, active)
38 | toggleClass(elements.menuLink, active)
39 | }
40 |
41 | function handleEvent(e) {
42 | var elements = getElements()
43 | console.log(
44 | 'target',
45 | e.target.id,
46 | elements.menuLink.id,
47 | elements.menu.className.indexOf('active') !== -1
48 | )
49 |
50 | if (e.target.id === elements.menuLink.id) {
51 | console.log('toggle all')
52 | toggleAll()
53 | e.preventDefault()
54 | } else if (elements.menu.className.indexOf('active') !== -1) {
55 | toggleAll()
56 | }
57 | }
58 |
59 | document.addEventListener('click', handleEvent)
60 | })(this, this.document)
61 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/remote-schema.graphql:
--------------------------------------------------------------------------------
1 | # This file was generated. Do not edit manually.
2 |
3 | schema {
4 | query: Query
5 | mutation: Mutation
6 | subscription: Subscription
7 | }
8 |
9 | type Bet {
10 | betOptions: [BetOption]
11 | createdAt: String!
12 | id: ID!
13 | potentialWinnings: Float!
14 | stake: Float!
15 | status: BetStatus!
16 | user: User
17 | }
18 |
19 | type BetOption {
20 | bet: Bet!
21 | id: Int!
22 | marketOption: MarketOption!
23 | status: BetStatus
24 | }
25 |
26 | "An Event represents a sports match or competition on which users can place bets."
27 | type Event {
28 | awayTeamName: String!
29 | "Is this event completed?"
30 | completed: Boolean!
31 | "The id of the event in the source system."
32 | externalId: String
33 | homeTeamName: String!
34 | id: ID!
35 | "Is this event currently live?"
36 | isLive: Boolean!
37 | markets(source: String): [Market]
38 | name: String!
39 | result: EventResult
40 | scoreUpdates: [ScoreUpdate]
41 | sport: Sport!
42 | startTime: String!
43 | }
44 |
45 | """
46 |
47 | A Market represents a specific betting opportunity within an event.
48 | It's usually associated with one aspect of the event that users can bet on.
49 | A single event can have multiple markets. Some common examples of markets
50 | include Moneyline, Point Spread, and Totals (Over/Under).
51 | """
52 | type Market {
53 | event: Event
54 | id: ID!
55 | "Is this Market available for live betting?"
56 | isLive: Boolean!
57 | """
58 |
59 | When was this Market last updated? Used to track when the odds were last
60 | updated during live betting.
61 | """
62 | lastUpdated: String
63 | name: String!
64 | options: [MarketOption]
65 | "What is the source or bookmaker that provides the odds for this Market?"
66 | source: String!
67 | }
68 |
69 | """
70 |
71 | A MarketOption represents a specific choice or outcome within a market
72 | that users can bet on. Each market typically has two or more options to choose from.
73 | """
74 | type MarketOption {
75 | description: String
76 | id: ID!
77 | """
78 |
79 | When was this Market last updated? Used to track when the odds were last
80 | updated during live betting.
81 | """
82 | lastUpdated: String
83 | market: Market
84 | name: String!
85 | odds: Float!
86 | point: Float
87 | }
88 |
89 | " Mutations"
90 | type Mutation {
91 | " should be available for admins only:"
92 | createEvent(name: String!, sport: String!, startTime: String!): Event
93 | createMarket(eventId: ID!, name: String!): Market
94 | createMarketOption(marketId: ID!, name: String!, odds: Float!): MarketOption
95 | createUser(email: String!, username: String!): User
96 | depositFunds(amount: Float!, userId: ID!): Wallet
97 | "Places a bet on the provided market options."
98 | placeBet(betType: BetType!, marketOptions: [ID!]!, stake: Float!): Bet
99 | "Place multiple single bets, one for each option provided."
100 | placeSingleBets(options: [BetOptionInput!]!): [Bet]
101 | updateResult(eventId: ID!): Event
102 | withdrawFunds(amount: Float!, userId: ID!): Wallet
103 | }
104 |
105 | " Queries"
106 | type Query {
107 | "List events available for betting in the specified sport group. Lists both live an upcoming events."
108 | eventsBySportGroup(group: String!): [Event]
109 | getBet(id: ID!): Bet
110 | getEvent(id: ID!): Event
111 | getMarket(id: ID!): Market
112 | "List all events available for betting. Lists both live an upcoming events."
113 | listAllEvents: [Event]
114 | listBets: [Bet]
115 | "List upcoming events available for betting."
116 | listEvents: [Event]
117 | "List live events available for betting."
118 | listLiveEvents: [Event]
119 | listLiveMarkets(eventId: ID!): [Market]
120 | listMarkets(eventId: ID!): [Market]
121 | "List sports by group."
122 | listSports(group: String): [Sport]
123 | me: User
124 | }
125 |
126 | type ScoreUpdate {
127 | event: Event!
128 | id: ID!
129 | name: String!
130 | score: String!
131 | timestamp: String!
132 | }
133 |
134 | type Sport {
135 | "Is this sport in season at the moment?"
136 | active: Boolean!
137 | activeEventCount: Int
138 | description: String!
139 | "List all events available for betting in this sport."
140 | events: [Event]
141 | group: String!
142 | "Does this sport have outright markets?"
143 | hasOutrights: Boolean!
144 | id: ID!
145 | key: String!
146 | title: String!
147 | }
148 |
149 | type Subscription {
150 | eventScoresUpdated: [Event]
151 | eventStatusUpdated: [Event]
152 | liveMarketOptionsUpdated: [MarketOption]
153 | }
154 |
155 | type Transaction {
156 | amount: Float!
157 | createdAt: String!
158 | id: ID!
159 | transactionType: TransactionType!
160 | wallet: Wallet
161 | }
162 |
163 | type User {
164 | bets: [Bet]
165 | email: String
166 | externalId: String
167 | id: ID!
168 | username: String
169 | wallet: Wallet
170 | }
171 |
172 | type Wallet {
173 | balance: Float!
174 | id: ID!
175 | transactions: [Transaction]
176 | user: User
177 | }
178 |
179 | enum BetStatus {
180 | CANCELED
181 | LOST
182 | PENDING
183 | WON
184 | }
185 |
186 | "https://chat.openai.com/share/92b7bc9f-6fc6-4f57-9a4e-f217270ad271"
187 | enum BetType {
188 | PARLAY
189 | SINGLE
190 | " same as long bet or accumulator"
191 | SYSTEM
192 | }
193 |
194 | enum EventResult {
195 | AWAY_TEAM_WIN
196 | DRAW
197 | HOME_TEAM_WIN
198 | }
199 |
200 | enum TransactionType {
201 | BET_PLACED
202 | BET_REFUNDED
203 | BET_WON
204 | DEPOSIT
205 | WITHDRAWAL
206 | }
207 |
208 | "A slightly refined version of RFC-3339 compliant DateTime Scalar"
209 | scalar DateTime
210 |
211 | input BetOptionInput {
212 | marketOptionId: ID!
213 | stake: Float!
214 | }
215 |
--------------------------------------------------------------------------------
/src/app/(events)/[group]/[market]/page.tsx:
--------------------------------------------------------------------------------
1 | import { getClient } from '@/lib/client'
2 | import { ListSportsWithEventsDocument, SportWithEventsFragment } from '@/gql/documents.generated'
3 | import styles from '../../page.module.css'
4 | import React from 'react'
5 | import { MarketNav, PageNav } from '@/ui/page-nav'
6 | import classnames from 'classnames'
7 | import { SportList } from '@/ui/sport-list/sport-list'
8 |
9 | export const revalidate = 0
10 | export const dynamic = 'force-dynamic'
11 |
12 | async function fetchSports(params: { group: string; market: string }) {
13 | const start = new Date().getTime()
14 | console.log('fetchSports', params.group)
15 | try {
16 | const result = await getClient().query({
17 | query: ListSportsWithEventsDocument,
18 | variables: { group: (params.group ? decodeURIComponent(params.group) : 'all') ?? 'all' },
19 | })
20 | console.log('fetchSports completed in ', new Date().getTime() - start)
21 | return result
22 | } catch (e) {
23 | console.error('fetchSports', e)
24 | return { data: { listSports: [] } }
25 | }
26 | }
27 |
28 | export default async function Page({ params }: { params: { group: string; market: string } }) {
29 | const data = await fetchSports(params)
30 | const sports = data.data.listSports as SportWithEventsFragment[]
31 |
32 | return (
33 |
34 |
35 |
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/src/app/(events)/[group]/page.tsx:
--------------------------------------------------------------------------------
1 | import MarketPage from './[market]/page'
2 |
3 | export const revalidate = 60
4 |
5 | export default async function Page({ params }: { params: { group: string; market?: string } }) {
6 | return MarketPage({ params: { ...params, market: params.market ?? 'h2h' } })
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/(events)/apollo-wrapper.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { ApolloLink, HttpLink } from '@apollo/client'
4 | import {
5 | ApolloNextAppProvider,
6 | NextSSRInMemoryCache,
7 | SSRMultipartLink,
8 | // @ts-ignore
9 | NextSSRApolloClient,
10 | } from '@apollo/experimental-nextjs-app-support/ssr'
11 | import { splitLink } from '@/lib/apollo-link'
12 | import { useUser } from '@auth0/nextjs-auth0/client'
13 |
14 | const makeClient = (isAuthenticated: boolean) => () => {
15 | const httpLink = new HttpLink({
16 | uri:
17 | (isAuthenticated
18 | ? process.env.NEXT_PUBLIC_BETTING_API_URL
19 | : process.env.NEXT_PUBLIC_API_URL) + '/graphql',
20 | })
21 |
22 | return new NextSSRApolloClient({
23 | cache: new NextSSRInMemoryCache(),
24 | link:
25 | typeof window === 'undefined'
26 | ? ApolloLink.from([
27 | // in a SSR environment, if you use multipart features like
28 | // @defer, you need to decide how to handle these.
29 | // This strips all interfaces with a `@defer` directive from your queries.
30 | new SSRMultipartLink({
31 | stripDefer: true,
32 | }),
33 | httpLink,
34 | ])
35 | : splitLink(httpLink),
36 | })
37 | }
38 |
39 | export function ApolloWrapper({ children }: React.PropsWithChildren) {
40 | const { user } = useUser()
41 | console.log('ApolloWrapper user', user)
42 | return {children}
43 | }
44 |
--------------------------------------------------------------------------------
/src/app/(events)/layout.tsx:
--------------------------------------------------------------------------------
1 | import { ApolloWrapper } from './apollo-wrapper'
2 | import { SideMenu } from '@/ui/side-menu/side-menu'
3 | import React from 'react'
4 | import { getClient } from '@/lib/client'
5 | import { ListSportsDocument } from '@/gql/documents.generated'
6 | import { Sport } from '@/gql/types.generated'
7 | import '../../app/global-layout.css'
8 |
9 | export default async function EventsLayout({ children }: { children: React.ReactNode }) {
10 | const data = await getClient(false).query({
11 | query: ListSportsDocument,
12 | })
13 | const sports = data.data.listSports as Sport[]
14 |
15 | return (
16 | <>
17 |
18 | {/**/}
19 |
20 |
21 |
22 |
23 | {children}
24 |
25 | >
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/(events)/page.module.css:
--------------------------------------------------------------------------------
1 | .eventsContainer {
2 | padding: 2em;
3 | background-color: var(--off-white);
4 | }
5 |
6 | .eventsContainer h2 {
7 | margin-top: 0;
8 | }
--------------------------------------------------------------------------------
/src/app/(events)/page.tsx:
--------------------------------------------------------------------------------
1 | import MarketPage from './[group]/page'
2 |
3 | export const revalidate = 60
4 | // export const dynamic = 'force-dynamic'
5 |
6 | export default async function Page({ params }: { params: { group: string } }) {
7 | return MarketPage({ params: { ...params, group: params.group } })
8 | }
9 |
--------------------------------------------------------------------------------
/src/app/api/auth/[auth0]/route.ts:
--------------------------------------------------------------------------------
1 | import { handleAuth, handleLogin } from '@auth0/nextjs-auth0'
2 |
3 | export const GET = handleAuth({
4 | login: handleLogin({
5 | authorizationParams: {
6 | audience: process.env.AUTH0_AUDIENCE,
7 |
8 | // Add the `offline_access` scope to also get a Refresh Token
9 | scope: 'openid profile email',
10 | },
11 | }),
12 | })
13 |
--------------------------------------------------------------------------------
/src/app/api/slip-options/[id]/route.ts:
--------------------------------------------------------------------------------
1 | import { getSession, withApiAuthRequired } from '@auth0/nextjs-auth0'
2 | import { NextRequest, NextResponse } from 'next/server'
3 | import { kv } from '@vercel/kv'
4 | import { BetSlipOption } from '@/ui/bet-slip/bet-slip'
5 | import { MarketOption } from '@/gql/types.generated'
6 |
7 | export const DELETE = withApiAuthRequired(async function removeSlipOption(req: NextRequest) {
8 | const res = new NextResponse()
9 | const session = await getSession(req, res)
10 | if (!session) {
11 | return NextResponse.json({ error: 'Unauthorized', status: 400 }, res)
12 | }
13 | const { sub } = session.user
14 | const optionId = req.url.split('/').pop()
15 | if (typeof optionId !== 'string') {
16 | return NextResponse.json({ error: 'A valid optionId is required', status: 400 }, res)
17 | }
18 | await kv.hdel(`betslip:${sub}`, optionId)
19 | return NextResponse.json({ success: true }, res)
20 | })
21 |
--------------------------------------------------------------------------------
/src/app/api/slip-options/route.ts:
--------------------------------------------------------------------------------
1 | import { getSession, withApiAuthRequired } from '@auth0/nextjs-auth0'
2 | import { NextRequest, NextResponse } from 'next/server'
3 | import { MarketOption } from '@/gql/types.generated'
4 | import { kv } from '@vercel/kv'
5 |
6 | export const POST = withApiAuthRequired(async function addSlipOption(req: NextRequest) {
7 | const res = new NextResponse()
8 | const session = await getSession(req, res)
9 | if (!session) {
10 | return NextResponse.json({ error: 'Unauthorized', status: 400 }, res)
11 | }
12 | const { sub } = session.user
13 | const option = (await req.json()) as MarketOption
14 | console.log('option', option)
15 |
16 | await kv.hset(`betslip:${sub}`, { [`${option.id}`]: option })
17 | return NextResponse.json({ success: true }, res)
18 | })
19 |
--------------------------------------------------------------------------------
/src/app/api/slip/route.ts:
--------------------------------------------------------------------------------
1 | import { kv } from '@vercel/kv'
2 | import { NextRequest, NextResponse } from 'next/server'
3 | import { getAccessToken, getSession, withApiAuthRequired } from '@auth0/nextjs-auth0'
4 | import { getClient } from '@/lib/client'
5 | import { PlaceBetDocument, PlaceSingleBetsDocument } from '@/gql/documents.generated'
6 | import { BetType } from '@/gql/types.generated'
7 | import { BetSlipOption, Slip } from '@/ui/bet-slip/bet-slip'
8 |
9 | // https://github.com/auth0/nextjs-auth0/blob/main/EXAMPLES.md#app-router-1
10 | export const GET = withApiAuthRequired(async function myApiRoute(req) {
11 | const res = new NextResponse()
12 | const session = await getSession(req, res)
13 | if (!session) {
14 | return NextResponse.json({ error: 'Unauthorized', status: 400 }, res)
15 | }
16 | const slip: Slip = (await kv.hgetall(`betslip:${session.user.sub}`)) ?? {}
17 | return NextResponse.json({ slip, id: session.user.sub }, res)
18 | })
19 |
20 | async function clearSlip(options: BetSlipOption[], sub: string) {
21 | console.log(`clearing slip for user ${sub}`, options)
22 | await Promise.all(options.map((option) => kv.hdel(`betslip:${sub}`, option.id)))
23 | }
24 |
25 | export const POST = withApiAuthRequired(async function postSlipRoute(req) {
26 | const res = new NextResponse()
27 | const token = await getAccessToken(req, res, { scopes: ['email', 'profile', 'openid'] })
28 | console.log('got access token', token)
29 |
30 | const bets = await req.json()
31 | console.log('bets', bets)
32 |
33 | const betOptions = Object.values(bets.singles) as BetSlipOption[]
34 | const singles = betOptions.filter((single) => !!single.stake)
35 | const long = bets.long as BetSlipOption
36 |
37 | try {
38 | const client = getClient(true)
39 | const [placeBetData, placeSingleBetsData] = await Promise.all([
40 | long?.stake ?? 0 > 0
41 | ? client.mutate({
42 | mutation: PlaceBetDocument,
43 | variables: {
44 | marketOptions: betOptions.map((option) => option.id),
45 | betType: BetType.Parlay,
46 | stake: long.stake as number,
47 | },
48 | context: {
49 | headers: {
50 | Authorization: `Bearer ${token.accessToken}`,
51 | },
52 | },
53 | })
54 | : Promise.resolve(null),
55 | singles.length > 0
56 | ? client.mutate({
57 | mutation: PlaceSingleBetsDocument,
58 | variables: {
59 | options: singles.map((option) => ({
60 | marketOptionId: option.id,
61 | stake: option.stake as number,
62 | })),
63 | },
64 | context: {
65 | headers: {
66 | Authorization: `Bearer ${token.accessToken}`,
67 | },
68 | },
69 | })
70 | : Promise.resolve(null),
71 | ])
72 | console.log('got response data', JSON.stringify({ placeBetData, placeSingleBetsData }, null, 2))
73 |
74 | const session = await getSession(req, res)
75 | await clearSlip([...betOptions, long], session?.user.sub)
76 |
77 | const longBet = placeBetData?.data?.placeBet
78 | const singleBets = placeSingleBetsData?.data?.placeSingleBets
79 | // res.status(200).json({ success: true, data: { singles: singleBets, long: longBet } })
80 | return NextResponse.json({ success: true, data: { singles: singleBets, long: longBet } })
81 | } catch (error) {
82 | console.error('error placing bet', error)
83 | // @ts-ignore
84 | return NextResponse.json({ error: error.message, status: 400 }, res)
85 | }
86 | })
87 |
--------------------------------------------------------------------------------
/src/app/bets/loading.tsx:
--------------------------------------------------------------------------------
1 | import styles from './page.module.css'
2 | import React from 'react'
3 | import { Card } from '@/ui/card/card'
4 | import CardHeader from '@/ui/card/card-header'
5 | import { CardContent } from '@/ui/card/card-content'
6 |
7 | export default function Loading() {
8 | return (
9 |
10 | My Bets
11 |
12 |
13 |
14 |
15 | loading...
16 |
17 |
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/src/app/bets/page.module.css:
--------------------------------------------------------------------------------
1 |
2 | .main {
3 | background-color: var(--off-white);
4 | color: var(--dark-grey);
5 | padding-top: 50px;
6 | display: flex;
7 | flex-direction: column;
8 | align-items: center;
9 | gap: 1em;
10 | min-height: 100vh;
11 | /* no left sidebar here */
12 | margin: 0 0 0 -190px;
13 | }
14 |
15 | .header {
16 | }
17 |
18 | .betItem {
19 | width: 80%;
20 | display: flex;
21 | flex-direction: column;
22 | color: var(--dark-grey);
23 | border: 1px solid rgb(255, 255, 0, 0.4);
24 | }
25 |
26 | .options {
27 | padding-left: 1em;
28 | }
29 |
30 | .betHeader {
31 | display: flex;
32 | flex-direction: row;
33 | justify-content: space-between;
34 | font-weight: bold;
35 | }
36 |
37 | .betDetails {
38 | list-style: none;
39 | position: relative;
40 | gap: 0.5em;
41 | margin: 0.2em 1.5em;
42 | }
43 |
44 | .betMeta {
45 | font-size: var(--font-size-body);
46 | font-weight: 200;
47 | }
48 |
49 | .status::before {
50 | content: "";
51 | position: absolute;
52 | width: 7px;
53 | height: 7px;
54 | left: -20px;
55 | top: 5px;
56 | border: 1px solid var(--dark-grey);
57 | border-radius: 50%;
58 | }
59 |
60 | .status-pending {
61 | color: var(--dark-grey);
62 | }
63 |
64 | .status-lost::before {
65 | background-color: red;
66 | border: 2px solid red;
67 | }
68 |
69 | .status-won::before {
70 | background-color: green;
71 | border: 2px solid green;
72 | }
73 |
74 | .status-canceled::before {
75 | border: 2px solid darkgray;
76 | }
77 |
78 | .marketOption {
79 | display: flex;
80 | flex-direction: row;
81 | justify-content: space-between;
82 | font-size: 1.2rem;
83 | }
84 |
85 | .smallText {
86 | font-size: var(--font-size-caption);
87 | font-weight: 400;
88 | }
89 |
90 | .smallestText {
91 | font-size: 0.4rem;
92 | font-weight: 100;
93 | }
94 |
95 | .eventName {
96 | padding: 0.5em;
97 | }
98 |
99 | .score {
100 | padding: 1em;
101 | color: var(--egyptian-blue);
102 | .live {
103 | text-transform: uppercase;
104 | font-family: monospace;
105 | color: var(--salmon);
106 | padding-left: 0.5rem;
107 | }
108 | }
109 |
110 | .numbers {
111 | display: flex;
112 | flex-direction: row;
113 | justify-content: space-between;
114 | align-items: baseline;
115 | gap: 1em;
116 | margin: 0 1.5em;
117 | border-top: 2px solid var(--salmon);
118 | padding-top: 0.5em
119 | }
120 |
121 | .number {
122 | display: flex;
123 | flex-direction: column;
124 | gap: 0.5em;
125 | }
--------------------------------------------------------------------------------
/src/app/bets/page.tsx:
--------------------------------------------------------------------------------
1 | import { getClient } from '@/lib/client'
2 | import { ListBetsDocument } from '@/gql/documents.generated'
3 | import styles from './page.module.css'
4 | import React from 'react'
5 | import { Bet, BetOption, BetStatus, Maybe } from '@/gql/types.generated'
6 | import { fetchAccessToken, getLongBetName } from '@/lib/util'
7 | import { redirect } from 'next/navigation'
8 | import { formatTime } from '@/ui/date-util'
9 | import { getOptionPointLabel, getSpreadOptionLabel, renderScore } from '@/ui/event-util'
10 | import { Card } from '@/ui/card/card'
11 | import CardHeader from '@/ui/card/card-header'
12 | import { CardContent } from '@/ui/card/card-content'
13 |
14 | export const revalidate = 60
15 |
16 | const BetListItem: React.FC<{ bet: Bet }> = ({ bet }) => {
17 | const betType = getLongBetName(bet.betOptions?.length ?? 1)
18 |
19 | const betWinLabel = (status: BetStatus): string => {
20 | return status === BetStatus.Won ? 'Won' : 'To Return'
21 | }
22 | const options = bet.betOptions?.filter((option) => !!option) as BetOption[]
23 | const betOptions = options.map((option: BetOption) => {
24 | const event = option?.marketOption.market?.event
25 | return (
26 |
27 |
28 |
29 |
32 | {option?.marketOption?.name}{' '}
33 | {option &&
34 | getOptionPointLabel(
35 | option.marketOption ?? null,
36 | option.marketOption?.market?.name ?? 'totals'
37 | )}{' '}
38 | {option?.marketOption.id}
39 |
40 |
{option?.marketOption.odds}
41 |
42 |
43 | {option?.marketOption.market?.event?.homeTeamName}{' '}
44 | {getSpreadOptionLabel(option.marketOption.market?.event ?? null, true)} vs{' '}
45 | {option?.marketOption.market?.event?.awayTeamName}{' '}
46 | {getSpreadOptionLabel(option.marketOption.market?.event ?? null, false)}
47 | {event && !event.sport?.key.startsWith('tennis') && (
48 |
49 | {renderScore(event)}{' '}
50 |
51 | {!event?.result && event.isLive && !event.completed ? ' live' : ''}
52 |
53 |
54 | )}
55 |
56 |
57 | )
58 | })
59 |
60 | return (
61 |
62 |
63 |
64 |
Placed {formatTime(new Date(bet.createdAt))}
65 |
66 |
67 |
68 |
69 |
70 |
71 |
Stake
72 |
€{bet.stake}
73 |
74 |
75 |
{betWinLabel(bet.status)}
76 |
€{Number(bet.potentialWinnings).toFixed(2)}
77 |
78 |
79 |
80 |
81 | )
82 | }
83 |
84 | export default async function Page() {
85 | const accessToken = await fetchAccessToken()
86 | if (!accessToken) {
87 | console.info('No access token, redirecting to login')
88 | redirect('/api/auth/login')
89 | }
90 | const data = await getClient(true).query({
91 | query: ListBetsDocument,
92 | context: {
93 | headers: {
94 | authorization: `Bearer ${accessToken}`,
95 | },
96 | },
97 | })
98 | const bets: Bet[] = data.data.listBets as Bet[]
99 |
100 | if (bets.length === 0) {
101 | return (
102 |
103 | My Bets
104 | No bets placed yet.
105 |
106 | )
107 | }
108 |
109 | return (
110 |
111 | My Bets
112 | {bets?.map((bet) => (
113 |
114 | ))}
115 |
116 | )
117 | }
118 |
--------------------------------------------------------------------------------
/src/app/design-tokens.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --egyptian-blue: hsla(227, 75%, 38%, 1);
3 | --peach-yellow: hsla(41, 100%, 71%, 1);
4 | --salmon: hsla(9, 93%, 72%, 1);
5 | --light-blue: hsla(227, 40%, 90%, 1);
6 | --pink-lavender: hsla(314, 48%, 82%, 1);
7 | --off-white: hsla(0, 0%, 96%, 1);
8 | --off-white-transparent: hsla(0, 0%, 96%, 0.8);
9 | --white: hsla(0, 0%, 100%, 1);
10 | --black: hsla(0, 0%, 0%, 1);
11 | --black-transparent: hsla(0, 0%, 0%, 0.7);
12 | --dark-grey: hsla(0, 0%, 20%, 1);
13 |
14 | --spacing-small: 4px;
15 | --spacing-medium: 8px;
16 | --spacing-large: 12px;
17 | --spacing-large-minus: -12px;
18 | --spacing-x-large: 16px;
19 | --duration-paused: paused;
20 | --duration-slow: 3s;
21 | --duration-fast: 500ms;
22 | --easing-easeInSine: cubic-bezier(0.12, 0, 0.39, 0);
23 | --easing-easeOutSine: cubic-bezier(0.61, 1, 0.88, 1)500ms;
24 | --radius-circle: 50%;
25 | --radius-large: 8px;
26 | --radius-small: 2px;
27 | --opacity-opacity-25: 0.25;
28 | --opacity-opacity-50: 0.5;
29 | --opacity-opacity-75: 0.75;
30 | --shadow-level-1: 0 1px 1px 0 rgba(0,0,0,0.14), 0 2px 1px -1px rgba(0,0,0,0.12), 0 1px 3px 0 rgba(0,0,0,0.20);;
31 | --shadow-level-2: 0 3px 4px 0 rgba(0,0,0,0.14), 0 3px 3px -2px rgba(0,0,0,0.12), 0 1px 8px 0 rgba(0,0,0,0.20);;
32 | --shadow-level-3: 0 6px 10px 0 rgba(0,0,0,0.14), 0 1px 18px 0 rgba(0,0,0,0.12), 0 3px 5px -1px rgba(0,0,0,0.20);
33 | --media-query-max-width-mobile: 600px;
34 | --media-query-max-width-tablet: 1024px;
35 | --font-family-body: Arial, Helvetica, sans-serif;
36 | --font-family-headings: Palatino Linotype, serif;
37 | --font-size-caption: 14px;
38 | --font-size-body: 16px;
39 | --font-size-headings: 26px;
40 | --letter-spacing-dense: -1px;
41 | --letter-spacing-double: 2px;
42 | --line-height-heading: 1.25;
43 | --line-height-reset: 1;
44 | --line-height-text: 1.5
45 | }
46 |
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anssip/ultrabet-ui/e27ce2d0794b7fe1ed69374bd1cea33b2e53b3e8/src/app/favicon.ico
--------------------------------------------------------------------------------
/src/app/global-layout.css:
--------------------------------------------------------------------------------
1 | body {
2 | color: #777;
3 | }
4 |
5 | /*
6 | Add transition to containers so they can push in and out.
7 | */
8 | #layout,
9 | #menu,
10 | .menu-link {
11 | -webkit-transition: all 0.2s ease-out;
12 | -moz-transition: all 0.2s ease-out;
13 | -ms-transition: all 0.2s ease-out;
14 | -o-transition: all 0.2s ease-out;
15 | transition: all 0.2s ease-out;
16 | }
17 |
18 | /*
19 | This is the parent `` that contains the menu and the content area.
20 | */
21 | #layout {
22 | position: relative;
23 | left: 0;
24 | padding-left: 0;
25 | }
26 | #layout.active #menu {
27 | left: 190px;
28 | width: 190px;
29 | }
30 |
31 | #layout.active .menu-link {
32 | left: 190px;
33 | }
34 | /*
35 | The content `
` is where all your content goes.
36 | */
37 | .content {
38 | margin: 0 auto;
39 | padding: 0 2em;
40 | max-width: 800px;
41 | margin-bottom: 50px;
42 | line-height: 1.6em;
43 | }
44 |
45 | .header {
46 | margin: 0;
47 | color: var(--off-white);
48 | text-align: center;
49 | padding: 2.5em 2em 0;
50 | border-bottom: 1px solid #eee;
51 | }
52 | .header h1 {
53 | margin: 0.2em 0;
54 | font-size: 3em;
55 | font-weight: 300;
56 | }
57 | .header h2 {
58 | font-weight: 300;
59 | color: #ccc;
60 | padding: 0;
61 | margin-top: 0;
62 | }
63 |
64 | .content-subhead {
65 | margin: 50px 0 20px 0;
66 | font-weight: 300;
67 | color: #888;
68 | }
69 |
70 |
71 |
72 | /*
73 | The `#menu` `
` is the parent `
` that contains the `.pure-menu` that
74 | appears on the left side of the page.
75 | */
76 |
77 | #menu {
78 | margin-left: -190px; /* "#menu" width */
79 | width: 190px;
80 | position: fixed;
81 | top: 2.1em;
82 | left: 0;
83 | bottom: 0;
84 | z-index: 1000; /* so the menu or its navicon stays above all content */
85 | background: var(--egyptian-blue);
86 | overflow-y: auto;
87 | }
88 | /*
89 | All anchors inside the menu should be styled like this.
90 | */
91 | #menu a {
92 | color: var(--off-white);
93 | border: none;
94 | padding: 0.6em 0 0.6em 0.6em;
95 | }
96 |
97 | /*
98 | Remove all background/borders, since we are applying them to #menu.
99 | */
100 | #menu .pure-menu,
101 | #menu .pure-menu ul {
102 | border: none;
103 | background: transparent;
104 | }
105 |
106 | /*
107 | Add that light border to separate items into groups.
108 | */
109 | #menu .pure-menu ul,
110 | #menu .pure-menu .menu-item-divided {
111 | border-top: 1px solid var(--off-white);
112 | }
113 | /*
114 | Change color of the anchor links on hover/focus.
115 | */
116 | #menu .pure-menu li a:hover,
117 | #menu .pure-menu li a:focus {
118 | background: var(--light-blue);
119 | color: var(--black);
120 | }
121 |
122 | /*
123 | This styles the selected menu item `
`.
124 | */
125 | #menu .pure-menu-selected,
126 | #menu .pure-menu-heading {
127 | background: var(--pink-lavender);
128 | color: var(--black);
129 | }
130 | /*
131 | This styles a link within a selected menu item ``.
132 | */
133 | #menu .pure-menu-selected a {
134 | color: var(--black);
135 | }
136 |
137 | /*
138 | This styles the menu heading.
139 | */
140 | #menu .pure-menu-heading {
141 | font-size: 110%;
142 | color: var(--white);
143 | margin: 0;
144 | }
145 |
146 | /* -- Dynamic Button For Responsive Menu -------------------------------------*/
147 |
148 | /*
149 | The button to open/close the Menu is custom-made and not part of Pure. Here's
150 | how it works:
151 | */
152 |
153 | /*
154 | `.menu-link` represents the responsive menu toggle that shows/hides on
155 | small screens.
156 | */
157 | .menu-link {
158 | position: fixed;
159 | display: block; /* show this only on small screens */
160 | top: 3.4em;
161 | left: 0; /* "#menu width" */
162 | background: var(--black);
163 | background: rgba(0,0,0,0.7);
164 | font-size: 10px; /* change this value to increase/decrease button size */
165 | z-index: 10;
166 | width: 2em;
167 | height: auto;
168 | padding: 2.1em 1.6em;
169 | }
170 |
171 | .menu-link:hover,
172 | .menu-link:focus {
173 | background: var(--black);
174 | }
175 |
176 | .menu-link span {
177 | position: relative;
178 | display: block;
179 | }
180 |
181 | .menu-link span,
182 | .menu-link span:before,
183 | .menu-link span:after {
184 | background-color: var(--white);
185 | pointer-events: none;
186 | width: 100%;
187 | height: 0.2em;
188 | }
189 |
190 | .menu-link span:before,
191 | .menu-link span:after {
192 | position: absolute;
193 | margin-top: -0.6em;
194 | content: " ";
195 | }
196 |
197 | .menu-link span:after {
198 | margin-top: 0.6em;
199 | }
200 |
201 |
202 | /* -- Responsive Styles (Media Queries) ------------------------------------- */
203 |
204 | /*
205 | Hides the menu at `48em`, but modify this based on your app's needs.
206 | */
207 | @media (min-width: 48em) {
208 |
209 | .header,
210 | .content {
211 | padding-left: 2em;
212 | padding-right: 2em;
213 | }
214 |
215 | #layout {
216 | padding-left: 95px; /* left col width "#menu" */
217 | left: 0;
218 | background-color: var(--off-white);
219 | }
220 | #menu {
221 | left: 190px;
222 | }
223 |
224 | .menu-link {
225 | position: fixed;
226 | left: 190px;
227 | display: none;
228 | }
229 |
230 | #layout.active .menu-link {
231 | left: 190px;
232 | }
233 | }
234 |
235 | @media (max-width: 48em) {
236 | /* Only apply this when the window is small. Otherwise, the following
237 | case results in extra padding on the left:
238 | * Make the window small.
239 | * Tap the menu to trigger the active state.
240 | * Make the window large again.
241 | */
242 | #layout.active {
243 | position: relative;
244 | left: 190px;
245 | }
246 | }
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Inter } from 'next/font/google'
2 | import TopBar from '@/ui/top-bar/top-bar'
3 | import { UserProvider } from '@auth0/nextjs-auth0/client'
4 | import React from 'react'
5 | import { Analytics } from '@vercel/analytics/react'
6 | import { getClient } from '@/lib/client'
7 | import { MeDocument } from '@/gql/documents.generated'
8 | import { User } from '@/gql/types.generated'
9 | import { redirect } from 'next/navigation'
10 | import './global-layout.css'
11 | import './design-tokens.css'
12 | import './utilities.css'
13 | import { fetchAccessToken } from '@/lib/util'
14 |
15 | const inter = Inter({ subsets: ['latin'] })
16 |
17 | export const metadata = {
18 | title: 'Parabolic Bet',
19 | description: 'Where your bet gains go parabolic',
20 | }
21 |
22 | async function getBettingUser(accessToken: string | undefined | null): Promise {
23 | try {
24 | const response = accessToken
25 | ? await getClient(true).query({
26 | query: MeDocument,
27 | context: {
28 | headers: {
29 | authorization: `Bearer ${accessToken}`,
30 | },
31 | },
32 | })
33 | : null
34 | return response?.data?.me ?? null
35 | } catch (e) {
36 | console.error(e)
37 | return null
38 | }
39 | }
40 |
41 | // @ts-ignore
42 | export default async function RootLayout({ children }: { children: React.ReactNode }) {
43 | const accessToken = await fetchAccessToken()
44 | const me = accessToken ? await getBettingUser(accessToken) : null
45 |
46 | if (accessToken && !me) {
47 | console.info('redirecting to login')
48 | redirect('/api/auth/login')
49 | }
50 |
51 | return (
52 |
53 |
54 | Parabolic Bet
55 |
59 |
65 |
69 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
{children}
80 |
81 |
82 |
83 |
84 |
85 | )
86 | }
87 |
--------------------------------------------------------------------------------
/src/app/utilities.css:
--------------------------------------------------------------------------------
1 | .bg-dark {
2 | background-color: var(--egyptian-blue)
3 | }
4 |
5 | .bg-light {
6 | background-color: var(--light-blue)
7 | }
8 |
9 | .bg-primary {
10 | background-color: var(--peach-yellow)
11 | }
12 |
13 | .bg-secondary {
14 | background-color: var(--pink-lavender)
15 | }
16 |
17 | .bg-danger {
18 | background-color: var(--salmon)
19 | }
20 |
21 | .text-dark {
22 | color: var(--black)
23 | }
24 |
25 | .text-mid {
26 | background-color: var(--off-white)
27 | }
28 |
29 | .text-light {
30 | color: var(--white)
31 | }
32 |
33 | .text-danger {
34 | color: var(--salmon)
35 | }
36 |
37 | .text-secondary {
38 | color: var(--light-blue)
39 | }
40 |
41 | .text-primary {
42 | color: var(--egyptian-blue)
43 | }
--------------------------------------------------------------------------------
/src/gql/documents.generated.ts:
--------------------------------------------------------------------------------
1 | import * as Types from './types.generated';
2 |
3 | import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
4 | export type MarketFragment = { __typename?: 'Market', id: string, name: string, source: string, lastUpdated?: string | null, isLive: boolean, options?: Array<{ __typename?: 'MarketOption', id: string, name: string, odds: number, point?: number | null, description?: string | null } | null> | null };
5 |
6 | export type EventFragment = { __typename?: 'Event', id: string, externalId?: string | null, name: string, startTime: string, homeTeamName: string, awayTeamName: string, result?: Types.EventResult | null, isLive: boolean, completed: boolean, sport: { __typename?: 'Sport', id: string, key: string, title: string, description: string, active: boolean, group: string, hasOutrights: boolean }, markets?: Array<{ __typename?: 'Market', id: string, name: string, source: string, lastUpdated?: string | null, isLive: boolean, options?: Array<{ __typename?: 'MarketOption', id: string, name: string, odds: number, point?: number | null, description?: string | null } | null> | null } | null> | null, scoreUpdates?: Array<{ __typename?: 'ScoreUpdate', id: string, name: string, score: string } | null> | null };
7 |
8 | export type SportFragment = { __typename?: 'Sport', id: string, title: string, active: boolean, group: string, description: string, hasOutrights: boolean };
9 |
10 | export type MeQueryVariables = Types.Exact<{ [key: string]: never; }>;
11 |
12 |
13 | export type MeQuery = { __typename?: 'Query', me?: { __typename?: 'User', id: string, externalId?: string | null, username?: string | null, email?: string | null, wallet?: { __typename?: 'Wallet', id: string, balance: number } | null } | null };
14 |
15 | export type ListLiveEventsQueryVariables = Types.Exact<{ [key: string]: never; }>;
16 |
17 |
18 | export type ListLiveEventsQuery = { __typename?: 'Query', listLiveEvents?: Array<{ __typename?: 'Event', id: string, externalId?: string | null, name: string, startTime: string, homeTeamName: string, awayTeamName: string, result?: Types.EventResult | null, isLive: boolean, completed: boolean, sport: { __typename?: 'Sport', id: string, key: string, title: string, description: string, active: boolean, group: string, hasOutrights: boolean }, markets?: Array<{ __typename?: 'Market', id: string, name: string, source: string, lastUpdated?: string | null, isLive: boolean, options?: Array<{ __typename?: 'MarketOption', id: string, name: string, odds: number, point?: number | null, description?: string | null } | null> | null } | null> | null, scoreUpdates?: Array<{ __typename?: 'ScoreUpdate', id: string, name: string, score: string } | null> | null } | null> | null };
19 |
20 | export type ListEventsBySportQueryVariables = Types.Exact<{
21 | group: Types.Scalars['String'];
22 | }>;
23 |
24 |
25 | export type ListEventsBySportQuery = { __typename?: 'Query', eventsBySportGroup?: Array<{ __typename?: 'Event', id: string, externalId?: string | null, name: string, startTime: string, homeTeamName: string, awayTeamName: string, result?: Types.EventResult | null, isLive: boolean, completed: boolean, sport: { __typename?: 'Sport', id: string, key: string, title: string, description: string, active: boolean, group: string, hasOutrights: boolean }, markets?: Array<{ __typename?: 'Market', id: string, name: string, source: string, lastUpdated?: string | null, isLive: boolean, options?: Array<{ __typename?: 'MarketOption', id: string, name: string, odds: number, point?: number | null, description?: string | null } | null> | null } | null> | null, scoreUpdates?: Array<{ __typename?: 'ScoreUpdate', id: string, name: string, score: string } | null> | null } | null> | null };
26 |
27 | export type ListBetsQueryVariables = Types.Exact<{ [key: string]: never; }>;
28 |
29 |
30 | export type ListBetsQuery = { __typename?: 'Query', listBets?: Array<{ __typename?: 'Bet', id: string, stake: number, potentialWinnings: number, createdAt: string, status: Types.BetStatus, betOptions?: Array<{ __typename?: 'BetOption', id: number, status?: Types.BetStatus | null, marketOption: { __typename?: 'MarketOption', id: string, name: string, odds: number, point?: number | null, description?: string | null, market?: { __typename?: 'Market', id: string, name: string, event?: { __typename?: 'Event', id: string, externalId?: string | null, name: string, startTime: string, homeTeamName: string, awayTeamName: string, result?: Types.EventResult | null, isLive: boolean, completed: boolean, sport: { __typename?: 'Sport', id: string, key: string, title: string, description: string, active: boolean, group: string, hasOutrights: boolean }, markets?: Array<{ __typename?: 'Market', id: string, name: string, source: string, lastUpdated?: string | null, isLive: boolean, options?: Array<{ __typename?: 'MarketOption', id: string, name: string, odds: number, point?: number | null, description?: string | null } | null> | null } | null> | null, scoreUpdates?: Array<{ __typename?: 'ScoreUpdate', id: string, name: string, score: string } | null> | null } | null } | null } } | null> | null } | null> | null };
31 |
32 | export type ListSportsQueryVariables = Types.Exact<{ [key: string]: never; }>;
33 |
34 |
35 | export type ListSportsQuery = { __typename?: 'Query', listSports?: Array<{ __typename?: 'Sport', id: string, title: string, active: boolean, group: string, description: string, hasOutrights: boolean } | null> | null };
36 |
37 | export type SportWithEventsFragment = { __typename?: 'Sport', id: string, title: string, active: boolean, group: string, description: string, hasOutrights: boolean, events?: Array<{ __typename?: 'Event', id: string, externalId?: string | null, name: string, startTime: string, homeTeamName: string, awayTeamName: string, result?: Types.EventResult | null, isLive: boolean, completed: boolean, sport: { __typename?: 'Sport', id: string, key: string, title: string, description: string, active: boolean, group: string, hasOutrights: boolean }, markets?: Array<{ __typename?: 'Market', id: string, name: string, source: string, lastUpdated?: string | null, isLive: boolean, options?: Array<{ __typename?: 'MarketOption', id: string, name: string, odds: number, point?: number | null, description?: string | null } | null> | null } | null> | null, scoreUpdates?: Array<{ __typename?: 'ScoreUpdate', id: string, name: string, score: string } | null> | null } | null> | null };
38 |
39 | export type ListSportsWithEventsQueryVariables = Types.Exact<{
40 | group: Types.Scalars['String'];
41 | }>;
42 |
43 |
44 | export type ListSportsWithEventsQuery = { __typename?: 'Query', listSports?: Array<{ __typename?: 'Sport', id: string, title: string, active: boolean, group: string, description: string, hasOutrights: boolean, events?: Array<{ __typename?: 'Event', id: string, externalId?: string | null, name: string, startTime: string, homeTeamName: string, awayTeamName: string, result?: Types.EventResult | null, isLive: boolean, completed: boolean, sport: { __typename?: 'Sport', id: string, key: string, title: string, description: string, active: boolean, group: string, hasOutrights: boolean }, markets?: Array<{ __typename?: 'Market', id: string, name: string, source: string, lastUpdated?: string | null, isLive: boolean, options?: Array<{ __typename?: 'MarketOption', id: string, name: string, odds: number, point?: number | null, description?: string | null } | null> | null } | null> | null, scoreUpdates?: Array<{ __typename?: 'ScoreUpdate', id: string, name: string, score: string } | null> | null } | null> | null } | null> | null };
45 |
46 | export type MarketOptionUpdatesSubscriptionVariables = Types.Exact<{ [key: string]: never; }>;
47 |
48 |
49 | export type MarketOptionUpdatesSubscription = { __typename?: 'Subscription', liveMarketOptionsUpdated?: Array<{ __typename?: 'MarketOption', id: string, name: string, odds: number, lastUpdated?: string | null } | null> | null };
50 |
51 | export type ScoreUpdatesSubscriptionVariables = Types.Exact<{ [key: string]: never; }>;
52 |
53 |
54 | export type ScoreUpdatesSubscription = { __typename?: 'Subscription', eventScoresUpdated?: Array<{ __typename?: 'Event', id: string, externalId?: string | null, name: string, startTime: string, homeTeamName: string, awayTeamName: string, result?: Types.EventResult | null, isLive: boolean, completed: boolean, sport: { __typename?: 'Sport', id: string, key: string, title: string, description: string, active: boolean, group: string, hasOutrights: boolean }, markets?: Array<{ __typename?: 'Market', id: string, name: string, source: string, lastUpdated?: string | null, isLive: boolean, options?: Array<{ __typename?: 'MarketOption', id: string, name: string, odds: number, point?: number | null, description?: string | null } | null> | null } | null> | null, scoreUpdates?: Array<{ __typename?: 'ScoreUpdate', id: string, name: string, score: string } | null> | null } | null> | null };
55 |
56 | export type EventStatusUpdatesSubscriptionVariables = Types.Exact<{ [key: string]: never; }>;
57 |
58 |
59 | export type EventStatusUpdatesSubscription = { __typename?: 'Subscription', eventStatusUpdated?: Array<{ __typename?: 'Event', id: string, externalId?: string | null, name: string, startTime: string, homeTeamName: string, awayTeamName: string, result?: Types.EventResult | null, isLive: boolean, completed: boolean, sport: { __typename?: 'Sport', id: string, key: string, title: string, description: string, active: boolean, group: string, hasOutrights: boolean }, markets?: Array<{ __typename?: 'Market', id: string, name: string, source: string, lastUpdated?: string | null, isLive: boolean, options?: Array<{ __typename?: 'MarketOption', id: string, name: string, odds: number, point?: number | null, description?: string | null } | null> | null } | null> | null, scoreUpdates?: Array<{ __typename?: 'ScoreUpdate', id: string, name: string, score: string } | null> | null } | null> | null };
60 |
61 | export type PlaceBetMutationVariables = Types.Exact<{
62 | betType: Types.BetType;
63 | marketOptions: Array | Types.Scalars['ID'];
64 | stake: Types.Scalars['Float'];
65 | }>;
66 |
67 |
68 | export type PlaceBetMutation = { __typename?: 'Mutation', placeBet?: { __typename?: 'Bet', id: string, status: Types.BetStatus } | null };
69 |
70 | export type PlaceSingleBetsMutationVariables = Types.Exact<{
71 | options: Array | Types.BetOptionInput;
72 | }>;
73 |
74 |
75 | export type PlaceSingleBetsMutation = { __typename?: 'Mutation', placeSingleBets?: Array<{ __typename?: 'Bet', id: string, status: Types.BetStatus } | null> | null };
76 |
77 | export const SportFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Sport"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Sport"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"active"}},{"kind":"Field","name":{"kind":"Name","value":"group"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"hasOutrights"}}]}}]} as unknown as DocumentNode;
78 | export const MarketFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Market"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Market"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"source"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdated"}},{"kind":"Field","name":{"kind":"Name","value":"isLive"}},{"kind":"Field","name":{"kind":"Name","value":"options"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"odds"}},{"kind":"Field","name":{"kind":"Name","value":"point"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]}}]} as unknown as DocumentNode;
79 | export const EventFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Event"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Event"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"externalId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"startTime"}},{"kind":"Field","name":{"kind":"Name","value":"homeTeamName"}},{"kind":"Field","name":{"kind":"Name","value":"awayTeamName"}},{"kind":"Field","name":{"kind":"Name","value":"result"}},{"kind":"Field","name":{"kind":"Name","value":"sport"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"active"}},{"kind":"Field","name":{"kind":"Name","value":"group"}},{"kind":"Field","name":{"kind":"Name","value":"hasOutrights"}}]}},{"kind":"Field","name":{"kind":"Name","value":"isLive"}},{"kind":"Field","name":{"kind":"Name","value":"completed"}},{"kind":"Field","name":{"kind":"Name","value":"markets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Market"}}]}},{"kind":"Field","name":{"kind":"Name","value":"scoreUpdates"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"score"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Market"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Market"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"source"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdated"}},{"kind":"Field","name":{"kind":"Name","value":"isLive"}},{"kind":"Field","name":{"kind":"Name","value":"options"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"odds"}},{"kind":"Field","name":{"kind":"Name","value":"point"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]}}]} as unknown as DocumentNode;
80 | export const SportWithEventsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SportWithEvents"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Sport"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Sport"}},{"kind":"Field","name":{"kind":"Name","value":"events"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Event"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Market"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Market"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"source"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdated"}},{"kind":"Field","name":{"kind":"Name","value":"isLive"}},{"kind":"Field","name":{"kind":"Name","value":"options"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"odds"}},{"kind":"Field","name":{"kind":"Name","value":"point"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Sport"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Sport"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"active"}},{"kind":"Field","name":{"kind":"Name","value":"group"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"hasOutrights"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Event"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Event"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"externalId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"startTime"}},{"kind":"Field","name":{"kind":"Name","value":"homeTeamName"}},{"kind":"Field","name":{"kind":"Name","value":"awayTeamName"}},{"kind":"Field","name":{"kind":"Name","value":"result"}},{"kind":"Field","name":{"kind":"Name","value":"sport"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"active"}},{"kind":"Field","name":{"kind":"Name","value":"group"}},{"kind":"Field","name":{"kind":"Name","value":"hasOutrights"}}]}},{"kind":"Field","name":{"kind":"Name","value":"isLive"}},{"kind":"Field","name":{"kind":"Name","value":"completed"}},{"kind":"Field","name":{"kind":"Name","value":"markets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Market"}}]}},{"kind":"Field","name":{"kind":"Name","value":"scoreUpdates"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"score"}}]}}]}}]} as unknown as DocumentNode;
81 | export const MeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"me"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"me"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"externalId"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"wallet"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"balance"}}]}}]}}]}}]} as unknown as DocumentNode;
82 | export const ListLiveEventsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ListLiveEvents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"listLiveEvents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Event"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Market"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Market"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"source"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdated"}},{"kind":"Field","name":{"kind":"Name","value":"isLive"}},{"kind":"Field","name":{"kind":"Name","value":"options"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"odds"}},{"kind":"Field","name":{"kind":"Name","value":"point"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Event"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Event"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"externalId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"startTime"}},{"kind":"Field","name":{"kind":"Name","value":"homeTeamName"}},{"kind":"Field","name":{"kind":"Name","value":"awayTeamName"}},{"kind":"Field","name":{"kind":"Name","value":"result"}},{"kind":"Field","name":{"kind":"Name","value":"sport"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"active"}},{"kind":"Field","name":{"kind":"Name","value":"group"}},{"kind":"Field","name":{"kind":"Name","value":"hasOutrights"}}]}},{"kind":"Field","name":{"kind":"Name","value":"isLive"}},{"kind":"Field","name":{"kind":"Name","value":"completed"}},{"kind":"Field","name":{"kind":"Name","value":"markets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Market"}}]}},{"kind":"Field","name":{"kind":"Name","value":"scoreUpdates"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"score"}}]}}]}}]} as unknown as DocumentNode;
83 | export const ListEventsBySportDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ListEventsBySport"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"group"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"eventsBySportGroup"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"group"},"value":{"kind":"Variable","name":{"kind":"Name","value":"group"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Event"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Market"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Market"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"source"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdated"}},{"kind":"Field","name":{"kind":"Name","value":"isLive"}},{"kind":"Field","name":{"kind":"Name","value":"options"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"odds"}},{"kind":"Field","name":{"kind":"Name","value":"point"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Event"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Event"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"externalId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"startTime"}},{"kind":"Field","name":{"kind":"Name","value":"homeTeamName"}},{"kind":"Field","name":{"kind":"Name","value":"awayTeamName"}},{"kind":"Field","name":{"kind":"Name","value":"result"}},{"kind":"Field","name":{"kind":"Name","value":"sport"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"active"}},{"kind":"Field","name":{"kind":"Name","value":"group"}},{"kind":"Field","name":{"kind":"Name","value":"hasOutrights"}}]}},{"kind":"Field","name":{"kind":"Name","value":"isLive"}},{"kind":"Field","name":{"kind":"Name","value":"completed"}},{"kind":"Field","name":{"kind":"Name","value":"markets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Market"}}]}},{"kind":"Field","name":{"kind":"Name","value":"scoreUpdates"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"score"}}]}}]}}]} as unknown as DocumentNode;
84 | export const ListBetsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ListBets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"listBets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"betOptions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"marketOption"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"odds"}},{"kind":"Field","name":{"kind":"Name","value":"point"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"market"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"event"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Event"}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"stake"}},{"kind":"Field","name":{"kind":"Name","value":"potentialWinnings"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Market"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Market"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"source"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdated"}},{"kind":"Field","name":{"kind":"Name","value":"isLive"}},{"kind":"Field","name":{"kind":"Name","value":"options"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"odds"}},{"kind":"Field","name":{"kind":"Name","value":"point"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Event"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Event"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"externalId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"startTime"}},{"kind":"Field","name":{"kind":"Name","value":"homeTeamName"}},{"kind":"Field","name":{"kind":"Name","value":"awayTeamName"}},{"kind":"Field","name":{"kind":"Name","value":"result"}},{"kind":"Field","name":{"kind":"Name","value":"sport"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"active"}},{"kind":"Field","name":{"kind":"Name","value":"group"}},{"kind":"Field","name":{"kind":"Name","value":"hasOutrights"}}]}},{"kind":"Field","name":{"kind":"Name","value":"isLive"}},{"kind":"Field","name":{"kind":"Name","value":"completed"}},{"kind":"Field","name":{"kind":"Name","value":"markets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Market"}}]}},{"kind":"Field","name":{"kind":"Name","value":"scoreUpdates"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"score"}}]}}]}}]} as unknown as DocumentNode;
85 | export const ListSportsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"listSports"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"listSports"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"group"},"value":{"kind":"StringValue","value":"all","block":false}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Sport"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Sport"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Sport"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"active"}},{"kind":"Field","name":{"kind":"Name","value":"group"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"hasOutrights"}}]}}]} as unknown as DocumentNode;
86 | export const ListSportsWithEventsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"listSportsWithEvents"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"group"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"listSports"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"group"},"value":{"kind":"Variable","name":{"kind":"Name","value":"group"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SportWithEvents"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Sport"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Sport"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"active"}},{"kind":"Field","name":{"kind":"Name","value":"group"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"hasOutrights"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Market"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Market"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"source"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdated"}},{"kind":"Field","name":{"kind":"Name","value":"isLive"}},{"kind":"Field","name":{"kind":"Name","value":"options"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"odds"}},{"kind":"Field","name":{"kind":"Name","value":"point"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Event"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Event"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"externalId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"startTime"}},{"kind":"Field","name":{"kind":"Name","value":"homeTeamName"}},{"kind":"Field","name":{"kind":"Name","value":"awayTeamName"}},{"kind":"Field","name":{"kind":"Name","value":"result"}},{"kind":"Field","name":{"kind":"Name","value":"sport"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"active"}},{"kind":"Field","name":{"kind":"Name","value":"group"}},{"kind":"Field","name":{"kind":"Name","value":"hasOutrights"}}]}},{"kind":"Field","name":{"kind":"Name","value":"isLive"}},{"kind":"Field","name":{"kind":"Name","value":"completed"}},{"kind":"Field","name":{"kind":"Name","value":"markets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Market"}}]}},{"kind":"Field","name":{"kind":"Name","value":"scoreUpdates"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"score"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SportWithEvents"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Sport"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Sport"}},{"kind":"Field","name":{"kind":"Name","value":"events"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Event"}}]}}]}}]} as unknown as DocumentNode;
87 | export const MarketOptionUpdatesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"marketOptionUpdates"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"liveMarketOptionsUpdated"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"odds"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdated"}}]}}]}}]} as unknown as DocumentNode;
88 | export const ScoreUpdatesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"scoreUpdates"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"eventScoresUpdated"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Event"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Market"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Market"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"source"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdated"}},{"kind":"Field","name":{"kind":"Name","value":"isLive"}},{"kind":"Field","name":{"kind":"Name","value":"options"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"odds"}},{"kind":"Field","name":{"kind":"Name","value":"point"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Event"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Event"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"externalId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"startTime"}},{"kind":"Field","name":{"kind":"Name","value":"homeTeamName"}},{"kind":"Field","name":{"kind":"Name","value":"awayTeamName"}},{"kind":"Field","name":{"kind":"Name","value":"result"}},{"kind":"Field","name":{"kind":"Name","value":"sport"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"active"}},{"kind":"Field","name":{"kind":"Name","value":"group"}},{"kind":"Field","name":{"kind":"Name","value":"hasOutrights"}}]}},{"kind":"Field","name":{"kind":"Name","value":"isLive"}},{"kind":"Field","name":{"kind":"Name","value":"completed"}},{"kind":"Field","name":{"kind":"Name","value":"markets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Market"}}]}},{"kind":"Field","name":{"kind":"Name","value":"scoreUpdates"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"score"}}]}}]}}]} as unknown as DocumentNode;
89 | export const EventStatusUpdatesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"eventStatusUpdates"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"eventStatusUpdated"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Event"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Market"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Market"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"source"}},{"kind":"Field","name":{"kind":"Name","value":"lastUpdated"}},{"kind":"Field","name":{"kind":"Name","value":"isLive"}},{"kind":"Field","name":{"kind":"Name","value":"options"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"odds"}},{"kind":"Field","name":{"kind":"Name","value":"point"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Event"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Event"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"externalId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"startTime"}},{"kind":"Field","name":{"kind":"Name","value":"homeTeamName"}},{"kind":"Field","name":{"kind":"Name","value":"awayTeamName"}},{"kind":"Field","name":{"kind":"Name","value":"result"}},{"kind":"Field","name":{"kind":"Name","value":"sport"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"active"}},{"kind":"Field","name":{"kind":"Name","value":"group"}},{"kind":"Field","name":{"kind":"Name","value":"hasOutrights"}}]}},{"kind":"Field","name":{"kind":"Name","value":"isLive"}},{"kind":"Field","name":{"kind":"Name","value":"completed"}},{"kind":"Field","name":{"kind":"Name","value":"markets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Market"}}]}},{"kind":"Field","name":{"kind":"Name","value":"scoreUpdates"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"score"}}]}}]}}]} as unknown as DocumentNode;
90 | export const PlaceBetDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"placeBet"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"betType"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"BetType"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"marketOptions"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"stake"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Float"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"placeBet"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"betType"},"value":{"kind":"Variable","name":{"kind":"Name","value":"betType"}}},{"kind":"Argument","name":{"kind":"Name","value":"marketOptions"},"value":{"kind":"Variable","name":{"kind":"Name","value":"marketOptions"}}},{"kind":"Argument","name":{"kind":"Name","value":"stake"},"value":{"kind":"Variable","name":{"kind":"Name","value":"stake"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]} as unknown as DocumentNode;
91 | export const PlaceSingleBetsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"placeSingleBets"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"options"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"BetOptionInput"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"placeSingleBets"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"options"},"value":{"kind":"Variable","name":{"kind":"Name","value":"options"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]} as unknown as DocumentNode;
--------------------------------------------------------------------------------
/src/gql/documents.graphql:
--------------------------------------------------------------------------------
1 | fragment Market on Market {
2 | id
3 | name
4 | source
5 | lastUpdated
6 | isLive
7 | options {
8 | id
9 | name
10 | odds
11 | point
12 | description
13 | }
14 | }
15 |
16 | fragment Event on Event {
17 | id
18 | externalId
19 | name
20 | startTime
21 | homeTeamName
22 | awayTeamName
23 | result
24 | sport {
25 | id
26 | key
27 | title
28 | description
29 | active
30 | group
31 | hasOutrights
32 | }
33 | isLive
34 | completed
35 | markets {
36 | ...Market
37 | }
38 | scoreUpdates {
39 | id
40 | name
41 | score
42 | }
43 | }
44 |
45 | fragment EventWithoutSport on Event {
46 | id
47 | externalId
48 | name
49 | startTime
50 | homeTeamName
51 | awayTeamName
52 | result
53 | isLive
54 | completed
55 | markets {
56 | ...Market
57 | }
58 | scoreUpdates {
59 | id
60 | name
61 | score
62 | }
63 | }
64 |
65 | fragment Sport on Sport {
66 | id
67 | title
68 | active
69 | group
70 | description
71 | hasOutrights
72 | }
73 |
74 | query me {
75 | me {
76 | id
77 | externalId
78 | username
79 | email
80 | wallet {
81 | id
82 | balance
83 | }
84 | }
85 | }
86 |
87 | query ListLiveEvents {
88 | listLiveEvents {
89 | ...Event
90 | }
91 | }
92 |
93 | query ListEventsBySport($group: String!) {
94 | eventsBySportGroup(group: $group) {
95 | ...Event
96 | }
97 | }
98 |
99 |
100 | query ListBets {
101 | listBets {
102 | id
103 | betOptions {
104 | id
105 | status
106 | marketOption {
107 | id
108 | name
109 | odds
110 | point
111 | description
112 | market {
113 | id
114 | name
115 | event {
116 | ...Event
117 | }
118 | }
119 | }
120 | }
121 | stake
122 | potentialWinnings
123 | createdAt
124 | status
125 | }
126 | }
127 |
128 | query listSports {
129 | listSports(group: "all") {
130 | ...Sport
131 | }
132 | }
133 |
134 | fragment SportWithEvents on Sport {
135 | ...Sport
136 | events {
137 | ...EventWithoutSport
138 | }
139 | }
140 |
141 | query listSportsWithEvents($group: String!) {
142 | listSports(group: $group) {
143 | ...SportWithEvents
144 | }
145 | }
146 |
147 | subscription marketOptionUpdates {
148 | liveMarketOptionsUpdated {
149 | id
150 | name
151 | odds
152 | lastUpdated
153 | }
154 | }
155 |
156 | subscription scoreUpdates {
157 | eventScoresUpdated {
158 | ...Event
159 | }
160 | }
161 |
162 | subscription eventStatusUpdates {
163 | eventStatusUpdated {
164 | ...Event
165 | }
166 | }
167 |
168 | mutation placeBet($betType: BetType!, $marketOptions: [ID!]!, $stake: Float!) {
169 | placeBet(betType: $betType, marketOptions: $marketOptions, stake: $stake) {
170 | id
171 | status
172 | }
173 | }
174 |
175 | mutation placeSingleBets($options: [BetOptionInput!]!) {
176 | placeSingleBets(options: $options) {
177 | id
178 | status
179 | }
180 | }
181 |
182 |
--------------------------------------------------------------------------------
/src/gql/types.generated.ts:
--------------------------------------------------------------------------------
1 | export type Maybe = T | null;
2 | export type InputMaybe = Maybe;
3 | export type Exact = { [K in keyof T]: T[K] };
4 | export type MakeOptional = Omit & { [SubKey in K]?: Maybe };
5 | export type MakeMaybe = Omit & { [SubKey in K]: Maybe };
6 | /** All built-in and custom scalars, mapped to their actual values */
7 | export type Scalars = {
8 | ID: string;
9 | String: string;
10 | Boolean: boolean;
11 | Int: number;
12 | Float: number;
13 | DateTime: any;
14 | };
15 |
16 | export type Bet = {
17 | __typename?: 'Bet';
18 | betOptions?: Maybe>>;
19 | createdAt: Scalars['String'];
20 | id: Scalars['ID'];
21 | potentialWinnings: Scalars['Float'];
22 | stake: Scalars['Float'];
23 | status: BetStatus;
24 | user?: Maybe;
25 | };
26 |
27 | export type BetOption = {
28 | __typename?: 'BetOption';
29 | bet: Bet;
30 | id: Scalars['Int'];
31 | marketOption: MarketOption;
32 | status?: Maybe;
33 | };
34 |
35 | export type BetOptionInput = {
36 | marketOptionId: Scalars['ID'];
37 | stake: Scalars['Float'];
38 | };
39 |
40 | export enum BetStatus {
41 | Canceled = 'CANCELED',
42 | Lost = 'LOST',
43 | Pending = 'PENDING',
44 | Won = 'WON'
45 | }
46 |
47 | /** https://chat.openai.com/share/92b7bc9f-6fc6-4f57-9a4e-f217270ad271 */
48 | export enum BetType {
49 | Parlay = 'PARLAY',
50 | Single = 'SINGLE',
51 | /** same as long bet or accumulator */
52 | System = 'SYSTEM'
53 | }
54 |
55 | /** An Event represents a sports match or competition on which users can place bets. */
56 | export type Event = {
57 | __typename?: 'Event';
58 | awayTeamName: Scalars['String'];
59 | /** Is this event completed? */
60 | completed: Scalars['Boolean'];
61 | /** The id of the event in the source system. */
62 | externalId?: Maybe;
63 | homeTeamName: Scalars['String'];
64 | id: Scalars['ID'];
65 | /** Is this event currently live? */
66 | isLive: Scalars['Boolean'];
67 | markets?: Maybe>>;
68 | name: Scalars['String'];
69 | result?: Maybe;
70 | scoreUpdates?: Maybe>>;
71 | sport: Sport;
72 | startTime: Scalars['String'];
73 | };
74 |
75 |
76 | /** An Event represents a sports match or competition on which users can place bets. */
77 | export type EventMarketsArgs = {
78 | source?: InputMaybe;
79 | };
80 |
81 | export enum EventResult {
82 | AwayTeamWin = 'AWAY_TEAM_WIN',
83 | Draw = 'DRAW',
84 | HomeTeamWin = 'HOME_TEAM_WIN'
85 | }
86 |
87 | /**
88 | * A Market represents a specific betting opportunity within an event.
89 | * It's usually associated with one aspect of the event that users can bet on.
90 | * A single event can have multiple markets. Some common examples of markets
91 | * include Moneyline, Point Spread, and Totals (Over/Under).
92 | */
93 | export type Market = {
94 | __typename?: 'Market';
95 | event?: Maybe;
96 | id: Scalars['ID'];
97 | /** Is this Market available for live betting? */
98 | isLive: Scalars['Boolean'];
99 | /**
100 | * When was this Market last updated? Used to track when the odds were last
101 | * updated during live betting.
102 | */
103 | lastUpdated?: Maybe;
104 | name: Scalars['String'];
105 | options?: Maybe>>;
106 | /** What is the source or bookmaker that provides the odds for this Market? */
107 | source: Scalars['String'];
108 | };
109 |
110 | /**
111 | * A MarketOption represents a specific choice or outcome within a market
112 | * that users can bet on. Each market typically has two or more options to choose from.
113 | */
114 | export type MarketOption = {
115 | __typename?: 'MarketOption';
116 | description?: Maybe;
117 | id: Scalars['ID'];
118 | /**
119 | * When was this Market last updated? Used to track when the odds were last
120 | * updated during live betting.
121 | */
122 | lastUpdated?: Maybe;
123 | market?: Maybe;
124 | name: Scalars['String'];
125 | odds: Scalars['Float'];
126 | point?: Maybe;
127 | };
128 |
129 | /** Mutations */
130 | export type Mutation = {
131 | __typename?: 'Mutation';
132 | /** should be available for admins only: */
133 | createEvent?: Maybe;
134 | createMarket?: Maybe;
135 | createMarketOption?: Maybe;
136 | createUser?: Maybe;
137 | depositFunds?: Maybe;
138 | /** Places a bet on the provided market options. */
139 | placeBet?: Maybe;
140 | /** Place multiple single bets, one for each option provided. */
141 | placeSingleBets?: Maybe>>;
142 | updateResult?: Maybe;
143 | withdrawFunds?: Maybe;
144 | };
145 |
146 |
147 | /** Mutations */
148 | export type MutationCreateEventArgs = {
149 | name: Scalars['String'];
150 | sport: Scalars['String'];
151 | startTime: Scalars['String'];
152 | };
153 |
154 |
155 | /** Mutations */
156 | export type MutationCreateMarketArgs = {
157 | eventId: Scalars['ID'];
158 | name: Scalars['String'];
159 | };
160 |
161 |
162 | /** Mutations */
163 | export type MutationCreateMarketOptionArgs = {
164 | marketId: Scalars['ID'];
165 | name: Scalars['String'];
166 | odds: Scalars['Float'];
167 | };
168 |
169 |
170 | /** Mutations */
171 | export type MutationCreateUserArgs = {
172 | email: Scalars['String'];
173 | username: Scalars['String'];
174 | };
175 |
176 |
177 | /** Mutations */
178 | export type MutationDepositFundsArgs = {
179 | amount: Scalars['Float'];
180 | userId: Scalars['ID'];
181 | };
182 |
183 |
184 | /** Mutations */
185 | export type MutationPlaceBetArgs = {
186 | betType: BetType;
187 | marketOptions: Array;
188 | stake: Scalars['Float'];
189 | };
190 |
191 |
192 | /** Mutations */
193 | export type MutationPlaceSingleBetsArgs = {
194 | options: Array;
195 | };
196 |
197 |
198 | /** Mutations */
199 | export type MutationUpdateResultArgs = {
200 | eventId: Scalars['ID'];
201 | };
202 |
203 |
204 | /** Mutations */
205 | export type MutationWithdrawFundsArgs = {
206 | amount: Scalars['Float'];
207 | userId: Scalars['ID'];
208 | };
209 |
210 | /** Queries */
211 | export type Query = {
212 | __typename?: 'Query';
213 | /** List events available for betting in the specified sport group. Lists both live an upcoming events. */
214 | eventsBySportGroup?: Maybe>>;
215 | getBet?: Maybe;
216 | getEvent?: Maybe;
217 | getMarket?: Maybe;
218 | /** List all events available for betting. Lists both live an upcoming events. */
219 | listAllEvents?: Maybe>>;
220 | listBets?: Maybe>>;
221 | /** List upcoming events available for betting. */
222 | listEvents?: Maybe>>;
223 | /** List live events available for betting. */
224 | listLiveEvents?: Maybe>>;
225 | listLiveMarkets?: Maybe>>;
226 | listMarkets?: Maybe>>;
227 | /** List sports by group. */
228 | listSports?: Maybe>>;
229 | me?: Maybe;
230 | };
231 |
232 |
233 | /** Queries */
234 | export type QueryEventsBySportGroupArgs = {
235 | group: Scalars['String'];
236 | };
237 |
238 |
239 | /** Queries */
240 | export type QueryGetBetArgs = {
241 | id: Scalars['ID'];
242 | };
243 |
244 |
245 | /** Queries */
246 | export type QueryGetEventArgs = {
247 | id: Scalars['ID'];
248 | };
249 |
250 |
251 | /** Queries */
252 | export type QueryGetMarketArgs = {
253 | id: Scalars['ID'];
254 | };
255 |
256 |
257 | /** Queries */
258 | export type QueryListLiveMarketsArgs = {
259 | eventId: Scalars['ID'];
260 | };
261 |
262 |
263 | /** Queries */
264 | export type QueryListMarketsArgs = {
265 | eventId: Scalars['ID'];
266 | };
267 |
268 |
269 | /** Queries */
270 | export type QueryListSportsArgs = {
271 | group?: InputMaybe;
272 | };
273 |
274 | export type ScoreUpdate = {
275 | __typename?: 'ScoreUpdate';
276 | event: Event;
277 | id: Scalars['ID'];
278 | name: Scalars['String'];
279 | score: Scalars['String'];
280 | timestamp: Scalars['String'];
281 | };
282 |
283 | export type Sport = {
284 | __typename?: 'Sport';
285 | /** Is this sport in season at the moment? */
286 | active: Scalars['Boolean'];
287 | activeEventCount?: Maybe;
288 | description: Scalars['String'];
289 | /** List all events available for betting in this sport. */
290 | events?: Maybe>>;
291 | group: Scalars['String'];
292 | /** Does this sport have outright markets? */
293 | hasOutrights: Scalars['Boolean'];
294 | id: Scalars['ID'];
295 | key: Scalars['String'];
296 | title: Scalars['String'];
297 | };
298 |
299 | export type Subscription = {
300 | __typename?: 'Subscription';
301 | eventScoresUpdated?: Maybe>>;
302 | eventStatusUpdated?: Maybe>>;
303 | liveMarketOptionsUpdated?: Maybe>>;
304 | };
305 |
306 | export type Transaction = {
307 | __typename?: 'Transaction';
308 | amount: Scalars['Float'];
309 | createdAt: Scalars['String'];
310 | id: Scalars['ID'];
311 | transactionType: TransactionType;
312 | wallet?: Maybe;
313 | };
314 |
315 | export enum TransactionType {
316 | BetPlaced = 'BET_PLACED',
317 | BetRefunded = 'BET_REFUNDED',
318 | BetWon = 'BET_WON',
319 | Deposit = 'DEPOSIT',
320 | Withdrawal = 'WITHDRAWAL'
321 | }
322 |
323 | export type User = {
324 | __typename?: 'User';
325 | bets?: Maybe>>;
326 | email?: Maybe;
327 | externalId?: Maybe;
328 | id: Scalars['ID'];
329 | username?: Maybe;
330 | wallet?: Maybe;
331 | };
332 |
333 | export type Wallet = {
334 | __typename?: 'Wallet';
335 | balance: Scalars['Float'];
336 | id: Scalars['ID'];
337 | transactions?: Maybe>>;
338 | user?: Maybe;
339 | };
340 |
--------------------------------------------------------------------------------
/src/lib/apollo-link.ts:
--------------------------------------------------------------------------------
1 | import { HttpLink, split } from '@apollo/client'
2 | import { GraphQLWsLink } from '@apollo/client/link/subscriptions'
3 | import { createClient } from 'graphql-ws'
4 | import { getMainDefinition } from '@apollo/client/utilities'
5 |
6 | export const wsLink = new GraphQLWsLink(
7 | createClient({
8 | url: `${process.env.NEXT_PUBLIC_API_URL}/subscriptions`.replace(/^https*/, 'wss'),
9 | })
10 | )
11 | export const splitLink = (httpLink: HttpLink) =>
12 | split(
13 | ({ query }) => {
14 | const definition = getMainDefinition(query)
15 | return definition.kind === 'OperationDefinition' && definition.operation === 'subscription'
16 | },
17 | wsLink,
18 | httpLink
19 | )
20 |
--------------------------------------------------------------------------------
/src/lib/client.ts:
--------------------------------------------------------------------------------
1 | import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client'
2 | import { registerApolloClient } from '@apollo/experimental-nextjs-app-support/rsc'
3 | import { splitLink } from '@/lib/apollo-link'
4 | // @ts-ignore
5 |
6 | // TODO: this creates a new client (and new cache) every time, fix that
7 | export const getClient = (isAuthenticated: boolean = false) => {
8 | const { getClient } = registerApolloClient(() => {
9 | const uri =
10 | (isAuthenticated
11 | ? process.env.NEXT_PUBLIC_BETTING_API_URL
12 | : process.env.NEXT_PUBLIC_API_URL) + '/graphql'
13 | console.log(`uri, is authenticated? ${isAuthenticated}`, uri)
14 | const httpLink = new HttpLink({
15 | uri,
16 | })
17 | return new ApolloClient({
18 | cache: new InMemoryCache(),
19 | link: splitLink(httpLink),
20 | })
21 | })
22 | return getClient()
23 | }
24 |
--------------------------------------------------------------------------------
/src/lib/slip-context.tsx:
--------------------------------------------------------------------------------
1 | import useSlip, { PlaceBetResponse } from '@/lib/useSlip'
2 | import React from 'react'
3 | import { BetSlipOption } from '@/ui/bet-slip/bet-slip'
4 | import { MarketOption } from '@/gql/types.generated'
5 |
6 | export type SlipType = {
7 | options: BetSlipOption[]
8 | refetchSlip: () => void
9 | loading: boolean
10 | removeOption: (optionId: string) => Promise
11 | addOption: (option: BetSlipOption) => Promise
12 | postBet: (singles: BetSlipOption[], long: BetSlipOption | null) => Promise
13 | }
14 |
15 | export const SlipContext = React.createContext(null)
16 |
17 | export const SlipProvider = ({ children }: { children: React.ReactNode }) => {
18 | const slipState = useSlip()
19 |
20 | return {children}
21 | }
22 |
--------------------------------------------------------------------------------
/src/lib/useSlip.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 | import { BetSlipOption, Slip } from '@/ui/bet-slip/bet-slip'
3 | import { kv } from '@vercel/kv'
4 | import { UserContext, UserProfile, useUser } from '@auth0/nextjs-auth0/client'
5 | import { MarketOption } from '@/gql/types.generated'
6 | import { SlipType } from '@/lib/slip-context'
7 |
8 | async function loadSlip(): Promise {
9 | console.log('loading betslip')
10 | const response = await fetch('/api/slip')
11 | const data = await response.json()
12 | console.log('loaded slip', data)
13 | return data.slip
14 | }
15 |
16 | async function removeSlipOption(optionId: string) {
17 | console.log('removing slip option', optionId)
18 | const response = await fetch(`/api/slip-options/${optionId}`, {
19 | method: 'DELETE',
20 | })
21 | const data = await response.json()
22 | console.log('removed slip option', data)
23 | }
24 |
25 | async function addSlipOption(option: BetSlipOption) {
26 | console.log('adding slip option', option)
27 | const response = await fetch('/api/slip-options', {
28 | method: 'POST',
29 | body: JSON.stringify(option),
30 | })
31 | const data = await response.json()
32 | console.log('added slip option', data)
33 | }
34 |
35 | export type PlaceBetResponse = {
36 | singles: BetSlipOption[]
37 | long: BetSlipOption | null
38 | }
39 |
40 | async function placeBet(
41 | singles: BetSlipOption[],
42 | long: BetSlipOption | null
43 | ): Promise {
44 | console.log('placing bet', singles, long)
45 | const response = await fetch('/api/slip', {
46 | method: 'POST',
47 | body: JSON.stringify({ singles, long }),
48 | })
49 | return response.json()
50 | }
51 |
52 | const useSlip = (): SlipType => {
53 | const { user } = useUser()
54 | const [loading, setIsLoading] = useState(false)
55 | const [options, setOptions] = useState([])
56 |
57 | useEffect(() => {
58 | if (!user) {
59 | return
60 | }
61 | const fetchData = async () => {
62 | setIsLoading(true)
63 | try {
64 | const newData = await loadSlip()
65 | if (newData) {
66 | setOptions(newData ? Object.values(newData) : [])
67 | }
68 | } finally {
69 | setIsLoading(false)
70 | }
71 | }
72 | fetchData()
73 | }, [user])
74 |
75 | const refetchSlip = async () => {
76 | if (!user) {
77 | return
78 | }
79 | const slip = await loadSlip()
80 | setOptions(Object.values(slip))
81 | }
82 |
83 | const removeOption = async (optionId: string) => {
84 | console.log('removing option', optionId)
85 | setIsLoading(true)
86 | await removeSlipOption(optionId)
87 | // delete optionId from slip
88 | const newSlip = options.filter((option) => option.id !== optionId)
89 |
90 | console.log('new slip', newSlip)
91 | console.log('Is slip changed?', newSlip !== options)
92 |
93 | setOptions(newSlip)
94 | await refetchSlip()
95 | setIsLoading(false)
96 | }
97 |
98 | const addOption = async (option: BetSlipOption) => {
99 | setIsLoading(true)
100 | await addSlipOption(option)
101 | await refetchSlip()
102 | setIsLoading(false)
103 | }
104 |
105 | const postBet = async (
106 | singles: BetSlipOption[],
107 | long: BetSlipOption | null
108 | ): Promise => {
109 | setIsLoading(true)
110 | const response = await placeBet(singles, long)
111 | setOptions([])
112 | setIsLoading(false)
113 | return response
114 | }
115 |
116 | return { options, refetchSlip, loading, removeOption, addOption, postBet }
117 | }
118 |
119 | export default useSlip
120 |
--------------------------------------------------------------------------------
/src/lib/util.ts:
--------------------------------------------------------------------------------
1 | import { getAccessToken } from '@auth0/nextjs-auth0'
2 |
3 | export function getLongBetName(length: number) {
4 | const names = new Map([
5 | [1, 'Single'],
6 | [2, 'Double'],
7 | [3, 'Treble'],
8 | [4, 'Fourfold'],
9 | [5, 'Fivefold'],
10 | [6, 'Sixfold'],
11 | [7, 'Sevenfold'],
12 | [8, 'Eightfold'],
13 | [9, 'Ninefold'],
14 | [10, 'Tenfold'],
15 | ])
16 | return names.get(length) ?? `${length}-fold`
17 | }
18 |
19 | export async function fetchAccessToken() {
20 | try {
21 | const { accessToken } = await getAccessToken()
22 | return accessToken
23 | } catch (e) {
24 | return null
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/ui/bet-slip/bet-slip.module.css:
--------------------------------------------------------------------------------
1 |
2 | .betslip {
3 | position: fixed;
4 | bottom: 0;
5 | right: 2em;
6 | width: 400px;
7 | background-color: var(--off-white) !important;
8 | box-shadow: -2px 2px 10px rgba(0, 0, 0, 0.3);
9 | overflow-y: auto; /* Allows scrolling if the content overflows */
10 | max-height: 100%; /* Ensures it doesn't grow beyond the height of the viewport */
11 | transition: max-height 0.1s ease-out;
12 | border-radius: var(--radius-large);
13 | padding: var(--spacing-medium);
14 | border-top: 10px solid var(--pink-lavender);
15 | }
16 |
17 | .betSlipTitle {
18 | display: flex;
19 | flex-direction: row;
20 | justify-content: space-between;
21 | align-items: center;
22 | cursor: pointer;
23 | }
24 |
25 | .showAction {
26 | color: var(--egyptian-blue);
27 | }
28 |
29 | .open {
30 | max-height: 80%; /* Example value, you can adjust based on your UI */
31 | min-height: 200px;
32 | }
33 |
34 | .collapsed {
35 | max-height: 0;
36 | overflow: hidden;
37 | }
38 |
39 |
40 | .visible {
41 | opacity: 1;
42 | }
43 |
44 | .hidden {
45 | opacity: 0;
46 | }
47 |
48 | .header {
49 | padding: 20px;
50 | }
51 |
52 | .closebtn {
53 | position: absolute;
54 | top: 1px;
55 | right: 1px;
56 | border: none;
57 | border-radius: 5px;
58 | cursor: pointer;
59 | font-size: 15px;
60 | height: 28px;
61 | width: 28px;
62 | background: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='6' viewBox='0 0 12 12'%3E%3Cpath fill='%23367A65' fill-rule='evenodd' d='M12 .784L11.243 0 6 5.431 .757 0 0 .784l5.243 5.432L6 7l.757-.784z'/%3E%3C/svg%3E") center center no-repeat;
63 | background-size: contain;
64 | }
65 |
66 | .closebtn:hover {
67 | background-color: var(--white);
68 | }
69 |
70 | .optionInfo {
71 | display: flex;
72 | flex-direction: column;
73 | flex-grow: 1;
74 | }
75 |
76 | .options {
77 | padding: 0 0.5rem;
78 | display: flex;
79 | flex-direction: row;
80 | }
81 |
82 | .options .column {
83 | display: flex;
84 | flex-direction: column;
85 | gap: 1rem;
86 | padding: 0 5px;
87 | }
88 |
89 | .options .right {
90 | border-bottom: 1px solid var(--salmon);
91 | }
92 |
93 | .options .option {
94 | display: flex;
95 | flex-direction: row;
96 | gap: 1em;
97 | width: 100%;
98 | font-size: .8rem;
99 | color: #777;
100 | align-items: flex-start;
101 | justify-content: space-between;
102 | height: 70px;
103 | padding-bottom: 1em;
104 | }
105 |
106 | .options .option .header {
107 | display: flex;
108 | flex-direction: row;
109 | justify-content: space-between;
110 | width: 100%;
111 | padding: 0;
112 | }
113 |
114 | .options .option .content {
115 | padding-left: 5px;
116 | width: 100%;
117 | font-weight: normal;
118 | font-size: var(--font-size-caption);
119 | overflow: visible;
120 | }
121 |
122 | .options .marketName {
123 | font-weight: 600;
124 | color: var(--salmon);
125 | margin-bottom: 0.3rem;
126 | }
127 |
128 | .options .option .name {
129 | display: flex;
130 | flex-direction: row;
131 | justify-content: space-between;
132 | flex-grow: 1;
133 | gap: 0.5em;
134 | font-size: var(--font-size-body);
135 | font-weight: 600;
136 | margin-bottom: 0.3rem;
137 | margin-left: 5px;
138 | }
139 |
140 | .iconButton {
141 | border: none;
142 | width: 20px;
143 | height: 20px;
144 | background-color: var(--off-white);
145 | }
146 |
147 | .iconButton:hover {
148 | background-color: var(--white);
149 | }
150 |
151 | input.stake {
152 | width: 4em;
153 | height: 30px;
154 | border: 1px solid var(--salmon);
155 | border-radius: 5px;
156 | padding: 0 5px;
157 | font-size: 1rem;
158 | }
159 |
160 | .actions {
161 | display: flex;
162 | flex-direction: row;
163 | justify-content: space-between;
164 | width: 100%;
165 | padding: 1rem 0;
166 | margin-left: -10px;
167 | }
168 |
169 | .betForm {
170 | display: flex;
171 | flex-direction: column;
172 | flex-grow: 1;
173 | }
174 |
175 | .submitting {
176 | opacity: 0.5;
177 | }
178 |
179 | .empty {
180 | padding: 1em;
181 | font-size: var(--font-size-body);
182 | font-weight: 600;
183 | border-bottom: dashed 4px var(--salmon);
184 | text-align: center;
185 | line-height: 1.5em;
186 | }
--------------------------------------------------------------------------------
/src/ui/bet-slip/bet-slip.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React, { useContext, useState } from 'react'
4 | import styles from './bet-slip.module.css'
5 | import { MarketOption } from '@/gql/types.generated'
6 | import { PlaceBetForm } from '@/ui/bet-slip/place-bet-form'
7 | import { EventFragment } from '@/gql/documents.generated'
8 | import classnames from 'classnames'
9 | import { SlipContext } from '@/lib/slip-context'
10 |
11 | export type BetSlipOption = MarketOption & {
12 | stake?: number
13 | marketName: string
14 | event: EventFragment | null
15 | }
16 |
17 | export type Slip = { [key: string]: BetSlipOption }
18 |
19 | const BetSlip = () => {
20 | const [isOpen, setIsOpen] = useState(true)
21 | const slipState = useContext(SlipContext)
22 | const options = slipState?.options ?? []
23 |
24 | const getSlipTitle = () => {
25 | if (!options) {
26 | return 'Bet Slip'
27 | }
28 | if (options.length === 1) {
29 | return 'Single Bet'
30 | }
31 | if (options.length > 1) {
32 | return 'Long Bet'
33 | }
34 | return 'Bet Slip'
35 | }
36 |
37 | const handleOptionRemoved = (option: MarketOption) => {}
38 |
39 | return (
40 |
41 |
setIsOpen(true)}
44 | >
45 |
46 | {getSlipTitle()}
47 |
Show selections ^
48 |
49 |
50 |
51 |
55 |
56 |
59 |
60 | )
61 | }
62 |
63 | export default BetSlip
64 |
--------------------------------------------------------------------------------
/src/ui/bet-slip/place-bet-form.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import styles from '@/ui/bet-slip/bet-slip.module.css'
4 | import globals from '@/ui/globals.module.css'
5 | import React, { useContext, useState } from 'react'
6 | import { BetSlipOption } from '@/ui/bet-slip/bet-slip'
7 | import { Bet } from '@/gql/types.generated'
8 | import { RemoveSlipOptionForm } from '@/ui/bet-slip/remove-slip-option-form'
9 | import { useRouter } from 'next/navigation'
10 | import { getLongBetName } from '@/lib/util'
11 | import { getOptionPointLabel, getSpreadOptionLabel } from '@/ui/event-util'
12 | import { SlipContext, SlipType } from '@/lib/slip-context'
13 | import { PlaceBetResponse } from '@/lib/useSlip'
14 | import classnames from 'classnames'
15 |
16 | export type CreatedBets = {
17 | singles: Bet[]
18 | long?: Bet[]
19 | }
20 |
21 | function SubmitButton({
22 | onClick,
23 | loading,
24 | }: {
25 | onClick: (e: { preventDefault: () => void; stopPropagation: () => void }) => void
26 | loading: boolean
27 | }) {
28 | return (
29 |
37 | )
38 | }
39 |
40 | const initialState = {
41 | message: null,
42 | }
43 |
44 | export function PlaceBetForm() {
45 | const slipState: SlipType | null = useContext(SlipContext)
46 | const [stakes, setStakes] = useState
124 | )
125 | }
126 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES6",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "plugins": [
18 | {
19 | "name": "next"
20 | }
21 | ],
22 | "paths": {
23 | "@/*": ["./src/*"]
24 | }
25 | },
26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
27 | "exclude": ["node_modules"]
28 | }
29 |
--------------------------------------------------------------------------------