diff --git a/src/App.tsx b/src/App.tsx index e44d72b..c332979 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,85 +1,187 @@ -import './App.css' -import api from './api/api' -import type { Session, SessionResult } from './api/api' -import { useQuery } from '@tanstack/react-query' +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 DriverCard from "./components/DriverCard"; +import SearchAndFilter from "./components/SearchAndFilter"; -const sessionDate = new Intl.DateTimeFormat('en-GB', { dateStyle: 'medium', timeZone: 'UTC' }) +const sessionDate = new Intl.DateTimeFormat("en-GB", { + dateStyle: "medium", + timeZone: "UTC", +}); + +type SortOrder = "none" | "asc" | "desc"; function App() { - const { getDriversWithStandings, getAllTeamStandings, getAllSessionResults, getAllSessions, getAllMeetings } = api() + const [search, setSearch] = useState( + () => sessionStorage.getItem("search") ?? "", + ); + const [team, setTeam] = useState( + () => sessionStorage.getItem("team") ?? "all", + ); + + const [sortOrder, setSortOrder] = useState( + () => (sessionStorage.getItem("sortOrder") as SortOrder) ?? "none", + ); + + useEffect(() => { + sessionStorage.setItem("team", team); + }, [team]); + + useEffect(() => { + sessionStorage.setItem("sortOrder", sortOrder); + }, [sortOrder]); - const {data: driversWithStandings = [], isPending: driversWithStandingsPending, isError: driversWithStandingsError} = useQuery({ - queryKey: ['driversWithStandings'], - queryFn: getDriversWithStandings - }) + const { + getDriversWithStandings, + getAllTeamStandings, + getAllSessionResults, + getAllSessions, + getAllMeetings, + } = api(); - const {data: teamStandings = [], isPending: teamStandingsPending, isError: teamStandingsError} = useQuery({ - queryKey: ['teamStandings'], + const { + data: driversWithStandings = [], + isPending: driversWithStandingsPending, + isError: driversWithStandingsError, + } = useQuery({ + queryKey: ["driversWithStandings"], + queryFn: getDriversWithStandings, + }); + + const { + data: teamStandings = [], + isPending: teamStandingsPending, + isError: teamStandingsError, + } = useQuery({ + queryKey: ["teamStandings"], queryFn: getAllTeamStandings, - enabled: !driversWithStandingsPending - }) + enabled: !driversWithStandingsPending, + }); - const {data: allSessions = [], isPending: sessionsPending, isError: sessionsError} = useQuery({ - queryKey: ['sessions', 2026], + const { + data: allSessions = [], + isPending: sessionsPending, + isError: sessionsError, + } = useQuery({ + queryKey: ["sessions", 2026], queryFn: getAllSessions, - enabled: !driversWithStandingsPending - }) + enabled: !driversWithStandingsPending, + }); - const {data: allSessionResults = [], isPending: sessionResultsPending, isError: sessionResultsError} = useQuery({ - queryKey: ['sessionResults', 2026, allSessions.map((session) => session.session_key)], + const { + data: allSessionResults = [], + isPending: sessionResultsPending, + isError: sessionResultsError, + } = useQuery({ + queryKey: [ + "sessionResults", + 2026, + allSessions.map((session) => session.session_key), + ], queryFn: () => getAllSessionResults(allSessions), - enabled: !sessionsPending && !sessionsError - }) + enabled: !sessionsPending && !sessionsError, + }); - const {data: allMeetings = [], isPending: meetingsPending, isError: meetingsError} = useQuery({ - queryKey: ['meetings', 2026], + const { + data: allMeetings = [], + isPending: meetingsPending, + isError: meetingsError, + } = useQuery({ + queryKey: ["meetings", 2026], queryFn: getAllMeetings, - enabled: !driversWithStandingsPending - }) + enabled: !driversWithStandingsPending, + }); + + const filteredDrivers = driversWithStandings + .filter((driver) => { + const matchesSearch = driver.full_name + .toLowerCase() + .includes(search.toLowerCase()); + const matchesTeam = team === "all" || driver.team_name === team; + return matchesSearch && matchesTeam; + }) + .sort((a, b) => { + if (sortOrder === "asc") { + return b.position_current - a.position_current; + } else if (sortOrder === "desc") { + return a.position_current - b.position_current; + } else { + return 0; // No sorting, maintain original order + } + }); - 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)) + 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 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() + 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) + 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)) + results.sort((a, b) => (a.position ?? Infinity) - (b.position ?? Infinity)); } return ( <> + driver.team_name)), + )} + />

F1 The Website

- { driversWithStandingsPending ? "loading drivers..." : driversWithStandingsError ? "Could not load drivers." : - driversWithStandings.map((driver) => ( - team.team_name === driver.team_name)} - teamStandingsPending={teamStandingsPending} - teamStandingsError={teamStandingsError} - driver={{ - name: driver.full_name, - number: driver.driver_number, - headshot_url: driver.headshot_url, - team_colour: driver.team_colour, - team_name: driver.team_name, - points_current: driver.points_current, - position_current: driver.position_current - }} /> - )) - } + {driversWithStandingsPending + ? "loading drivers..." + : driversWithStandingsError + ? "Could not load drivers." + : filteredDrivers.length === 0 + ? "No drivers match your search and team filter." + : filteredDrivers.map((driver) => ( + team.team_name === driver.team_name, + )} + teamStandingsPending={teamStandingsPending} + teamStandingsError={teamStandingsError} + driver={{ + name: driver.full_name, + number: driver.driver_number, + headshot_url: driver.headshot_url, + team_colour: driver.team_colour, + team_name: driver.team_name, + points_current: driver.points_current, + position_current: driver.position_current, + }} + /> + ))}
{sessionsError || sessionResultsError ? ( @@ -91,28 +193,44 @@ function App() { ) : (

2026 meetings

- {meetingsError &&

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

} + {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] + const meeting = meetingsByKey.get(meetingKey); + const firstSession = sessions[0]; + const lastSession = sessions[sessions.length - 1]; return (
- {meeting?.meeting_name ?? firstSession.circuit_short_name} + + {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))} + {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) ?? [] + const results = + resultsBySession.get(session.session_key) ?? []; return ( -
+
{session.session_name} {results.length === 0 ? ( -

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

+

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

) : (
    {results.map((result) => ( -
  • - Pos {result.position ?? '—'}: - {driverNames.get(result.driver_number) ?? `Driver ${result.driver_number}`} +
  • + Pos {result.position ?? "—"}: + + {driverNames.get(result.driver_number) ?? + `Driver ${result.driver_number}`} +
  • ))}
)}
- ) + ); })}
- ) + ); })}
)} - ) + ); } -export default App +export default App; diff --git a/src/api/api.tsx b/src/api/api.tsx index c5dbd94..e2d4e67 100644 --- a/src/api/api.tsx +++ b/src/api/api.tsx @@ -124,7 +124,7 @@ function fetchApi(url: string, errorMessage: string): Promise { } export default function api() { - + const API = "https://api.openf1.org/v1" async function getAllMeetings(): Promise { @@ -157,7 +157,7 @@ export default function api() { const completedRaces = races.filter( (race) => new Date(race.date_end) < new Date() ) - + const latestRace = completedRaces.at(-1) if (!latestRace) { throw new Error("No completed race session found") } console.log(latestRace.session_key) diff --git a/src/components/SearchAndFilter.tsx b/src/components/SearchAndFilter.tsx new file mode 100644 index 0000000..c6e8461 --- /dev/null +++ b/src/components/SearchAndFilter.tsx @@ -0,0 +1,50 @@ +interface SearchAndFilterProps { + search: string; + onSearchChange: (value: string) => void; + team: string; + onTeamChange: (value: string) => void; + teams: string[]; + sortOrder: "none" | "asc" | "desc"; + onSortOrderChange: (value: "none" | "asc" | "desc") => void; +} + +function SearchAndFilter({ + search, + onSearchChange, + team, + onTeamChange, + teams, + sortOrder, + onSortOrderChange, +}: SearchAndFilterProps) { + return ( +
+ onSearchChange(e.target.value)} + /> + + +
+ ); +} + +export default SearchAndFilter;