diff --git a/src/App.css b/src/App.css index 9e60301..1d1a421 100644 --- a/src/App.css +++ b/src/App.css @@ -20,6 +20,12 @@ body { padding: 10px; margin: 9px; + + cursor: pointer; +} + +.driver-card-selected { + box-shadow: 0 0 0 3px white; } .driver-card img { @@ -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 { @@ -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; diff --git a/src/App.tsx b/src/App.tsx index fdc70b9..7312224 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,6 +6,7 @@ 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", @@ -45,6 +46,10 @@ function App() { () => (sessionStorage.getItem("sortOrder") as SortOrder) ?? "none", ); + useEffect(() => { + sessionStorage.setItem("search", search); + }, [search]); + useEffect(() => { sessionStorage.setItem("team", team); }, [team]); @@ -134,6 +139,23 @@ 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) + : 0; + const selectedDriver = filteredDrivers[clampedIndex]; + + function selectDriverByNumber(driverNumber: number) { + const index = filteredDrivers.findIndex( + (driver) => driver.driver_number === driverNumber, + ); + if (index !== -1) { + setSelectedIndex(index); + } + } + const meetingsByKey = new Map( allMeetings.map((meeting) => [meeting.meeting_key, meeting]), ); @@ -165,6 +187,8 @@ function App() { return ( <> +
{driver.points_current}
{driver.position_current}
diff --git a/src/components/DriverDetail.tsx b/src/components/DriverDetail.tsx new file mode 100644 index 0000000..79e5fcd --- /dev/null +++ b/src/components/DriverDetail.tsx @@ -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 ( +
Loading driver...
+ ) : isError ? ( +Could not load driver details.
+ ) : !driver ? ( +No driver selected.
+ ) : ( + <> +