diff --git a/src/app/(features)/LandingNextEvent.tsx b/src/app/(features)/LandingNextEvent.tsx new file mode 100644 index 0000000..519f385 --- /dev/null +++ b/src/app/(features)/LandingNextEvent.tsx @@ -0,0 +1,36 @@ +'use client'; + +import { useAtom } from 'jotai'; + +import { nextEventAtom, nextEventLiveAtom } from '@/atoms/nextEvent'; +import { formatSessionUrl } from '@/utils/transformers'; + +import { EventCountDown } from '../ui/EventCountdown'; + +export const LandingNextEvent = () => { + const [nextEvent] = useAtom(nextEventAtom); + const [liveEvent] = useAtom(nextEventLiveAtom); + + return ( +
+ {nextEvent && ( +
+

{nextEvent.name}

+

+ {liveEvent ? ( + nextEvent.session + ' Live Now' + ) : ( + <> + {formatSessionUrl(nextEvent.session).toUpperCase()} in{' '} + + + )} +

+ +
+ )} +
+ ); +}; diff --git a/src/app/[season]/page.tsx b/src/app/[season]/page.tsx index a23f6e0..fa7e88c 100644 --- a/src/app/[season]/page.tsx +++ b/src/app/[season]/page.tsx @@ -2,6 +2,7 @@ import { useAtom } from 'jotai'; +import { fetchStandings } from '@/atoms/fetchCalls'; import { seasonAtom } from '@/atoms/seasons'; import { constructorStandingsAtom, @@ -20,6 +21,7 @@ export default function ResultsPage() { const [constructorStandings] = useAtom(constructorStandingsAtom); const [driverStandings] = useAtom(driverStandingsAtom); const [season] = useAtom(seasonAtom); + useAtom(fetchStandings); return (
diff --git a/src/app/page.tsx b/src/app/page.tsx index fd0811b..250e447 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,3 +1,4 @@ +import { LandingNextEvent } from './(features)/LandingNextEvent'; import { MainFilters } from './ui/MainFilters'; export default function Home() { @@ -6,7 +7,7 @@ export default function Home() { {/* Suspense */} - +
); } @@ -25,12 +26,3 @@ const Hero = () => ( ); - -const NextRace = () => { - return ( -
-

Winter Testing

- Bahrain Feb 21, 2024 -
- ); -}; diff --git a/src/app/ui/Countdown.tsx b/src/app/ui/Countdown.tsx new file mode 100644 index 0000000..4d8c70c --- /dev/null +++ b/src/app/ui/Countdown.tsx @@ -0,0 +1,18 @@ +import clsx from 'clsx'; + +import { formatDuration } from '@/utils/helpers'; + +export const Countdown = ({ + time, + className, +}: { + time: number; + className?: string; +}) => { + return ( + + {/* Remove last 4 characters which as milliseconds */} + {formatDuration(time).slice(0, -4)} + + ); +}; diff --git a/src/app/ui/EventCountdown.tsx b/src/app/ui/EventCountdown.tsx new file mode 100644 index 0000000..c7622c8 --- /dev/null +++ b/src/app/ui/EventCountdown.tsx @@ -0,0 +1,10 @@ +import { useAtom } from 'jotai'; + +import { nextEventTimeAtom } from '@/atoms/nextEvent'; + +import { Countdown } from './Countdown'; + +export const EventCountDown = () => { + const [nextEventCountdown] = useAtom(nextEventTimeAtom); + return ; +}; diff --git a/src/app/ui/Nav.tsx b/src/app/ui/Nav.tsx index 5c3bee2..347ec33 100644 --- a/src/app/ui/Nav.tsx +++ b/src/app/ui/Nav.tsx @@ -1,11 +1,44 @@ 'use client'; +import clsx from 'clsx'; import { useAtom } from 'jotai'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; +import { fetchNextEvent } from '@/atoms/fetchCalls'; +import { + nextEventAtom, + nextEventEffect, + nextEventLiveAtom, +} from '@/atoms/nextEvent'; import { seasonAtom } from '@/atoms/seasons'; +import { EventCountDown } from './EventCountdown'; + +export const NextEvent = () => { + const [nextEvent] = useAtom(nextEventAtom); + const [liveEvent] = useAtom(nextEventLiveAtom); + + useAtom(fetchNextEvent); + useAtom(nextEventEffect); + + if (!nextEvent) return <>; + return ( + <> +
+

+ {nextEvent.name}
+ in +

+ + ); +}; + export const Nav = () => { const router = useRouter(); const [season] = useAtom(seasonAtom); @@ -72,11 +105,8 @@ export const Nav = () => { -
-
-

- 53 days until Winter Testing -

+
+
diff --git a/src/app/ui/Table.tsx b/src/app/ui/Table.tsx deleted file mode 100644 index c338a74..0000000 --- a/src/app/ui/Table.tsx +++ /dev/null @@ -1,72 +0,0 @@ -'use client'; - -import { Fragment } from 'react'; - -export interface IStanding { - headings: string[]; - data: { [key: string]: React.ReactNode }[]; -} - -interface ITable extends IStanding { - title?: string; - // headings: string[]; -} - -export const Table = ({ title, headings, data }: ITable) => { - if (data.length <= 0 && headings.length <= 0) return; - - const Title = title &&

{title}

; - - return ( -
- {Title} -
- - {/* head */} - - - {headings.map((header) => ( - - ))} - - - - {/* body */} - - {data.length > 0 && - data.map((row, i) => ( - - - {headings.map( - (key) => - row && ( - - ), - )} - {/* */} - - - ))} - -
- {header.replace('_', ' ')} - {/* Placeholder for button */}
- {row[key]} - - -
-
-
- ); -}; diff --git a/src/atoms/fetchCalls.tsx b/src/atoms/fetchCalls.tsx index 3ec8054..c2fb351 100644 --- a/src/atoms/fetchCalls.tsx +++ b/src/atoms/fetchCalls.tsx @@ -5,10 +5,18 @@ import { atomEffect } from 'jotai-effect'; import { f1Seasons } from '@/utils/fakerData'; import { fetchAPI, lastSession, sessionTitles } from '@/utils/helpers'; -import { formatConstructorResults } from '@/utils/transformers'; +import { + formatConstructorResults, + formatNextEvent, +} from '@/utils/transformers'; import { allConstructorAtom } from './constructors'; import { allDriversAtom } from './drivers'; +import { + nextEventAtom, + nextEventLiveAtom, + nextEventTimeAtom, +} from './nextEvent'; import { raceAtom, seasonRacesAtom } from './races'; import { allSeasonsAtom, seasonAtom } from './seasons'; import { allSessionsAtom, sessionAtom } from './sessions'; @@ -133,3 +141,29 @@ export const fetchStandings = atomEffect((get, set) => { // seasonAtom // raceAtom }); + +// Get upcoming event this should be done once +export const fetchNextEvent = atomEffect( + (get, set) => { + // Next event do not change, only fetch if null + if (!get(nextEventAtom)) { + fetchAPI('next-event').then((data: ScheduleSchema) => { + // Get session times + const now = Date.now(); + const nextEvent = formatNextEvent(data); + + if (nextEvent === 'No session') return; + + set(nextEventAtom, nextEvent); + + if (nextEvent.time < now) { + set(nextEventLiveAtom, true); + set(nextEventTimeAtom, now - nextEvent.endTime); + } else { + set(nextEventTimeAtom, nextEvent.time - now); + } + }); + } + }, + // Dependencies: nextEventAtom +); diff --git a/src/atoms/nextEvent.tsx b/src/atoms/nextEvent.tsx new file mode 100644 index 0000000..29b76ef --- /dev/null +++ b/src/atoms/nextEvent.tsx @@ -0,0 +1,15 @@ +import { atom } from 'jotai'; +import { atomEffect } from 'jotai-effect'; + +// Next Event +export const nextEventLiveAtom = atom(false); +export const nextEventAtom = atom(null); +export const nextEventTimeAtom = atom(0); +export const nextEventEffect = atomEffect((get, set) => { + if (get(nextEventTimeAtom) !== 0) { + const intervalId = setInterval(() => { + set(nextEventTimeAtom, (prev: number) => prev - 1000); + }, 1000); + return () => clearInterval(intervalId); + } +}); diff --git a/src/results.d.ts b/src/results.d.ts index bee5304..0699074 100644 --- a/src/results.d.ts +++ b/src/results.d.ts @@ -108,3 +108,11 @@ interface DataConfigSchema { constructors: ConstructorResult[]; }; } + +// UI Format Next Event +interface NextEventProps { + name: string; + session: string; + time: number; + endTime: number; +} diff --git a/src/utils/helpers.tsx b/src/utils/helpers.tsx index 8f99871..713f2c1 100644 --- a/src/utils/helpers.tsx +++ b/src/utils/helpers.tsx @@ -43,30 +43,54 @@ export const fastestLap = (position: number, points: number) => { } }; -export const formatDuration = (durationInMilliseconds: number) => { +const _second = 1000; +const _minute = _second * 60; +const _hour = _minute * 60; +const _day = _hour * 24; + +export const formatDuration = (timeInterval: number) => { // Pad single-digit values with leading zeros const pad = (value: number) => { return value < 10 ? '0' + value : value; }; // Calculate hours, minutes, seconds, and milliseconds - const hours = Math.floor(durationInMilliseconds / 3600000); - const minutes = Math.floor((durationInMilliseconds % 3600000) / 60000); - const seconds = Math.floor((durationInMilliseconds % 60000) / 1000); - const milliseconds = durationInMilliseconds % 1000; - - if (hours === 0 && minutes === 0 && seconds === 0 && milliseconds === 0) + const milliseconds = timeInterval % _second; + const seconds = Math.floor((timeInterval % _minute) / _second); + const minutes = Math.floor((timeInterval % _hour) / _minute); + const hours = Math.floor((timeInterval % _day) / _hour); + const days = Math.floor(timeInterval / _day); + + if ( + days === 0 && + hours === 0 && + minutes === 0 && + seconds === 0 && + milliseconds === 0 + ) return '-'; - else if (hours === 0 && minutes === 0 && seconds === 0) + else if (days === 0 && hours === 0 && minutes === 0 && seconds === 0) return '0.' + pad(milliseconds); - else if (hours === 0 && minutes === 0) + else if (days === 0 && hours === 0 && minutes === 0) return seconds + '.' + pad(milliseconds); - else if (hours === 0) + else if (days === 0 && hours === 0) return minutes + ':' + pad(seconds) + '.' + pad(milliseconds); - else + else if (days === 0) return ( hours + ':' + pad(minutes) + ':' + pad(seconds) + '.' + pad(milliseconds) ); + else + return ( + days + + ' days ' + + hours + + ':' + + pad(minutes) + + ':' + + pad(seconds) + + '.' + + pad(milliseconds) + ); }; export const sessionTitles = (event: ScheduleSchema) => { diff --git a/src/utils/transformers.tsx b/src/utils/transformers.tsx index f33e8e0..29524e9 100644 --- a/src/utils/transformers.tsx +++ b/src/utils/transformers.tsx @@ -61,3 +61,39 @@ export const formatConstructorResults = (drivers: DriverResult[]) => con.position = i + 1; return con; }); + +const timeAjustment = (name: string) => (name === 'Race' ? 7200000 : 3600000); + +export const formatNextEvent = (data: ScheduleSchema) => { + const sessionTimes = Object.keys(data).filter((key) => + key.match(/Session[1-5]DateUtc/g), + ); + + // Find the next session + const nextSessionTime = sessionTimes.find((session) => { + const sessionName = data[ + session.slice(0, 8) as keyof ScheduleSchema + ] as string; + const timeWithAdjustment = Date.now() - timeAjustment(sessionName); + return ( + timeWithAdjustment < + new Date(data[session as keyof ScheduleSchema] as string).getTime() + ); + }); + + if (!nextSessionTime) return 'No session'; + + const sessionStartTime = new Date( + data[nextSessionTime as keyof ScheduleSchema] as string, + ).getTime(); + const sessionName = data[ + nextSessionTime.slice(0, 8) as keyof ScheduleSchema + ] as string; + + return { + name: data.EventName, + session: sessionName, + time: sessionStartTime, + endTime: sessionStartTime + timeAjustment(sessionName), + }; +};