flightcn
blockflight-ops-overviewsource: ok · hit
Flight Ops Overview
Operations dashboard with live KPI cards, a multi-flight tracking map, and an en-route sidebar that focuses flights on selection.
npx shadcn add "https://flightcn.yencheng.dev/r/flight-ops-overview.json"free
Where it goes
statssidebardashboard-shellkanban
Interaction
animated-counterdropdown
Use cases
dashboard
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lighticonslucide-react
Registry dependencies
@flightcn/flighthttps://mapcn.dev/r/map.json
Files1 · source cached 10/9/2026, 2:39:39 AM
src/registry/blocks/flight-ops-overview/flight-ops-overview.tsx
"use client";
import { Gauge, Plane, PlaneTakeoff, Timer } from "lucide-react";
import { useEffect, useState } from "react";
import { Map } from "@/components/ui/map";
import {
FlightTracker,
getAirportInfo,
SingleWorldZoomLimit,
} from "@/components/ui/flight";
type OpsFlight = {
number: string;
from: string;
to: string;
/** Initial progress from 0 to 1. */
progress: number;
/** Progress advanced per simulation tick. */
rate: number;
delayed?: boolean;
};
const INITIAL_FLIGHTS: readonly OpsFlight[] = [
{ number: "FC 128", from: "TPE", to: "LAX", progress: 0.42, rate: 0.0005 },
{ number: "FC 204", from: "LHR", to: "JFK", progress: 0.63, rate: 0.0007 },
{ number: "FC 317", from: "SIN", to: "SYD", progress: 0.18, rate: 0.0006 },
{
number: "FC 452",
from: "DXB",
to: "CDG",
progress: 0.51,
rate: 0.0007,
delayed: true,
},
{ number: "FC 583", from: "GRU", to: "AMS", progress: 0.77, rate: 0.0005 },
];
function usePrefersReducedMotion() {
const [reduced, setReduced] = useState(false);
useEffect(() => {
const query = window.matchMedia("(prefers-reduced-motion: reduce)");
const update = () => setReduced(query.matches);
update();
query.addEventListener("change", update);
return () => query.removeEventListener("change", update);
}, []);
return reduced;
}
export function FlightOpsOverview() {
const reducedMotion = usePrefersReducedMotion();
const [flights, setFlights] = useState(INITIAL_FLIGHTS);
const [selected, setSelected] = useState(INITIAL_FLIGHTS[0].number);
useEffect(() => {
if (reducedMotion) return;
const timer = window.setInterval(() => {
setFlights((current) =>
current.map((flight) => ({
...flight,
progress: (flight.progress + flight.rate) % 1,
})),
);
}, 50);
return () => window.clearInterval(timer);
}, [reducedMotion]);
const delayedCount = flights.filter((flight) => flight.delayed).length;
const onTimeRate = Math.round(
((flights.length - delayedCount) / flights.length) * 100,
);
const kpis = [
{ label: "Active flights", value: String(flights.length), icon: Plane },
{ label: "On-time rate", value: `${onTimeRate}%`, icon: Timer },
{ label: "Delayed", value: String(delayedCount), icon: Gauge },
{ label: "Departures 24h", value: "38", icon: PlaneTakeoff },
] as const;
return (
<div className="flex min-h-[480px] w-full min-w-0 flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white">
<dl className="grid grid-cols-2 gap-px border-b border-slate-200 bg-slate-200 lg:grid-cols-4">
{kpis.map((kpi) => (
<div key={kpi.label} className="bg-white p-4">
<dt className="flex items-center gap-1.5 text-[10px] tracking-widest text-slate-500 uppercase">
<kpi.icon size={12} aria-hidden="true" /> {kpi.label}
</dt>
<dd className="mt-1 font-mono text-xl text-slate-950">
{kpi.value}
</dd>
</div>
))}
</dl>
<div className="grid grid-cols-1 min-h-0 flex-1 lg:grid-cols-[1fr_300px]">
<div className="relative min-h-[320px]">
<Map
theme="light"
center={[20, 25]}
zoom={1.2}
scrollZoom={false}
renderWorldCopies
className="h-full w-full"
>
<SingleWorldZoomLimit />
{flights.map((flight) => {
const isActive = flight.number === selected;
return (
<FlightTracker
key={flight.number}
from={flight.from}
to={flight.to}
progress={flight.progress}
completedColor={isActive ? "#c65d24" : "#8a8f86"}
remainingColor="#cbd5e1"
width={isActive ? 3 : 2}
iconSize={isActive ? 26 : 18}
showAirports={isActive}
showInfo={isActive}
>
<span className="font-mono text-[10px]">{flight.number}</span>
</FlightTracker>
);
})}
</Map>
</div>
<aside className="flex min-h-0 flex-col border-t border-slate-200 lg:border-t-0 lg:border-l">
<div className="border-b border-slate-200 bg-slate-50 px-5 py-3">
<p className="font-mono text-[10px] tracking-widest text-slate-500">
EN ROUTE
</p>
</div>
<ul className="min-h-0 flex-1 overflow-y-auto p-2">
{flights.map((flight) => {
const isActive = flight.number === selected;
const from = getAirportInfo(flight.from);
const to = getAirportInfo(flight.to);
return (
<li key={flight.number}>
<button
type="button"
onClick={() => setSelected(flight.number)}
aria-pressed={isActive}
className={`w-full rounded-lg px-3 py-2.5 text-left hover:bg-slate-100 ${
isActive ? "bg-orange-50" : ""
}`}
>
<span className="flex items-baseline justify-between gap-2">
<span
className={`font-mono text-sm font-medium ${
isActive ? "text-orange-800" : "text-slate-950"
}`}
>
{flight.number}
</span>
<span className="font-mono text-[10px] text-slate-500">
{flight.delayed ? "DELAYED" : "ON TIME"}
</span>
</span>
<span className="mt-0.5 block truncate text-xs text-slate-500">
{from?.city} → {to?.city}
</span>
<span
role="progressbar"
aria-label={`${flight.number} progress`}
aria-valuenow={Math.round(flight.progress * 100)}
aria-valuemin={0}
aria-valuemax={100}
className="mt-2 block h-1 overflow-hidden rounded-full bg-slate-200"
>
<span
className={`block h-full rounded-full ${
isActive ? "bg-orange-600" : "bg-slate-400"
}`}
style={{ width: `${flight.progress * 100}%` }}
/>
</span>
</button>
</li>
);
})}
</ul>
<p className="border-t border-slate-200 px-4 py-3 font-mono text-[10px] text-slate-500">
{flights.length} FLIGHTS TRACKED LIVE
</p>
</aside>
</div>
</div>
);
}