feat(dashboard): add TfL types and helper functions
- Add TypeScript types for TfL disruptions API - Add TanStack Query integration with 2-minute stale time - Add helper functions for severity colors and labels - Add line color mapping using official TfL colors as Tailwind classes - Add status severity to border color mapping for visual indicators Co-authored-by: Ona <no-reply@ona.com>
This commit is contained in:
217
apps/dashboard/src/tfl.ts
Normal file
217
apps/dashboard/src/tfl.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* TfL (Transport for London) API TypeScript Types
|
||||
* For London transport status and disruptions
|
||||
*/
|
||||
|
||||
import { queryOptions } from "@tanstack/react-query"
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_URL || "http://localhost:8000"
|
||||
|
||||
// Disruption Summary
|
||||
export interface DisruptionSummary {
|
||||
lineId: string
|
||||
lineName: string
|
||||
mode: string
|
||||
status: string
|
||||
statusSeverity: number
|
||||
reason?: string
|
||||
validFrom?: string
|
||||
validTo?: string
|
||||
}
|
||||
|
||||
// Disruptions Response
|
||||
export interface DisruptionsResponse {
|
||||
lastUpdated: string
|
||||
disruptions: DisruptionSummary[]
|
||||
goodService: string[]
|
||||
totalLines: number
|
||||
disruptedLines: number
|
||||
}
|
||||
|
||||
// Status severity levels
|
||||
export enum StatusSeverity {
|
||||
SpecialService = 0,
|
||||
Closed = 1,
|
||||
Suspended = 2,
|
||||
PartSuspended = 3,
|
||||
PlannedClosure = 4,
|
||||
PartClosure = 5,
|
||||
SevereDelays = 6,
|
||||
ReducedService = 7,
|
||||
BusService = 8,
|
||||
MinorDelays = 9,
|
||||
GoodService = 10,
|
||||
PartClosed = 11,
|
||||
ExitOnly = 12,
|
||||
NoStepFreeAccess = 13,
|
||||
ChangeOfFrequency = 14,
|
||||
Diverted = 15,
|
||||
NotRunning = 16,
|
||||
IssuesReported = 17,
|
||||
NoIssues = 18,
|
||||
Information = 19,
|
||||
ServiceClosed = 20,
|
||||
}
|
||||
|
||||
// Helper function to get severity color
|
||||
export function getSeverityColor(severity: number): string {
|
||||
if (severity >= 10) return "green" // Good Service
|
||||
if (severity >= 9) return "orange" // Minor Delays
|
||||
if (severity >= 6) return "red" // Severe Delays or worse
|
||||
return "darkred" // Suspended/Closed
|
||||
}
|
||||
|
||||
// Helper function to get severity label
|
||||
export function getSeverityLabel(severity: number): string {
|
||||
switch (severity) {
|
||||
case 10:
|
||||
return "Good Service"
|
||||
case 9:
|
||||
return "Minor Delays"
|
||||
case 8:
|
||||
return "Bus Service"
|
||||
case 7:
|
||||
return "Reduced Service"
|
||||
case 6:
|
||||
return "Severe Delays"
|
||||
case 5:
|
||||
return "Part Closure"
|
||||
case 4:
|
||||
return "Planned Closure"
|
||||
case 3:
|
||||
return "Part Suspended"
|
||||
case 2:
|
||||
return "Suspended"
|
||||
case 1:
|
||||
return "Closed"
|
||||
default:
|
||||
return "Special Service"
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to format line name for display
|
||||
export function formatLineName(lineId: string): string {
|
||||
const lineNames: Record<string, string> = {
|
||||
bakerloo: "Bakerloo",
|
||||
central: "Central",
|
||||
circle: "Circle",
|
||||
district: "District",
|
||||
"hammersmith-city": "Hammersmith & City",
|
||||
jubilee: "Jubilee",
|
||||
metropolitan: "Metropolitan",
|
||||
northern: "Northern",
|
||||
piccadilly: "Piccadilly",
|
||||
victoria: "Victoria",
|
||||
"waterloo-city": "Waterloo & City",
|
||||
"london-overground": "London Overground",
|
||||
dlr: "DLR",
|
||||
"elizabeth-line": "Elizabeth Line",
|
||||
tram: "Tram",
|
||||
}
|
||||
return lineNames[lineId] || lineId
|
||||
}
|
||||
|
||||
// Map of tube lines to their official TfL colors (as Tailwind classes)
|
||||
export function getLineColor(lineId: string): string {
|
||||
const lineColors: Record<string, string> = {
|
||||
bakerloo: "bg-amber-700",
|
||||
central: "bg-red-600",
|
||||
circle: "bg-yellow-400",
|
||||
district: "bg-green-600",
|
||||
"hammersmith-city": "bg-pink-400",
|
||||
jubilee: "bg-slate-500",
|
||||
metropolitan: "bg-purple-800",
|
||||
northern: "bg-black",
|
||||
piccadilly: "bg-blue-900",
|
||||
victoria: "bg-sky-500",
|
||||
"waterloo-city": "bg-teal-500",
|
||||
"london-overground": "bg-orange-500",
|
||||
dlr: "bg-teal-600",
|
||||
"elizabeth-line": "bg-purple-600",
|
||||
tram: "bg-green-500",
|
||||
}
|
||||
return lineColors[lineId] || "bg-gray-500"
|
||||
}
|
||||
|
||||
// Map of status severity to border colors
|
||||
export function getStatusBorderColor(severity: number): string {
|
||||
const borderColors: Record<number, string> = {
|
||||
[StatusSeverity.SpecialService]: "border-gray-500",
|
||||
[StatusSeverity.Closed]: "border-red-700",
|
||||
[StatusSeverity.Suspended]: "border-red-600",
|
||||
[StatusSeverity.PartSuspended]: "border-red-500",
|
||||
[StatusSeverity.PlannedClosure]: "border-orange-600",
|
||||
[StatusSeverity.PartClosure]: "border-yellow-500",
|
||||
[StatusSeverity.SevereDelays]: "border-red-500",
|
||||
[StatusSeverity.ReducedService]: "border-orange-500",
|
||||
[StatusSeverity.BusService]: "border-blue-500",
|
||||
[StatusSeverity.MinorDelays]: "border-yellow-500",
|
||||
[StatusSeverity.GoodService]: "border-green-500",
|
||||
[StatusSeverity.PartClosed]: "border-orange-600",
|
||||
[StatusSeverity.ExitOnly]: "border-gray-600",
|
||||
[StatusSeverity.NoStepFreeAccess]: "border-gray-500",
|
||||
[StatusSeverity.ChangeOfFrequency]: "border-blue-400",
|
||||
[StatusSeverity.Diverted]: "border-purple-500",
|
||||
[StatusSeverity.NotRunning]: "border-red-600",
|
||||
[StatusSeverity.IssuesReported]: "border-yellow-400",
|
||||
[StatusSeverity.NoIssues]: "border-green-500",
|
||||
[StatusSeverity.Information]: "border-blue-400",
|
||||
[StatusSeverity.ServiceClosed]: "border-red-700",
|
||||
}
|
||||
return borderColors[severity] || "border-gray-400"
|
||||
}
|
||||
|
||||
// Helper function to check if there are any disruptions
|
||||
export function hasDisruptions(data: DisruptionsResponse): boolean {
|
||||
return data.disruptedLines > 0
|
||||
}
|
||||
|
||||
// Helper function to get critical disruptions (severe or worse)
|
||||
export function getCriticalDisruptions(data: DisruptionsResponse): DisruptionSummary[] {
|
||||
return data.disruptions.filter((d) => d.statusSeverity <= 6)
|
||||
}
|
||||
|
||||
// TanStack Query Options
|
||||
|
||||
/**
|
||||
* Query options for fetching current TfL disruptions
|
||||
* Returns disruptions across Tube, Overground, DLR, Elizabeth Line, and Tram
|
||||
*/
|
||||
export function tflDisruptionsQuery() {
|
||||
return queryOptions({
|
||||
queryKey: ["tfl", "disruptions"],
|
||||
queryFn: async (): Promise<DisruptionsResponse> => {
|
||||
const response = await fetch(`${API_BASE_URL}/api/tfl/disruptions`)
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch TfL disruptions")
|
||||
}
|
||||
return response.json()
|
||||
},
|
||||
select: (data) =>
|
||||
data.disruptions.sort((a, b) => {
|
||||
if (a.lineName.match(/northern/i)) return -1
|
||||
return a.statusSeverity - b.statusSeverity
|
||||
}),
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes (TfL updates frequently)
|
||||
gcTime: 5 * 60 * 1000, // 5 minutes
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Query options for fetching status of specific line(s)
|
||||
* @param lineIds - Comma-separated line IDs (e.g., "central,northern")
|
||||
*/
|
||||
export function tflLineStatusQuery(lineIds: string) {
|
||||
return queryOptions({
|
||||
queryKey: ["tfl", "line", lineIds],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(`${API_BASE_URL}/api/tfl/line/${lineIds}`)
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to fetch TfL line status")
|
||||
}
|
||||
return response.json()
|
||||
},
|
||||
staleTime: 2 * 60 * 1000, // 2 minutes
|
||||
gcTime: 5 * 60 * 1000, // 5 minutes
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user