refactor: extract location JS into static file with nearby keyboard nav

Move locateUser and nearbyKeydown functions from an Askama macro into
static/js/location.js served as a deferred script. Add arrow key, Enter,
and Escape navigation for nearby cafe search results.
This commit is contained in:
Jon Seager 2026-02-09 19:54:49 +00:00
parent f4411fdc76
commit aad927085b
No known key found for this signature in database
3 changed files with 74 additions and 14 deletions

View file

@ -55,6 +55,7 @@ pub(super) fn router() -> axum::Router<AppState> {
get(searchable_select_js),
)
.route("/static/js/components/chip-scroll.js", get(chip_scroll_js))
.route("/static/js/location.js", get(location_js))
.route("/static/js/components/world-map.js", get(world_map_js))
.route("/static/js/components/donut-chart.js", get(donut_chart_js))
.route("/static/favicon-light.svg", get(favicon_light))
@ -87,6 +88,16 @@ async fn webauthn_js() -> impl IntoResponse {
)
}
async fn location_js() -> impl IntoResponse {
(
[
("content-type", "application/javascript; charset=utf-8"),
("cache-control", "public, max-age=604800"),
],
include_str!("../../../../static/js/location.js"),
)
}
async fn photo_capture_js() -> impl IntoResponse {
(
[

62
static/js/location.js Normal file
View file

@ -0,0 +1,62 @@
const nearbyKeydown = (e, el) => {
const root = el.closest("[data-location-root]");
const results = root?.querySelector("#nearby-results");
if (!results) return;
const buttons = [...results.querySelectorAll("button")];
if (!buttons.length) return;
const active = results.querySelector(".ss-active");
let idx = active ? buttons.indexOf(active) : -1;
if (e.key === "ArrowDown") {
e.preventDefault();
if (active) active.classList.remove("ss-active");
idx = (idx + 1) % buttons.length;
buttons[idx].classList.add("ss-active");
buttons[idx].scrollIntoView({ block: "nearest" });
} else if (e.key === "ArrowUp") {
e.preventDefault();
if (active) active.classList.remove("ss-active");
idx = idx <= 0 ? buttons.length - 1 : idx - 1;
buttons[idx].classList.add("ss-active");
buttons[idx].scrollIntoView({ block: "nearest" });
} else if (e.key === "Enter" && active) {
e.preventDefault();
active.click();
} else if (e.key === "Escape") {
results.classList.add("hidden");
}
};
const locateUser = (triggerEl) => {
const root = triggerEl.closest("[data-location-root]");
const emit = (name, detail) =>
root.dispatchEvent(new CustomEvent(name, { detail, bubbles: true }));
if (!navigator.geolocation) {
emit("location-error", {
message: "Geolocation not supported by this browser.",
});
return;
}
emit("location-start");
navigator.geolocation.getCurrentPosition(
(pos) => {
emit("location-found", {
lat: pos.coords.latitude,
lng: pos.coords.longitude,
});
},
(err) => {
if (err.code === 1) {
emit("location-error", {
message: "Location access denied. Search by name instead.",
});
} else {
emit("location-error", {
message: "Could not determine location. Search by name instead.",
});
}
},
{ enableHighAccuracy: true, timeout: 15000 },
);
};

View file

@ -1,19 +1,5 @@
{% import "partials/icons.html" as icons %}
{% macro location_search_js() %}
const locateUser = (triggerEl) => { const root =
triggerEl.closest('[data-location-root]'); const emit = (name, detail) =>
root.dispatchEvent(new CustomEvent(name, { detail, bubbles: true })); if
(!navigator.geolocation) { emit('location-error', { message: 'Geolocation not
supported by this browser.' }); return; } emit('location-start');
navigator.geolocation.getCurrentPosition( (pos) => { emit('location-found', {
lat: pos.coords.latitude, lng: pos.coords.longitude }); }, (err) => { if
(err.code === 1) { emit('location-error', { message: 'Location access denied.
Search by name instead.' }); } else { emit('location-error', { message: 'Could
not determine location. Search by name instead.' }); } }, {
enableHighAccuracy: true, timeout: 15000 }, ); };
{% endmacro %}
{% macro location_search(error_signal) %}
<div
data-location-root
@ -49,6 +35,7 @@
data-bind:_cafe-search
class="input-field w-full"
placeholder="Search for a cafe&hellip;"
data-on:keydown="nearbyKeydown(evt, el)"
data-on:input__debounce.350ms="if ($_locationFound) { @get('/api/v1/nearby-cafes?lat=' + $_userLat + '&lng=' + $_userLng + '&q=' + encodeURIComponent($_cafeSearch), {responseOverrides: {selector: '#nearby-results', mode: 'replace'}}) } else { $_cityName.length >= 2 && $_cafeSearch.length >= 2 && @get('/api/v1/nearby-cafes?near=' + encodeURIComponent($_cityName) + '&q=' + encodeURIComponent($_cafeSearch), {responseOverrides: {selector: '#nearby-results', mode: 'replace'}}) }"
/>
<div