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[]; + }; +}