Skip to content
Open
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
9 changes: 9 additions & 0 deletions dashboard/src/app/(nav)/help/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Image from "next/image";
import Link from "next/link";

import Note from "@/components/Note";
import DriverDRS from "@/components/driver/DriverDRS";
Expand Down Expand Up @@ -151,6 +152,14 @@ export default function HelpPage() {
In this example, the driver has a soft tire which is 12 laps old and he pitted one time.
</p>

<Note className="mb-4">
For a complete history of all tire stints and outings during the session, you can visit the dedicated{" "}
<Link href="/dashboard/tires" className="text-blue-500 underline">
Tires
</Link>{" "}
page in the dashboard.
</Note>

<div className="mb-4">
<DriverTire
stints={[
Expand Down
16 changes: 16 additions & 0 deletions dashboard/src/app/dashboard/tires/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import OutingTable from "@/components/dashboard/OutingTable";

export default function TiresPage() {
return (
<div className="flex min-h-full w-full flex-col">
<div className="flex flex-col gap-1 border-b border-zinc-800 p-4">
<h1 className="text-2xl font-bold tracking-tight">Tire Outings</h1>
<p className="text-sm text-zinc-500">Real-time history of tire compounds and outing progression.</p>
</div>

<div className="flex-1">
<OutingTable />
</div>
</div>
);
}
4 changes: 4 additions & 0 deletions dashboard/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ const liveTimingItems = [
href: "/dashboard/track-map",
name: "Track Map",
},
{
href: "/dashboard/tires",
name: "Tires",
},
{
href: "/dashboard/standings",
name: "Standings",
Expand Down
90 changes: 90 additions & 0 deletions dashboard/src/components/dashboard/OutingTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"use client";

import { AnimatePresence, LayoutGroup, motion } from "motion/react";
import clsx from "clsx";

import { useDataStore } from "@/stores/useDataStore";
import { useSettingsStore } from "@/stores/useSettingsStore";
import { sortPos } from "@/lib/sorting";

import DriverTag from "@/components/driver/DriverTag";
import DriverHistoryTires from "@/components/driver/DriverHistoryTires";

export default function OutingTable() {
const drivers = useDataStore(({ state }) => state?.DriverList);
const driversTiming = useDataStore(({ state }) => state?.TimingData);
const appDriversTiming = useDataStore(({ state }) => state?.TimingAppData);

const oledMode = useSettingsStore((state) => state.oledMode);

return (
<div className="flex w-full flex-col">
<div
className="grid items-center gap-4 border-b border-zinc-800 bg-zinc-900/10 px-4 py-2 text-xs font-bold uppercase tracking-widest text-zinc-500"
style={{ gridTemplateColumns: "7rem 1fr" }}
>
<p>Driver</p>
<p>Tire History / Outings</p>
</div>

{(!drivers || !driversTiming) &&
new Array(20).fill("").map((_, index) => <SkeletonRow key={`outing.loading.${index}`} />)}

<LayoutGroup id="outings">
<div className="flex flex-col divide-y divide-zinc-800/50">
{drivers && driversTiming && (
<AnimatePresence mode="popLayout">
{Object.values(driversTiming.Lines)
.sort(sortPos)
.map((timingDriver, index) => {
const driver = drivers[timingDriver.RacingNumber];
const appTiming = appDriversTiming?.Lines[timingDriver.RacingNumber];

return (
<motion.div
key={`outing.row.${timingDriver.RacingNumber}`}
layout
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className={clsx("grid items-center gap-4 px-4 py-3 transition-colors hover:bg-zinc-900/40", {
"bg-black": oledMode,
"bg-zinc-950/20": !oledMode,
})}
style={{ gridTemplateColumns: "7rem 1fr" }}
>
<DriverTag
className="min-w-full!"
short={driver.Tla}
teamColor={driver.TeamColour}
position={index + 1}
/>
<div className="no-scrollbar overflow-x-auto">
<DriverHistoryTires stints={appTiming?.Stints} />
</div>
</motion.div>
);
})}
</AnimatePresence>
)}
</div>
</LayoutGroup>
</div>
);
}

function SkeletonRow() {
return (
<div className="grid items-center gap-4 border-b border-zinc-800/30 px-4 py-3" style={{ gridTemplateColumns: "7rem 1fr" }}>
<div className="h-10 animate-pulse rounded-lg bg-zinc-800" />
<div className="flex gap-3">
{[1, 2, 3].map((i) => (
<div key={i} className="flex flex-col items-center gap-2">
<div className="h-8 w-8 animate-pulse rounded-full bg-zinc-800" />
<div className="h-3 w-6 animate-pulse rounded bg-zinc-800" />
</div>
))}
</div>
</div>
);
}
6 changes: 4 additions & 2 deletions dashboard/src/components/driver/DriverHistoryTires.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default function DriverHistoryTires({ stints }: Props) {
<div className="flex flex-row items-center justify-start gap-1">
{stints &&
stints.map((stint, i) => (
<div className="flex flex-col items-center gap-1" key={`driver.${i}`}>
<div className="flex flex-col items-center gap-1" key={`driver.stint.${i}`}>
{unknownCompound(stint) && <Image src={"/tires/unknown.svg"} width={32} height={32} alt="unknown" />}
{!unknownCompound(stint) && stint.Compound && (
<Image
Expand All @@ -25,7 +25,9 @@ export default function DriverHistoryTires({ stints }: Props) {
/>
)}

<p className="text-sm leading-none font-medium whitespace-nowrap text-zinc-600">{stint.TotalLaps}L</p>
<p className="whitespace-nowrap text-sm font-medium leading-none text-zinc-400">
{stint.TotalLaps}L{stint.New !== "TRUE" ? "*" : ""}
</p>
Comment on lines +28 to +30
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stint.New is typed as a string and the help example models a used set by omitting New (only setting New: "TRUE" for new tires). With the current check stint.New === "FALSE", stints where New is undefined (likely used sets) won’t get the used-set marker. Consider aligning this logic with the existing DriverTire behavior and treating anything other than an explicit "TRUE" as used (or otherwise normalize/clarify the data contract).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking at the data form the api (like here: https://rt-api.f1-dash.com/api/current)

stint.New has "true" or "false" as values. and not uppercase.

</div>
))}

Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/components/driver/DriverTire.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default function DriverTire({ stints }: Props) {
<div>
<p className="leading-none font-medium">
L {currentStint?.TotalLaps ?? 0}
{currentStint?.New ? "" : "*"}
{currentStint?.New === "TRUE" ? "" : "*"}
</p>

<p className="text-sm leading-none text-zinc-500">PIT {stops}</p>
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/types/state.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export type TimingAppDataDriver = {
export type Stint = {
TotalLaps?: number;
Compound?: "SOFT" | "MEDIUM" | "HARD" | "INTERMEDIATE" | "WET";
New?: string; // TRUE | FALSE
New?: "TRUE" | "FALSE";
};

export type WeatherData = {
Expand Down
2 changes: 1 addition & 1 deletion dashboard/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
Expand Down