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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pnpm-debug.log*
lerna-debug.log*

node_modules
node_modules/
dist
dist-ssr
*.local
Expand Down
27 changes: 27 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"@tanstack/react-query": "^5.102.8",
"react": "^19.2.8",
"react-dom": "^19.2.8"
},
Expand Down
73 changes: 73 additions & 0 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,76 @@ body {
.header {
align-self: center;
}

.meeting-list {
width: 100%;
max-width: 760px;
margin-top: 2rem;
}

.meeting {
margin-bottom: 0.75rem;
border: 1px solid #656581;
border-radius: 0.5rem;
background-color: #15151e;
}

.meeting summary {
padding: 1rem;
cursor: pointer;
}

.meeting summary:focus-visible {
outline: 2px solid #aaaaff;
outline-offset: 2px;
border-radius: 0.5rem;
}

.meeting-title {
font-size: 1.125rem;
font-weight: 600;
}

.meeting-meta {
display: block;
margin-top: 0.25rem;
color: #c6c6d8;
font-size: 0.875rem;
}

.meeting-sessions {
padding: 0 1rem 1rem;
}

.meeting-session {
border-top: 1px solid #45455e;
}

.meeting-session summary {
padding: 0.75rem 0;
font-weight: 600;
}

.meeting-session time {
display: inline-block;
margin-left: 0.75rem;
color: #c6c6d8;
font-size: 0.875rem;
font-weight: 400;
}

.session-results {
list-style: none;
padding: 0;
margin: 0 0 1rem;
}

.session-results li {
display: flex;
align-items: center;
gap: 0.5rem;
border: 1px solid #45455e;
border-radius: 0.375rem;
padding: 0.5rem;
margin-bottom: 0.5rem;
}
143 changes: 124 additions & 19 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,145 @@
import './App.css'
import api, { type Driver } from './api/api'
import { useEffect, useState } from "react";
import api from './api/api'
import type { Session, SessionResult } from './api/api'
import { useQuery } from '@tanstack/react-query'

import DriverCard from './components/DriverCard'

const sessionDate = new Intl.DateTimeFormat('en-GB', { dateStyle: 'medium', timeZone: 'UTC' })

function App() {
const { getAllDrivers } = api()
const [drivers, setDrivers] = useState<Driver[]>([])

useEffect(() => {
async function loadDrivers() {
const allDrivers = await getAllDrivers()
setDrivers(allDrivers)
console.log(allDrivers)
}
loadDrivers()
}, [])
const { getDriversWithStandings, getAllTeamStandings, getAllSessionResults, getAllSessions, getAllMeetings } = api()

return (
<>
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
})

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 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 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)
}
for (const results of resultsBySession.values()) {
results.sort((a, b) => (a.position ?? Infinity) - (b.position ?? Infinity))
}

return (
<>
<div className="standing-drivers">
<h1 className="header">F1 The Website</h1>
{drivers.map((driver) => (

{ 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_colour: driver.team_colour,
team_name: driver.team_name,
points_current: driver.points_current,
position_current: driver.position_current
}} />
))}
))
}
</div>

{sessionsError || sessionResultsError ? (
<p>Could not load session results.</p>
) : sessionsPending || sessionResultsPending || meetingsPending ? (
<p>Loading session results...</p>
) : allSessions.length === 0 ? (
<p>No sessions available yet.</p>
) : (
<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>}
{Array.from(sessionsByMeeting, ([meetingKey, sessions]) => {
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-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))}
</span>
</summary>
<div className="meeting-sessions">
{sessions.map((session) => {
const results = resultsBySession.get(session.session_key) ?? []

return (
<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>
) : (
<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>
))}
</ul>
)}
</details>
)
})}
</div>
</details>
)
})}
</section>
)}
</>
)
}
Expand Down
Loading