feat(dashboard): add TfL disruptions tile to dashboard

- Add TFLTile component displaying real-time transport disruptions
- Show Northern line status prominently when good service
- Display disruptions sorted by severity with line colors
- Add optional tile decorations prop for flexible styling
- Auto-refresh every 5 minutes with background updates

Co-authored-by: Ona <no-reply@ona.com>
This commit is contained in:
2025-10-24 23:03:55 +00:00
parent 9b828bd7cf
commit f884982766

View File

@@ -1,6 +1,7 @@
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import cn from "./components/lib/cn" import cn from "./components/lib/cn"
import { StatusSeverity, getLineColor, getStatusBorderColor, tflDisruptionsQuery } from "./tfl"
import { import {
DEFAULT_LATITUDE, DEFAULT_LATITUDE,
DEFAULT_LONGITUDE, DEFAULT_LONGITUDE,
@@ -15,17 +16,26 @@ function App() {
<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-black gap-4 text-neutral-200 grid grid-cols-4 grid-rows-5 p-4">
<DateTimeTile /> <DateTimeTile />
<WeatherTile /> <WeatherTile />
<TFLTile />
</div> </div>
) )
} }
function Tile({ children, className }: { children: React.ReactNode; className?: string }) { function Tile({
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 className={cn("relative bg-neutral-900 flex flex-col justify-end items-start", className)}>
{decorations && (
<>
<div className="absolute top-0 left-0 w-4 h-[1px] bg-neutral-200" /> <div className="absolute top-0 left-0 w-4 h-[1px] bg-neutral-200" />
<div className="absolute top-0 left-0 w-[1px] h-4 bg-neutral-200" /> <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-4 h-[1px] bg-neutral-200" />
<div className="absolute bottom-0 right-0 w-[1px] h-4 bg-neutral-200" /> <div className="absolute bottom-0 right-0 w-[1px] h-4 bg-neutral-200" />
</>
)}
{children} {children}
</div> </div>
) )
@@ -195,4 +205,90 @@ function WeatherTile() {
) )
} }
function TFLTile() {
const {
data: tflData,
isLoading: isLoadingTFL,
error: errorTFL,
} = useQuery({
...tflDisruptionsQuery(),
select: (data) => {
data.disruptions.sort((a, b) => {
if (a.lineName.match(/northern/i)) return -1
return a.statusSeverity - b.statusSeverity
})
return data
},
refetchInterval: 5 * 60 * 1000, // 5 minutes
refetchIntervalInBackground: true,
})
if (isLoadingTFL) {
return (
<Tile className="col-start-3 h-full row-start-1 col-span-2 row-span-2 flex flex-row justify-start items-center p-8">
<p className="text-2xl font-light animate-pulse">Loading TfL</p>
</Tile>
)
}
return (
<Tile
decorations={false}
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"
>
{tflData?.goodService.includes("Northern") && (
<TFLDistruptionItem
lineId="northern"
lineName="Northern"
reason="Good service"
severity={StatusSeverity.GoodService}
/>
)}
{tflData?.disruptions.map((disruption) => (
<>
<TFLDistruptionItem
key={disruption.lineId}
lineId={disruption.lineId}
lineName={disruption.lineName}
reason={disruption.reason ?? "Unknown reason"}
severity={disruption.statusSeverity}
/>
<hr className="col-span-2 border-neutral-700" />
</>
))}
</Tile>
)
}
function TFLDistruptionItem({
lineId,
lineName,
reason,
severity,
}: { lineId: string; lineName: string; reason: string; severity: number }) {
return (
<>
<div className="h-full flex items-center justify-center px-2 py-0.5">
<p
className={cn(
"text-xl uppercase font-bold bg-blue-500 w-full text-center px-1 rounded-sm",
getLineColor(lineId),
getStatusBorderColor(severity),
)}
>
{lineName}
</p>
</div>
<p
className={cn(
"text-xl text-wrap text-neutral-300 leading-tight self-center pr-2 py-1 font-light border-r-4",
getStatusBorderColor(severity),
)}
>
{reason}
</p>
</>
)
}
export default App export default App