diff --git a/src/App.tsx b/src/App.tsx index 7312224..072daf3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,18 +1,11 @@ import "./App.css"; import api from "./api/api"; -import type { Session, SessionResult } from "./api/api"; import { useQuery } from "@tanstack/react-query"; import { useEffect, useState } from "react"; - import DriverCard from "./components/DriverCard"; import SearchAndFilter from "./components/SearchAndFilter"; import DriverDetail from "./components/DriverDetail"; -const sessionDate = new Intl.DateTimeFormat("en-GB", { - dateStyle: "medium", - timeZone: "UTC", -}); - type SortOrder = "none" | "asc" | "desc"; function App() { @@ -61,9 +54,6 @@ function App() { const { getDriversWithStandings, getAllTeamStandings, - getAllSessionResults, - getAllSessions, - getAllMeetings, } = api(); const { @@ -85,40 +75,6 @@ function App() { enabled: !driversWithStandingsPending, }); - const { - data: allSessions = [], - isPending: sessionsPending, - isError: sessionsError, - } = useQuery({ - queryKey: ["sessions", 2026], - queryFn: getAllSessions, - enabled: !driversWithStandingsPending, - }); - - const { - data: allSessionResults = [], - isPending: sessionResultsPending, - isError: sessionResultsError, - } = useQuery({ - queryKey: [ - "sessionResults", - 2026, - allSessions.map((session) => session.session_key), - ], - queryFn: () => getAllSessionResults(allSessions), - enabled: !sessionsPending && !sessionsError, - }); - - const { - data: allMeetings = [], - isPending: meetingsPending, - isError: meetingsError, - } = useQuery({ - queryKey: ["meetings", 2026], - queryFn: getAllMeetings, - enabled: !driversWithStandingsPending, - }); - const filteredDrivers = driversWithStandings .filter((driver) => { const matchesSearch = driver.full_name @@ -139,8 +95,6 @@ function App() { } }); - // Detail panel selection: Previous/Next step through the currently filtered - // list only, so it stays in sync with whatever the left list is showing. const [selectedIndex, setSelectedIndex] = useState(0); const clampedIndex = filteredDrivers.length ? Math.min(selectedIndex, filteredDrivers.length - 1) @@ -156,35 +110,6 @@ function App() { } } - const meetingsByKey = new Map( - allMeetings.map((meeting) => [meeting.meeting_key, meeting]), - ); - const sessionsByMeeting = new Map(); - const orderedSessions = [...allSessions].sort( - (a, b) => Date.parse(a.date_start) - Date.parse(b.date_start), - ); - for (const session of orderedSessions) { - const sessions = sessionsByMeeting.get(session.meeting_key) ?? []; - sessions.push(session); - sessionsByMeeting.set(session.meeting_key, sessions); - } - - const driverNames = new Map( - driversWithStandings.map((driver) => [ - driver.driver_number, - driver.full_name, - ]), - ); - const resultsBySession = new Map(); - for (const result of allSessionResults) { - const results = resultsBySession.get(result.session_key) ?? []; - results.push(result); - resultsBySession.set(result.session_key, results); - } - for (const results of resultsBySession.values()) { - results.sort((a, b) => (a.position ?? Infinity) - (b.position ?? Infinity)); - } - return ( <>

F1 The Website

@@ -267,90 +192,6 @@ function App() { } /> - - {sessionsError || sessionResultsError ? ( -

Could not load session results.

- ) : sessionsPending || sessionResultsPending || meetingsPending ? ( -

Loading session results...

- ) : allSessions.length === 0 ? ( -

No sessions available yet.

- ) : ( -
-

2026 meetings

- {meetingsError && ( -

- Meeting names could not be loaded. Showing circuits and dates. -

- )} - {Array.from(sessionsByMeeting, ([meetingKey, sessions]) => { - const meeting = meetingsByKey.get(meetingKey); - const firstSession = sessions[0]; - const lastSession = sessions[sessions.length - 1]; - - return ( -
- - - {meeting?.meeting_name ?? firstSession.circuit_short_name} - - - {firstSession.country_name} ·{" "} - {firstSession.circuit_short_name} ·{" "} - {sessionDate.format( - new Date(meeting?.date_start ?? firstSession.date_start), - )}{" "} - –{" "} - {sessionDate.format( - new Date(meeting?.date_end ?? lastSession.date_end), - )} - - -
- {sessions.map((session) => { - const results = - resultsBySession.get(session.session_key) ?? []; - - return ( -
- - {session.session_name} - - - {results.length === 0 ? ( -

- {session.is_cancelled - ? "Session cancelled." - : "Results not available yet."} -

- ) : ( -
    - {results.map((result) => ( -
  • - Pos {result.position ?? "—"}: - - {driverNames.get(result.driver_number) ?? - `Driver ${result.driver_number}`} - -
  • - ))} -
- )} -
- ); - })} -
-
- ); - })} -
- )} ); } diff --git a/src/api/api.tsx b/src/api/api.tsx index e2d4e67..fbd7539 100644 --- a/src/api/api.tsx +++ b/src/api/api.tsx @@ -1,9 +1,13 @@ +interface RaceSession { + session_key: number; + date_end: string; +} export interface Driver { broadcast_name: string; driver_number: number; - first_name: string, - last_name: string, + first_name: string; + last_name: string; full_name: string; headshot_url: string; meeting_key: number; @@ -13,51 +17,6 @@ export interface Driver { team_name: string; } -export interface Session { - circuit_key: number; - circuit_short_name: string; - country_code: string; - country_key: number; - country_name: string; - date_end: string; - date_start: string; - gmt_offset: string; - is_cancelled: boolean; - location: string; - meeting_key: number; - session_key: number; - session_name: string; - session_type: string; - year: number; -} - -export interface SessionResult { - dnf: boolean; - dns: boolean; - dsq: boolean; - driver_number: number; - duration: number | (number | null)[] | null; - gap_to_leader: number | string | (number | string | null)[] | null; - number_of_laps: number; - meeting_key: number; - position: number | null; - session_key: number; -} - -export interface Meeting { - meeting_key: number; - meeting_name: string; - country_name: string; - circuit_short_name: string; - date_start: string; - date_end: string; -} - -interface RaceSession { - session_key: number; - date_end: string; -} - export interface DriverStandings { driver_number: number; position_current: number; @@ -67,8 +26,8 @@ export interface DriverStandings { } export interface DriverWithStandings extends Driver { - position_current: number - points_current: number + position_current: number; + points_current: number; } export interface TeamStandings { @@ -81,94 +40,36 @@ export interface TeamStandings { meeting_key: number; } -// Share cached data and request spacing across every api() instance. -const apiCache = new Map() -const pendingRequests = new Map>() -let requestQueue: Promise = Promise.resolve() -let nextRequestAt = 0 - -function fetchApi(url: string, errorMessage: string): Promise { - const cached = apiCache.get(url) - if (cached && cached.expiresAt > Date.now()) { - return Promise.resolve(cached.data as T) +async function fetchApi(url: string, errorMessage: string): Promise { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`${errorMessage} (${response.status})`); } - const pending = pendingRequests.get(url) - if (pending) { return pending as Promise } - - const request = requestQueue.then(async () => { - // Stay below OpenF1's free limits of 3 requests/second and 30/minute. - const delay = nextRequestAt - Date.now() - if (delay > 0) { await new Promise((resolve) => setTimeout(resolve, delay)) } - nextRequestAt = Date.now() + 2100 - const response = await fetch(url) - if (response.status === 429) { - const retryAfter = response.headers.get('Retry-After') - const retryAt = retryAfter - ? (Number.isFinite(Number(retryAfter)) - ? Date.now() + Number(retryAfter) * 1000 - : Date.parse(retryAfter)) - : NaN - nextRequestAt = Math.max(nextRequestAt, Number.isFinite(retryAt) ? retryAt : Date.now() + 60000) - } - if (!response.ok) { throw new Error(`${errorMessage} (${response.status})`) } - const data: T = await response.json() - apiCache.set(url, { data, expiresAt: Date.now() + 60000 }) - return data - }).finally(() => { - pendingRequests.delete(url) - }) - - pendingRequests.set(url, request) - requestQueue = request.then(() => {}, () => {}) - return request + return response.json(); } export default function api() { const API = "https://api.openf1.org/v1" - async function getAllMeetings(): Promise { - return fetchApi(`${API}/meetings?year=2026`, "Could not fetch meetings") - } - - async function getAllSessions(): Promise { //Fetches every 2026 session that has started so far. - const sessions = await fetchApi(`${API}/sessions?year=2026`, "Could not fetch sessions") - const now = Date.now() - console.log(sessions.filter((session) => new Date(session.date_start).getTime() <= now)) - return sessions.filter((session) => new Date(session.date_start).getTime() <= now) - } - async function getAllSessionResults(sessions?: Session[]): Promise { - sessions = sessions ?? await getAllSessions() - if (sessions.length === 0) { return [] } - - // Fetch the key range in one request, then keep only the requested sessions. - const sessionKeys = new Set(sessions.map((session) => session.session_key)) - const params = new URLSearchParams({ - 'session_key>': String(Math.min(...sessionKeys)), - 'session_key<': String(Math.max(...sessionKeys)), - }) - const results = await fetchApi(`${API}/session_result?${params}`, "Could not fetch session results") - return results.filter((sessionResult) => sessionKeys.has(sessionResult.session_key)) - } - - async function getLatestRaceSessionKey(){ //Fetches latest race that has completed, because latest session may not have started. + async function getLatestRaceSessionKey(): Promise { //Fetches latest race that has completed, because latest session may not have started. const races = await fetchApi(`${API}/sessions?year=2026&session_type=Race`, "Could not fetch race sessions") const completedRaces = races.filter( (race) => new Date(race.date_end) < new Date() ) - - const latestRace = completedRaces.at(-1) + const latestRace = completedRaces.sort( + (a, b) => Date.parse(b.date_end) - Date.parse(a.date_end) + )[0] if (!latestRace) { throw new Error("No completed race session found") } - console.log(latestRace.session_key) return latestRace.session_key } - async function getAllDrivers(): Promise { //fetch all driver info + async function getAllDrivers(): Promise { //get information about all the drivers return fetchApi(`${API}/drivers?session_key=latest`, "Failed to fetch drivers") } - async function getAllStandings( ) : Promise{ //fetches all standings for all drivers + async function getAllDriverStandings(): Promise { //get the standings for all the drivers after the last completed race const sessionKey = await getLatestRaceSessionKey() return fetchApi(`${API}/championship_drivers?session_key=${sessionKey}`, "Could not fetch driver standings data") } @@ -178,17 +79,9 @@ export default function api() { return fetchApi(`${API}/championship_teams?session_key=${sessionKey}`, "Could not fetch team standings data") } - async function getStandingsByTeamName(teamName: string): Promise { //fetches standings for a single team - const sessionKey = await getLatestRaceSessionKey() - return fetchApi( - `${API}/championship_teams?session_key=${sessionKey}&team_name=${encodeURIComponent(teamName)}`, - "Could not fetch team standings data" - ) - } - - async function getDriversWithStandings(){ //combines the driver info and driver standings into a single array. + async function getDriversWithStandings(): Promise { //combines the driver info and driver standings into a single array. const drivers = await getAllDrivers() - const standings = await getAllStandings() + const standings = await getAllDriverStandings() const combinedDrivers = drivers.map((driver) => { const standing = standings.find( (standing) => standing.driver_number === driver.driver_number @@ -201,15 +94,10 @@ export default function api() { }) return combinedDrivers } - + + //Apis that can be imported return { - getAllMeetings, - getAllSessions, - getAllSessionResults, - getAllDrivers, - getAllStandings, getDriversWithStandings, - getStandingsByTeamName, getAllTeamStandings, }; }