"use client"; import { useEffect, useId, useMemo, useState } from "react"; import { COUNTRIES, CONTINENTS, MAP_HEIGHT, MAP_WIDTH, projectPoint, } from "@/lib/world-map"; /** * World map with real country borders and a live flight network out of Dhaka. * * Country outlines are pregenerated SVG paths (Natural Earth 110m, Miller * projection) so nothing is projected at runtime. Bangladesh is tinted as the * home base and every route is a spoke from DAC; hovering any country lifts its * whole continent. Aircraft ride their arcs with SVG animateMotion and the * trails run on CSS keyframes, so the loop stays off the main thread. */ const CONTINENT_TINT: Record = { AF: "#f7b267", AS: "#f4845f", EU: "#4cc9f0", NA: "#7bdff2", SA: "#c77dff", OC: "#80ed99", }; type Airport = { code: string; city: string; lon: number; lat: number; /** Nudge the label when two airports sit on top of each other. */ labelDy?: number; }; /** Every route flies out of Dhaka — the map is a hub-and-spoke, not a mesh. */ const ORIGIN: Airport = { code: "DAC", city: "Dhaka", lon: 90.4, lat: 23.84, }; /** The country tinted permanently as the home base. */ const ORIGIN_COUNTRY = "BD"; const AIRPORTS: Record = { JFK: { code: "JFK", city: "New York", lon: -73.78, lat: 40.64 }, LHR: { code: "LHR", city: "London", lon: -0.46, lat: 51.47 }, DXB: { code: "DXB", city: "Dubai", lon: 55.36, lat: 25.25 }, SIN: { code: "SIN", city: "Singapore", lon: 103.99, lat: 1.36, labelDy: 12 }, HND: { code: "HND", city: "Tokyo", lon: 139.78, lat: 35.55 }, GRU: { code: "GRU", city: "São Paulo", lon: -46.47, lat: -23.43 }, LOS: { code: "LOS", city: "Lagos", lon: 3.32, lat: 6.58 }, SYD: { code: "SYD", city: "Sydney", lon: 151.18, lat: -33.94 }, FRA: { code: "FRA", city: "Frankfurt", lon: 8.57, lat: 50.03 }, SFO: { code: "SFO", city: "San Francisco", lon: -122.38, lat: 37.62 }, NBO: { code: "NBO", city: "Nairobi", lon: 36.93, lat: -1.32 }, JNB: { code: "JNB", city: "Johannesburg", lon: 28.24, lat: -26.13 }, YYZ: { code: "YYZ", city: "Toronto", lon: -79.63, lat: 43.68 }, IST: { code: "IST", city: "Istanbul", lon: 28.75, lat: 41.26 }, JED: { code: "JED", city: "Jeddah", lon: 39.16, lat: 21.68 }, KUL: { code: "KUL", city: "Kuala Lumpur", lon: 101.71, lat: 2.75 }, CAN: { code: "CAN", city: "Guangzhou", lon: 113.3, lat: 23.39 }, ICN: { code: "ICN", city: "Seoul", lon: 126.45, lat: 37.46 }, }; /** [destination, seconds per trip, start delay] — all depart Dhaka */ const ROUTES: [string, number, number][] = [ ["LHR", 11, 0], ["JFK", 13, 1.4], ["DXB", 7.5, 2.6], ["SIN", 6.5, 0.7], ["HND", 8.5, 3.8], ["SYD", 10.5, 2.1], ["FRA", 10.5, 5.2], ["JED", 7, 4.3], ["KUL", 6, 1.9], ["CAN", 6, 6.1], ["ICN", 8, 3.1], ["IST", 9, 7.4], ["NBO", 8.5, 5.8], ["JNB", 10, 0.4], ["LOS", 10, 6.7], ["SFO", 14, 4.9], ["YYZ", 13, 8.2], ["GRU", 15, 2.9], ]; function flightPath(a: Airport, b: Airport) { const [x1, y1] = projectPoint(a.lon, a.lat); const [x2, y2] = projectPoint(b.lon, b.lat); const dx = x2 - x1; const dy = y2 - y1; const distance = Math.hypot(dx, dy) || 1; // Bow perpendicular to the route, deeper for longer hops — a route-map arc. const lift = Math.min(distance * 0.26, 120); const mx = (x1 + x2) / 2 - (dy / distance) * lift; const my = (y1 + y2) / 2 + (dx / distance) * lift; return `M${x1.toFixed(1)} ${y1.toFixed(1)}Q${mx.toFixed(1)} ${my.toFixed(1)} ${x2.toFixed(1)} ${y2.toFixed(1)}`; } function usePrefersReducedMotion() { const [reduced, setReduced] = useState(false); useEffect(() => { const query = window.matchMedia("(prefers-reduced-motion: reduce)"); const sync = () => setReduced(query.matches); sync(); query.addEventListener("change", sync); return () => query.removeEventListener("change", sync); }, []); return reduced; } export default function WorldFlightMap() { // useId gives every instance its own gradient and path ids const uid = useId().replace(/:/g, ""); const reduced = usePrefersReducedMotion(); const [hovered, setHovered] = useState<{ name: string; continent: string; x: number; y: number; } | null>(null); const flights = useMemo( () => ROUTES.map(([to, duration, delay], i) => ({ key: `${ORIGIN.code}-${to}`, pathId: `${uid}-route-${i}`, to: AIRPORTS[to], d: flightPath(ORIGIN, AIRPORTS[to]), duration, delay, })), [uid], ); const destinations = useMemo( () => ROUTES.map(([code]) => { const airport = AIRPORTS[code]; const [x, y] = projectPoint(airport.lon, airport.lat); return { ...airport, x, y }; }), [], ); const origin = useMemo(() => { const [x, y] = projectPoint(ORIGIN.lon, ORIGIN.lat); return { x, y }; }, []); return (
{/* Countries — every border is a path's own stroke */} {COUNTRIES.map((country) => { const active = hovered?.continent === country.continent; const home = country.id === ORIGIN_COUNTRY; const tint = CONTINENT_TINT[country.continent] ?? "#8b93a7"; return ( { const box = event.currentTarget.ownerSVGElement?.getBoundingClientRect(); if (!box) return; setHovered({ name: country.name, continent: country.continent, x: ((event.clientX - box.left) / box.width) * 100, y: ((event.clientY - box.top) / box.height) * 100, }); }} onMouseLeave={() => setHovered(null)} /> ); })} {/* Routes: a faint guide, a travelling trail, and the aircraft */} {flights.map((flight) => ( {/* pathLength=1 normalises the dash, so every route moves at its own tempo regardless of how long the arc actually is */} {!reduced && ( )} ))} {/* Destination airports */} {destinations.map((hub, i) => ( {hub.code} {hub.city} ({hub.code}) ))} {/* Dhaka — the hub every route departs from */} DHAKA · DAC Dhaka, Bangladesh (DAC) {/* Continent legend — dims to the hovered continent */}
{Object.entries(CONTINENTS).map(([code, name]) => ( {name} ))}
{hovered && ( {hovered.name} {CONTINENTS[hovered.continent] ?? hovered.continent} )}
); }