diff --git a/main.go b/main.go index 0549b13..ba7a666 100644 --- a/main.go +++ b/main.go @@ -137,7 +137,8 @@ func main() { http.HandleFunc("/api/stats/region", handleRegionStats) http.HandleFunc("/api/station-deltas", handleStationDeltas) - // Static assets (map.js, stats.js, etc.) + // Static assets (style.css, map.js, stats.js, etc.) + http.Handle("/style.css", http.FileServer(http.FS(staticSub))) http.Handle("/map.js", http.FileServer(http.FS(staticSub))) http.Handle("/stats.js", http.FileServer(http.FS(staticSub))) diff --git a/static/map.js b/static/map.js index fd79339..573b453 100644 --- a/static/map.js +++ b/static/map.js @@ -7,6 +7,7 @@ let currentFuel = 'regular'; let currentRegion = ''; let clusterMode = 'avg'; // 'min' | 'avg' | 'max' + let showCostco = true; let allStations = window.__stations || []; let allMarkers = []; let visibleSet = new Set(); @@ -112,9 +113,11 @@ clusterGroup.clearLayers(); visibleSet.clear(); - const scopedStations = currentRegion - ? allStations.filter(s => s.region === currentRegion) - : allStations; + const scopedStations = allStations.filter(s => { + if (currentRegion && s.region !== currentRegion) return false; + if (!showCostco && (s.brand || '').toLowerCase() === 'costco') return false; + return true; + }); const prices = scopedStations.map(s => s[currentFuel]).filter(p => p > 0); minPrice = prices.length ? Math.min(...prices) : 0; maxPrice = prices.length ? Math.max(...prices) : 300; @@ -164,7 +167,8 @@ allStations.forEach((s, i) => { const price = s[currentFuel]; const inRegion = !currentRegion || s.region === currentRegion; - const visible = inRegion && (price <= 0 || price <= priceMax); + const isCostco = (s.brand || '').toLowerCase() === 'costco'; + const visible = inRegion && (price <= 0 || price <= priceMax) && (!isCostco || showCostco); if (visible && !visibleSet.has(i)) { toAdd.push(allMarkers[i]); visibleSet.add(i); @@ -223,6 +227,11 @@ rebuildMarkers(); }); }); + + document.getElementById('costco-toggle').addEventListener('change', function () { + showCostco = this.checked; + rebuildMarkers(); + }); } // ── Initialise ───────────────────────────────────────────── diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..1556751 --- /dev/null +++ b/static/style.css @@ -0,0 +1,235 @@ +/* ── Theme: Gouvernement du Québec blue ─────────────── */ +:root { + --primary: rgb(9 87 151); /* #095797 */ + --primary-foreground: rgb(255 255 255); + --ring: rgb(9 87 151); +} +@media (prefers-color-scheme: dark) { + :root { + --primary: rgb(41 121 193); + --primary-foreground: rgb(255 255 255); + --ring: rgb(41 121 193); + } +} + +/* ── Layout ─────────────────────────────────────────── */ +html, body { height: 100%; margin: 0; padding: 0; } + +/* ── Top nav ─────────────────────────────────────────── */ +#topnav { + display: flex; align-items: center; gap: 0; + height: 44px; padding: 0 16px; + background: var(--card); + border-bottom: 1px solid var(--border); + flex-shrink: 0; + z-index: 2000; +} +#topnav .brand { + font-weight: 700; font-size: 15px; letter-spacing: -0.2px; + margin-right: 24px; color: var(--foreground); + text-decoration: none; +} +#topnav nav { display: flex; gap: 2px; } +#topnav nav a { + display: flex; align-items: center; gap: 6px; + padding: 5px 12px; border-radius: 6px; + text-decoration: none; color: var(--muted-foreground); + font-size: 13px; font-weight: 500; + transition: background 0.12s, color 0.12s; + cursor: pointer; +} +#topnav nav a:hover { + background: var(--muted); + color: var(--foreground); +} +#topnav nav a[aria-current="page"] { + background: var(--primary); + color: var(--primary-foreground); +} +#topnav nav a svg { flex-shrink: 0; } + +/* ── App shell ───────────────────────────────────────── */ +#app { + display: flex; flex-direction: column; + height: 100%; +} +#content { + flex: 1; min-height: 0; + display: flex; flex-direction: column; +} + +/* ── Map page ────────────────────────────────────────── */ +#page-map { display: flex; flex-direction: column; flex: 1; position: relative; } +#map { flex: 1; min-height: 0; } + +/* Slider + fuel panel */ +#slider-panel { + position: absolute; top: 12px; left: 12px; + padding: 10px 14px; + z-index: 1000; font-size: 13px; min-width: 280px; +} +#slider-panel .fuel-btns { display: flex; gap: 6px; margin-bottom: 10px; } +#region-select { + width: 100%; margin-bottom: 10px; + padding: 5px 8px; font-size: 12px; + border: 1px solid var(--border); border-radius: 4px; + background: var(--secondary); color: var(--secondary-foreground); + cursor: pointer; +} +#slider-panel label { display: block; margin-bottom: 6px; font-weight: 600; } +#price-slider { width: 100%; cursor: pointer; } +#slider-value { + display: flex; justify-content: space-between; + margin-top: 4px; font-size: 11px; + color: var(--muted-foreground); +} +#visible-count { margin-top: 6px; font-size: 11px; color: var(--muted-foreground); } + +/* Map overlay panels — sit above the Leaflet tiles */ +.map-panel { + background: var(--card); + color: var(--card-foreground); + border: 1px solid var(--border); + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0,0,0,0.15); +} + +/* Legend */ +#legend { + position: absolute; bottom: 20px; right: 20px; + padding: 12px 16px; + z-index: 1000; font-size: 13px; min-width: 180px; +} +#legend h3 { margin-bottom: 8px; font-size: 14px; } +.legend-gradient { + height: 16px; border-radius: 4px; + background: linear-gradient(to right, #16a34a, #65a30d, #ca8a04, #ea580c, #dc2626); + margin-bottom: 4px; +} +.legend-labels { + display: flex; justify-content: space-between; + font-size: 11px; color: var(--muted-foreground); +} +#map-stats { margin-top: 8px; font-size: 11px; color: var(--muted-foreground); } + +#last-updated { + position: absolute; bottom: 8px; left: 12px; + 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)); } + +.cluster-info-tip { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 16px; height: 16px; + border-radius: 50%; + border: 1px solid var(--muted-foreground); + color: var(--muted-foreground); + font-size: 10px; font-weight: 700; + cursor: default; + flex-shrink: 0; +} +.cluster-info-tip:hover .cluster-info-tooltip { display: block; } +.cluster-info-tooltip { + display: none; + position: absolute; + left: 22px; top: 50%; + transform: translateY(-50%); + background: var(--card); + color: var(--card-foreground); + border: 1px solid var(--border); + border-radius: 6px; + box-shadow: 0 2px 8px rgba(0,0,0,0.15); + padding: 10px 12px; + font-size: 12px; font-weight: 400; + white-space: nowrap; + z-index: 2000; + pointer-events: none; + line-height: 1.6; +} + +.fuel-btn { + padding: 4px 12px; + border: 1px solid var(--border); + border-radius: 4px; + background: var(--secondary); + color: var(--secondary-foreground); + cursor: pointer; font-size: 12px; + transition: all 0.15s; +} +.fuel-btn:hover { background: var(--muted); } +.fuel-btn.active { + background: var(--primary); + color: var(--primary-foreground); + border-color: var(--primary); +} + +/* ── Loading overlay ─────────────────────────────────── */ +#loading { + position: fixed; inset: 0; + background: var(--background); + opacity: 0.92; + display: flex; align-items: center; justify-content: center; + z-index: 10000; font-size: 1.1em; + color: var(--foreground); + gap: 12px; +} + +/* ── Stats page ──────────────────────────────────────── */ +#page-stats { display: flex; padding: 24px; overflow-y: auto; flex: 1; flex-direction: column; } +#page-stats h2 { margin-bottom: 20px; } + +.stat-cards { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 16px; + margin-bottom: 28px; +} +.stat-cards .card { margin: 0; } +.stat-cards .card header { padding-bottom: 4px; } +.stat-cards .card header p { font-size: 12px; color: var(--muted-foreground); margin: 0; } +.stat-value { font-size: 28px; font-weight: 700; margin: 4px 0 0; } +.stat-sub { font-size: 12px; color: var(--muted-foreground); margin-top: 2px; } + +.chart-wrap { + background: var(--card); + color: var(--card-foreground); + border: 1px solid var(--border); + border-radius: 8px; + padding: 20px; + margin-bottom: 28px; +} +.chart-wrap h3 { margin-bottom: 4px; } +.chart-wrap .chart-subtitle { + font-size: 12px; color: var(--muted-foreground); margin-bottom: 16px; +} + +.range-btns { display: flex; gap: 6px; margin-bottom: 16px; } +.range-btn { + padding: 4px 12px; + border: 1px solid var(--border); + border-radius: 4px; + background: var(--secondary); + color: var(--secondary-foreground); + cursor: pointer; font-size: 12px; +} +.range-btn:hover { background: var(--muted); } +.range-btn.active { + background: var(--primary); + color: var(--primary-foreground); + border-color: var(--primary); +} + +#stats-chart-canvas { width: 100% !important; max-height: 320px; } + +.no-data { color: var(--muted-foreground); font-size: 14px; padding: 24px 0; text-align: center; } + +.table-wrap { overflow-x: auto; } + +/* ── htmx loading indicator ──────────────────────────── */ +.htmx-request #stats-content { opacity: 0.5; transition: opacity 0.2s; } diff --git a/templates/layout.html b/templates/layout.html index afefe1b..7c3e34e 100644 --- a/templates/layout.html +++ b/templates/layout.html @@ -13,243 +13,7 @@ - +