Skip to content
Closed
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
111 changes: 109 additions & 2 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ body {

padding: 10px;
margin: 9px;

cursor: pointer;
}

.driver-card-selected {
box-shadow: 0 0 0 3px white;
}

.driver-card img {
Expand All @@ -35,10 +41,11 @@ body {
font-weight: 400;
}

.standing-drivers {
.content-row {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2rem;
flex-wrap: wrap;
}

.standing-drivers {
Expand All @@ -52,6 +59,106 @@ body {
align-self: center;
}

.driver-detail {
background-color: #15151e;
border-radius: 30px;

color: white;
font-family: "Titillium Web", sans-serif;

width: 360px;
padding: 2rem;
margin: 10px;

display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;

position: sticky;
top: 1rem;
}

.driver-detail-nav {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}

.driver-detail-nav-button {
background-color: transparent;
border: 1px solid grey;
border-radius: 10px;
color: white;
font-size: 1rem;
cursor: pointer;
}

.driver-detail-nav-button:disabled {
color: grey;
border-color: grey;
cursor: not-allowed;
}

.driver-detail-photo {
height: 240px;
width: auto;
max-width: 100%;
border-radius: 25px;
}

.driver-detail-heading {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 1rem;
}

.driver-detail-name {
font-size: 22px;
font-weight: 400;
margin: 0;
text-align: center;
}

.driver-detail-number {
font-size: 50px;
font-weight: 700;
}

.driver-detail-stats {
display: flex;
flex-direction: column;
width: 100%;
margin-top: 0.5rem;
}

.driver-detail-stat-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.65rem 0;
}

.driver-detail-stat-label {
color: grey;
font-size: 1rem;
}

.driver-detail-stat-value {
font-size: 1.25rem;
font-weight: 600;
}

.driver-detail-divider {
width: 100%;
margin: 0 auto;
border-top: 1px solid grey;
}

.meeting-list {
width: 100%;
max-width: 760px;
Expand Down
74 changes: 55 additions & 19 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import './App.css'
import { 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'
import DriverDetail from './components/DriverDetail'

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

Expand All @@ -15,6 +17,15 @@ function App() {
queryFn: getDriversWithStandings
})

const [selectedIndex, setSelectedIndex] = useState(0)
const clampedIndex = driversWithStandings.length ? Math.min(selectedIndex, driversWithStandings.length - 1) : 0
const selectedDriver = driversWithStandings[clampedIndex]

function selectDriverByNumber(driverNumber: number) {
const index = driversWithStandings.findIndex((driver) => driver.driver_number === driverNumber)
if (index !== -1) { setSelectedIndex(index) }
}

const {data: teamStandings = [], isPending: teamStandingsPending, isError: teamStandingsError} = useQuery({
queryKey: ['teamStandings'],
queryFn: getAllTeamStandings,
Expand Down Expand Up @@ -61,25 +72,50 @@ function App() {

return (
<>
<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
}} />
))
}
<h1 className="header">F1 The Website</h1>
<div className="content-row">
<div className="standing-drivers">
{ 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}
isSelected={selectedDriver?.driver_number === driver.driver_number}
onSelect={() => selectDriverByNumber(driver.driver_number)}
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>

<DriverDetail
isLoading={driversWithStandingsPending}
isError={driversWithStandingsError}
driver={selectedDriver ? {
name: selectedDriver.full_name,
number: selectedDriver.driver_number,
headshot_url: selectedDriver.headshot_url,
team_colour: selectedDriver.team_colour,
team_name: selectedDriver.team_name,
points_current: selectedDriver.points_current,
position_current: selectedDriver.position_current
} : undefined}
teamStanding={selectedDriver ? teamStandings.find((team) => team.team_name === selectedDriver.team_name) : undefined}
teamStandingsPending={teamStandingsPending}
teamStandingsError={teamStandingsError}
hasPrev={clampedIndex > 0}
hasNext={clampedIndex < driversWithStandings.length - 1}
onPrev={() => setSelectedIndex((index) => Math.max(0, index - 1))}
onNext={() => setSelectedIndex((index) => Math.min(driversWithStandings.length - 1, index + 1))}
/>
</div>

{sessionsError || sessionResultsError ? (
Expand Down
17 changes: 13 additions & 4 deletions src/components/DriverCard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type { TeamStandings } from '../api/api'

interface DriverProps {
// Default "avatar" used when api does not return a driver photo
const DEFAULT_AVATAR_URL = 'https://canvas.ntnu.no/images/messages/avatar-50.png'

export interface DriverProps {
name: string;
number: number;
headshot_url: string;
Expand All @@ -16,15 +19,21 @@ interface DriverCardProps {
teamStanding?: TeamStandings;
teamStandingsPending: boolean;
teamStandingsError: boolean;
isSelected?: boolean;
onSelect?: () => void;
}

export default function DriverCard({ driver, teamStanding, teamStandingsPending, teamStandingsError }: DriverCardProps) {
export default function DriverCard({ driver, teamStanding, teamStandingsPending, teamStandingsError, isSelected, onSelect }: DriverCardProps) {

return (
<div className="driver-card" style={{ border: `2px solid #${driver.team_colour}` }}>
<div
className={isSelected ? "driver-card driver-card-selected" : "driver-card"}
style={{ border: `2px solid #${driver.team_colour}` }}
onClick={onSelect}
>
<h1 style={{ color: `#${driver.team_colour}` }}>{driver.number}</h1>
<h2>{driver.name}</h2>
<img style={{ backgroundColor: `#${driver.team_colour}` }} src={driver.headshot_url ? driver.headshot_url : 'https://placehold.net/avatar.svg'} alt={driver.name} />
<img style={{ backgroundColor: `#${driver.team_colour}` }} src={driver.headshot_url ? driver.headshot_url : DEFAULT_AVATAR_URL} alt={driver.name} />
<p>{driver.points_current}</p>
<p>{driver.position_current}</p>
<p>
Expand Down
110 changes: 110 additions & 0 deletions src/components/DriverDetail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import type { TeamStandings } from '../api/api'
import type { DriverProps } from './DriverCard'

// Default "avatar" used when api does not return a driver photo
const DEFAULT_AVATAR_URL = 'https://canvas.ntnu.no/images/messages/avatar-50.png'

interface DriverDetailProps {
driver?: DriverProps;
teamStanding?: TeamStandings;
teamStandingsPending: boolean;
teamStandingsError: boolean;
isLoading: boolean;
isError: boolean;
hasPrev: boolean;
hasNext: boolean;
onPrev: () => void;
onNext: () => void;
}

export default function DriverDetail({
driver,
teamStanding,
teamStandingsPending,
teamStandingsError,
isLoading,
isError,
hasPrev,
hasNext,
onPrev,
onNext,
}: DriverDetailProps) {

return (
<div className="driver-detail">
<div className="driver-detail-nav">
<button
type="button"
className="driver-detail-nav-button"
onClick={onPrev}
disabled={!hasPrev}
>
Previous
</button>
<button
type="button"
className="driver-detail-nav-button"
onClick={onNext}
disabled={!hasNext}
>
Next
</button>
</div>

{isLoading ? (
<p>Loading driver...</p>
) : isError ? (
<p>Could not load driver details.</p>
) : !driver ? (
<p>No driver selected.</p>
) : (
<>
<img
key={driver.number}
className="driver-detail-photo"
style={{ backgroundColor: `#${driver.team_colour}` }}
src={driver.headshot_url ? driver.headshot_url : DEFAULT_AVATAR_URL}
alt={driver.name}
onError={(event) => { event.currentTarget.src = DEFAULT_AVATAR_URL }}
/>

<div className="driver-detail-heading">
<h2 className="driver-detail-name">{driver.name}</h2>
<span className="driver-detail-number" style={{ color: `#${driver.team_colour}` }}>{driver.number}</span>
</div>

<div className="driver-detail-stats">
<div className="driver-detail-stat-row">
<span className="driver-detail-stat-label">Team</span>
<span className="driver-detail-stat-value">{driver.team_name}</span>
</div>
<div className="driver-detail-divider" />

<div className="driver-detail-stat-row">
<span className="driver-detail-stat-label">Position</span>
<span className="driver-detail-stat-value">{driver.position_current || '—'}</span>
</div>
<div className="driver-detail-divider" />

<div className="driver-detail-stat-row">
<span className="driver-detail-stat-label">Driver points</span>
<span className="driver-detail-stat-value">{driver.points_current}</span>
</div>
<div className="driver-detail-divider" />

<div className="driver-detail-stat-row">
<span className="driver-detail-stat-label">Team points</span>
<span className="driver-detail-stat-value">
{teamStandingsPending
? 'Loading...'
: teamStandingsError
? 'Unavailable'
: teamStanding?.points_current ?? 'Unavailable'}
</span>
</div>
</div>
</>
)}
</div>
)
}