134 lines
4.2 KiB
TypeScript
134 lines
4.2 KiB
TypeScript
import { JSX } from "react";
|
|
|
|
// --- TYPES ---
|
|
interface TfLStatusReason {
|
|
statusSeverityDescription: string;
|
|
reason?: string;
|
|
}
|
|
|
|
interface TfLLineStatus {
|
|
id: string;
|
|
name: string;
|
|
lineStatuses: TfLStatusReason[];
|
|
}
|
|
|
|
interface OpenMeteoWeather {
|
|
current: {
|
|
temperature_2m: number;
|
|
weather_code: number;
|
|
};
|
|
current_units: {
|
|
temperature_2m: string;
|
|
};
|
|
}
|
|
|
|
// --- FETCH FUNCTIONS ---
|
|
async function getTfLTransitStatus(): Promise<TfLLineStatus[]> {
|
|
// Public endpoint - No API key needed
|
|
const res = await fetch(
|
|
"https://api.tfl.gov.uk/line/mode/tube,elizabeth-line,dlr/status",
|
|
{
|
|
next: { revalidate: 300 }, // Cache for 5 minutes
|
|
},
|
|
);
|
|
|
|
if (!res.ok) throw new Error("Failed to fetch TfL line statuses");
|
|
return res.json();
|
|
}
|
|
|
|
async function getLondonWeather(): Promise<OpenMeteoWeather> {
|
|
// Public endpoint - No API key needed
|
|
const res = await fetch(
|
|
"https://api.open-meteo.com/v1/forecast?latitude=51.5074&longitude=-0.1278¤t=temperature_2m,weather_code",
|
|
{ next: { revalidate: 300 } },
|
|
);
|
|
|
|
if (!res.ok) throw new Error("Failed to fetch London weather");
|
|
return res.json();
|
|
}
|
|
|
|
// Helper: Badge color depending on TfL status
|
|
function getStatusBadge(statusText: string): string {
|
|
if (statusText === "Good Service") return "badge-success";
|
|
if (statusText.includes("Minor") || statusText.includes("Part"))
|
|
return "badge-warning";
|
|
return "badge-error";
|
|
}
|
|
|
|
// --- SERVER COMPONENT ---
|
|
export default async function TrafficAndWeatherPage(): Promise<JSX.Element> {
|
|
// PARALLEL FETCHING: Triggers both external network calls simultaneously
|
|
const [transitData, weatherData] = await Promise.all([
|
|
getTfLTransitStatus(),
|
|
getLondonWeather(),
|
|
]);
|
|
|
|
return (
|
|
<main className="min-h-screen bg-base-200 p-8">
|
|
<div className="max-w-5xl mx-auto space-y-8">
|
|
{/* Header */}
|
|
<header className="flex justify-between items-end border-b border-base-300 pb-4">
|
|
<div>
|
|
<h1 className="text-3xl font-bold">London Transit & Weather</h1>
|
|
<p className="text-sm text-base-content/60">
|
|
Fetched in parallel on the server via{" "}
|
|
<code className="badge badge-outline font-mono">
|
|
Promise.all()
|
|
</code>
|
|
</p>
|
|
</div>
|
|
|
|
{/* Quick Weather Stat Card */}
|
|
<div className="stat bg-base-100 shadow rounded-box w-auto py-2 px-4">
|
|
<div className="stat-title text-xs">London Temp</div>
|
|
<div className="stat-value text-xl text-primary">
|
|
{weatherData.current.temperature_2m}
|
|
{weatherData.current_units.temperature_2m}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* TfL Tube & Rail Status Grid */}
|
|
<section className="space-y-4">
|
|
<h2 className="text-xl font-semibold flex items-center gap-2">
|
|
<span>🚇</span> Real-Time Transit Status (TfL Live)
|
|
</h2>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{transitData.map((line) => {
|
|
const currentStatus = line.lineStatuses[0];
|
|
const statusDesc =
|
|
currentStatus?.statusSeverityDescription || "Unknown";
|
|
const disruptionDetail = currentStatus?.reason;
|
|
|
|
return (
|
|
<div
|
|
key={line.id}
|
|
className="card bg-base-100 shadow-sm border border-base-300"
|
|
>
|
|
<div className="card-body p-4">
|
|
<div className="flex justify-between items-center">
|
|
<h3 className="font-bold text-base">{line.name}</h3>
|
|
<span
|
|
className={`badge ${getStatusBadge(statusDesc)} text-xs font-semibold`}
|
|
>
|
|
{statusDesc}
|
|
</span>
|
|
</div>
|
|
|
|
{disruptionDetail && (
|
|
<p className="text-xs text-base-content/70 mt-2 line-clamp-2">
|
|
{disruptionDetail}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|