// main.js // Function to update weather widget async function updateWeather() { try { const res = await fetch("/api/weather"); if (!res.ok) throw new Error("Failed to fetch weather"); const data = await res.json(); const weatherDiv = document.getElementById("weather-widget"); if (!weatherDiv) return; // Set temperature weatherDiv.querySelector(".weather-temp").textContent = `${data.temp_c} °C`; // Set icon const icon = weatherDiv.querySelector(".weather-icon"); if (data.condition === "sunny") { icon.src = "https://cdn-icons-png.flaticon.com/512/869/869869.png"; icon.alt = "Sunny"; } else if (data.condition === "rain") { icon.src = "https://cdn-icons-png.flaticon.com/512/414/414974.png"; icon.alt = "Rainy"; } else if (data.condition === "snow") { icon.src = "https://cdn-icons-png.flaticon.com/512/642/642102.png"; icon.alt = "Snowy"; } } catch (err) { // Show error in widget const weatherDiv = document.getElementById("weather-widget"); if (weatherDiv) { weatherDiv.querySelector(".weather-temp").textContent = "N/A"; weatherDiv.querySelector(".weather-icon").src = ""; weatherDiv.querySelector(".weather-icon").alt = "Error"; } } } // Initial load updateWeather(); // Update every 5 minutes setInterval(updateWeather, 5 * 60 * 1000); // --- Time Widget Logic --- // Time zone offsets in hours relative to UTC const timeZones = { london: 0, // UTC+0 (BST not handled for simplicity) est: -4, // UTC-4 (EDT, adjust for DST if needed) nigeria: 1, // UTC+1 pakistan: 5, // UTC+5 }; function pad(n) { return n < 10 ? "0" + n : n; } async function updateClocks() { try { const res = await fetch("/api/utc"); if (!res.ok) throw new Error("Failed to fetch UTC time"); const { utc } = await res.json(); const utcDate = new Date(utc); // Update each clock for (const [zone, offset] of Object.entries(timeZones)) { const local = new Date(utcDate.getTime() + offset * 60 * 60 * 1000); const h = pad(local.getUTCHours()); const m = pad(local.getUTCMinutes()); const s = pad(local.getUTCSeconds()); const el = document.getElementById(`${zone}-clock`); if (el) el.textContent = `${h}:${m}:${s}`; } } catch (err) { // Show error in all clocks for (const zone of Object.keys(timeZones)) { const el = document.getElementById(`${zone}-clock`); if (el) el.textContent = "N/A"; } } } // Initial load and update every second updateClocks(); setInterval(updateClocks, 1000); // --- Airport Autocomplete --- const airportInput = document.querySelector('input[placeholder*="airport"]'); let dropdownDiv; let selectedAirport; // Declare globally to store selection if (airportInput) { dropdownDiv = document.createElement("div"); dropdownDiv.className = "absolute bg-white border border-gray-300 rounded shadow z-10 w-full max-h-48 overflow-y-auto top-[calc(100%+10px)]"; dropdownDiv.style.display = "none"; dropdownDiv.setAttribute("role", "listbox"); // ARIA role airportInput.parentNode.appendChild(dropdownDiv); // Event Delegation for dropdown clicks (fixes listener duplication) dropdownDiv.addEventListener("click", (e) => { const item = e.target.closest("[data-index]"); if (!item) return; const index = item.dataset.index; selectedAirport = currentAirports[index]; // Use latest fetched data airportInput.value = `${selectedAirport.name} (${selectedAirport.code})`; // Set once onAirportSelected(selectedAirport); dropdownDiv.style.display = "none"; }); // Debounce input (300ms delay) let debounceTimer; let currentAirports = []; // Track latest results airportInput.addEventListener("input", async function () { clearTimeout(debounceTimer); debounceTimer = setTimeout(async () => { const val = airportInput.value.trim(); if (val.length < 3) { dropdownDiv.style.display = "none"; return; } try { const res = await fetch(`/airports?search=${encodeURIComponent(val)}`); if (!res.ok) throw new Error("Failed to fetch"); const airports = await res.json(); currentAirports = airports; // Store for click handler if (!airports.length) { dropdownDiv.innerHTML = '
No results
'; dropdownDiv.style.display = "block"; return; } // Escape HTML to prevent XSS dropdownDiv.innerHTML = airports .map((a, i) => { const name = a.name.replace( /[&<>]/g, (c) => ({ "&": "&", "<": "<", ">": ">" }[c]) ); return `
${name}
`; }) .join(""); dropdownDiv.style.display = "block"; } catch (err) { dropdownDiv.innerHTML = '
Error loading airports
'; dropdownDiv.style.display = "block"; } }, 300); }); // Hide dropdown only when focus leaves input/dropdown document.addEventListener("click", (e) => { if (!airportInput.contains(e.target) && !dropdownDiv.contains(e.target)) { dropdownDiv.style.display = "none"; } }); } // --- Map Widget Logic --- // let selectedAirport = null; function showMap(lat, lon) { console.log(lat, lon); const mapDiv = document.getElementById("map-widget"); if (!mapDiv) return; mapDiv.innerHTML = ""; mapDiv.style.height = "200px"; mapDiv.style.width = "300px"; // OpenLayers map if (window.ol && window.ol.Map) { new ol.Map({ target: mapDiv, layers: [new ol.layer.Tile({ source: new ol.source.OSM() })], view: new ol.View({ center: ol.proj.fromLonLat([-79.6248, 43.6532]), zoom: 13, }), }); } else { mapDiv.innerHTML = '
Map library not loaded
'; } } // --- Distance from Arctic Circle Widget --- function haversine(lat1, lon1, lat2, lon2) { const toRad = (deg) => (deg * Math.PI) / 180; const R = 6371; // Earth radius in km const dLat = toRad(lat2 - lat1); const dLon = toRad(lon2 - lon1); const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; } function updateDistanceWidget(lat, lon) { const distDiv = document.getElementById("distance-widget"); if (!distDiv) return; // Arctic Circle: latitude 66.56333, longitude 0 const arcticLat = 66.56333; const arcticLon = 0; const dist = haversine(arcticLat, arcticLon, lat, lon); distDiv.textContent = dist.toFixed(1) + " KM"; } // Update distance when airport is selected function onAirportSelected(airport) { if ( airport && airport.latitude_deg && airport.longitude_deg && !isNaN(Number(airport.latitude_deg)) && !isNaN(Number(airport.longitude_deg)) ) { showMap(Number(airport.latitude_deg), Number(airport.longitude_deg)); updateDistanceWidget( Number(airport.latitude_deg), Number(airport.longitude_deg) ); } } // --- Analytic Logging --- function logWidgetClick(widgetName) { fetch("/analytic", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ widget_name: widgetName, browser_type: navigator.userAgent, }), }).then(async (res) => { if (res.status === 429) { const data = await res.json(); if (data.redirect) window.location.href = data.redirect; } }); } // --- Click Counter Widget --- async function updateClickCounter() { try { const res = await fetch("/analytic/count"); if (!res.ok) throw new Error("Failed to fetch count"); const { count } = await res.json(); const el = document.getElementById("click-counter-widget"); if (el) el.textContent = count; } catch (err) { const el = document.getElementById("click-counter-widget"); if (el) el.textContent = "N/A"; } } updateClickCounter(); setInterval(updateClickCounter, 60 * 1000); // --- Export XML Widget --- const exportBtn = document.querySelector( "button.bg-sky-400.text-white.font-semibold.px-8.py-2.rounded" ); if (exportBtn && exportBtn.textContent.trim().toLowerCase() === "export") { exportBtn.addEventListener("click", function () { window.location.href = "/analytic/export"; }); } // --- Reddit News Widget --- async function updateRedditNews() { try { const res = await fetch("/reddit"); if (!res.ok) throw new Error("Failed to fetch reddit"); const posts = await res.json(); const newsDiv = document.getElementById("news-widget"); if (!newsDiv) return; newsDiv.innerHTML = posts .map( (post) => `
${post.title}
by ${post.author}
` ) .join(""); } catch (err) { const newsDiv = document.getElementById("news-widget"); if (newsDiv) newsDiv.innerHTML = '
Failed to load news
'; } } updateRedditNews(); // --- Coin Calculator Widget --- const coinInput = document.querySelector('input[placeholder*="coin"]'); const coinBtn = document.querySelector( "button.bg-sky-400.text-white.font-semibold.px-8.py-2.rounded.mt-2" ); const coinResult = document.getElementById("coin-result-list"); if (coinInput && coinBtn && coinResult) { coinBtn.addEventListener("click", async function () { const val = coinInput.value.trim(); if (!val || isNaN(val) || Number(val) < 0) { coinResult.innerHTML = '
  • Enter a valid amount
  • '; return; } try { const res = await fetch("/coin-calc", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ amount: val }), }); if (!res.ok) throw new Error("Failed to calculate"); const data = await res.json(); coinResult.innerHTML = data .map((c) => `
  • ${c.count} x $${c.denomination}
  • `) .join(""); } catch (err) { coinResult.innerHTML = '
  • Error calculating coins
  • '; } }); } // --- Upload Widget --- const uploadInput = document.querySelector( 'input[type="file"][id="upload-input"]' ); const uploadBtn = document.getElementById("upload-btn"); const uploadPreview = document.getElementById("upload-preview"); const uploadError = document.getElementById("upload-error"); async function fetchLatestUpload() { try { const res = await fetch("/upload/latest"); const data = await res.json(); if (data.url && uploadPreview) { uploadPreview.src = data.url; uploadPreview.style.display = ""; } else if (uploadPreview) { uploadPreview.style.display = "none"; } } catch {} } if (uploadBtn && uploadInput) { uploadBtn.addEventListener("click", async function () { if (!uploadInput.files || !uploadInput.files[0]) { if (uploadError) uploadError.textContent = "Please select an image."; return; } const formData = new FormData(); formData.append("image", uploadInput.files[0]); try { const res = await fetch("/upload", { method: "POST", body: formData }); const data = await res.json(); if (data.url) { if (uploadPreview) { uploadPreview.src = data.url; uploadPreview.style.display = ""; } if (uploadError) uploadError.textContent = ""; } else { if (uploadError) uploadError.textContent = data.error || "Upload failed."; } } catch (err) { if (uploadError) uploadError.textContent = "Upload failed."; } }); } fetchLatestUpload(); // Attach click listeners to widgets function attachAnalyticListeners() { const widgets = [ { id: "weather-widget", name: "weather" }, { id: "nigeria-clock", name: "nigeria-clock" }, { id: "london-clock", name: "london-clock" }, { id: "est-clock", name: "est-clock" }, { id: "pakistan-clock", name: "pakistan-clock" }, { id: "map-widget", name: "map" }, { id: "distance-widget", name: "distance" }, { id: "airport-autocomplete", name: "airport-autocomplete" }, { id: "click-counter-widget", name: "click-counter" }, // Add more as needed ]; widgets.forEach((w) => { const el = document.getElementById(w.id); if (el) { el.addEventListener("click", () => logWidgetClick(w.name)); } }); } // Call after DOMContentLoaded if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", attachAnalyticListeners); } else { attachAnalyticListeners(); }