Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
275 changes: 201 additions & 74 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -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<SortOrder>(
() => (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<number, Session[]>()
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<number, Session[]>();
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<number, SessionResult[]>()
const driverNames = new Map(
driversWithStandings.map((driver) => [
driver.driver_number,
driver.full_name,
]),
);
const resultsBySession = new Map<number, SessionResult[]>();
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 (
<>
<SearchAndFilter
search={search}
onSearchChange={setSearch}
team={team}
onTeamChange={setTeam}
sortOrder={sortOrder}
onSortOrderChange={setSortOrder}
teams={Array.from(
new Set(driversWithStandings.map((driver) => driver.team_name)),
)}
/>
<div className="standing-drivers">
<h1 className="header">F1 The Website</h1>
{ driversWithStandingsPending ? "loading drivers..." : driversWithStandingsError ? "Could not load drivers." :
driversWithStandings.map((driver) => (
<DriverCard key={driver.driver_number}
teamStanding={teamStandings.find((team) => 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) => (
<DriverCard
key={driver.driver_number}
teamStanding={teamStandings.find(
(team) => 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,
}}
/>
))}
</div>

{sessionsError || sessionResultsError ? (
Expand All @@ -91,57 +193,82 @@ function App() {
) : (
<section className="meeting-list" aria-labelledby="meetings-heading">
<h2 id="meetings-heading">2026 meetings</h2>
{meetingsError && <p>Meeting names could not be loaded. Showing circuits and dates.</p>}
{meetingsError && (
<p>
Meeting names could not be loaded. Showing circuits and dates.
</p>
)}
{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 (
<details className="meeting" key={meetingKey}>
<summary>
<span className="meeting-title">{meeting?.meeting_name ?? firstSession.circuit_short_name}</span>
<span className="meeting-title">
{meeting?.meeting_name ?? firstSession.circuit_short_name}
</span>
<span className="meeting-meta">
{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),
)}
</span>
</summary>
<div className="meeting-sessions">
{sessions.map((session) => {
const results = resultsBySession.get(session.session_key) ?? []
const results =
resultsBySession.get(session.session_key) ?? [];

return (
<details className="meeting-session" key={session.session_key}>
<details
className="meeting-session"
key={session.session_key}
>
<summary>
<span>{session.session_name}</span>
<time dateTime={session.date_start}>
{sessionDate.format(new Date(session.date_start))}
</time>
</summary>
{results.length === 0 ? (
<p>{session.is_cancelled ? 'Session cancelled.' : 'Results not available yet.'}</p>
<p>
{session.is_cancelled
? "Session cancelled."
: "Results not available yet."}
</p>
) : (
<ul className="session-results">
{results.map((result) => (
<li key={`${result.session_key}-${result.driver_number}`}>
<span>Pos {result.position ?? '—'}:</span>
<span>{driverNames.get(result.driver_number) ?? `Driver ${result.driver_number}`}</span>
<li
key={`${result.session_key}-${result.driver_number}`}
>
<span>Pos {result.position ?? "—"}:</span>
<span>
{driverNames.get(result.driver_number) ??
`Driver ${result.driver_number}`}
</span>
</li>
))}
</ul>
)}
</details>
)
);
})}
</div>
</details>
)
);
})}
</section>
)}
</>
)
);
}

export default App
export default App;
4 changes: 2 additions & 2 deletions src/api/api.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ function fetchApi<T>(url: string, errorMessage: string): Promise<T> {
}

export default function api() {

const API = "https://api.openf1.org/v1"

async function getAllMeetings(): Promise<Meeting[]> {
Expand Down Expand Up @@ -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)
Expand Down
Loading