From 9b828bd7cfd3d924951f9cc6c6741462dee295a9 Mon Sep 17 00:00:00 2001 From: kenneth Date: Fri, 24 Oct 2025 23:03:50 +0000 Subject: [PATCH] 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 --- apps/dashboard/src/tfl.ts | 217 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 apps/dashboard/src/tfl.ts diff --git a/apps/dashboard/src/tfl.ts b/apps/dashboard/src/tfl.ts new file mode 100644 index 0000000..f99b20f --- /dev/null +++ b/apps/dashboard/src/tfl.ts @@ -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 = { + 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 = { + 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 = { + [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 => { + 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 + }) +}