From 7dc41ba9ee6adeaa275fc7d0b94596eb75e2ad9c Mon Sep 17 00:00:00 2001
From: Ovidiu U
Date: Mon, 20 Apr 2026 15:51:02 +0100
Subject: [PATCH] feat: add location-based search, redesign station cards, and
implement URL state management
- Support geolocation search (lat/lng) as alternative to postcode with automatic fallback
- Redesign StationCard with expanded layout showing address, distance in miles, reliability status, directions link, and optional remove button
- Add directions integration with Google Maps including origin parameter support
- Persist search parameters (postcode/coords, fuel type, radius, sort) in URL query and hydrate on mount
- Implement compact map markers with inline directions link and click-to-zoom behavior
- Auto-trigger search when filters change (fuel type, radius, sort) if search already performed
- Add removable prop to StationCard for watchlist integration
- Display reliability status (Current/Stale/Outdated) with color-coded pricing
- Remove 2-mile radius option from search filters
---
resources/js/components/LeafletMap.vue | 64 +++++++-------
resources/js/components/SearchBar.vue | 59 +++++++++----
resources/js/components/StationCard.vue | 109 ++++++++++++++++++------
resources/js/components/StationList.vue | 3 +
resources/js/views/Home.vue | 87 ++++++++++++++++---
5 files changed, 236 insertions(+), 86 deletions(-)
diff --git a/resources/js/components/LeafletMap.vue b/resources/js/components/LeafletMap.vue
index 6e7abcb..fc1aa92 100644
--- a/resources/js/components/LeafletMap.vue
+++ b/resources/js/components/LeafletMap.vue
@@ -40,7 +40,6 @@
import {ref, watch, onMounted, onUnmounted, nextTick} from 'vue'
import L from 'leaflet'
import 'leaflet/dist/leaflet.css'
-import {zoom} from "leaflet/src/control/Control.Zoom.js";
const CLASSIFICATION_COLOURS = {
current: '#22c55e',
@@ -56,37 +55,28 @@ const CLASSIFICATION_BORDER_COLOURS = {
outdated: '#dc2626',
}
-function buildMarkerHtml(station, index, colour, borderColour) {
+function buildDirectionsUrl(station, origin) {
+ const base = `https://www.google.com/maps/dir/?api=1&destination=${station.lat},${station.lng}`
+ if (origin?.lat != null && origin?.lng != null) {
+ return `${base}&origin=${origin.lat},${origin.lng}`
+ }
+ return base
+}
+
+function buildMarkerHtml(station, index, colour, borderColour, origin) {
const isFirst = index === 0
- const w = isFirst ? 63 : 59
- const h = isFirst ? 58 : 51
- const bw = isFirst ? 56 : 52
- const bh = isFirst ? 43 : 38
- const br = isFirst ? 17 : 15
- const tailTop = isFirst ? 45 : 40
- const tailW = isFirst ? 9 : 7
- const tailH = isFirst ? 11 : 9
- const badgeSize = isFirst ? 18 : 16
- const badgeFontSize = isFirst ? 10 : 8
- const priceFontSize = isFirst ? 12 : 11
+ const h = isFirst ? 20 : 18
+ const fontSize = isFirst ? 11 : 10
+ const iconSize = isFirst ? 11 : 10
+ const star = isFirst
+ ? `★`
+ : ''
- const initial = escHtml((station.brand || station.name || '?')[0].toUpperCase())
+ const directionsUrl = escHtml(buildDirectionsUrl(station, origin))
+ const navSvg = ``
- const badge = isFirst
- ? `★
`
- : `${index + 1}
`
-
- return `
- ${badge}
-
-
- ${initial}
-
-
- ${Number(station.price).toFixed(1)}p
-
-
-
+ return `
+ ${star}
${Number(station.price).toFixed(1)}${navSvg}
`
}
@@ -102,6 +92,7 @@ const props = defineProps({
stations: {type: Array, required: true},
defaultOpen: {type: Boolean, default: false},
radiusMiles: {type: Number, default: 10},
+ origin: {type: Object, default: null},
})
const mapContainer = ref(null)
@@ -216,22 +207,29 @@ function renderMarkers() {
`
const isFirst = index === 0
- const w = isFirst ? 63 : 59
- const h = isFirst ? 58 : 51
+ const w = isFirst ? 65 : 56
+ const h = isFirst ? 20 : 18
const icon = L.divIcon({
className: '',
iconSize: [w, h],
- iconAnchor: [w / 2, h],
- html: buildMarkerHtml(station, index, colour, borderColour),
+ iconAnchor: [w / 2, h / 2],
+ html: buildMarkerHtml(station, index, colour, borderColour, props.origin),
})
const marker = L.marker([station.lat, station.lng], {icon}).bindPopup(popup)
+ marker.on('click', () => {
+ const target = Math.max(mapInstance.getZoom(), 16)
+ mapInstance.setView([station.lat, station.lng], target, {animate: true})
+ })
+
markersLayer.addLayer(marker)
bounds.push([station.lat, station.lng])
})
+ const zoom = getZoomForRadius(props.radiusMiles)
+
if (bounds.length === 1) {
mapInstance.setView(bounds[0], zoom)
} else {
diff --git a/resources/js/components/SearchBar.vue b/resources/js/components/SearchBar.vue
index b381017..4c60714 100644
--- a/resources/js/components/SearchBar.vue
+++ b/resources/js/components/SearchBar.vue
@@ -20,14 +20,14 @@
id="postcode-input"
v-model="postcode"
type="text"
- placeholder="Enter postcode, e.g. SW1A 1AA"
+ :placeholder="coords ? 'Using your current location' : 'Enter postcode, e.g. SW1A 1AA'"
class="w-full h-14 pr-28 pl-4 bg-white border border-zinc-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-accent shadow-inner text-base"
@keyup.enter="onSearch"
/>
-
+
@@ -118,8 +118,8 @@
No stations found near you. Try a different postcode or increase the radius.
-
-
+
+
@@ -427,8 +427,8 @@