diff --git a/ui/src/client/api.ts b/ui/src/client/api.ts index 93b6e6c..d3367c6 100644 --- a/ui/src/client/api.ts +++ b/ui/src/client/api.ts @@ -1,81 +1,85 @@ -import axios, { AxiosInstance } from 'axios'; -import { Game } from '~/interface.ts'; +import axios, {AxiosInstance} from 'axios'; +import {Game} from '~/interface.ts'; interface DateResponse { - dates: string[]; - model_name: string; + dates: string[]; + model_name: string; } export class AccuribetAPI { - private client: AxiosInstance; - private static instance: AccuribetAPI; - - - - static getInstance(): AccuribetAPI { - if (!AccuribetAPI.instance) { - AccuribetAPI.instance = new AccuribetAPI(); - } - return AccuribetAPI.instance; - } - private constructor() { - this.client = axios.create({ - baseURL: import.meta.env.VITE_API_BASE_URL as string, - validateStatus: (status: number) => { - return status < 500; + private client: AxiosInstance; + private static instance: AccuribetAPI; + + + static getInstance(): AccuribetAPI { + if (!AccuribetAPI.instance) { + AccuribetAPI.instance = new AccuribetAPI(); + } + return AccuribetAPI.instance; + } + + private constructor() { + this.client = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL as string, + validateStatus: (status: number) => { + return status < 500; + } + }) + + } + + + async getPredictedGames( + date: string, + model_name: string + ) { + if (localStorage.getItem(`predictedGames-${date}-${model_name}`)) { + return JSON.parse(localStorage.getItem(`predictedGames-${date}-${model_name}`) as string); + } + const res = await this.client.get('/model/history', { + params: { + date, + model_name + } + }) + localStorage.setItem(`predictedGames-${date}-${model_name}`, JSON.stringify(res.data)); + return res.data; + + } + + + async predict( + model: string + ) { + const res = await this.client.get(`/model/predict/${model}`); + return res.data; + } + + async dailyGames( + withOdds: boolean = false + ): Promise { + const res = await this.client.get(`/games/daily?with_odds=${withOdds}`); + return res.data; + } + + async listModels(): Promise { + const res = await this.client.get('/model/list'); + return res.data; + } + + async modelAccuracy( + modelName: string + ): Promise { + const res = await this.client.get(`/model/accuracy/${modelName}`); + return res.data; + } + + + async getDates(): Promise { + const res = await this.client.get('/model/history/dates'); + return res.data; } - }) - - } - - - async getPredictedGames( - date: string, - model_name: string - ) { - const res = await this.client.get('/model/history', { - params: { - date, - model_name - } - }) - return res.data; - - } - - - async predict( - model: string - ) { - const res = await this.client.get(`/model/predict/${model}`); - return res.data; - } - - async dailyGames( - withOdds: boolean = false - ): Promise { - const res = await this.client.get(`/games/daily?with_odds=${withOdds}`); - return res.data; - } - - async listModels(): Promise { - const res = await this.client.get('/model/list'); - return res.data; - } - - async modelAccuracy( - modelName: string - ): Promise { - const res = await this.client.get(`/model/accuracy/${modelName}`); - return res.data; - } - - - async getDates(): Promise { - const res = await this.client.get('/model/history/dates'); - return res.data; - } } \ No newline at end of file diff --git a/ui/src/components/display-card.tsx b/ui/src/components/display-card.tsx index 9e07f11..59cc456 100644 --- a/ui/src/components/display-card.tsx +++ b/ui/src/components/display-card.tsx @@ -1,279 +1,286 @@ -import { Component, For, Show } from "solid-js"; -import { Game, GameWithPrediction, Period, Team } from "~/interface"; -import { FiClock } from "solid-icons/fi"; -import { IoLocationOutline } from "solid-icons/io"; -import { OcDotfill3 } from "solid-icons/oc"; -import { Motion } from "solid-motionone"; -import { Avatar, AvatarImage } from "~/components/ui/avatar"; -import { Badge } from "~/components/ui/badge"; -import { Button } from "~/components/ui/button"; +import {Component, For, Show} from "solid-js"; +import {Game, GameWithPrediction, Period, Team} from "~/interface"; +import {FiClock} from "solid-icons/fi"; +import {IoLocationOutline} from "solid-icons/io"; +import {OcDotfill3} from "solid-icons/oc"; +import {Motion} from "solid-motionone"; +import {Avatar, AvatarImage} from "~/components/ui/avatar"; +import {Badge} from "~/components/ui/badge"; +import {Button} from "~/components/ui/button"; import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle } from "~/components/ui/card"; import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow } from "~/components/ui/table"; -import { isLive, timeUntilGame } from "~/lib/utils.ts"; -import { Prediction } from "~/model/prediction.ts"; -import { AnimationDiv } from "~/components/animated-div.tsx"; -import { DialogModal } from "~/components/dialog-modal.tsx"; +import {isLive, timeUntilGame} from "~/lib/utils.ts"; +import {Prediction} from "~/model/prediction.ts"; +import {AnimationDiv} from "~/components/animated-div.tsx"; +import {DialogModal} from "~/components/dialog-modal.tsx"; -const logos = import.meta.glob("../assets/teams/*.svg", { eager: true }); +const logos = import.meta.glob("../assets/teams/*.svg", {eager: true}); -export const getLogo = (team: string) => { - // if team is not all lower make it lower +export const getLogo = (team?: string) => { + if (!team) return null; + // if team is not all lower make it lower if (team !== team.toLowerCase()) team = team.toLowerCase() - let strIndex = `../assets/teams/${team}.svg`; - // @ts-ignore - return logos[strIndex].default; + let strIndex = `../assets/teams/${team}.svg`; + try { + let x = logos[strIndex] + // @ts-ignore + return x.default + } catch { + return null + } }; const formattedTimeForUser = (time: number): string => { - /** - * Takes in a unix seconds timestamp and returns a formatted time string - * for the user. - * Like so: 12:00 PM EST - */ - const date = new Date(time * 1000); // Convert seconds to milliseconds - const options: Intl.DateTimeFormatOptions = { - hour: "2-digit", - minute: "2-digit", - hour12: true, - timeZoneName: "short" - }; - return new Intl.DateTimeFormat("en-US", options).format(date); + /** + * Takes in a unix seconds timestamp and returns a formatted time string + * for the user. + * Like so: 12:00 PM EST + */ + const date = new Date(time * 1000); // Convert seconds to milliseconds + const options: Intl.DateTimeFormatOptions = { + hour: "2-digit", + minute: "2-digit", + hour12: true, + timeZoneName: "short" + }; + return new Intl.DateTimeFormat("en-US", options).format(date); }; const getColorFromStatusAndOutcome = (status: string, winner: boolean): string => { - if (status === "Final" || status === "Final/OT") { - if (winner) { - return "bg-emerald-600"; + if (status === "Final" || status === "Final/OT") { + if (winner) { + return "bg-emerald-600"; + } else { + return "bg-red-600"; + } } else { - return "bg-red-600"; + return "bg-700"; } - } else { - return "bg-700"; - } }; const winningTeam = (game: GameWithPrediction): number => { - if (game.status === "Final" || game.status === "Final/OT") { - return game.home_team.score.points > game.away_team.score.points - ? game.home_team.id - : game.away_team.id; - } - return 0; + if (game.status === "Final" || game.status === "Final/OT") { + return game.home_team.score.points > game.away_team.score.points + ? game.home_team.id + : game.away_team.id; + } + return 0; }; interface IDisplayCard { - game: GameWithPrediction; + game: GameWithPrediction; } interface ITeamProps { - team: Team; + team: Team; } interface ITeamInfoProps { - team: Team; - winner: number; - prediction?: Prediction; - game: GameWithPrediction; + team: Team; + winner: number; + prediction?: Prediction; + game: GameWithPrediction; } interface IQuickDisplayProps { - game: Game; + game: Game; } export const ScoreTable: Component = (props: ITeamProps) => { - const formatPeriodType = (period: Period) => { - if (period.period_type === "REGULAR") { - return `Quarter ${period.period}`; - } else { - return period.period_type; - } - }; + const formatPeriodType = (period: Period) => { + if (period.period_type === "REGULAR") { + return `Quarter ${period.period}`; + } else { + return period.period_type; + } + }; - return ( - - - - - {(period, _) => ( - {formatPeriodType(period)} - )} - - - - - - - {(period, _) => ( - - {period.score === null || period.score === 0 ? "-" : period.score} - - )} - - - -
- ); + return ( + + + + + {(period, _) => ( + {formatPeriodType(period)} + )} + + + + + + + {(period, _) => ( + + {period.score === null || period.score === 0 ? "-" : period.score} + + )} + + + +
+ ); }; export const KeyPlayer: Component = (props: ITeamProps) => { - return ( -
-

Key Player - {props.team.name}

-

{props.team.leader.name}

-

Points: {props.team.leader.points}

-

Rebounds: {props.team.leader.rebounds}

-

Assists: {props.team.leader.assists}

-
- ); + return ( +
+

Key Player - {props.team.name}

+

{props.team.leader.name}

+

Points: {props.team.leader.points}

+

Rebounds: {props.team.leader.rebounds}

+

Assists: {props.team.leader.assists}

+
+ ); }; export const TeamInfo: Component = (props: ITeamInfoProps) => { - return ( -
- - - - {`${props.team.city} ${props.team.name}`} - - {`${props.team.wins} - ${props.team.losses}`} - + return ( +
+ + + + {`${props.team.city} ${props.team.name}`} + + {`${props.team.wins} - ${props.team.losses}`} + Winner - + Projected Winner - -
- ); +
+
+ ); }; export const QuickDisplay: Component = (props: IQuickDisplayProps) => { - const handleClick = (event: MouseEvent) => { - event.preventDefault(); - const targetId = `#game-card-${props.game.id}`; - const targetElement = document.querySelector(targetId); - if (targetElement) { - targetElement.scrollIntoView({ - behavior: "smooth", - block: "start", - inline: "nearest" - }); - } - }; + const handleClick = (event: MouseEvent) => { + event.preventDefault(); + const targetId = `#game-card-${props.game.id}`; + const targetElement = document.querySelector(targetId); + if (targetElement) { + targetElement.scrollIntoView({ + behavior: "smooth", + block: "start", + inline: "nearest" + }); + } + }; - return ( - -
- - - - {props.game.home_team.name} -
-
vs
-
- {props.game.away_team.name} - - - -
-
- ); + return ( + +
+ + + + {props.game.home_team.name} +
+
vs
+
+ {props.game.away_team.name} + + + +
+
+ ); }; export const AdvancedGameCard: Component = (props: ITeamProps) => { - return ( -
-
-

Score Breakdown - {props.team.name}

- -
-
- ); + return ( +
+
+

Score Breakdown - {props.team.name}

+ +
+
+ ); }; export const DemoCard: Component = (props: IDisplayCard) => { - return ( -
- - -
- - vs - -
-
- -
- -
- - {`${props.game.location.name}, ${props.game.location.city}, ${props.game.location.state}`} -
-
- -
- - + return ( +
+ + +
+ + vs + +
+
+ +
+ +
+ + {`${props.game.location.name}, ${props.game.location.city}, ${props.game.location.state}`} +
+
+ +
+ +

Postponed

@@ -288,112 +295,118 @@ export const DemoCard: Component = (props: IDisplayCard) => {

-
-
-
-
- - {(team, _) => ( - - - - )} - - -
-
-
- +
+ +
+
+ + {(team, _) => ( + + + + )} + + +
+
+
+ - + - Live - -
-
-
- {props.game.home_team.name} - + Live + +
+
+
+ {props.game.home_team.name} + {props.game.home_team.score.points} -
- - -
- {props.game.away_team.name} - +
+ - +
+ {props.game.away_team.name} + {props.game.away_team.score.points} +
+
+

+ {props.game.status.includes("ET") ? "Starting soon!" : props.game.status} +

+
+
+ + {(team, _) => } + +
-
-

- {props.game.status.includes("ET") ? "Starting soon!" : props.game.status} -

-
-
- - {(team, _) => } - - -
- - -
- -

Prediction Confidence

-
-

- The prediction model has a confidence of{" "} - {((props.game.prediction?.confidence ?? 0) * 100).toFixed(1)}% for the{" "} - {props.game.prediction?.prediction} to win. -

-
-
-
-
- team.injuries.length > 0 - )} - > - - View Injury Report - - } - > - - - - Team - Player - Status - - - - - {(team, _) => ( - - {(injury, _) => ( - - {team.name} - {injury.player} - {injury.status} - - )} - - )} - - -
-
-
-
-
- -
- ); + + +
+ +

