diff --git a/.gitignore b/.gitignore
index d6ae58d..5a64b4c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@
/result
*.db
.direnv/
+/prixdugaz
diff --git a/go.mod b/go.mod
index 351c584..d47486e 100644
--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module github.com/polen/prixdugaz
+module github.com/Polensky/prixdugaz
go 1.25.0
diff --git a/static/map.js b/static/map.js
index 5b175d8..e4c56ed 100644
--- a/static/map.js
+++ b/static/map.js
@@ -4,20 +4,54 @@
(function () {
'use strict';
- let currentFuel = 'regular';
- let currentRegion = '';
- let clusterMode = 'avg'; // 'min' | 'avg' | 'max'
- let showCostco = true;
+ // ── URL param helpers ──────────────────────────────────────
+ function getParams() {
+ return new URLSearchParams(window.location.search);
+ }
+
+ function updateURL() {
+ const p = new URLSearchParams();
+ if (currentFuel !== 'regular') p.set('fuel', currentFuel);
+ if (currentRegion !== '') p.set('region', currentRegion);
+ if (clusterMode !== 'avg') p.set('cluster', clusterMode);
+ if (!showCostco) p.set('costco', '0');
+ const slider = document.getElementById('price-slider');
+ if (slider && parseFloat(slider.value) < parseFloat(slider.max)) {
+ p.set('price', slider.value);
+ }
+ if (map) {
+ const c = map.getCenter();
+ p.set('lat', c.lat.toFixed(5));
+ p.set('lng', c.lng.toFixed(5));
+ p.set('zoom', map.getZoom());
+ }
+ const qs = p.toString();
+ history.replaceState(null, '', qs ? '?' + qs : window.location.pathname);
+ }
+
+ // Read initial state from URL params (fall back to defaults).
+ const _p = getParams();
+ let currentFuel = ['regular','super','diesel'].includes(_p.get('fuel')) ? _p.get('fuel') : 'regular';
+ let currentRegion = _p.get('region') || '';
+ let clusterMode = ['min','avg','max'].includes(_p.get('cluster')) ? _p.get('cluster') : 'avg';
+ let showCostco = _p.get('costco') !== '0';
+ let _initPrice = _p.has('price') ? parseFloat(_p.get('price')) : null;
+ let _initLat = _p.has('lat') ? parseFloat(_p.get('lat')) : null;
+ let _initLng = _p.has('lng') ? parseFloat(_p.get('lng')) : null;
+ let _initZoom = _p.has('zoom') ? parseInt(_p.get('zoom'), 10) : null;
+
let allStations = window.__stations || [];
let allMarkers = [];
let visibleSet = new Set();
let minPrice = 0, maxPrice = 300;
let sliderTimer = null;
+ let mapMoveTimer = null;
const stationDeltas = window.__deltas || {};
// ── Map setup (deferred to init section below) ─────────────
let map;
let clusterGroup;
+ let locateMarker = null;
// ── Colour helpers ─────────────────────────────────────────
function priceColor(price, min, max) {
@@ -220,13 +254,14 @@
document.getElementById('region-select').addEventListener('change', function () {
currentRegion = this.value;
rebuildMarkers(true);
+ updateURL();
});
document.getElementById('price-slider').addEventListener('input', function () {
const val = parseFloat(this.value);
document.getElementById('slider-label').textContent = val.toFixed(1) + '¢/L';
clearTimeout(sliderTimer);
- sliderTimer = setTimeout(() => applyPriceFilter(val), 80);
+ sliderTimer = setTimeout(() => { applyPriceFilter(val); updateURL(); }, 80);
});
document.querySelectorAll('.cluster-btn').forEach(btn => {
@@ -236,6 +271,7 @@
document.querySelectorAll('.cluster-btn').forEach(b =>
b.classList.toggle('active', b.dataset.mode === clusterMode));
if (allStations.length) clusterGroup.refreshClusters();
+ updateURL();
});
});
@@ -246,14 +282,39 @@
document.querySelectorAll('.fuel-btn[data-fuel]').forEach(b =>
b.classList.toggle('active', b.dataset.fuel === currentFuel));
rebuildMarkers();
+ updateURL();
});
});
document.getElementById('costco-toggle').addEventListener('change', function () {
showCostco = this.checked;
rebuildMarkers();
+ updateURL();
});
+ // ── Locate-me button ───────────────────────────────────
+ var locateBtn = document.getElementById('locate-btn');
+ if (locateBtn) {
+ locateBtn.addEventListener('click', function () {
+ if (!navigator.geolocation) return;
+ navigator.geolocation.getCurrentPosition(function (pos) {
+ var latlng = [pos.coords.latitude, pos.coords.longitude];
+ map.setView(latlng, 14);
+ if (locateMarker) {
+ locateMarker.setLatLng(latlng);
+ } else {
+ var dotIcon = L.divIcon({
+ html: '
',
+ className: '',
+ iconSize: [20, 20],
+ iconAnchor: [10, 10],
+ });
+ locateMarker = L.marker(latlng, { icon: dotIcon, pane: 'locatePane' }).addTo(map);
+ }
+ });
+ });
+ }
+
// ── Mobile filter panel toggle ─────────────────────────
var filterToggle = document.getElementById('filter-toggle');
var sliderPanel = document.getElementById('slider-panel');
@@ -282,10 +343,18 @@
const loadingEl = document.getElementById('loading');
try {
// Create map and cluster group now that the DOM is ready.
- map = L.map('map', { zoomControl: false }).setView([46.8, -71.2], 7);
+ const initCenter = (_initLat !== null && _initLng !== null) ? [_initLat, _initLng] : [46.8, -71.2];
+ const initZoom = _initZoom !== null ? _initZoom : 7;
+ map = L.map('map', { zoomControl: false }).setView(initCenter, initZoom);
+ map.createPane('locatePane').style.zIndex = 650;
L.control.zoom({ position: 'topright' }).addTo(map);
+ var lastUpdatedText = (document.getElementById('last-updated') || {}).textContent || '';
+ var lastUpdatedMobile = document.getElementById('last-updated-mobile');
+ if (lastUpdatedMobile && lastUpdatedText) {
+ lastUpdatedMobile.textContent = lastUpdatedText;
+ }
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
- attribution: '© OpenStreetMap | Données: Régie de l\'énergie du Québec',
+ attribution: '© OpenStreetMap | Données: Régie de l\'énergie du Québec' + (lastUpdatedText ? ' | ' + lastUpdatedText : ''),
maxZoom: 18,
}).addTo(map);
@@ -324,11 +393,46 @@
loadingEl.style.display = 'none';
+ // Apply initial state from URL params to UI controls.
+ if (currentFuel !== 'regular') {
+ document.querySelectorAll('.fuel-btn[data-fuel]').forEach(b =>
+ b.classList.toggle('active', b.dataset.fuel === currentFuel));
+ }
+ if (clusterMode !== 'avg') {
+ document.querySelectorAll('.cluster-btn').forEach(b =>
+ b.classList.toggle('active', b.dataset.mode === clusterMode));
+ }
+ if (!showCostco) {
+ const ct = document.getElementById('costco-toggle');
+ if (ct) ct.checked = false;
+ }
+ if (currentRegion) {
+ const rs = document.getElementById('region-select');
+ if (rs) rs.value = currentRegion;
+ }
+
rebuildMarkers();
+
+ // Override slider value from URL after rebuildMarkers set it.
+ if (_initPrice !== null) {
+ const slider = document.getElementById('price-slider');
+ if (slider && _initPrice < parseFloat(slider.max)) {
+ slider.value = _initPrice;
+ document.getElementById('slider-label').textContent = _initPrice.toFixed(1) + '¢/L';
+ applyPriceFilter(_initPrice, false);
+ }
+ }
+
map.addLayer(clusterGroup);
bindControls();
+ // Update URL on map move/zoom.
+ map.on('moveend zoomend', function () {
+ clearTimeout(mapMoveTimer);
+ mapMoveTimer = setTimeout(updateURL, 200);
+ });
+
// Expose for use after htmx page-swap back to the map page.
window.__mapInvalidate = function () {
setTimeout(() => map.invalidateSize(), 50);
diff --git a/static/style.css b/static/style.css
index d70adbd..9bd4d3c 100644
--- a/static/style.css
+++ b/static/style.css
@@ -13,7 +13,7 @@
}
/* ── Layout ─────────────────────────────────────────── */
-html, body { height: 100%; margin: 0; padding: 0; }
+html, body { height: 100%; margin: 0; padding: 0; overflow-x: hidden; }
/* ── Top nav ─────────────────────────────────────────── */
#topnav {
@@ -47,6 +47,17 @@ html, body { height: 100%; margin: 0; padding: 0; }
color: var(--primary-foreground);
}
#topnav nav a svg { flex-shrink: 0; }
+.github-link {
+ margin-left: auto;
+ display: flex; align-items: center;
+ color: var(--muted-foreground);
+ padding: 5px 6px; border-radius: 6px;
+ transition: background 0.12s, color 0.12s;
+}
+.github-link:hover {
+ background: var(--muted);
+ color: var(--foreground);
+}
/* ── App shell ───────────────────────────────────────── */
#app {
@@ -59,7 +70,7 @@ html, body { height: 100%; margin: 0; padding: 0; }
}
/* ── Map page ────────────────────────────────────────── */
-#page-map { display: flex; flex-direction: column; flex: 1; position: relative; }
+#page-map { display: flex; flex-direction: column; flex: 1; position: relative; overflow: hidden; }
#map { flex: 1; min-height: 0; }
/* Slider + fuel panel */
@@ -85,6 +96,11 @@ html, body { height: 100%; margin: 0; padding: 0; }
}
#visible-count { margin-top: 6px; font-size: 11px; color: var(--muted-foreground); }
+/* Last-updated: only shown inside the mobile filter drawer */
+#last-updated-mobile {
+ display: none;
+}
+
/* Map overlay panels — sit above the Leaflet tiles */
.map-panel {
background: var(--card);
@@ -117,15 +133,54 @@ html, body { height: 100%; margin: 0; padding: 0; }
font-size: 10px; color: var(--muted-foreground);
}
-#last-updated {
- position: absolute; bottom: 24px; right: 8px;
- padding: 5px 10px;
- z-index: 1000; font-size: 11px;
- color: var(--muted-foreground);
-}
-
.pin-icon { filter: drop-shadow(0 1px 3px rgba(0,0,0,0.35)); }
+#locate-btn {
+ position: absolute; bottom: 34px; right: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 30px;
+ height: 30px;
+ padding: 0;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ background: var(--card);
+ color: var(--card-foreground);
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
+ cursor: pointer;
+ z-index: 1000;
+}
+#locate-btn:hover { background: var(--muted); }
+
+/* Locate-me blue dot + pulse ring */
+.locate-dot {
+ position: relative;
+ width: 20px;
+ height: 20px;
+}
+.locate-dot-inner {
+ position: absolute;
+ inset: 4px;
+ border-radius: 50%;
+ background: #2563eb;
+ border: 2.5px solid #fff;
+ box-shadow: 0 1px 4px rgba(0,0,0,0.4);
+ z-index: 1;
+}
+.locate-dot-pulse {
+ position: absolute;
+ inset: 0;
+ border-radius: 50%;
+ background: #2563eb;
+ opacity: 0.35;
+ animation: locate-pulse 1.8s ease-out infinite;
+}
+@keyframes locate-pulse {
+ 0% { transform: scale(0.6); opacity: 0.5; }
+ 100% { transform: scale(2.2); opacity: 0; }
+}
+
.cluster-info-tip {
position: relative;
display: inline-flex;
@@ -265,6 +320,9 @@ html, body { height: 100%; margin: 0; padding: 0; }
@media (max-width: 768px) {
#filter-toggle { display: flex; }
+ #filter-toggle,
+ #locate-btn { bottom: 24px; }
+
#slider-panel {
position: fixed;
top: auto; left: 0; right: 0; bottom: 0;
@@ -317,4 +375,15 @@ html, body { height: 100%; margin: 0; padding: 0; }
.cluster-info-tip { display: none; }
.cluster-hint { display: block; }
.cluster-row { margin-bottom: 0; }
+
+ /* Hide Leaflet attribution bar on mobile */
+ .leaflet-control-attribution { display: none; }
+
+ /* Show last-updated inside the filter drawer */
+ #last-updated-mobile {
+ display: block;
+ margin-top: 6px;
+ font-size: 11px;
+ color: var(--muted-foreground);
+ }
}
diff --git a/templates/layout.html b/templates/layout.html
index 540d47e..7490b36 100644
--- a/templates/layout.html
+++ b/templates/layout.html
@@ -3,7 +3,7 @@
- Essence Québec
+ Prix du Gaz
@@ -51,6 +51,11 @@
Statistiques
+
+
+
diff --git a/templates/map.html b/templates/map.html
index 66636dd..e341357 100644
--- a/templates/map.html
+++ b/templates/map.html
@@ -55,6 +55,7 @@
-
+
- {{.LastUpdated}}
+
+ {{.LastUpdated}}