feat(ui): consolidate map filters and rework station selection
- replace inline filter pills with a single "Filters" popover containing small pill buttons for fuel/radius/sort/brand (no native <select>s) - map polish: Carto Positron tiles, hidden zoom buttons, locate-me floating button + accuracy ring, smooth flyTo transitions, slim ⓘ attribution - map markers no longer open Leaflet popups; clicking a marker selects the station and surfaces the existing StationCard inline over the map, with swipe-down-to-close and a small overlay × button - price colour now reflects deal quality (cheap / average / expensive vs search avg ± 3p) on both list and map — stable across sort/filter - promote the "X ago" timestamp into the card header so it stays visible in the expanded state Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -79,8 +79,37 @@
|
||||
:is-open="mapOpen"
|
||||
:origin="searchOrigin"
|
||||
:radius-miles="radiusMiles"
|
||||
:selected-station-id="selectedStationId"
|
||||
:stations="filteredStations"
|
||||
/>
|
||||
@station-select="selectedStationId = $event"
|
||||
>
|
||||
<template #overlay>
|
||||
<Transition
|
||||
enter-active-class="transition duration-200 ease-out"
|
||||
enter-from-class="opacity-0 translate-y-3"
|
||||
enter-to-class="opacity-100 translate-y-0"
|
||||
leave-active-class="transition duration-150 ease-in"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0 translate-y-3"
|
||||
>
|
||||
<div
|
||||
v-if="selectedStation"
|
||||
class="absolute left-3 right-3 bottom-3 md:right-auto md:max-w-sm z-[1000] pointer-events-auto"
|
||||
>
|
||||
<StationCard
|
||||
:avg-pence="avgPenceForCard"
|
||||
:dismissible="true"
|
||||
:expanded="true"
|
||||
:lowest-price="lowestPriceForCard"
|
||||
:origin="searchOrigin"
|
||||
:station="selectedStation"
|
||||
class="shadow-xl"
|
||||
@dismiss="selectedStationId = null"
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
</LeafletMap>
|
||||
<UpsellBanner :station-count="liveStats.stationCount" />
|
||||
<StationList
|
||||
:current-sort="sort"
|
||||
@@ -387,6 +416,7 @@ import PostSearchFilters from '../components/PostSearchFilters.vue'
|
||||
import PredictionCard from '../components/PredictionCard.vue'
|
||||
import StationList from '../components/StationList.vue'
|
||||
import UpsellBanner from '../components/UpsellBanner.vue'
|
||||
import StationCard from '../components/StationCard.vue'
|
||||
|
||||
const LeafletMap = defineAsyncComponent(() => import('../components/LeafletMap.vue'))
|
||||
import LandingNav from '../components/landing/LandingNav.vue'
|
||||
@@ -512,6 +542,32 @@ watch(() => stations.value, () => {
|
||||
}
|
||||
})
|
||||
|
||||
const selectedStationId = ref(null)
|
||||
const selectedStation = computed(() =>
|
||||
selectedStationId.value
|
||||
? filteredStations.value.find(s => s.station_id === selectedStationId.value) ?? null
|
||||
: null,
|
||||
)
|
||||
|
||||
const lowestPriceForCard = computed(() => {
|
||||
const reliable = filteredStations.value.filter(s => s.reliability === 'reliable')
|
||||
const pool = reliable.length ? reliable : filteredStations.value
|
||||
if (!pool.length) return null
|
||||
return Math.min(...pool.map(s => s.price_pence))
|
||||
})
|
||||
|
||||
const avgPenceForCard = computed(() => {
|
||||
const prices = filteredStations.value.map(s => s.price_pence).filter(p => typeof p === 'number')
|
||||
if (!prices.length) return null
|
||||
return prices.reduce((a, b) => a + b, 0) / prices.length
|
||||
})
|
||||
|
||||
watch(filteredStations, (next) => {
|
||||
if (selectedStationId.value && !next.find(s => s.station_id === selectedStationId.value)) {
|
||||
selectedStationId.value = null
|
||||
}
|
||||
})
|
||||
|
||||
const searchInitial = computed(() => ({
|
||||
postcode: route.query.postcode ?? '',
|
||||
lat: route.query.lat ? Number(route.query.lat) : null,
|
||||
|
||||
Reference in New Issue
Block a user