feat(dashboard): design refresh
All checks were successful
Build and Publish Docker Image / build-and-push (push) Successful in 1m5s

This commit is contained in:
2025-10-26 16:49:35 +00:00
parent 6e9c5291ba
commit 2e63609129
2 changed files with 110 additions and 75 deletions

View File

@@ -1,9 +1,9 @@
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import Chart from "chart.js/auto" import Chart from "chart.js/auto"
import { Fragment, useEffect, useLayoutEffect, useRef, useState } from "react" import { useEffect, useLayoutEffect, useRef, useState } from "react"
import { beszelSystemsQuery } from "./beszel" import { beszelSystemsQuery } from "./beszel"
import cn from "./components/lib/cn" import cn from "./components/lib/cn"
import { StatusSeverity, formatLineName, tflDisruptionsQuery } from "./tfl" import { StatusSeverity, TubeLine, formatLineName, tflDisruptionsQuery } from "./tfl"
import { import {
DEFAULT_LATITUDE, DEFAULT_LATITUDE,
DEFAULT_LONGITUDE, DEFAULT_LONGITUDE,
@@ -15,31 +15,27 @@ import {
function App() { function App() {
return ( return (
<div className="h-screen bg-black gap-4 text-neutral-200 grid grid-cols-4 grid-rows-5 p-4"> <div className="h-screen bg-neutral-300 dark:bg-neutral-800 p-2">
<DateTimeTile /> <div className="w-full h-full grid grid-cols-4 grid-rows-5 gap-2 bg-neutral-300 text-neutral-700 dark:bg-neutral-800 dark:text-neutral-300">
<WeatherTile /> <DateTimeTile />
<TFLTile /> <WeatherTile />
<SystemTile systemName="helian" displayName="Helian" /> <SystemTile className="row-start-2 row-span-1" systemName="helian" displayName="Helian" />
<SystemTile systemName="akira" displayName="Akira" /> <SystemTile className="row-start-2 row-span-1" systemName="akira" displayName="Akira" />
<TFLTile className="row-start-1 row-span-1" />
<Tile className="row-start-3 col-span-2 row-span-3" />
</div>
</div> </div>
) )
} }
function Tile({ function Tile({ children, className }: { children?: React.ReactNode; className?: string }) {
decorations = true,
children,
className,
}: { decorations?: boolean; children: React.ReactNode; className?: string }) {
return ( return (
<div className={cn("relative bg-neutral-900 flex flex-col justify-end items-start", className)}> <div
{decorations && ( className={cn(
<> "relative rounded-xl bg-neutral-200 dark:bg-neutral-900 flex flex-col justify-end items-start",
<div className="absolute top-0 left-0 w-4 h-[1px] bg-neutral-200" /> className,
<div className="absolute top-0 left-0 w-[1px] h-4 bg-neutral-200" />
<div className="absolute bottom-0 right-0 w-4 h-[1px] bg-neutral-200" />
<div className="absolute bottom-0 right-0 w-[1px] h-4 bg-neutral-200" />
</>
)} )}
>
{children} {children}
</div> </div>
) )
@@ -69,8 +65,8 @@ function DateTimeTile() {
return ( return (
<Tile className="col-start-1 row-start-1 col-span-2 row-span-3 p-6"> <Tile className="col-start-1 row-start-1 col-span-2 row-span-3 p-6">
<p className="text-4xl mb-2 font-extralight">{formattedDate}</p> <p className="text-4xl mb-2 font-mono uppercase tracking-tigher">{formattedDate}</p>
<p className="text-8xl font-bold">{formattedTime}</p> <p className="text-8xl font-extralight tracking-tight">{formattedTime}</p>
</Tile> </Tile>
) )
} }
@@ -164,14 +160,14 @@ function WeatherTile() {
<div <div
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation> // biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
key={index} key={index}
className={cn("w-10 bg-teal-400 h-[2px]")} className={cn("w-10 bg-teal-500 dark:bg-teal-400 h-[2px]")}
/> />
<div <div
className={cn( className={cn(
"absolute flex flex-row items-center space-x-1 top-0 right-0 bg-teal-400 text-neutral-900 px-2 py-1 text-4xl font-bold rounded-r-sm translate-x-[calc(100%-1px)]", "absolute flex flex-row items-center space-x-1 top-0 right-0 bg-teal-500 dark:bg-teal-400 text-neutral-200 dark:text-neutral-900 px-2 py-1 text-4xl font-bold rounded-r translate-x-[calc(100%-1px)]",
percentage < 0.3 percentage < 0.3
? "-translate-y-[calc(100%-2px)] rounded-tl-sm" ? "-translate-y-[calc(100%-2px)] rounded-tl"
: "rounded-bl-sm", : "rounded-bl",
)} )}
> >
<p className="leading-none translate-y-px">{temperature}°</p> <p className="leading-none translate-y-px">{temperature}°</p>
@@ -187,7 +183,7 @@ function WeatherTile() {
className={cn( className={cn(
"w-4", "w-4",
index >= highlightIndexStart index >= highlightIndexStart
? "bg-teal-400 w-8 h-[2px]" ? "bg-teal-500 dark:bg-teal-400 w-8 h-[2px]"
: "bg-neutral-400 w-4 h-[1px]", : "bg-neutral-400 w-4 h-[1px]",
)} )}
/> />
@@ -209,7 +205,16 @@ function WeatherTile() {
) )
} }
function TFLTile() { function TFLTile({ className }: { className?: string }) {
const linesIDontCareAbout = [
TubeLine.WaterlooCity,
TubeLine.Windrush,
TubeLine.Lioness,
TubeLine.Lioness,
TubeLine.Tram,
TubeLine.Mildmay,
]
const { const {
data: tflData, data: tflData,
isLoading: isLoadingTFL, isLoading: isLoadingTFL,
@@ -222,6 +227,7 @@ function TFLTile() {
if (b.lineName.match(/northern/i)) return 1 if (b.lineName.match(/northern/i)) return 1
return a.statusSeverity - b.statusSeverity return a.statusSeverity - b.statusSeverity
}) })
data.disruptions = data.disruptions.filter((disruption) => !linesIDontCareAbout.includes(disruption.lineId))
return data return data
}, },
refetchInterval: 5 * 60 * 1000, // 5 minutes refetchInterval: 5 * 60 * 1000, // 5 minutes
@@ -230,7 +236,9 @@ function TFLTile() {
if (isLoadingTFL) { if (isLoadingTFL) {
return ( return (
<Tile className="col-start-3 h-full row-start-1 col-span-2 row-span-1 flex flex-row justify-start items-center p-8"> <Tile
className={cn("h-full col-span-2 row-span-1 flex flex-row justify-start items-center p-8", className)}
>
<p className="text-2xl font-light animate-pulse">Loading tube status</p> <p className="text-2xl font-light animate-pulse">Loading tube status</p>
</Tile> </Tile>
) )
@@ -238,7 +246,9 @@ function TFLTile() {
if (errorTFL) { if (errorTFL) {
return ( return (
<Tile className="col-start-3 h-full row-start-1 col-span-2 row-span-1 flex flex-row justify-start items-center p-8"> <Tile
className={cn("h-full col-span-2 row-span-1 flex flex-row justify-start items-center p-8", className)}
>
<p className="text-2xl font-light text-red-400">Error loading from TfL</p> <p className="text-2xl font-light text-red-400">Error loading from TfL</p>
<p className="text-neutral-400">{errorTFL?.message}</p> <p className="text-neutral-400">{errorTFL?.message}</p>
</Tile> </Tile>
@@ -247,7 +257,9 @@ function TFLTile() {
if (!tflData) { if (!tflData) {
return ( return (
<Tile className="col-start-3 h-full row-start-1 col-span-2 row-span-1 flex flex-row justify-start items-center p-8"> <Tile
className={cn("h-full col-span-2 row-span-1 flex flex-row justify-start items-center p-8", className)}
>
<p className="text-2xl font-light">No TfL data available</p> <p className="text-2xl font-light">No TfL data available</p>
</Tile> </Tile>
) )
@@ -255,33 +267,33 @@ function TFLTile() {
return ( return (
<Tile <Tile
decorations={false} className={cn(
className="gap-x-1 col-start-3 h-full row-start-1 col-span-2 row-span-1 grid grid-cols-[min-content_1fr] auto-rows-min overflow-y-auto" "gap-x-1 pt-1 h-full col-span-2 row-span-1 grid grid-cols-[min-content_1fr] auto-rows-min overflow-y-auto",
className,
)}
> >
{tflData.goodService.includes("Northern") && ( {tflData.goodService.includes("Northern") && (
<> <TFLDistruptionItem
<TFLDistruptionItem lineId="northern" reason="Good service" severity={StatusSeverity.GoodService} /> lineId={TubeLine.Northern}
<hr className="col-span-2 border-neutral-700" /> reason="Good service"
</> severity={StatusSeverity.GoodService}
/>
)} )}
{tflData.disruptions.map((disruption, i) => ( {tflData.disruptions.map((disruption) => (
<Fragment key={disruption.lineId}> <TFLDistruptionItem
<TFLDistruptionItem key={disruption.lineId}
lineId={disruption.lineId} lineId={disruption.lineId}
reason={disruption.reason ?? "Unknown reason"} reason={disruption.reason ?? "Unknown reason"}
severity={disruption.statusSeverity} severity={disruption.statusSeverity}
/> />
{i < tflData.disruptions.length - 1 && <hr className="col-span-2 border-neutral-700" />}
</Fragment>
))} ))}
</Tile> </Tile>
) )
} }
function TFLDistruptionItem({ lineId, reason, severity }: { lineId: string; reason: string; severity: number }) { function TFLDistruptionItem({ lineId, reason, severity }: { lineId: TubeLine; reason: string; severity: number }) {
const lineName = formatLineName(lineId) const lineName = formatLineName(lineId)
console.log(lineId)
let lineStyleClass: string let lineStyleClass: string
switch (lineId) { switch (lineId) {
case "bakerloo": case "bakerloo":
@@ -377,13 +389,18 @@ function TFLDistruptionItem({ lineId, reason, severity }: { lineId: string; reas
return ( return (
<> <>
<div className="h-full flex items-center justify-center px-2 py-0.5"> <div className="h-full flex items-center justify-center px-2 py-0.5">
<p className={cn("text-xl uppercase font-bold w-full text-center px-1 rounded-sm", lineStyleClass)}> <p
className={cn(
"text-neutral-200 text-xl uppercase font-bold w-full text-center px-1 rounded-lg",
lineStyleClass,
)}
>
{lineName} {lineName}
</p> </p>
</div> </div>
<p <p
className={cn( className={cn(
"text-xl text-wrap text-neutral-300 leading-tight self-center pr-2 py-1 font-light border-r-4", "text-xl text-wrap leading-tight self-center pr-2 py-1.5 font-light border-r-4",
statusBorderClass, statusBorderClass,
)} )}
> >
@@ -470,17 +487,14 @@ function SystemTile({
if (!beszelSystemsData) { if (!beszelSystemsData) {
return ( return (
<Tile className={cn("h-full row-start-2 flex flex-row justify-start items-center p-8", className)}> <Tile className={cn("h-full flex flex-row justify-start items-center p-8", className)}>
<p className="text-2xl font-light">No system status available</p> <p className="text-2xl font-light">No system status available</p>
</Tile> </Tile>
) )
} }
return ( return (
<Tile <Tile className={cn("h-full flex flex-col justify-start items-start", className)}>
decorations={false}
className={cn("h-full row-start-2 flex flex-col justify-start items-start", className)}
>
<div className="grid grid-cols-6 px-4 pt-3 w-full"> <div className="grid grid-cols-6 px-4 pt-3 w-full">
<div className="col-span-3 flex flex-row items-center space-x-2"> <div className="col-span-3 flex flex-row items-center space-x-2">
<p className="text-2xl">{displayName}</p> <p className="text-2xl">{displayName}</p>

View File

@@ -9,7 +9,7 @@ const API_BASE_URL = import.meta.env.VITE_API_URL || "http://localhost:8000"
// Disruption Summary // Disruption Summary
export interface DisruptionSummary { export interface DisruptionSummary {
lineId: string lineId: TubeLine
lineName: string lineName: string
mode: string mode: string
status: string status: string
@@ -53,6 +53,27 @@ export enum StatusSeverity {
ServiceClosed = 20, ServiceClosed = 20,
} }
export enum TubeLine {
Bakerloo = "bakerloo",
Central = "central",
Circle = "circle",
District = "district",
HammersmithCity = "hammersmith-city",
Jubilee = "jubilee",
Metropolitan = "metropolitan",
Northern = "northern",
Piccadilly = "piccadilly",
Victoria = "victoria",
WaterlooCity = "waterloo-city",
LondonOverground = "london-overground",
DLR = "dlr",
Elizabeth = "elizabeth",
Tram = "tram",
Lioness = "lioness",
Windrush = "windrush",
Mildmay = "mildmay",
}
// Helper function to get severity color // Helper function to get severity color
export function getSeverityColor(severity: number): string { export function getSeverityColor(severity: number): string {
if (severity >= 10) return "green" // Good Service if (severity >= 10) return "green" // Good Service
@@ -90,25 +111,25 @@ export function getSeverityLabel(severity: number): string {
} }
// Helper function to format line name for display // Helper function to format line name for display
export function formatLineName(lineId: string): string { export function formatLineName(line: TubeLine): string {
const lineNames: Record<string, string> = { const lineNames: Record<TubeLine, string> = {
bakerloo: "Bakerloo", [TubeLine.Bakerloo]: "Bakerloo",
central: "Central", [TubeLine.Central]: "Central",
circle: "Circle", [TubeLine.Circle]: "Circle",
district: "District", [TubeLine.District]: "District",
"hammersmith-city": "H&C", [TubeLine.HammersmithCity]: "H&C",
jubilee: "Jubilee", [TubeLine.Jubilee]: "Jubilee",
metropolitan: "Metropolitan", [TubeLine.Metropolitan]: "Met",
northern: "Northern", [TubeLine.Northern]: "Northern",
piccadilly: "Piccadilly", [TubeLine.Piccadilly]: "Piccadilly",
victoria: "Victoria", [TubeLine.Victoria]: "Victoria",
"waterloo-city": "W&C", [TubeLine.WaterlooCity]: "W&C",
"london-overground": "London Overground", [TubeLine.LondonOverground]: "London Overground",
dlr: "DLR", [TubeLine.DLR]: "DLR",
"elizabeth-line": "Elizabeth Line", [TubeLine.Elizabeth]: "Lizzie",
tram: "Tram", [TubeLine.Tram]: "Tram",
} }
return lineNames[lineId] || lineId return lineNames[line] || line
} }
/** /**