feat: expand station cards with detailed information and add live statistics endpoint
- 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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user