From ecc5b77501ba2c75d76404bb0d2648e4cf37e528 Mon Sep 17 00:00:00 2001 From: cris lombardo Date: Mon, 22 Jan 2024 11:49:12 -0500 Subject: [PATCH] chore: consume standings data from server Initialize the use of standings data from the server Refactor the drivers and constructors standings ui Include results type declarations at results.d.ts Update faker data to match standings schema --- package.json | 2 +- src/app/lib/utils.tsx | 98 +++++++----------- src/app/results/RaceResults.tsx | 6 +- src/app/results/SeasonResults.tsx | 79 ++++----------- src/app/results/StandingsTimeline.tsx | 137 ++++++++++++++++++++++++++ src/app/ui/Tabs.tsx | 32 +++--- src/atoms/results.tsx | 53 +++++----- src/results.d.ts | 75 ++++++++++++++ 8 files changed, 318 insertions(+), 164 deletions(-) create mode 100644 src/app/results/StandingsTimeline.tsx create mode 100644 src/results.d.ts diff --git a/package.json b/package.json index 5d92989..b4c5d2e 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "typecheck": "tsc --pretty --noEmit --incremental false" }, "dependencies": { - "axios": "^1.6.5", + "axios": "latest", "clsx": "latest", "jotai": "latest", "jotai-effect": "latest", diff --git a/src/app/lib/utils.tsx b/src/app/lib/utils.tsx index e1fde80..966d4f8 100644 --- a/src/app/lib/utils.tsx +++ b/src/app/lib/utils.tsx @@ -40,64 +40,7 @@ export const f1Seasons = (): string[] => { ); }; -export interface ISchedule { - RoundNumber: number; - Country: string; - Location: string; - OfficialEventName: string; - EventDate: string; - EventName: string; - EventFormat: string; - Session1: string; - Session1Date: string; - Session1DateUtc: string; - Session2: string; - Session2Date: string; - Session2DateUtc: string; - Session3: string; - Session3Date: string; - Session3DateUtc: string; - Session4: string; - Session4Date: string; - Session4DateUtc: string; - Session5: string; - Session5Date: string; - Session5DateUtc: string; - F1ApiSupport: boolean; -} - -// Raw Fetch Format -export type IConstructorStandingsFetch = { - [key in 'position' | 'points' | 'wins']: string; -} & { - Constructor: { - name: string; - }; -}; -// UI format -export type IConstructorStandings = { - [key in 'pos' | 'points' | 'wins' | 'name']: string; -}; - -interface IDataConfigs { - seasons: string[]; - schedule: ISchedule[]; - drivers: string[]; - sessions: string[]; - standings: { - // drivers: { - // position: string, - // points: string, - // wins: string, - // Constructor?: { - // name: string, - // } - // }[], - constructors: IConstructorStandingsFetch[]; - }; -} - -const dataConfig: IDataConfigs = { +const dataConfig: DataConfigSchema = { seasons: f1Seasons(), schedule: Array.from(Array(3).keys()).map(() => ({ RoundNumber: 0, @@ -134,21 +77,48 @@ const dataConfig: IDataConfigs = { ], sessions: ['Practice 1', 'Practice 2', 'Practice 3', 'Qualifying', 'Race'], standings: { - // drivers: { - - // }, - constructors: Array.from(Array(10).keys()).map(() => ({ + DriverStandings: Array.from(Array(5).keys()).map(() => ({ + positionText: faker.number.int(20).toString(), + position: faker.number.int(20).toString(), + points: faker.number.int(25).toString(), + wins: faker.number.int(10).toString(), + Driver: { + driverId: faker.person.middleName(), + permanentNumber: faker.number.int(99).toString(), + code: faker.word.sample(3), + url: faker.internet.domainName(), + givenName: faker.person.firstName(), + familyName: faker.person.lastName(), + dateOfBirth: faker.date.birthdate().toString(), + nationality: faker.location.country(), + }, + Constructors: [ + { + constructorId: faker.person.middleName(), + url: faker.internet.domainName(), + name: faker.person.middleName(), + nationality: faker.location.country(), + }, + ], + })), + ConstructorStandings: Array.from(Array(5).keys()).map(() => ({ + positionText: faker.number.int(20).toString(), position: faker.number.int(20).toString(), points: faker.number.int(25).toString(), wins: faker.number.int(10).toString(), Constructor: { + constructorId: faker.person.middleName(), + url: faker.internet.domainName(), name: faker.person.middleName(), + nationality: faker.location.country(), }, })), + season: 0, + round: 0, }, }; -const serverURL = 'http://0.0.0.0:8081'; +const serverURL = 'http://127.0.0.1:8081'; export const fetchAPI = async ( endpoint: string, statusCheck: boolean = false, @@ -158,7 +128,7 @@ export const fetchAPI = async ( const options = statusCheck ? { headers: { cache: 'no-store' } } : {}; // Get dummy data or return false - const dummy: string[] | ISchedule[] | false = + const dummy: string[] | ScheduleSchema[] | false = dataConfig[ endpoint.split('?')[0] as 'seasons' | 'schedule' | 'drivers' | 'sessions' ] || false; diff --git a/src/app/results/RaceResults.tsx b/src/app/results/RaceResults.tsx index 05501ea..52f1008 100644 --- a/src/app/results/RaceResults.tsx +++ b/src/app/results/RaceResults.tsx @@ -4,9 +4,7 @@ import { useMemo } from 'react'; import { seasonRacesAtom } from '@/atoms/results'; -import { ISchedule } from '../lib/utils'; - -const ResultCard = ({ data }: { data: ISchedule }) => { +const ResultCard = ({ data }: { data: ScheduleSchema }) => { const eventDate = new Date(data.EventDate); const eventPassed = new Date() > eventDate; @@ -49,7 +47,7 @@ const ResultCard = ({ data }: { data: ISchedule }) => { ); }; -const WinterTesting = ({ data }: { data: ISchedule }) => { +const WinterTesting = ({ data }: { data: ScheduleSchema }) => { const eventDate = new Date(data.EventDate); const eventPassed = new Date() > eventDate; diff --git a/src/app/results/SeasonResults.tsx b/src/app/results/SeasonResults.tsx index bf4a2bc..15144f1 100644 --- a/src/app/results/SeasonResults.tsx +++ b/src/app/results/SeasonResults.tsx @@ -2,76 +2,33 @@ import { useAtom } from 'jotai'; -import { constructorStandingsAtom, fetchStandings } from '@/atoms/results'; +import { + constructorStandingsAtom, + driverStandingsAtom, + fetchStandings, +} from '@/atoms/results'; import { RaceSchedule } from './RaceResults'; -import { driverData, DriverHeadings } from '../lib/placerholder-results'; -import { IConstructorStandings, positionEnding } from '../lib/utils'; -import { Table } from '../ui/Table'; +import { StandingsTimeline } from './StandingsTimeline'; import { Tabs } from '../ui/Tabs'; -const ConstuctorHeadings = ['position', 'points', 'wins', 'name']; - -const ConstructorCard = ({ data }: { data: IConstructorStandings }) => ( -
-
-

