feat: expand station cards with detailed information and add live statistics endpoint
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled

- Add `/stats/live` endpoint returning station count and latest price timestamp with 5-minute cache
- Transform StationCard into expandable component with click/keyboard interaction showing full details
- Display brand label, badges (24h/Supermarket/Motorway), fuel types, amenities, opening hours, and price delta vs average
- Add brand filter dropdown to StationList with dynamic brand extraction from results
- Calculate and display price comparison against filtered stations average
- Redesign map markers to simpler price display; move directions link to popup alongside station details
- Add "locate-me" button to SearchBar for geolocation trigger
- Show "Live" indicator with station count and last-update time on homepage hero
- Remove standalone directions link from marker HTML; consolidate in popup with click propagation handling
- Persist `avgPence` calculation across StationList and pass to cards for delta display
- Add `@iconify-json/lucide` dev dependency and register collection on app mount
- Stop click propagation on card action buttons (directions, remove)
This commit is contained in:
Ovidiu U
2026-04-20 18:58:13 +01:00
parent c2466e5a61
commit 831637380c
14 changed files with 438 additions and 48 deletions

View File

@@ -30,15 +30,19 @@
</nav>
<!-- Hero -->
<section id="hero" class="relative pt-24 md:pt-40 pb-12 md:pb-24 px-6 hero-gradient overflow-hidden">
<section id="hero" class="relative pt-24 md:pt-40 pb-6 md:pb-10 px-6 hero-gradient overflow-hidden">
<div class="max-w-7xl mx-auto grid lg:grid-cols-2 gap-16 items-center">
<div class="space-y-8">
<div class="inline-flex items-center gap-2 px-3 py-1 bg-accent/10 text-accent rounded-full text-xs font-bold uppercase tracking-wider">
<iconify-icon icon="lucide:sparkles"></iconify-icon>
Save up to £250/year on fuel
<div class="inline-flex items-center gap-2 px-3 py-1 text-accent text-xs tracking-wider">
<span class="inline-flex items-center gap-1.5 font-bold uppercase">
<span class="size-1.5 rounded-full bg-status-good animate-pulse"></span>
Live
</span>
<span v-if="liveStats.stationCount">· {{ formattedStationCount }} UK stations</span>
<span>· updated {{ updatedAgo || '…' }}</span>
</div>
<h1 class="text-4xl sm:text-5xl md:text-7xl font-black font-display text-zinc-800 leading-[1.1] tracking-tighter">
Stop Overpaying <br class="hidden sm:block"><span class="text-accent">for Fuel.</span>
Know exactly <br class="hidden sm:block"><span class="text-accent">when</span> to fuel.
</h1>
<p class="text-xl text-zinc-500 max-w-lg leading-relaxed">
Join 50,000+ UK drivers using real-time insights to find the cheapest petrol and time their fill-ups perfectly.
@@ -46,14 +50,14 @@
<SearchBar :initial="searchInitial" @search="onSearch" />
<div class="flex items-center gap-4 pt-4">
<!-- <div class="flex items-center gap-4 pt-4">
<div class="flex -space-x-2">
<img alt="User" class="w-8 h-8 rounded-full border-2 border-white" src="https://api.dicebear.com/7.x/avataaars/svg?seed=1">
<img alt="User" class="w-8 h-8 rounded-full border-2 border-white" src="https://api.dicebear.com/7.x/avataaars/svg?seed=2">
<img alt="User" class="w-8 h-8 rounded-full border-2 border-white" src="https://api.dicebear.com/7.x/avataaars/svg?seed=3">
</div>
<span class="text-sm text-zinc-500 font-medium italic">"Saved me £12 on my first tank!"</span>
</div>
</div>-->
</div>
<!-- Visual mockup card -->
@@ -427,16 +431,58 @@
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
import { RouterLink, useRoute, useRouter } from 'vue-router'
import { useAuth } from '../composables/useAuth.js'
import { useStations } from '../composables/useStations.js'
import api from '../axios.js'
import SearchBar from '../components/SearchBar.vue'
import LeafletMap from '../components/LeafletMap.vue'
import StationList from '../components/StationList.vue'
const { isAuthenticated, userTier } = useAuth()
const liveStats = ref({ stationCount: null, latestPriceAt: null })
const now = ref(Date.now())
let nowTicker = null
const formattedStationCount = computed(() => {
const n = liveStats.value.stationCount
return n == null ? '' : n.toLocaleString('en-GB')
})
const updatedAgo = computed(() => {
const iso = liveStats.value.latestPriceAt
if (!iso) return ''
const diffMin = Math.floor((now.value - new Date(iso).getTime()) / 60000)
if (diffMin < 1) return 'just now'
if (diffMin < 60) return `${diffMin} min ago`
const hours = Math.floor(diffMin / 60)
if (hours < 24) return `${hours} hr ago`
const days = Math.floor(hours / 24)
return `${days} day${days === 1 ? '' : 's'} ago`
})
onMounted(async () => {
try {
const { data } = await api.get('/stats/live')
liveStats.value = {
stationCount: data.station_count,
latestPriceAt: data.latest_price_at,
}
} catch {
// leave defaults; hero line degrades to "Live" only
}
nowTicker = setInterval(() => {
now.value = Date.now()
}, 60000)
})
onUnmounted(() => {
if (nowTicker) clearInterval(nowTicker)
})
const cadence = ref('monthly')
function ctaHref(tier) {