diff --git a/src/app/(features)/RaceSchedule.tsx b/src/app/(features)/RaceSchedule.tsx index db2971e..3b44b2d 100644 --- a/src/app/(features)/RaceSchedule.tsx +++ b/src/app/(features)/RaceSchedule.tsx @@ -6,21 +6,16 @@ import { seasonRacesAtom } from '@/atoms/races'; export const RaceSchedule = () => { const [races] = useAtom(seasonRacesAtom); - - const winterTesting = useMemo( - () => races.find((race) => race.EventFormat === 'testing'), - [races], - ); const mainEvents = useMemo( - () => races.filter((race) => race.EventFormat !== 'testing'), + () => (races ? races.filter((race) => race.EventFormat !== 'testing') : []), [races], ); - if (races.length === 0) + if (races && races.length === 0) return (
- {/* 3 Placeholder Cards */} + {/* 4 Placeholder Cards */} {Array.from(Array(4).keys()).map((_, i) => ( ))} @@ -31,7 +26,6 @@ export const RaceSchedule = () => { return (
{/* If seasonAom === current/upcomming season, then add button to bring user to next event */} - {winterTesting && }
{/* 10 Placeholder Cards */} {mainEvents.map((race) => ( @@ -97,39 +91,3 @@ const ResultCard = ({ data }: { data: ScheduleSchema }) => {
); }; - -const WinterTesting = ({ data }: { data: ScheduleSchema }) => { - const eventDate = new Date(data.EventDate); - const eventPassed = new Date() > eventDate; - - return ( -
-
- - 'https://daisyui.com/images/stock/photo-1606107557195-0e29a4b5b4aa.jpg' - } - src='/shoe.jpg' - alt='Shoes' - /> -
-
-
-

{data.OfficialEventName}

-

- {data.Location}, {data.Country} -

-

{eventDate.toDateString()}

-
- {eventPassed && ( - - Testing Results - - )} -
-
- ); -}; diff --git a/src/app/(features)/RaceTimeline.tsx b/src/app/(features)/RaceTimeline.tsx index f8b7e38..8dbd60f 100644 --- a/src/app/(features)/RaceTimeline.tsx +++ b/src/app/(features)/RaceTimeline.tsx @@ -1,7 +1,11 @@ import clsx from 'clsx'; import { BsFillStarFill } from 'react-icons/bs'; -import { fastestLap, formatDuration, positionEnding } from '../../lib/utils'; +import { + fastestLap, + formatDuration, + positionEnding, +} from '../../utils/helpers'; export const DriverResultsInfo = ({ driver, diff --git a/src/app/(features)/StandingsTimeline.tsx b/src/app/(features)/StandingsTimeline.tsx index a201338..862b8e0 100644 --- a/src/app/(features)/StandingsTimeline.tsx +++ b/src/app/(features)/StandingsTimeline.tsx @@ -1,6 +1,6 @@ import clsx from 'clsx'; -import { positionEnding } from '../../lib/utils'; +import { positionEnding } from '../../utils/helpers'; export const DriverStandingInfo = ({ driver, diff --git a/src/app/[season]/[round]/page.tsx b/src/app/[season]/[round]/page.tsx index 5844a86..678ddd2 100644 --- a/src/app/[season]/[round]/page.tsx +++ b/src/app/[season]/[round]/page.tsx @@ -24,7 +24,7 @@ export default function ResultsPage() { headers={['Drivers', 'Constructors']} containers={[ - {drivers.map((driver, index, allDrivers) => ( + {drivers?.map((driver, index, allDrivers) => ( void; } @@ -23,17 +23,23 @@ export const Dropdown = ({ value, items, action }: IDropdown) => { tabIndex={0} role='button' className={clsx( - { 'pointer-events-none opacity-50': items.length <= 0 }, + { 'pointer-events-none opacity-50': items && items.length <= 1 }, 'btn btn-ghost btn-sm rounded-btn underline', )} > - {value} + {items ? ( + <> + {value} + + ) : ( + + )}
    - {items.map((item) => ( + {items?.map((item) => (
  • handleClick(item)}>{item}
  • diff --git a/src/app/ui/MainFilters.tsx b/src/app/ui/MainFilters.tsx index 8c461b9..e486d3d 100644 --- a/src/app/ui/MainFilters.tsx +++ b/src/app/ui/MainFilters.tsx @@ -8,12 +8,8 @@ import { driverAtom, handleDriverChangeAtom, } from '@/atoms/drivers'; -import { - fetchSchedule, - handleRaceChangeAtom, - raceAtom, - seasonRacesAtom, -} from '@/atoms/races'; +import { useMainFiltersAtomFetch } from '@/atoms/fetchCalls'; +import { handleRaceChangeAtom, raceAtom, seasonRacesAtom } from '@/atoms/races'; import { handleMainFilterSubmit, telemetryDisableAtom, @@ -22,13 +18,11 @@ import { } from '@/atoms/results'; import { allSeasonsAtom, - fetchSeasons, handleSeasonChangeAtom, seasonAtom, } from '@/atoms/seasons'; import { allSessionsAtom, - fetchSessionResults, handleSessionChangeAtom, sessionAtom, } from '@/atoms/sessions'; @@ -58,12 +52,12 @@ export const MainFilters = () => { router.push('/' + url + (telemetry ? '/telemetry' : '')); }; - useAtom(fetchSeasons); - useAtom(fetchSchedule); - useAtom(fetchSessionResults); - + // hook to trigger if telemetry is disabled or not useAtom(toggleTelemetryDisableAtom); + // Fetch data when atoms change + useMainFiltersAtomFetch(); + // Handles hydration on page load useParamToSetAtoms(); @@ -117,7 +111,7 @@ const RaceDropdown = ({ action }: actionT) => { const [races] = useAtom(seasonRacesAtom); const handleAction = (val: string) => { - const match = races.find((race) => race.EventName === val); + const match = races && races.find((race) => race.EventName === val); if (match) { changeRace(match); action(); @@ -129,7 +123,7 @@ const RaceDropdown = ({ action }: actionT) => { return ( race.EventName)} + items={races && ['All Races', ...races.map((race) => race.EventName)]} action={handleAction} /> ); @@ -148,7 +142,12 @@ const DriverDropdown = ({ action }: actionT) => { return ( driver.FullName)} + items={ + driverList && [ + 'All Drivers', + ...driverList.map((driver) => driver.FullName), + ] + } action={handleAction} /> ); @@ -166,7 +165,7 @@ const SessionDropdown = ({ action }: actionT) => { return ( ); diff --git a/src/app/ui/Timeline.tsx b/src/app/ui/Timeline.tsx index 1e4c233..82af03a 100644 --- a/src/app/ui/Timeline.tsx +++ b/src/app/ui/Timeline.tsx @@ -1,6 +1,10 @@ import clsx from 'clsx'; -export const Timeline = ({ children }: { children: React.ReactNode[] }) => { +export const Timeline = ({ + children, +}: { + children: React.ReactNode | React.ReactNode[]; +}) => { return (
      {children} diff --git a/src/atoms/drivers.tsx b/src/atoms/drivers.tsx index 2593171..913d52d 100644 --- a/src/atoms/drivers.tsx +++ b/src/atoms/drivers.tsx @@ -1,28 +1,16 @@ import { atom } from 'jotai'; -import { raceAtom } from './races'; -import { seasonAtom } from './seasons'; - -// Drivers -export const allDriversAtom = atom([]); +export const allDriversAtom = atom(null); export const driverAtom = atom('All Drivers'); export const handleDriverChangeAtom = atom( null, async (get, set, driverName: string) => { - const baseUrl = '/' + get(seasonAtom); - - const race = get(raceAtom); - const raceLoc = race !== 'All Races' && race.Location.toLowerCase(); - const drivers = get(allDriversAtom); - const driver = drivers.find((driver) => driver.FullName === driverName); + const driver = drivers?.find((driver) => driver.FullName === driverName); if (driver) { set(driverAtom, driver); - return baseUrl + (raceLoc && `/${raceLoc}/${driver.DriverId}`); } - // return nagivation url - set(driverAtom, 'All Drivers'); }, ); diff --git a/src/atoms/fetchCalls.tsx b/src/atoms/fetchCalls.tsx new file mode 100644 index 0000000..42b2fae --- /dev/null +++ b/src/atoms/fetchCalls.tsx @@ -0,0 +1,137 @@ +// Get session results + +import { useAtom } from 'jotai'; +import { atomEffect } from 'jotai-effect'; + +import { f1Seasons } from '@/utils/fakerData'; +import { fetchAPI, lastSession, sessionTitles } from '@/utils/helpers'; +import { formatConstructorResults } from '@/utils/transformers'; + +import { allConstructorAtom } from './constructors'; +import { allDriversAtom } from './drivers'; +import { raceAtom, seasonRacesAtom } from './races'; +import { allSeasonsAtom, seasonAtom } from './seasons'; +import { allSessionsAtom, sessionAtom } from './sessions'; +import { constructorStandingsAtom, driverStandingsAtom } from './standings'; + +export const useMainFiltersAtomFetch = () => { + useAtom(fetchSeasons); + useAtom(fetchSchedule); + useAtom(fetchSessionResults); +}; + +// Get Seasons values, this should be done once +export const fetchSeasons = atomEffect( + (get, set) => { + const seasons = get(allSeasonsAtom); + // Seasons do not change, only fetch if empty array + if (!seasons || seasons.length <= 0) { + set(allSeasonsAtom, f1Seasons()); + } + + // This populates to show values are loaded + set(allSessionsAtom, []); + set(allDriversAtom, []); + }, + // Dependencies: allSeasonsAtom +); + +// Based off season data +// If season value set fetch that seasons schedule +// otherwise get the default schedule +export const fetchSchedule = atomEffect( + (get, set) => { + set(seasonRacesAtom, null); + const params = get(seasonAtom) && `?year=${get(seasonAtom)}`; + + fetchAPI('schedule' + params).then((data) => { + set(seasonRacesAtom, data.EventSchedule); + + // Sync default year with server + set(seasonAtom, data.year); + }); + }, + // Dependencies: + // seasonAtom +); + +// Based off race data +// Set session and sessions from race sessions +// Fetch race results to get drivers in the session +export const fetchSessionResults = atomEffect((get, set) => { + const race = get(raceAtom); + + // Confirm race has been selected + if (race && race !== 'All Races') { + // *** Base url for fetch + let url = `results/${get(seasonAtom)}/${race.RoundNumber}`; + + // Parse race data to get session titles + const sessions = sessionTitles(race); + // Set session to last session, ideally race + const session = lastSession(race); + + // Set values + set(sessionAtom, session); + set(allSessionsAtom, sessions); + + // *** If sessions available find session round and add to url + if (sessions.length > 0) { + const sessionRound = sessions.indexOf(session) + 1; + url += `?session=${sessionRound}`; + } + + fetchAPI(url).then((drivers: DriverResult[]) => { + // Formulate Constructors + const constructors = formatConstructorResults(drivers); + + // Update atom values + set(allDriversAtom, drivers); + set(allConstructorAtom, constructors); + }); + } + // Dependencies: + // raceAtom + // seasonAtom +}); + +// Get Driver & Constructor Standings +export const fetchStandings = atomEffect((get, set) => { + // Reset standings + set(driverStandingsAtom, []); + set(constructorStandingsAtom, []); + const race = get(raceAtom); + + // Year + const year = get(seasonAtom) && `?year=${get(seasonAtom)}`; + + // Round + const round = race === 'All Races' ? '' : `&round=${race.RoundNumber}`; + + // Fetch + fetchAPI('standings' + year + round).then( + ({ + DriverStandings, + ConstructorStandings, + }: DataConfigSchema['standings']) => { + // Include Drivers in Constructors Info + const constructors = ConstructorStandings.map((cs) => { + const { name } = cs.Constructor; + return { + ...cs, + Drivers: DriverStandings.filter((driver) => + driver.Constructors.find((c) => c.name === name), + ), + }; + }); + + // Update standings + set(constructorStandingsAtom, constructors); + set(driverStandingsAtom, DriverStandings); + }, + ); + + // dependencies + // seasonAtom + // raceAtom +}); diff --git a/src/atoms/races.tsx b/src/atoms/races.tsx index 2fb73e1..11ec9fa 100644 --- a/src/atoms/races.tsx +++ b/src/atoms/races.tsx @@ -1,30 +1,11 @@ import { atom } from 'jotai'; -import { atomEffect } from 'jotai-effect'; - -import { fetchAPI } from '@/lib/utils'; import { allDriversAtom, driverAtom } from './drivers'; -import { seasonAtom } from './seasons'; import { allSessionsAtom } from './sessions'; -// Races -export const seasonRacesAtom = atom([]); +export const seasonRacesAtom = atom(null); export const raceAtom = atom('All Races'); -// Get Races per season -export const fetchSchedule = atomEffect( - (get, set) => { - const params = get(seasonAtom) && `?year=${get(seasonAtom)}`; - fetchAPI('schedule' + params).then((data) => { - set(seasonRacesAtom, data.EventSchedule); - - // Sync default year with server - set(seasonAtom, data.year); - }); - }, - // Dependencies: seasonAtom -); - export const handleRaceChangeAtom = atom( null, async (get, set, raceEvent: ScheduleSchema) => { @@ -33,7 +14,7 @@ export const handleRaceChangeAtom = atom( // Reset Driver set(driverAtom, 'All Drivers'); - set(allDriversAtom, []); - set(allSessionsAtom, []); + set(allDriversAtom, null); + set(allSessionsAtom, null); }, ); diff --git a/src/atoms/results.tsx b/src/atoms/results.tsx index 1265212..ad5f2ad 100644 --- a/src/atoms/results.tsx +++ b/src/atoms/results.tsx @@ -1,6 +1,7 @@ import { atom, useAtom } from 'jotai'; import { atomEffect } from 'jotai-effect'; import { usePathname } from 'next/navigation'; +import { useRef } from 'react'; import { allDriversAtom, driverAtom } from './drivers'; import { raceAtom, seasonRacesAtom } from './races'; @@ -17,39 +18,12 @@ export const toggleTelemetryDisableAtom = atomEffect((get, set) => { ); }); -export const useParamToSetAtoms = () => { - const [season, location, driverId, session] = usePathname() - .split('/') - .slice(1); - - const [, setSeason] = useAtom(seasonAtom); - const [races] = useAtom(seasonRacesAtom); - const [, setRace] = useAtom(raceAtom); - const [drivers] = useAtom(allDriversAtom); - const [, setDriver] = useAtom(driverAtom); - const [sessions] = useAtom(allSessionsAtom); - const [, setSession] = useAtom(sessionAtom); - - // if (!season) return; - setSeason(season); - - // if (!location) return; - setRace( - races.find((r) => r.Location.toLowerCase() === location) || 'All Races', - ); - - // if (!driverId) return; - setDriver(drivers.find((d) => d.DriverId === driverId) || 'All Drivers'); - - // if (!sessions) return; - setSession(sessions.find((s) => s.toLowerCase() === session) || 'Race'); -}; - export const handleMainFilterSubmit = atom(null, (get) => { const season = get(seasonAtom); const race = get(raceAtom); const driver = get(driverAtom); const session = get(sessionAtom); + const sessions = get(allSessionsAtom); const url = []; // Return if no race specified @@ -68,10 +42,86 @@ export const handleMainFilterSubmit = atom(null, (get) => { else url.push(driver.DriverId); // Return if no sessions - if (get(allSessionsAtom).length === 0 || session === 'Race') + if ((sessions && sessions.length === 0) || session === 'Race') return url.join('/'); // Add session to url else url.push(session.toLowerCase()); return url.join('/'); }); + +export const useParamToSetAtoms = () => { + const [season, location, driverId, sessionParam] = usePathname() + .split('/') + .slice(1); + + const [seasonVal, setSeason] = useAtom(seasonAtom); + const [races] = useAtom(seasonRacesAtom); + const [race, setRace] = useAtom(raceAtom); + const [drivers] = useAtom(allDriversAtom); + const [driver, setDriver] = useAtom(driverAtom); + const [sessions] = useAtom(allSessionsAtom); + const [, setSession] = useAtom(sessionAtom); + + // Loading Refs + const seasonLoaded = useRef(false); + const raceLoaded = useRef(false); + const driverLoaded = useRef(false); + const sessionLoaded = useRef(false); + + if (!seasonLoaded.current) { + if (season && season !== seasonVal) setSeason(season); + seasonLoaded.current = true; + } + + if (!raceLoaded.current) { + // If no location nothing to load/check + if (!location) { + raceLoaded.current = true; + return; + } + + // if location and races to compare to + if (races) { + raceLoaded.current = true; + const raceMatch = races.find( + (r) => r.Location.toLowerCase() === location, + ); + if (race === 'All Races' || raceMatch?.EventName !== race?.EventName) { + setRace(raceMatch || 'All Races'); + } + } + } + + if (!driverLoaded.current) { + // If no driverId nothing to load/check + if (!driverId) { + driverLoaded.current = true; + return; + } + + // if drivers to compare to driverId + if (drivers) { + driverLoaded.current = true; + const driverMatch = drivers.find((d) => d.DriverId === driverId); + if (driver === 'All Drivers' || driverMatch?.DriverId !== driver.DriverId) + setDriver(driverMatch || 'All Drivers'); + } + } + + if (!sessionLoaded.current) { + // If no driverId nothing to load/check + if (!sessionParam) { + sessionLoaded.current = true; + return; + } + + // if dirver and driver to compare to + if (sessions) { + sessionLoaded.current = true; + setSession( + sessions.find((s) => s.toLowerCase() === sessionParam) || 'Race', + ); + } + } +}; diff --git a/src/atoms/seasons.tsx b/src/atoms/seasons.tsx index c02bef1..92cebb2 100644 --- a/src/atoms/seasons.tsx +++ b/src/atoms/seasons.tsx @@ -1,27 +1,13 @@ import { atom } from 'jotai'; -import { atomEffect } from 'jotai-effect'; - -import { f1Seasons } from '@/lib/fakerData'; import { allDriversAtom, driverAtom } from './drivers'; import { raceAtom, seasonRacesAtom } from './races'; import { allSessionsAtom, sessionAtom } from './sessions'; -// Seasons -export const allSeasonsAtom = atom([]); +export const allSeasonsAtom = atom(null); +// ! Need to set initial season from server default export const seasonAtom = atom(''); -// Get Seasons values, this is done clientside -export const fetchSeasons = atomEffect( - (get, set) => { - // Seasons do not change, only fetch if empty array - if (get(allSeasonsAtom).length <= 0) { - set(allSeasonsAtom, f1Seasons()); - } - }, - // Dependencies: allSeasonsAtom -); - export const handleSeasonChangeAtom = atom( null, async (_get, set, season: string) => { @@ -29,7 +15,7 @@ export const handleSeasonChangeAtom = atom( // Reset other filter values set(raceAtom, 'All Races'); - set(seasonRacesAtom, []); + set(seasonRacesAtom, null); set(driverAtom, 'All Drivers'); set(allDriversAtom, []); set(sessionAtom, 'Race'); diff --git a/src/atoms/sessions.tsx b/src/atoms/sessions.tsx index 0c841f1..6582efe 100644 --- a/src/atoms/sessions.tsx +++ b/src/atoms/sessions.tsx @@ -1,102 +1,8 @@ import { atom } from 'jotai'; -import { atomEffect } from 'jotai-effect'; - -import { fetchAPI, lastSession, sessionTitles } from '@/lib/utils'; - -import { allConstructorAtom } from './constructors'; -import { allDriversAtom } from './drivers'; -import { raceAtom } from './races'; -import { seasonAtom } from './seasons'; - -/** - * @description Format constructors results based on Driver results from race - * @param {DriverResult[]} drivers - */ -const formatConstructorResults = (drivers: DriverResult[]) => - drivers - .reduce((cons, driver) => { - // *** Find existint team from accumulator - const existingTeamIndex = cons.findIndex( - (team) => team.name === driver.TeamName, - ); - - // If: - // 1. Team exists in accumulator - // *** Update constructor values - if (existingTeamIndex >= 0) { - const update = { ...cons[existingTeamIndex] }; - const points = update.points + driver.Points; - const conDrivers = [...update.drivers, driver]; - - // *** Add Updated Constructor - cons.push({ - ...update, - points, - drivers: conDrivers, - }); - - // *** Remove Old Constructor - cons.splice(existingTeamIndex, 1); - } else { - // *** Add new Constructor - cons.push({ - name: driver.TeamName, - points: driver.Points, - position: driver.Position, // Placeholder - drivers: [driver], - }); - } - - return cons; - }, [] as ConstructorResult[]) - // Sort by points - .sort((a, b) => (a.points > b.points ? -1 : 1)) - // Set proper position - .map((con, i) => { - con.position = i + 1; - return con; - }); - // Sessions -export const allSessionsAtom = atom([]); +export const allSessionsAtom = atom(null); export const sessionAtom = atom('Race'); -// Get session results -// Set allDriversAtom to drivers from session -export const fetchSessionResults = atomEffect((get, set) => { - const race = get(raceAtom); - - // Confirm race has been selected - if (race && race !== 'All Races') { - // Make sure sessions match the race - - const sessions = sessionTitles(race); - const session = lastSession(race); - // Parse race data to get session titles - // Set session to last session, ideally race - set(sessionAtom, session); - set(allSessionsAtom, sessions); - - // const sessions = get(allSessionsAtom); - let url = `results/${get(seasonAtom)}/${race.RoundNumber + 1}`; - - // If sessions available find session round and add to url - if (sessions.length > 0) { - const sessionRound = sessions.indexOf(session) + 1; - url += `?session=${sessionRound}`; - } - - fetchAPI(url).then((drivers: DriverResult[]) => { - // Formulate Constructors - const constructors = formatConstructorResults(drivers); - - // Update atom values - set(allDriversAtom, drivers); - set(allConstructorAtom, constructors); - }); - } -}); - export const handleSessionChangeAtom = atom( null, async (get, set, session: string) => { diff --git a/src/atoms/standings.tsx b/src/atoms/standings.tsx index 5aca3c6..8544cfc 100644 --- a/src/atoms/standings.tsx +++ b/src/atoms/standings.tsx @@ -1,48 +1,5 @@ import { atom } from 'jotai'; -import { atomEffect } from 'jotai-effect'; - -import { fetchAPI } from '@/lib/utils'; - -import { raceAtom } from './races'; -import { seasonAtom } from './seasons'; // Cumulative Standings export const constructorStandingsAtom = atom([]); export const driverStandingsAtom = atom([]); - -// Get Driver & Constructor Standings -export const fetchStandings = atomEffect((get, set) => { - // Reset standings - set(driverStandingsAtom, []); - set(constructorStandingsAtom, []); - const race = get(raceAtom); - - // Year - const year = get(seasonAtom) && `?year=${get(seasonAtom)}`; - - // Round - const round = race === 'All Races' ? '' : `&round=${race.RoundNumber}`; - - // Fetch - fetchAPI('standings' + year + round).then( - ({ - DriverStandings, - ConstructorStandings, - }: DataConfigSchema['standings']) => { - // Include Drivers in Constructors Info - const constructors = ConstructorStandings.map((cs) => { - const { name } = cs.Constructor; - return { - ...cs, - Drivers: DriverStandings.filter((driver) => - driver.Constructors.find((c) => c.name === name), - ), - }; - }); - - // Update standings - set(constructorStandingsAtom, constructors); - set(driverStandingsAtom, DriverStandings); - }, - ); -}); diff --git a/src/lib/fakerData.tsx b/src/utils/fakerData.tsx similarity index 100% rename from src/lib/fakerData.tsx rename to src/utils/fakerData.tsx diff --git a/src/lib/utils.tsx b/src/utils/helpers.tsx similarity index 98% rename from src/lib/utils.tsx rename to src/utils/helpers.tsx index bc2ef68..8f99871 100644 --- a/src/lib/utils.tsx +++ b/src/utils/helpers.tsx @@ -109,6 +109,7 @@ export const fetchAPI = async ( } // Fetch from server + // console.log(`making fetch to: ${serverUrl}/${endpoint}`); const data = await fetch(`${serverUrl}/${endpoint}`, { ...options }) .then( (res) => { diff --git a/src/lib/placerholder-results.tsx b/src/utils/placerholder-results.tsx similarity index 95% rename from src/lib/placerholder-results.tsx rename to src/utils/placerholder-results.tsx index 233b8e4..79ffeea 100644 --- a/src/lib/placerholder-results.tsx +++ b/src/utils/placerholder-results.tsx @@ -1,6 +1,6 @@ import { faker } from '@faker-js/faker'; -import { positionEnding } from './utils'; +import { positionEnding } from './helpers'; export const DriverHeadings = [ 'position', diff --git a/src/utils/transformers.tsx b/src/utils/transformers.tsx new file mode 100644 index 0000000..e8747b3 --- /dev/null +++ b/src/utils/transformers.tsx @@ -0,0 +1,44 @@ +export const formatConstructorResults = (drivers: DriverResult[]) => + drivers + .reduce((cons, driver) => { + // *** Find existint team from accumulator + const existingTeamIndex = cons.findIndex( + (team) => team.name === driver.TeamName, + ); + + // If: + // 1. Team exists in accumulator + // *** Update constructor values + if (existingTeamIndex >= 0) { + const update = { ...cons[existingTeamIndex] }; + const points = update.points + driver.Points; + const conDrivers = [...update.drivers, driver]; + + // *** Add Updated Constructor + cons.push({ + ...update, + points, + drivers: conDrivers, + }); + + // *** Remove Old Constructor + cons.splice(existingTeamIndex, 1); + } else { + // *** Add new Constructor + cons.push({ + name: driver.TeamName, + points: driver.Points, + position: driver.Position, // Placeholder + drivers: [driver], + }); + } + + return cons; + }, [] as ConstructorResult[]) + // Sort by points + .sort((a, b) => (a.points > b.points ? -1 : 1)) + // Set proper position + .map((con, i) => { + con.position = i + 1; + return con; + });