flightcn
blockairport-search-mapsource: ok · refreshed
Airport Search Map
Searchable panel over the built-in 500+ airport database with fly-to markers on selection.
npx shadcn add "https://flightcn.yencheng.dev/r/airport-search-map.json"free
Interaction
dropdown
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/8/2026, 6:18:09 AM
src/registry/blocks/airport-search-map/airport-search-map.tsx
"use client";
import { MapPin, Search } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { Map, useMap } from "@/components/ui/map";
import {
FlightAirport,
airports,
type AirportInfo,
} from "@/components/ui/flight";
const FEATURED_CODES = ["TPE", "HND", "ICN", "SIN", "LHR", "JFK", "SFO", "SYD"];
const MAX_RESULTS = 30;
const ALL_AIRPORTS: readonly AirportInfo[] = Object.values(airports);
function searchAirports(query: string): AirportInfo[] {
const term = query.trim().toLowerCase();
if (!term) {
return FEATURED_CODES.map((code) => airports[code]).filter(Boolean);
}
const matches: { airport: AirportInfo; score: number }[] = [];
for (const airport of ALL_AIRPORTS) {
const code = airport.code.toLowerCase();
const haystack =
`${airport.city} ${airport.country} ${airport.name}`.toLowerCase();
let score = -1;
if (code === term) score = 0;
else if (code.startsWith(term)) score = 1;
else if (haystack.includes(term)) score = 2;
if (score >= 0) matches.push({ airport, score });
}
return matches
.sort(
(a, b) =>
a.score - b.score || a.airport.code.localeCompare(b.airport.code),
)
.slice(0, MAX_RESULTS)
.map((match) => match.airport);
}
function AirportCamera({ airport }: { airport: AirportInfo | null }) {
const { map, isLoaded } = useMap();
useEffect(() => {
if (!map || !isLoaded || !airport) return;
map.flyTo({
center: [airport.longitude, airport.latitude],
zoom: 9,
duration: 1200,
});
}, [map, isLoaded, airport]);
return null;
}
export function AirportSearchMap() {
const [query, setQuery] = useState("");
const [selected, setSelected] = useState<AirportInfo | null>(
airports.TPE ?? null,
);
const results = useMemo(() => searchAirports(query), [query]);
return (
<div className="grid min-h-[480px] w-full min-w-0 grid-cols-1 overflow-hidden rounded-2xl border border-slate-200 bg-white lg:grid-cols-[300px_1fr]">
<aside className="flex min-h-0 flex-col border-b border-slate-200 lg:border-r lg:border-b-0">
<div className="border-b border-slate-200 bg-slate-50 px-5 py-3">
<p className="flex items-center gap-2 font-mono text-[10px] tracking-widest text-slate-500">
<MapPin size={13} aria-hidden="true" /> AIRPORT DIRECTORY
</p>
</div>
<div className="border-b border-slate-200 p-3">
<label className="flex items-center gap-2 rounded-lg border border-slate-200 bg-slate-50 px-3 py-2.5 focus-within:ring-2 focus-within:ring-orange-600">
<Search size={15} className="shrink-0 text-slate-500" />
<input
aria-label="Search airports"
placeholder="IATA code, city or country"
value={query}
onChange={(event) => setQuery(event.target.value)}
autoComplete="off"
spellCheck={false}
className="w-full min-w-0 bg-transparent text-sm outline-none"
/>
</label>
</div>
<ul
aria-label="Airport results"
className="max-h-56 min-h-0 flex-1 overflow-y-auto p-2 lg:max-h-none"
>
{results.map((airport) => (
<li key={airport.code}>
<button
type="button"
onClick={() => setSelected(airport)}
aria-pressed={selected?.code === airport.code}
className={`flex w-full items-baseline gap-3 rounded-lg px-3 py-2 text-left text-sm hover:bg-slate-100 ${
selected?.code === airport.code
? "bg-orange-50 text-orange-800"
: "text-slate-700"
}`}
>
<span className="w-10 shrink-0 font-mono font-medium">
{airport.code}
</span>
<span className="min-w-0 flex-1">
<span className="block truncate">{airport.city}</span>
<span className="block truncate text-xs text-slate-500">
{airport.name}
</span>
</span>
</button>
</li>
))}
{results.length === 0 ? (
<li className="px-3 py-6 text-center text-sm text-slate-500">
No airports match “{query.trim()}”.
</li>
) : null}
</ul>
{selected ? (
<p className="border-t border-slate-200 px-4 py-3 font-mono text-[10px] text-slate-500">
{selected.code} · {selected.latitude.toFixed(2)},{" "}
{selected.longitude.toFixed(2)} · {selected.country}
</p>
) : null}
</aside>
<div className="relative min-h-[320px]">
<Map
theme="light"
center={[121.23, 25.08]}
zoom={6}
className="h-full w-full"
>
<AirportCamera airport={selected} />
{selected ? (
<FlightAirport key={selected.code} code={selected.code} showLabel />
) : null}
</Map>
</div>
</div>
);
}