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
This commit is contained in:
@@ -44,7 +44,7 @@
|
||||
Join 50,000+ UK drivers using real-time insights to find the cheapest petrol and time their fill-ups perfectly.
|
||||
</p>
|
||||
|
||||
<SearchBar @search="onSearch" />
|
||||
<SearchBar :initial="searchInitial" @search="onSearch" />
|
||||
|
||||
<div class="flex items-center gap-4 pt-4">
|
||||
<div class="flex -space-x-2">
|
||||
@@ -118,8 +118,8 @@
|
||||
<span class="font-medium">No stations found near you. Try a different postcode or increase the radius.</span>
|
||||
</div>
|
||||
<template v-else>
|
||||
<LeafletMap :default-open="true" :radius-miles="radiusMiles" :stations="stations" />
|
||||
<StationList :stations="stations" :current-sort="sort" @sort="onSort" />
|
||||
<LeafletMap :default-open="true" :origin="searchOrigin" :radius-miles="radiusMiles" :stations="stations" />
|
||||
<StationList :current-sort="sort" :origin="searchOrigin" :stations="stations" @sort="onSort" />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -427,8 +427,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { RouterLink, useRoute, useRouter } from 'vue-router'
|
||||
import { useAuth } from '../composables/useAuth.js'
|
||||
import { useStations } from '../composables/useStations.js'
|
||||
import SearchBar from '../components/SearchBar.vue'
|
||||
@@ -471,14 +471,63 @@ const PRICES = {
|
||||
annual: { basic: '£9.90', plus: '£24.90', pro: '£39.90' },
|
||||
}
|
||||
const PRICE_SUFFIX = { monthly: '/mo', annual: '/yr' }
|
||||
const { stations, loading, error, search } = useStations()
|
||||
const { stations, meta, loading, error, search } = useStations()
|
||||
|
||||
const searchOrigin = computed(() => {
|
||||
if (meta.value?.lat != null && meta.value?.lng != null) {
|
||||
return { lat: meta.value.lat, lng: meta.value.lng }
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const sort = ref('reliable')
|
||||
const lastParams = ref(null)
|
||||
const searchAttempted = ref(false)
|
||||
const radiusMiles = ref(10)
|
||||
|
||||
async function onSearch(params) {
|
||||
const searchInitial = computed(() => ({
|
||||
postcode: route.query.postcode ?? '',
|
||||
lat: route.query.lat ? Number(route.query.lat) : null,
|
||||
lng: route.query.lng ? Number(route.query.lng) : null,
|
||||
fuelType: route.query.fuel_type ?? 'e10',
|
||||
radius: route.query.radius ? Number(route.query.radius) : 10,
|
||||
sort: route.query.sort ?? 'reliable',
|
||||
}))
|
||||
|
||||
function paramsFromQuery(query) {
|
||||
const hasPostcode = typeof query.postcode === 'string' && query.postcode.trim().length > 0
|
||||
const hasCoords = query.lat && query.lng
|
||||
if (!hasPostcode && !hasCoords) return null
|
||||
|
||||
return {
|
||||
postcode: hasPostcode ? query.postcode.trim() : null,
|
||||
lat: hasCoords ? Number(query.lat) : null,
|
||||
lng: hasCoords ? Number(query.lng) : null,
|
||||
fuelType: query.fuel_type ?? 'e10',
|
||||
radius: query.radius ? Number(query.radius) : 10,
|
||||
sort: query.sort ?? 'reliable',
|
||||
}
|
||||
}
|
||||
|
||||
function queryFromParams(params) {
|
||||
const q = {
|
||||
fuel_type: params.fuelType,
|
||||
radius: String(params.radius),
|
||||
sort: params.sort,
|
||||
}
|
||||
if (params.postcode) {
|
||||
q.postcode = params.postcode
|
||||
} else if (params.lat && params.lng) {
|
||||
q.lat = String(params.lat)
|
||||
q.lng = String(params.lng)
|
||||
}
|
||||
return q
|
||||
}
|
||||
|
||||
async function runSearch(params) {
|
||||
lastParams.value = params
|
||||
sort.value = params.sort ?? sort.value
|
||||
radiusMiles.value = params.radius ?? radiusMiles.value
|
||||
@@ -486,10 +535,24 @@ async function onSearch(params) {
|
||||
await search(params)
|
||||
}
|
||||
|
||||
async function onSort(newSort) {
|
||||
sort.value = newSort
|
||||
if (lastParams.value) {
|
||||
await search({ ...lastParams.value, sort: newSort })
|
||||
}
|
||||
async function onSearch(params) {
|
||||
await router.push({ query: queryFromParams(params) })
|
||||
await runSearch(params)
|
||||
}
|
||||
|
||||
async function onSort(newSort) {
|
||||
if (!lastParams.value) return
|
||||
const next = { ...lastParams.value, sort: newSort }
|
||||
await router.push({ query: queryFromParams(next) })
|
||||
await runSearch(next)
|
||||
}
|
||||
|
||||
watch(() => route.query, (query) => {
|
||||
const params = paramsFromQuery(query)
|
||||
if (!params) return
|
||||
const sameAsLast = lastParams.value
|
||||
&& JSON.stringify(queryFromParams(lastParams.value)) === JSON.stringify(queryFromParams(params))
|
||||
if (sameAsLast) return
|
||||
runSearch(params)
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user