summaryrefslogtreecommitdiff
path: root/web/frontend
diff options
context:
space:
mode:
authorMartin Kagamino Lehoux <martin@lehoux.net>2026-08-18 21:32:28 +0200
committerMartin Kagamino Lehoux <martin@lehoux.net>2026-08-18 21:32:28 +0200
commit992c36c6c7ce7e1e732866b3cd4fb8269a905875 (patch)
tree15b7ae06b14341929e555ebc126f07d963213104 /web/frontend
parent51613971c1e25dedbc808c66b79d15111d250160 (diff)
feat(web): add ride library filtersHEADmain
Diffstat (limited to 'web/frontend')
-rw-r--r--web/frontend/ride-filters-logic.test.ts14
-rw-r--r--web/frontend/ride-filters-logic.ts15
-rw-r--r--web/frontend/ride-filters.ts49
3 files changed, 78 insertions, 0 deletions
diff --git a/web/frontend/ride-filters-logic.test.ts b/web/frontend/ride-filters-logic.test.ts
new file mode 100644
index 0000000..5225028
--- /dev/null
+++ b/web/frontend/ride-filters-logic.test.ts
@@ -0,0 +1,14 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import { formatRideFilterValue, normalizeRideFilterRange } from "./ride-filters-logic.ts";
+
+test("formats ride filter values", () => {
+ assert.equal(formatRideFilterValue("distance", 120), "120 km");
+ assert.equal(formatRideFilterValue("duration", 150), "2h 30m");
+ assert.equal(formatRideFilterValue("cotacol", 500), "500 points");
+});
+
+test("keeps range handles ordered when the minimum moves past the maximum", () => {
+ assert.deepEqual(normalizeRideFilterRange(200, 100, "min"), { min: 100, max: 100 });
+ assert.deepEqual(normalizeRideFilterRange(200, 100, "max"), { min: 200, max: 200 });
+});
diff --git a/web/frontend/ride-filters-logic.ts b/web/frontend/ride-filters-logic.ts
new file mode 100644
index 0000000..f3593c1
--- /dev/null
+++ b/web/frontend/ride-filters-logic.ts
@@ -0,0 +1,15 @@
+export type RideFilterRangeFormat = "distance" | "duration" | "cotacol";
+export type RideFilterRangeHandle = "min" | "max";
+
+export const formatRideFilterValue = (format: RideFilterRangeFormat, value: number): string => {
+ if (format === "distance") return `${value} km`;
+ if (format === "cotacol") return `${value} points`;
+ const hours = Math.floor(value / 60);
+ const minutes = value % 60;
+ return `${hours}h ${String(minutes).padStart(2, "0")}m`;
+};
+
+export const normalizeRideFilterRange = (min: number, max: number, changed: RideFilterRangeHandle): { min: number; max: number } => {
+ if (min <= max) return { min, max };
+ return changed === "min" ? { min: max, max } : { min, max: min };
+};
diff --git a/web/frontend/ride-filters.ts b/web/frontend/ride-filters.ts
new file mode 100644
index 0000000..24c23e8
--- /dev/null
+++ b/web/frontend/ride-filters.ts
@@ -0,0 +1,49 @@
+import { requireElement } from "./dom.js";
+import {
+ formatRideFilterValue,
+ normalizeRideFilterRange,
+ type RideFilterRangeFormat,
+ type RideFilterRangeHandle,
+} from "./ride-filters-logic.js";
+
+const mountRangeFilter = (container: HTMLElement): void => {
+ const format = container.dataset.rangeFormat as RideFilterRangeFormat | undefined;
+ if (!format) throw new Error("Missing ride filter range format");
+ const minInput = requireElement(container.querySelector<HTMLInputElement>("[data-range-min]"), "ride filter minimum input");
+ const maxInput = requireElement(container.querySelector<HTMLInputElement>("[data-range-max]"), "ride filter maximum input");
+ const track = requireElement(container.querySelector<HTMLElement>("[data-range-track]"), "ride filter range track");
+ const group = requireElement(container.parentElement, "ride filter group");
+ const minOutput = requireElement(group.querySelector<HTMLOutputElement>('[data-range-value="min"]'), "ride filter minimum output");
+ const maxOutput = requireElement(group.querySelector<HTMLOutputElement>('[data-range-value="max"]'), "ride filter maximum output");
+ const lowerBound = Number(minInput.min);
+ const upperBound = Number(minInput.max);
+
+ const update = (changed?: RideFilterRangeHandle): void => {
+ const range = normalizeRideFilterRange(Number(minInput.value), Number(maxInput.value), changed ?? "min");
+ minInput.value = String(range.min);
+ maxInput.value = String(range.max);
+ const span = Math.max(upperBound - lowerBound, 1);
+ const start = ((range.min - lowerBound) / span) * 100;
+ const end = ((range.max - lowerBound) / span) * 100;
+ track.style.setProperty("--range-start", `${start}%`);
+ track.style.setProperty("--range-end", `${end}%`);
+ const minLabel = formatRideFilterValue(format, range.min);
+ const maxLabel = formatRideFilterValue(format, range.max);
+ minOutput.value = minLabel;
+ maxOutput.value = maxLabel;
+ minOutput.textContent = minLabel;
+ maxOutput.textContent = maxLabel;
+ minInput.setAttribute("aria-valuetext", minLabel);
+ maxInput.setAttribute("aria-valuetext", maxLabel);
+ };
+
+ minInput.addEventListener("input", () => update("min"));
+ maxInput.addEventListener("input", () => update("max"));
+ update();
+};
+
+export const mountRideFilters = (): void => {
+ document.querySelectorAll<HTMLElement>("[data-range-filter]").forEach(mountRangeFilter);
+};
+
+mountRideFilters();