feat: expand station cards with detailed information and add live statistics endpoint
- Add `/stats/live` endpoint returning station count and latest price timestamp with 5-minute cache - Transform StationCard into expandable component with click/keyboard interaction showing full details - Display brand label, badges (24h/Supermarket/Motorway), fuel types, amenities, opening hours, and price delta vs average - Add brand filter dropdown to StationList with dynamic brand extraction from results - Calculate and display price comparison against filtered stations average - Redesign map markers to simpler price display; move directions link to popup alongside station details - Add "locate-me" button to SearchBar for geolocation trigger - Show "Live" indicator with station count and last-update time on homepage hero - Remove standalone directions link from marker HTML; consolidate in popup with click propagation handling - Persist `avgPence` calculation across StationList and pass to cards for delta display - Add `@iconify-json/lucide` dev dependency and register collection on app mount - Stop click propagation on card action buttons (directions, remove)
This commit is contained in:
@@ -63,20 +63,17 @@ function buildDirectionsUrl(station, origin) {
|
||||
return base
|
||||
}
|
||||
|
||||
function buildMarkerHtml(station, index, colour, borderColour, origin) {
|
||||
function buildMarkerHtml(station, index, colour, borderColour) {
|
||||
const isFirst = index === 0
|
||||
const w = isFirst ? 46 : 40
|
||||
const h = isFirst ? 20 : 18
|
||||
const fontSize = isFirst ? 11 : 10
|
||||
const iconSize = isFirst ? 11 : 10
|
||||
const star = isFirst
|
||||
? `<span style="margin-right:2px;color:#facc15;font-size:10px;line-height:1;">★</span>`
|
||||
: ''
|
||||
|
||||
const directionsUrl = escHtml(buildDirectionsUrl(station, origin))
|
||||
const navSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${iconSize}" height="${iconSize}" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="3 11 22 2 13 21 11 13 3 11"/></svg>`
|
||||
|
||||
return `<div style="display:inline-flex;align-items:center;height:${h}px;padding:0 4px 0 6px;background:${colour};color:#fff;font-weight:700;font-size:${fontSize}px;line-height:1;letter-spacing:-0.2px;border-radius:10px;border:1.5px solid ${borderColour};box-shadow:0 1px 3px rgba(0,0,0,0.25);white-space:nowrap;gap:3px;">
|
||||
${star}<span>${Number(station.price).toFixed(1)}</span><a data-directions href="${directionsUrl}" target="_blank" rel="noopener" aria-label="Directions" style="display:inline-flex;align-items:center;justify-content:center;width:${h - 6}px;height:${h - 6}px;margin-left:1px;border-radius:50%;background:rgba(255,255,255,0.22);color:#fff;text-decoration:none;">${navSvg}</a>
|
||||
return `<div style="display:inline-flex;align-items:center;justify-content:center;width:${w}px;height:${h}px;padding:0 5px;background:${colour};color:#fff;font-weight:700;font-size:${fontSize}px;line-height:1;letter-spacing:-0.2px;border-radius:10px;border:1.5px solid ${borderColour};box-shadow:0 1px 3px rgba(0,0,0,0.25);white-space:nowrap;">
|
||||
${star}${Number(station.price).toFixed(1)}
|
||||
</div>`
|
||||
}
|
||||
|
||||
@@ -197,24 +194,31 @@ function renderMarkers() {
|
||||
? '<span style="display:inline-block;background:#84cc16;color:#fff;font-size:10px;padding:1px 5px;border-radius:3px;margin-left:4px;">Supermarket</span>'
|
||||
: ''
|
||||
|
||||
const directionsUrl = escHtml(buildDirectionsUrl(station, props.origin))
|
||||
|
||||
const popup = `
|
||||
<div style="min-width:160px">
|
||||
<strong style="font-size:13px">${escHtml(station.name)}</strong>${supermarketTag}<br>
|
||||
<span style="font-size:20px;font-weight:700;color:${escHtml(colour)}">${Number(station.price).toFixed(1)}p</span><br>
|
||||
<div style="min-width:180px">
|
||||
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:8px;">
|
||||
<div style="min-width:0;flex:1;">
|
||||
<strong style="font-size:13px">${escHtml(station.name)}</strong>${supermarketTag}
|
||||
</div>
|
||||
<a data-directions href="${directionsUrl}" target="_blank" rel="noopener" aria-label="Directions" style="display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;flex-shrink:0;border-radius:8px;;color:black;text-decoration:none;"><iconify-icon icon="lucide:navigation" style="font-size:16px;"></iconify-icon></a>
|
||||
</div>
|
||||
<div style="margin-top:4px;"><span style="font-size:20px;font-weight:700;color:${escHtml(colour)}">${Number(station.price).toFixed(1)}p</span></div>
|
||||
<span style="font-size:12px;color:#6b7280">${escHtml(miles)} miles away</span><br>
|
||||
<span style="font-size:11px;color:#9ca3af">${escHtml(station.address)}, ${escHtml(station.postcode)}</span>
|
||||
</div>
|
||||
`
|
||||
|
||||
const isFirst = index === 0
|
||||
const w = isFirst ? 65 : 56
|
||||
const w = isFirst ? 46 : 40
|
||||
const h = isFirst ? 20 : 18
|
||||
|
||||
const icon = L.divIcon({
|
||||
className: '',
|
||||
iconSize: [w, h],
|
||||
iconAnchor: [w / 2, h / 2],
|
||||
html: buildMarkerHtml(station, index, colour, borderColour, props.origin),
|
||||
html: buildMarkerHtml(station, index, colour, borderColour),
|
||||
})
|
||||
|
||||
const marker = L.marker([station.lat, station.lng], {icon}).bindPopup(popup)
|
||||
@@ -224,6 +228,11 @@ function renderMarkers() {
|
||||
mapInstance.setView([station.lat, station.lng], target, {animate: true})
|
||||
})
|
||||
|
||||
marker.on('popupopen', (e) => {
|
||||
const link = e.popup.getElement()?.querySelector('a[data-directions]')
|
||||
if (link) L.DomEvent.disableClickPropagation(link)
|
||||
})
|
||||
|
||||
markersLayer.addLayer(marker)
|
||||
bounds.push([station.lat, station.lng])
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user