// Small touches on top of HTMX: "📍 Here", remembering the form, the route // map, quick follow-up chips and Enter-to-send. (() => { const ask = document.getElementById("ask"); const place = ask.elements.place; const lat = ask.elements.lat; const lon = ask.elements.lon; const locate = document.getElementById("locate"); // Remember the last choices in this browser only. const KEY = "workout-suggester:form"; const save = () => { const d = Object.fromEntries(new FormData(ask)); try { localStorage.setItem(KEY, JSON.stringify(d)); } catch {} }; try { const d = JSON.parse(localStorage.getItem(KEY) || "{}"); for (const [k, v] of Object.entries(d)) { const el = ask.elements[k]; if (!el) continue; if (el instanceof RadioNodeList) { for (const r of el) r.checked = r.value === v; } else if (v) { el.value = v; } } if (lat.value) place.placeholder = "📍 your location"; } catch {} ask.addEventListener("change", save); ask.addEventListener("htmx:beforeRequest", save); // Typing a place overrides the GPS fix. place.addEventListener("input", () => { lat.value = lon.value = ""; place.placeholder = "Town or city"; }); locate.addEventListener("click", () => { if (!navigator.geolocation) return alert("This browser can't share its location. Type a town instead."); locate.disabled = true; locate.textContent = "📍 …"; navigator.geolocation.getCurrentPosition( (pos) => { lat.value = pos.coords.latitude.toFixed(5); lon.value = pos.coords.longitude.toFixed(5); place.value = ""; place.placeholder = "📍 your location"; locate.disabled = false; locate.textContent = "📍 Here"; save(); ask.requestSubmit(); }, (err) => { locate.disabled = false; locate.textContent = "📍 Here"; alert("Couldn't get your location (" + err.message + "). Type a town instead."); }, { enableHighAccuracy: false, timeout: 10000, maximumAge: 600000 } ); }); // Draw route maps that arrive in HTMX swaps. const COLORS = ["#2f8f4e", "#3b6cf6", "#c2410c"]; const drawMaps = (root) => { if (!window.L) return; for (const el of root.querySelectorAll(".map[data-map]:not(.leaflet-container)")) { const d = JSON.parse(el.dataset.map); // The view must exist before vector layers are added, or Leaflet throws. const map = L.map(el, { scrollWheelZoom: false }).setView(d.you, 13); L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 18, attribution: '© OpenStreetMap', }).addTo(map); const layers = d.routes.map((r, i) => L.featureGroup([ L.polyline(r.ways, { color: COLORS[i % COLORS.length], weight: i === 0 ? 5 : 3, opacity: i === 0 ? 0.9 : 0.5 }), L.circleMarker(r.join, { radius: 5, color: COLORS[i % COLORS.length], fillOpacity: 1 }).bindTooltip("Start here"), ]).bindTooltip(r.name, { sticky: true }) ); layers.forEach((l) => l.addTo(map)); L.circleMarker(d.you, { radius: 7, color: "#fff", weight: 2, fillColor: "#111", fillOpacity: 1 }) .bindTooltip("You").addTo(map); const focus = (i) => { layers.forEach((l, j) => l.eachLayer((x) => x.setStyle && x.setStyle({ weight: j === i ? 5 : 3, opacity: j === i ? 0.9 : 0.4 }))); layers[i].bringToFront(); map.fitBounds(layers[i].getBounds().extend(d.you), { padding: [20, 20] }); }; focus(0); const list = el.parentElement.querySelectorAll("[data-route]"); list.forEach((li) => li.addEventListener("click", (e) => { if (e.target.closest("a")) return; list.forEach((x) => x.classList.toggle("active", x === li)); focus(+li.dataset.route); }) ); } }; // Follow-up chat: Enter to send, quick chips, auto-grow. document.body.addEventListener("keydown", (e) => { const t = e.target; if (t.matches("#composer textarea") && e.key === "Enter" && !e.shiftKey && !e.isComposing) { e.preventDefault(); if (t.value.trim()) t.form.requestSubmit(); } }); document.body.addEventListener("input", (e) => { if (e.target.matches("#composer textarea")) { e.target.style.height = "auto"; e.target.style.height = e.target.scrollHeight + "px"; } }); document.body.addEventListener("click", (e) => { const chip = e.target.closest("[data-ask]"); if (!chip) return; const form = document.getElementById("composer"); form.elements.message.value = chip.dataset.ask; form.requestSubmit(); }); document.body.addEventListener("htmx:afterRequest", (e) => { if (e.target.id === "composer" && e.detail.successful) { e.target.reset(); e.target.elements.message.style.height = "auto"; } }); // outerHTML swaps replace the event target, so just look everywhere. document.body.addEventListener("htmx:afterSettle", (e) => { drawMaps(document); if (e.target.id === "plan") e.target.scrollIntoView({ behavior: "smooth", block: "start" }); }); addEventListener("load", () => drawMaps(document)); })();