- {positionEnding(data.pos)} {data.name} -

-
-

- Points: -
- {data.points} -

-

- Wins: -
- {data.wins} -

-
-
-
-); - -const ConstructorResults = () => { - const [constructorStandings] = useAtom(constructorStandingsAtom); +export default function ResultsPage() { useAtom(fetchStandings); + const [constructorStandings] = useAtom(constructorStandingsAtom); + const [driverStandings] = useAtom(driverStandingsAtom); - return ( - <> -
- {constructorStandings.map((constructor) => ( - - ))} -
-
- - - - ); -}; - -const tabHeaders = ['Races', 'Drivers', 'Constructors']; -const tabs = [ - , -
, -]; - -export default function ResultsPage() { return (
]} + headers={['Races', 'Drivers', 'Constructors']} + containers={[ + , + , + , + ]} />
); diff --git a/src/app/results/StandingsTimeline.tsx b/src/app/results/StandingsTimeline.tsx new file mode 100644 index 0000000..42ffa36 --- /dev/null +++ b/src/app/results/StandingsTimeline.tsx @@ -0,0 +1,137 @@ +import clsx from 'clsx'; + +import { positionEnding } from '../lib/utils'; + +export const StandingsTimeline = ({ + data, +}: { + data: DriverStandingSchema[] | ConstructorStandingSchema[]; +}) => { + return ( + + ); +}; + +const DriverStandingInfo = ({ + driver, + subEl = false, +}: { + driver: DriverStandingSchema; + subEl?: boolean; +}) => { + return ( + <> + {/* Driver Standings */} + {driver.Driver && ( +

+ {driver.Driver.givenName} {driver.Driver.familyName} +

+ )} + {!subEl && ( +

+ {driver.Constructors.map(({ name }) => ( + {name} + ))} +

+ )} +

Points: {driver.points}

+

Wins: {driver.wins}

+ + ); +}; + +const ConstructorStandingInfo = ({ + con, +}: { + con: ConstructorStandingSchema; +}) => { + return ( + <> +

{con.Constructor.name}

+

Points: {con.points}

+

Wins: {con.wins}

+
+
+ {con.Drivers && + con.Drivers.map((driver) => ( +
+ +
+ ))} +
+ + ); +}; + +const PositionMarker = ({ + pos, + odd = false, +}: { + pos: string; + odd?: boolean; +}) => ( +
+

{pos}

+
+); + +const TimelineMarker = () => ( +
+ + + +
+); diff --git a/src/app/ui/Tabs.tsx b/src/app/ui/Tabs.tsx index fa06c91..8ff3f2d 100644 --- a/src/app/ui/Tabs.tsx +++ b/src/app/ui/Tabs.tsx @@ -13,16 +13,24 @@ export const Tabs = ({ headers, containers }: ITabs) => { const [tabIndex, setTabIndex] = useAtom(tabView); // Active tab has matching tabIndex - const TabButtons = headers.map((header, i) => ( - setTabIndex(i)} - > - {header} - - )); + const TabButtons = headers.map((header, i) => { + const switchTabs = () => { + setTabIndex(i); + window.scrollTo(0, 0); + }; + return ( + + {header} + + ); + }); // Hide containers not matching tabIndex const TabContainers = containers.map((tab, i) => ( @@ -39,8 +47,8 @@ export const Tabs = ({ headers, containers }: ITabs) => { return (
{TabContainers} -
-
+
+
{TabButtons}
diff --git a/src/atoms/results.tsx b/src/atoms/results.tsx index 95c20b6..fdb2638 100644 --- a/src/atoms/results.tsx +++ b/src/atoms/results.tsx @@ -1,15 +1,10 @@ import { atom } from 'jotai'; import { atomEffect } from 'jotai-effect'; -import { - fetchAPI, - IConstructorStandings, - IConstructorStandingsFetch, - ISchedule, -} from '../app/lib/utils'; +import { fetchAPI } from '../app/lib/utils'; export const raceAtom = atom('All Races'); -export const seasonRacesAtom = atom([]); +export const seasonRacesAtom = atom([]); export const seasonAtom = atom('2023'); export const allSeasonsAtom = atom([]); export const driverAtom = atom('All Drivers'); @@ -18,7 +13,8 @@ export const sessionAtom = atom('Race'); export const sessionsAtom = atom([]); export const telemetryDisableAtom = atom(true); export const resultUrlAtom = atom('/results'); -export const constructorStandingsAtom = atom([]); +export const constructorStandingsAtom = atom([]); +export const driverStandingsAtom = atom([]); // Derived Atoms @@ -50,28 +46,41 @@ export const fetchRaces = atomEffect((get, set) => { }); // Get Driver per ...season & race -export const fetchDriver = atomEffect((get, set) => { +export const fetchDriver = atomEffect((_get, set) => { fetchAPI('drivers').then((data) => set(driversAtom, data)); }); // Get sessions per ...season & race -export const fetchSessions = atomEffect((get, set) => { +export const fetchSessions = atomEffect((_get, set) => { fetchAPI('sessions').then((data) => set(sessionsAtom, data)); }); // Get Driver & Constructor Standings export const fetchStandings = atomEffect((get, set) => { - fetchAPI('standings').then((data) => { - // Flatten constructor values - const constructors = data.constructors.map( - (con: IConstructorStandingsFetch) => ({ - pos: con.position, - name: con.Constructor.name, - points: con.points, - wins: con.wins, - }), - ); - set(constructorStandingsAtom, constructors); - }); + fetchAPI('standings').then( + ({ + season, + DriverStandings, + ConstructorStandings, + }: DataConfigSchema['standings']) => { + if (season.toString() !== get(seasonAtom)) { + // Todo: Resolve error by setting the year with get(seasonAtom) in original fetch + return; + } + + const constructors = ConstructorStandings.map((cs) => { + const { name } = cs.Constructor; + return { + ...cs, + Drivers: DriverStandings.filter((driver) => + driver.Constructors.find((c) => c.name === name), + ), + }; + }); + + set(constructorStandingsAtom, constructors); + set(driverStandingsAtom, DriverStandings); + }, + ); }); // Telemetry is disabled if no race and driver are selected diff --git a/src/results.d.ts b/src/results.d.ts new file mode 100644 index 0000000..6520355 --- /dev/null +++ b/src/results.d.ts @@ -0,0 +1,75 @@ +interface ScheduleSchema { + RoundNumber: number; + Country: string; + Location: string; + OfficialEventName: string; + EventDate: string; + EventName: string; + EventFormat: string; + Session1: string; + Session1Date: string; + Session1DateUtc: string; + Session2: string; + Session2Date: string; + Session2DateUtc: string; + Session3: string; + Session3Date: string; + Session3DateUtc: string; + Session4: string; + Session4Date: string; + Session4DateUtc: string; + Session5: string; + Session5Date: string; + Session5DateUtc: string; + F1ApiSupport: boolean; +} + +// Raw Fetch Format +interface DriverSchema { + driverId: string; + permanentNumber: string; + code: string; + url: string; + givenName: string; + familyName: string; + dateOfBirth: string; + nationality: string; +} + +interface ConstructorSchema { + constructorId: string; + url: string; + name: string; + nationality: string; +} + +interface StandingsSchema { + position: string; + positionText: string; + points: string; + wins: string; +} + +interface DriverStandingSchema extends StandingsSchema { + Constructors: ConstructorSchema[]; + Driver: DriverSchema; +} + +interface ConstructorStandingSchema extends StandingsSchema { + Constructor: ConstructorSchema; + Drivers?: DriverStandingSchema[]; +} + +// UI format +interface DataConfigSchema { + seasons: string[]; + schedule: ScheduleSchema[]; + drivers: string[]; + sessions: string[]; + standings: { + season: number; + round: number; + DriverStandings: DriverStandingSchema[]; + ConstructorStandings: ConstructorStandingSchema[]; + }; +}