Files
fuel-alert/resources/js/components/StationList.vue
Ovidiu U 11a3b433ff 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>
2026-05-05 12:16:13 +01:00

112 lines
4.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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">37 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>