/* ============ Flight Lookup App ============ */ const HISTORY_KEY = "flight-lookup-history"; const THEME_KEY = "flight-lookup-theme"; const LANG_KEY = "flight-lookup-lang"; const MAX_HISTORY = 8; const DEBOUNCE_MS = 250; // ---- DOM refs ---- const airlineInput = document.getElementById("airline"); const flightInput = document.getElementById("flight-number"); const form = document.getElementById("lookup-form"); const suggestionsBox = document.getElementById("airline-suggestions"); const resultBox = document.getElementById("result"); const historyList = document.getElementById("history-list"); const historySection = document.getElementById("history-section"); const langSelect = document.getElementById("lang"); const themeToggle = document.getElementById("theme-toggle"); let lastFlight = null; let debounceTimer = null; let activeSuggestion = -1; let suggestionItems = []; // ============ Theme ============ function initTheme() { const saved = localStorage.getItem(THEME_KEY); const prefersLight = window.matchMedia("(prefers-color-scheme: light)").matches; const theme = saved || (prefersLight ? "light" : "dark"); document.documentElement.setAttribute("data-theme", theme); } function toggleTheme() { const current = document.documentElement.getAttribute("data-theme") || "dark"; const next = current === "dark" ? "light" : "dark"; document.documentElement.setAttribute("data-theme", next); localStorage.setItem(THEME_KEY, next); } // ============ Language ============ function initLang() { const saved = localStorage.getItem(LANG_KEY); if (saved && SUPPORTED_LANGS.includes(saved)) { setLang(saved); } else { setLang("en"); } langSelect.value = currentLang; } function onLangChange() { const lang = langSelect.value; localStorage.setItem(LANG_KEY, lang); setLang(lang); applyTranslations(); if (lastFlight) { renderResult(lastFlight); } } // ============ History ============ function getHistory() { try { return JSON.parse(localStorage.getItem(HISTORY_KEY)) || []; } catch { return []; } } function saveHistory(entry) { let history = getHistory(); history = history.filter((h) => h.code !== entry.code); history.unshift(entry); history = history.slice(0, MAX_HISTORY); localStorage.setItem(HISTORY_KEY, JSON.stringify(history)); renderHistory(); } function renderHistory() { const history = getHistory(); if (!history.length) { historySection.classList.add("hidden"); return; } historySection.classList.remove("hidden"); historyList.innerHTML = ""; for (const entry of history) { const li = document.createElement("li"); const btn = document.createElement("button"); btn.className = "history-item"; btn.type = "button"; const route = entry.from && entry.to ? ` ${entry.from} \u2192 ${entry.to}` : ""; btn.innerHTML = `${entry.code}${route}`; btn.addEventListener("click", () => { airlineInput.value = entry.airlineCode; flightInput.value = entry.flightNumber; doLookup(entry.airlineCode, entry.flightNumber); }); li.appendChild(btn); historyList.appendChild(li); } } // ============ Airline suggestions ============ function showSuggestions(query) { activeSuggestion = -1; if (!query || query.length < 1) { hideSuggestions(); return; } const matches = searchAirlines(query); if (!matches.length) { hideSuggestions(); return; } suggestionItems = matches; suggestionsBox.innerHTML = ""; matches.forEach((airline, i) => { const div = document.createElement("div"); div.className = "suggestion"; div.dataset.index = i; div.innerHTML = `${airline.code} ${airline.name}`; div.addEventListener("mousedown", (e) => { e.preventDefault(); selectSuggestion(airline); }); suggestionsBox.appendChild(div); }); suggestionsBox.classList.remove("hidden"); } function hideSuggestions() { suggestionsBox.classList.add("hidden"); suggestionItems = []; activeSuggestion = -1; } function selectSuggestion(airline) { airlineInput.value = airline.code; hideSuggestions(); flightInput.focus(); } function moveSuggestion(delta) { if (!suggestionItems.length) return; activeSuggestion = (activeSuggestion + delta + suggestionItems.length) % suggestionItems.length; const items = suggestionsBox.querySelectorAll(".suggestion"); items.forEach((el, i) => el.classList.toggle("active", i === activeSuggestion)); if (items[activeSuggestion]) { items[activeSuggestion].scrollIntoView({ block: "nearest" }); } } function commitSuggestion() { if (activeSuggestion >= 0 && suggestionItems[activeSuggestion]) { selectSuggestion(suggestionItems[activeSuggestion]); } } // ============ Lookup ============ function doLookup(airlineCode, flightNumber) { hideSuggestions(); showLoading(); lookupFlight(airlineCode, flightNumber) .then((flight) => { if (flight) { lastFlight = flight; renderResult(flight); saveHistory({ code: `${flight.airlineCode}${flight.flightNumber}`, airlineCode: flight.airlineCode, flightNumber: flight.flightNumber, from: flight.origin ? flight.origin.code : null, to: flight.destination ? flight.destination.code : null, }); } else { showError(t("noFlightFound", { flight: `${airlineCode} ${flightNumber}` })); } }) .catch(() => { showError(t("networkError")); }); } function showLoading() { resultBox.className = "result loading"; resultBox.innerHTML = `
${t("searching")}`; } function showError(message) { lastFlight = null; resultBox.className = "result error"; resultBox.textContent = message; } function renderResult(flight) { const statusText = t(flight.status) || flight.status; const shareUrl = `${location.origin}${location.pathname}?f=${flight.airlineCode}${flight.flightNumber}`; const isLive = flight.live && flight.position; let body; if (isLive) { body = renderLiveBody(flight); } else { body = renderMockBody(flight); } resultBox.className = "result" + (isLive ? " live" : ""); resultBox.innerHTML = `
${flight.airlineCode} ${flight.flightNumber} ${statusText} ${isLive ? `${t("live")}` : `${t("mock")}`}
${body}
${renderMap(flight)}
${flight.airline} ${t("copyLink")}
`; } function renderMockBody(flight) { const depTime = formatTime(new Date(flight.depTime)); const arrTime = formatTime(new Date(flight.arrTime)); const duration = formatDuration(Math.round((flight.arrTime - flight.depTime) / 60000)); return `
${flight.origin.code}
${flight.origin.city}
${depTime}
${flight.origin.tz}
${duration}
${flight.destination.code}
${flight.destination.city}
${arrTime}
${flight.destination.tz}
`; } function renderLiveBody(flight) { const p = flight.position; const seenAgo = formatAgo(flight.lastSeen); const rows = [ [t("altitude"), p.altitudeFt != null ? `${p.altitudeFt.toLocaleString()} ft` : "—"], [t("speed"), p.velocityKt != null ? `${p.velocityKt} kt` : "—"], [t("heading"), p.trackDeg != null ? `${p.trackDeg}°` : "—"], [t("vertRate"), p.vertRateFpm != null ? `${p.vertRateFpm > 0 ? "+" : ""}${p.vertRateFpm} fpm` : "—"], [t("position"), `${p.lat.toFixed(3)}, ${p.lon.toFixed(3)}`], [t("lastSeen"), seenAgo], ]; return `
${rows .map( ([k, v]) => `
${k} ${v}
` ) .join("")}
`; } // ============ Map rendering (equirectangular) ============ function renderMap(flight) { const W = 600; const H = 260; const PAD = 40; function project(lat, lon) { const x = PAD + ((lon + 180) / 360) * (W - PAD * 2); const y = PAD + ((90 - lat) / 180) * (H - PAD * 2); return { x, y }; } // Grid lines let grid = ""; for (let lon = -180; lon <= 180; lon += 30) { const p1 = project(-90, lon); const p2 = project(90, lon); grid += ``; } for (let lat = -90; lat <= 90; lat += 30) { const p1 = project(lat, -180); const p2 = project(lat, 180); grid += ``; } const isLive = flight.live && flight.position; let content; let aria; if (isLive) { const p = flight.position; const pos = project(p.lat, p.lon); // Heading arrow: a short line from the position in the direction of travel const rad = ((p.trackDeg || 0) - 90) * (Math.PI / 180); const len = 26; const hx = pos.x + Math.cos(rad) * len; const hy = pos.y + Math.sin(rad) * len; aria = `${flight.airlineCode}${flight.flightNumber} at ${p.lat.toFixed(2)}, ${p.lon.toFixed(2)}`; content = ` ${flight.airlineCode}${flight.flightNumber} ${p.altitudeFt != null ? p.altitudeFt.toLocaleString() + " ft" : ""} `; } else { const from = project(flight.origin.lat, flight.origin.lon); const to = project(flight.destination.lat, flight.destination.lon); const midX = (from.x + to.x) / 2; const midY = (from.y + to.y) / 2; const dx = to.x - from.x; const dy = to.y - from.y; const dist = Math.sqrt(dx * dx + dy * dy) || 1; const lift = Math.min(dist * 0.25, 60); const ctrlX = midX - (dy / dist) * lift; const ctrlY = midY + (dx / dist) * lift; aria = `${flight.origin.code} to ${flight.destination.code}`; content = ` ${flight.origin.code} ${flight.origin.city} ${flight.destination.code} ${flight.destination.city} `; } return ` ${grid} ${content} `; } // ============ Formatting helpers ============ function formatTime(date) { return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); } function formatDuration(minutes) { const h = Math.floor(minutes / 60); const m = minutes % 60; if (h === 0) return `${m}m`; return `${h}h ${m}m`; } function formatAgo(epochMs) { if (!epochMs) return "—"; const secs = Math.max(0, Math.round((Date.now() - epochMs) / 1000)); if (secs < 60) return `${secs}s ago`; const mins = Math.round(secs / 60); if (mins < 60) return `${mins}m ago`; const hrs = Math.floor(mins / 60); return `${hrs}h ${mins % 60}m ago`; } // ============ URL params ============ function checkUrlParams() { const params = new URLSearchParams(location.search); const f = params.get("f"); if (f) { const decoded = decodeFlight(f); if (decoded) { airlineInput.value = decoded.airline; flightInput.value = decoded.number; doLookup(decoded.airline, decoded.number); } } } // ============ Event wiring ============ function onAirlineInput() { clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { showSuggestions(airlineInput.value.trim()); }, DEBOUNCE_MS); } function onAirlineKeydown(e) { if (suggestionsBox.classList.contains("hidden")) return; if (e.key === "ArrowDown") { e.preventDefault(); moveSuggestion(1); } else if (e.key === "ArrowUp") { e.preventDefault(); moveSuggestion(-1); } else if (e.key === "Enter") { e.preventDefault(); commitSuggestion(); } else if (e.key === "Escape") { hideSuggestions(); } } function onFlightKeydown(e) { if (e.key === "Enter") { e.preventDefault(); submitForm(); } } function submitForm() { const airline = airlineInput.value.trim().toUpperCase(); const flight = flightInput.value.trim(); if (!airline || !flight) return; doLookup(airline, flight); } function onFormSubmit(e) { e.preventDefault(); submitForm(); } function onDocumentClick(e) { if (!e.target.closest(".field")) { hideSuggestions(); } } // ============ Service worker ============ function registerServiceWorker() { if ("serviceWorker" in navigator) { navigator.serviceWorker.register("sw.js").catch(() => { // SW registration failed (e.g. file:// or unsupported) — non-fatal }); } } // ============ Init ============ function init() { initTheme(); initLang(); renderHistory(); checkUrlParams(); form.addEventListener("submit", onFormSubmit); airlineInput.addEventListener("input", onAirlineInput); airlineInput.addEventListener("keydown", onAirlineKeydown); flightInput.addEventListener("keydown", onFlightKeydown); document.addEventListener("click", onDocumentClick); langSelect.addEventListener("change", onLangChange); themeToggle.addEventListener("click", toggleTheme); registerServiceWorker(); } document.addEventListener("DOMContentLoaded", init);