- 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>
112 lines
4.6 KiB
Vue
112 lines
4.6 KiB
Vue
<template>
|
||
<div class="space-y-3">
|
||
<!-- Grouped results when sorting by reliability -->
|
||
<template v-if="currentSort === 'reliable'">
|
||
<section v-if="reliable.length" class="space-y-2">
|
||
<header class="flex items-center gap-2 pt-2">
|
||
<iconify-icon class="text-status-good text-lg" icon="lucide:shield-check"></iconify-icon>
|
||
<h3 class="font-black text-zinc-800">Reliable</h3>
|
||
<span class="text-xs text-zinc-500 font-medium">Updated in the last 3 days</span>
|
||
</header>
|
||
<StationCard
|
||
v-for="station in reliable"
|
||
:key="station.station_id"
|
||
:avg-pence="avgPence"
|
||
:lowest-price="lowestPrice"
|
||
:origin="origin"
|
||
:station="station"
|
||
/>
|
||
</section>
|
||
|
||
<section v-if="stale.length" class="space-y-2 pt-4">
|
||
<header class="flex items-center gap-2">
|
||
<iconify-icon class="text-status-warn text-lg" icon="lucide:clock"></iconify-icon>
|
||
<h3 class="font-black text-zinc-800">Older prices</h3>
|
||
<span class="text-xs text-zinc-500 font-medium">3–7 days old — verify before driving</span>
|
||
</header>
|
||
<div>
|
||
<StationCard
|
||
v-for="station in stale"
|
||
:key="station.station_id"
|
||
:avg-pence="avgPence"
|
||
:lowest-price="lowestPrice"
|
||
:origin="origin"
|
||
:station="station"
|
||
class="mb-2"
|
||
/>
|
||
</div>
|
||
</section>
|
||
|
||
<section v-if="outdated.length" class="space-y-2 pt-4">
|
||
<button
|
||
:aria-expanded="outdatedOpen"
|
||
class="flex items-center gap-2 w-full text-left py-3 px-3 rounded-lg hover:bg-zinc-100/60 transition-colors"
|
||
type="button"
|
||
@click="outdatedOpen = !outdatedOpen"
|
||
>
|
||
<iconify-icon class="text-status-bad text-lg" icon="lucide:triangle-alert"></iconify-icon>
|
||
<h3 class="font-black text-zinc-800">Outdated</h3>
|
||
<span class="text-xs text-zinc-500 font-medium">Over 7 days old — likely inaccurate ({{ outdated.length }})</span>
|
||
<iconify-icon
|
||
:class="{ 'rotate-180': outdatedOpen }"
|
||
class="text-zinc-500 text-base ml-auto transition-transform"
|
||
icon="lucide:chevron-down"
|
||
></iconify-icon>
|
||
</button>
|
||
<div v-if="outdatedOpen">
|
||
<StationCard
|
||
v-for="station in outdated"
|
||
:key="station.station_id"
|
||
:avg-pence="avgPence"
|
||
:lowest-price="lowestPrice"
|
||
:origin="origin"
|
||
:station="station"
|
||
class="mb-2"
|
||
/>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<!-- Flat list for other sort modes -->
|
||
<div v-else class="space-y-2">
|
||
<StationCard
|
||
v-for="station in stations"
|
||
:key="station.station_id"
|
||
:avg-pence="avgPence"
|
||
:lowest-price="lowestPrice"
|
||
:origin="origin"
|
||
:station="station"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, ref } from 'vue'
|
||
import StationCard from './StationCard.vue'
|
||
|
||
const props = defineProps({
|
||
stations: { type: Array, required: true },
|
||
currentSort: { type: String, default: 'reliable' },
|
||
origin: { type: Object, default: null },
|
||
})
|
||
|
||
const reliable = computed(() => props.stations.filter(s => s.reliability === 'reliable'))
|
||
const stale = computed(() => props.stations.filter(s => s.reliability === 'stale'))
|
||
const outdated = computed(() => props.stations.filter(s => s.reliability === 'outdated'))
|
||
|
||
const outdatedOpen = ref(false)
|
||
|
||
const lowestPrice = computed(() => {
|
||
if (!reliable.value.length && !props.stations.length) return null
|
||
const pool = reliable.value.length ? reliable.value : props.stations
|
||
return Math.min(...pool.map(s => s.price_pence))
|
||
})
|
||
|
||
const avgPence = computed(() => {
|
||
const prices = props.stations.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
|
||
})
|
||
</script>
|