Prediction Confidence

+
+

+ The prediction model has a confidence of{" "} + {((props.game.prediction?.confidence ?? 0) * 100).toFixed(1)}% for the{" "} + {props.game.prediction?.prediction} to win. +

+
+
+
+
+ team.injuries.length > 0 + )} + > + + View Injury Report + + } + > + + + + Team + Player + Status + + + + + {(team, _) => ( + + {(injury, _) => ( + + {team.name} + {injury.player} + {injury.status} + + )} + + )} + + +
+
+
+
+
+ +
+ ); }; diff --git a/ui/src/pages/History.tsx b/ui/src/pages/History.tsx index 0327050..9783943 100644 --- a/ui/src/pages/History.tsx +++ b/ui/src/pages/History.tsx @@ -1,238 +1,249 @@ -import { AccuribetAPI } from "~/client/api.ts"; +import {AccuribetAPI} from "~/client/api.ts"; import {createEffect, createResource, For, Show, Suspense} from "solid-js"; -import { AnimationDiv } from "~/components/animated-div.tsx"; -import { Loading } from "~/components/loading.tsx"; -import { HistoryDate,} from "~/interface.ts"; -import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; -import { Label } from "~/components/ui/label"; +import {AnimationDiv} from "~/components/animated-div.tsx"; +import {Loading} from "~/components/loading.tsx"; +import {HistoryDate,} from "~/interface.ts"; +import {Card, CardContent, CardHeader, CardTitle} from "~/components/ui/card"; +import {Label} from "~/components/ui/label"; import {FiCalendar, FiAlertCircle, FiXCircle, FiCheckCircle, FiMapPin} from 'solid-icons/fi'; -import { ErrorBoundary } from "solid-js"; +import {ErrorBoundary} from "solid-js"; import {getLogo} from "~/components/display-card.tsx"; import {LOCATION_DATA, TEAM_NAME_ABBRV_MAP} from "~/constants.ts"; import {useSearchParams} from "@solidjs/router"; async function fetchHistory() { - const instance = AccuribetAPI.getInstance(); - return await instance.getDates(); + const instance = AccuribetAPI.getInstance(); + return await instance.getDates(); } interface IHistory { - model: string; - date: string; + model: string; + date: string; } - async function fetchHistoryForModelOnDate(value: IHistory): Promise { - if (value.date === "" || value.model === "") return undefined; - const instance = AccuribetAPI.getInstance(); - return await instance.getPredictedGames(value.date, value.model); + if (value.date === "" || value.model === "") return undefined; + const instance = AccuribetAPI.getInstance(); + return await instance.getPredictedGames(value.date, value.model); } function ModelSelector(props: { dates: HistoryDate[], onSelect: (model: string) => void, selectedModel: string }) { - return ( -
- - {date => ( -
- props.onSelect(date.model_name)} - class="peer sr-only" - /> - -
- )} -
-
- ); + return ( +
+ + {date => ( +
+ props.onSelect(date.model_name)} + class="peer sr-only" + /> + +
+ )} +
+
+ ); } + function DateSelector(props: { date: string, onChange: (date: string) => void, minDate?: string, maxDate: string }) { - return ( -
- -
- - props.onChange(event.target.value)} - max={props.maxDate} - min={props.minDate} - /> + return ( +
+ +
+ + props.onChange(event.target.value)} + max={props.maxDate} + min={props.minDate} + /> +
-
- ); + ); } function HistoryList(props: { games: any[] }) { - const correctPredictions = props.games.filter(game => game.prediction_was_correct).length; - - const percentageCorrect = (correctPredictions / props.games.length) * 100; - - return ( - 0} - fallback={ - - -

No games available for this date and model.

-

Try selecting a different date or model.

-
- } - > -
- -

- Predictions Correct: {percentageCorrect.toFixed(2)}% -

-
- - {game => { - - const abbrv = TEAM_NAME_ABBRV_MAP[game.home_team_name]; - const location = LOCATION_DATA[abbrv] || { name: "", city: "", state: "" } - - return ( - - -
-
- {game.home_team_name} -
-

{game.home_team_name}

-

{game.home_team_score}

-
-
-
-

vs

-

{new Date(game.date).toLocaleDateString()}

-
-
-
-

{game.away_team_name}

-

{game.away_team_score}

-
- {game.away_team_name} -
-
-
-

Prediction: {game.prediction}

- {game.prediction_was_correct ? ( - - ) : ( - - )} -
-
- - {location.name}, {location.city}, {location.state} -
-
-
- ); - }} -
-
-
- ); + const correctPredictions = props.games.filter(game => game.prediction_was_correct).length; + + const percentageCorrect = (correctPredictions / props.games.length) * 100; + + return ( + 0} + fallback={ + + +

No games available for this date and model.

+

Try selecting a different date or model.

+
+ } + > +
+ +

+ Predictions Correct: {percentageCorrect.toFixed(2)}% +

+
+ + {game => { + + const abbrv = TEAM_NAME_ABBRV_MAP[game.home_team_name]; + const location = LOCATION_DATA[abbrv] || {name: "", city: "", state: ""} + + return ( + + +
+
+ {game.home_team_name} +
+

{game.home_team_name}

+

{game.home_team_score}

+
+
+
+

vs

+

{new Date(game.date).toLocaleDateString()}

+
+
+
+

{game.away_team_name}

+

{game.away_team_score}

+
+ {game.away_team_name} +
+
+
+

Prediction: {game.prediction}

+ {game.prediction_was_correct ? ( + + ) : ( + + )} +
+
+ + {location.name}, {location.city}, {location.state} +
+
+
+ ); + }} +
+
+
+ ); } export function History() { - const [searchParams, setSearchParams] = useSearchParams(); - const [dates] = createResource(fetchHistory); - const [historyResource] = createResource( - () => ({ model: searchParams.model || "", date: searchParams.date || "" }), - fetchHistoryForModelOnDate - ); - - const oldestDateForModel = () => { - if (dates() && searchParams.model) { - const datesForModel = dates()?.find(date => date.model_name === searchParams.model)?.dates; - return datesForModel ? datesForModel[datesForModel.length - 1] : undefined; - } - }; - - const handleModelChange = (modelName: string) => { - setSearchParams({ model: modelName, date: searchParams.date || "" }); - }; - - const handleDateChange = (date: string) => { - setSearchParams({ model: searchParams.model || "", date }); - }; - - createEffect(() => { - - if (!(dates() && !searchParams.model && dates()?.length > 0)) { - return; - } - setSearchParams({model: dates()[0]?.model_name, date: searchParams.date || ""}); - }); - - return ( -
- - - - Prediction History - - - ( -
- -

An error occurred

-

{err.message}

-
- )}> - }> -
-
- - - - - - -
- - -
- Showing results for model: {searchParams.model} on {new Date(searchParams.date).toLocaleDateString()} -
-
- - }> - - - - -
-
-
-
-
-
-
- ); + const [searchParams, setSearchParams] = useSearchParams(); + const [dates] = createResource(fetchHistory); + const [historyResource] = createResource( + () => ({model: searchParams.model || "", date: searchParams.date || ""}), + fetchHistoryForModelOnDate + ); + + const oldestDateForModel = () => { + if (dates() && searchParams.model) { + const datesForModel = dates()?.find(date => date.model_name === searchParams.model)?.dates; + return datesForModel ? datesForModel[datesForModel.length - 1] : undefined; + } + }; + + const handleModelChange = (modelName: string) => { + setSearchParams({model: modelName, date: searchParams.date || ""}); + }; + + const handleDateChange = (date: string) => { + setSearchParams({model: searchParams.model || "", date}); + }; + + createEffect(() => { + if (!dates()) return; + + if (!(dates() && !searchParams.model && dates()?.length > 0)) { + return; + } + setSearchParams({model: dates()[0]?.model_name, date: searchParams.date || ""}); + }); + + return ( +
+ + + + Prediction History + + + { + console.error( + "An error occurred while rendering the history page", + err + ) + return ( +
+ +

An error occurred

+

{err.message}

+
+ ) + }}> + }> +
+
+ + + + + + +
+ + +
+ Showing results for + model: {searchParams.model} on {new Date(searchParams.date).toLocaleDateString()} +
+
+ + }> + + + + +
+
+
+
+
+
+
+ ); } \ No newline at end of file