Files
fuel-price/resources/js/views/Home.vue

589 lines
31 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="min-h-screen bg-zinc-100">
<LandingNav />
<div class="hero-gradient">
<!-- Hero -->
<section id="hero" class="relative pt-24 md:pt-36 pb-4 md:pb-8 px-6 overflow-hidden">
<div class="max-w-7xl mx-auto grid lg:grid-cols-[1.1fr_1fr] gap-10 lg:gap-16 items-center">
<div class="space-y-6 md:space-y-8">
<LiveTicker :latest-price-at="liveStats.latestPriceAt" :station-count="liveStats.stationCount" />
<h1 class="font-serif text-zinc-900 text-[40px] leading-[0.98] md:text-6xl lg:text-[88px] lg:leading-[0.95] max-w-[560px]">
Know <span class="text-accent">exactly</span> when to fill up.
</h1>
<HeroSearch :fuel-type="searchInitial.fuelType" :initial="searchInitial" :radius="searchInitial.radius" :sort="searchInitial.sort" @search="onSearch" />
<!-- Mobile verdict card: below search, only before first search -->
<div v-if="!searchAttempted" class="lg:hidden">
<VerdictCard variant="compact" />
</div>
<div id="stats-row">
<StatsRow :station-count="liveStats.stationCount" />
</div>
</div>
<!-- Desktop verdict card -->
<div class="hidden lg:block">
<VerdictCard variant="full" />
</div>
</div>
</section>
<!-- Search Results -->
<section v-if="searchAttempted" id="searchAttempted" class="px-6">
<div class="max-w-7xl mx-auto space-y-6">
<!-- Post-search filter bar -->
<PostSearchFilters
v-model:brand-filter="brandFilter"
:brands="availableBrands"
:initial="searchInitial"
:map-open="mapOpen"
:station-count="filteredStations.length"
@search="onSearch"
@toggle-map="mapOpen = !mapOpen"
/>
<!-- Loading -->
<div v-if="loading" class="flex items-center justify-center py-16">
<div class="flex items-center gap-3 text-zinc-500">
<iconify-icon icon="lucide:loader-circle" class="animate-spin text-2xl text-accent"></iconify-icon>
<span class="font-medium">Finding stations near you</span>
</div>
</div>
<!-- Error -->
<div v-else-if="error" class="flex items-center gap-3 p-4 bg-white border border-zinc-300 rounded-xl text-status-bad">
<iconify-icon icon="lucide:circle-alert" style="font-size:1.25rem"></iconify-icon>
<span class="font-medium">{{ Object.values(error).flat()[0] ?? 'Unable to load stations. Please try again.' }}</span>
</div>
<!-- Results -->
<template v-else>
<div v-if="!stations.length" class="flex items-center gap-3 p-4 bg-white border border-zinc-300 rounded-xl text-zinc-500">
<iconify-icon icon="lucide:map-pin-off" style="font-size:1.25rem"></iconify-icon>
<span class="font-medium">No stations found near you. Try a different postcode or increase the radius.</span>
</div>
<template v-else>
<LeafletMap
:is-open="mapOpen"
:origin="searchOrigin"
:radius-miles="radiusMiles"
:stations="filteredStations"
/>
<StationList
:current-sort="sort"
:origin="searchOrigin"
:stations="filteredStations"
/>
</template>
</template>
</div>
</section>
</div>
<!-- How It Works -->
<section id="how-it-works" class="py-12 md:py-24 px-6 bg-zinc-50">
<div class="max-w-7xl mx-auto">
<div class="text-center mb-16 space-y-4">
<h2 class="text-4xl md:text-5xl font-black font-display text-zinc-800">Smart Savings in 3 Steps</h2>
<p class="text-zinc-500 text-lg max-w-2xl mx-auto">Stop guessing when to fill up. Our engine analyzes thousands of data points daily to save you money.</p>
</div>
<div class="grid md:grid-cols-3 gap-12">
<div class="text-center space-y-4">
<div class="w-16 h-16 bg-accent/10 text-accent rounded-2xl flex items-center justify-center mx-auto text-3xl">
<iconify-icon icon="lucide:search"></iconify-icon>
</div>
<h3 class="text-2xl font-bold font-display">1. Search</h3>
<p class="text-zinc-500">Enter your postcode or location to find every forecourt within a 520 mile radius instantly.</p>
</div>
<div class="text-center space-y-4">
<div class="w-16 h-16 bg-accent/10 text-accent rounded-2xl flex items-center justify-center mx-auto text-3xl">
<iconify-icon icon="lucide:trending-up"></iconify-icon>
</div>
<h3 class="text-2xl font-bold font-display">2. Get Advice</h3>
<p class="text-zinc-500">Our AI compares local prices against national wholesale trends to give you a Fill Up / Wait recommendation.</p>
</div>
<div class="text-center space-y-4">
<div class="w-16 h-16 bg-accent/10 text-accent rounded-2xl flex items-center justify-center mx-auto text-3xl">
<iconify-icon icon="lucide:wallet"></iconify-icon>
</div>
<h3 class="text-2xl font-bold font-display">3. Fill Up Smart</h3>
<p class="text-zinc-500">Navigate to the cheapest station and fill up with confidence knowing you've secured the best price.</p>
</div>
</div>
</div>
</section>
<!-- Features -->
<section id="features" class="py-12 md:py-24 px-6">
<div class="max-w-7xl mx-auto">
<div class="grid lg:grid-cols-2 gap-20 items-center">
<div class="order-2 lg:order-1">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="p-6 bg-zinc-50 border border-zinc-300 rounded-2xl space-y-3">
<iconify-icon class="text-3xl text-accent" icon="lucide:zap"></iconify-icon>
<h4 class="font-bold text-lg font-display">Real-Time Prices</h4>
<p class="text-sm text-zinc-500">Verified daily prices from thousands of UK forecourts.</p>
</div>
<div class="p-6 bg-zinc-50 border border-zinc-300 rounded-2xl space-y-3">
<iconify-icon class="text-3xl text-accent" icon="lucide:calendar"></iconify-icon>
<h4 class="font-bold text-lg font-display">Timing Predictions</h4>
<p class="text-sm text-zinc-500">Proprietary 14-day forecasts for petrol and diesel trends.</p>
</div>
<div class="p-6 bg-zinc-50 border border-zinc-300 rounded-2xl space-y-3">
<iconify-icon class="text-3xl text-accent" icon="lucide:shopping-bag"></iconify-icon>
<h4 class="font-bold text-lg font-display">Supermarket Anchors</h4>
<p class="text-sm text-zinc-500">Track local supermarkets to find the absolute lowest base price.</p>
</div>
<div class="p-6 bg-zinc-50 border border-zinc-300 rounded-2xl space-y-3">
<iconify-icon class="text-3xl text-accent" icon="lucide:bell-ring"></iconify-icon>
<h4 class="font-bold text-lg font-display">Smart Price Alerts</h4>
<p class="text-sm text-zinc-500">Get notified when local prices drop below your set target.</p>
</div>
</div>
</div>
<div class="order-1 lg:order-2 space-y-8">
<h2 class="text-4xl md:text-5xl font-black font-display text-zinc-800">The ultimate fuel companion.</h2>
<p class="text-lg text-zinc-500">Whether you're a daily commuter, a delivery professional, or just planning a weekend road trip, FuelAlert gives you the edge at the pump.</p>
<ul class="space-y-4">
<li class="flex items-center gap-3 font-bold">
<iconify-icon class="text-accent" icon="lucide:check-circle-2"></iconify-icon>
Coverage for 98% of UK Forecourts
</li>
<li class="flex items-center gap-3 font-bold">
<iconify-icon class="text-accent" icon="lucide:check-circle-2"></iconify-icon>
Hyper-local Map Visualization
</li>
<li class="flex items-center gap-3 font-bold">
<iconify-icon class="text-accent" icon="lucide:check-circle-2"></iconify-icon>
Historic Price Benchmarking
</li>
</ul>
<button class="inline-flex items-center gap-2 text-accent font-black text-lg group">
Explore all features
<iconify-icon class="group-hover:translate-x-1 transition-transform" icon="lucide:arrow-right"></iconify-icon>
</button>
</div>
</div>
</div>
</section>
<!-- Pricing -->
<section id="pricing" class="py-12 md:py-24 px-6 bg-zinc-50">
<div class="max-w-7xl mx-auto">
<div class="text-center mb-16">
<h2 class="text-4xl md:text-5xl font-black font-display text-zinc-800 mb-4">Pricing for every driver</h2>
<p class="text-zinc-500 text-lg mb-8">Save hundreds for less than the cost of a coffee.</p>
<div class="inline-flex items-center gap-1 p-1 bg-white border border-zinc-300 rounded-full">
<button
:class="cadence === 'monthly' ? 'bg-accent text-white' : 'text-zinc-500'"
class="px-5 py-2 rounded-full text-sm font-bold transition-colors"
type="button"
@click="cadence = 'monthly'"
>
Monthly
</button>
<button
:class="cadence === 'annual' ? 'bg-accent text-white' : 'text-zinc-500'"
class="px-5 py-2 rounded-full text-sm font-bold transition-colors"
type="button"
@click="cadence = 'annual'"
>
Annual <span class="text-[10px] opacity-80">(save 17%)</span>
</button>
</div>
</div>
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
<!-- Free -->
<div class="bg-white border border-zinc-300 p-8 rounded-3xl flex flex-col h-full">
<div class="mb-8">
<h4 class="text-xl font-bold font-display mb-2">Free</h4>
<div class="flex items-baseline gap-1">
<span class="text-4xl font-black">£0</span>
<span class="text-zinc-500 text-sm">/mo</span>
</div>
</div>
<ul class="space-y-4 mb-8 flex-1">
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> Basic Search</li>
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> Daily Updates</li>
<li class="text-sm flex gap-2 text-zinc-500"><iconify-icon class="text-zinc-300" icon="lucide:x"></iconify-icon> No Alerts</li>
</ul>
<a :href="ctaHref('free')" class="w-full py-3 px-4 border border-zinc-300 rounded-xl text-center font-bold hover:bg-zinc-100 transition-colors">{{ ctaLabel('free') }}</a>
</div>
<!-- Daily (backend: basic) -->
<div class="bg-white border border-zinc-300 p-8 rounded-3xl flex flex-col h-full">
<div class="mb-8">
<h4 class="text-xl font-bold font-display mb-2">Daily</h4>
<div class="flex items-baseline gap-1">
<span class="text-4xl font-black">{{ PRICES[cadence].basic }}</span>
<span class="text-zinc-500 text-sm">{{ PRICE_SUFFIX[cadence] }}</span>
</div>
</div>
<ul class="space-y-4 mb-8 flex-1">
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> Ad-free Experience</li>
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> 14-day Trend Data</li>
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> 3 Daily Price Alerts</li>
</ul>
<a :href="ctaHref('basic')" class="w-full py-3 px-4 border border-zinc-300 rounded-xl text-center font-bold hover:bg-zinc-100 transition-colors">{{ ctaLabel('basic') }}</a>
</div>
<!-- Smart (backend: plus) -->
<div class="bg-white border-2 border-accent p-8 rounded-3xl flex flex-col h-full relative">
<div class="absolute -top-4 left-1/2 -translate-x-1/2 bg-accent text-white px-4 py-1 rounded-full text-[10px] font-black uppercase tracking-widest whitespace-nowrap">Most pick this</div>
<div class="mb-8">
<h4 class="text-xl font-bold font-display mb-2">Smart</h4>
<div class="flex items-baseline gap-1">
<span class="text-4xl font-black text-accent">{{ PRICES[cadence].plus }}</span>
<span class="text-zinc-500 text-sm">{{ PRICE_SUFFIX[cadence] }}</span>
</div>
</div>
<ul class="space-y-4 mb-8 flex-1">
<li class="text-sm flex gap-2 font-bold"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> Supermarket Anchor</li>
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> Priority Price Alerts</li>
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> Multi-location tracking</li>
</ul>
<a :href="ctaHref('plus')" class="w-full py-3 px-4 bg-accent text-white rounded-xl text-center font-bold shadow-lg hover:bg-primary-dark transition-all">{{ ctaLabel('plus') }}</a>
</div>
<!-- Pro -->
<div class="bg-zinc-800 border border-zinc-800 p-8 rounded-3xl flex flex-col h-full text-white">
<div class="mb-8">
<h4 class="text-xl font-bold font-display mb-2">Pro</h4>
<div class="flex items-baseline gap-1">
<span class="text-4xl font-black">{{ PRICES[cadence].pro }}</span>
<span class="text-zinc-400 text-sm">{{ PRICE_SUFFIX[cadence] }}</span>
</div>
</div>
<ul class="space-y-4 mb-8 flex-1">
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:sparkles"></iconify-icon> AI Price Predictions</li>
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> Multi-Vehicle Fleet</li>
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> Exportable Price History</li>
</ul>
<a :href="ctaHref('pro')" class="w-full py-3 px-4 bg-white text-zinc-800 rounded-xl text-center font-bold hover:bg-zinc-100 transition-colors">{{ ctaLabel('pro') }}</a>
</div>
</div>
</div>
</section>
<!-- Testimonials -->
<section class="py-12 md:py-24 px-6">
<div class="max-w-7xl mx-auto">
<div class="flex flex-col md:flex-row gap-12 items-center">
<div class="md:w-1/3">
<h2 class="text-4xl font-black font-display text-zinc-800 mb-4">Loved by commuters.</h2>
<div class="flex items-center gap-1 text-status-warn mb-4 text-xl">
<iconify-icon icon="lucide:star"></iconify-icon>
<iconify-icon icon="lucide:star"></iconify-icon>
<iconify-icon icon="lucide:star"></iconify-icon>
<iconify-icon icon="lucide:star"></iconify-icon>
<iconify-icon icon="lucide:star"></iconify-icon>
</div>
<p class="text-zinc-500">Join thousands of UK drivers saving every single month.</p>
</div>
<div class="md:w-2/3 grid sm:grid-cols-2 gap-6">
<div class="p-6 bg-zinc-50 border border-zinc-300 rounded-2xl shadow-sm italic text-zinc-800">
"I used to just go to the station on my way home. Now I check FuelAlert and realise there's a station 2 miles away that's 5p cheaper! Over a month, it adds up to a free tank per year."
<div class="mt-4 flex items-center gap-3 not-italic">
<img alt="James R." class="w-10 h-10 rounded-full" src="https://api.dicebear.com/7.x/avataaars/svg?seed=John">
<div>
<p class="font-bold text-sm">James R.</p>
<p class="text-[10px] text-zinc-500 uppercase font-bold tracking-widest">Daily Commuter</p>
</div>
</div>
</div>
<div class="p-6 bg-zinc-50 border border-zinc-300 rounded-2xl shadow-sm italic text-zinc-800">
"The predictions are eerily accurate. I was going to fill up Friday, but FuelAlert said 'Hold on' for Monday. Sure enough, prices dropped at my local Tesco by 3p. Brilliant."
<div class="mt-4 flex items-center gap-3 not-italic">
<img alt="Sarah M." class="w-10 h-10 rounded-full" src="https://api.dicebear.com/7.x/avataaars/svg?seed=Sarah">
<div>
<p class="font-bold text-sm">Sarah M.</p>
<p class="text-[10px] text-zinc-500 uppercase font-bold tracking-widest">Delivery Driver</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="py-12 md:py-24 px-6 bg-accent text-white text-center">
<div class="max-w-3xl mx-auto space-y-8">
<h2 class="text-4xl md:text-5xl font-black font-display leading-tight">Ready to outsmart the pumps?</h2>
<p class="text-xl text-white/80">Sign up for free today and never pay over the odds for fuel again.</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a class="bg-white text-accent px-10 py-4 rounded-xl text-lg font-black shadow-2xl hover:bg-zinc-100 transition-all" href="/register">Create Free Account</a>
<a class="bg-transparent border-2 border-white/30 text-white px-10 py-4 rounded-xl text-lg font-bold hover:bg-white/10 transition-all" href="#">Watch Demo Video</a>
</div>
</div>
</section>
<!-- Footer -->
<footer class="bg-zinc-50 border-t border-zinc-300 pt-16 pb-8 px-6">
<div class="max-w-7xl mx-auto grid grid-cols-2 md:grid-cols-4 gap-12 mb-12">
<div class="col-span-2 md:col-span-1 space-y-4">
<RouterLink class="flex items-center gap-2" to="/">
<div class="w-8 h-8 rounded bg-accent flex items-center justify-center">
<iconify-icon class="text-white" icon="lucide:fuel"></iconify-icon>
</div>
<span class="text-xl font-black font-display tracking-tighter text-accent">FuelAlert</span>
</RouterLink>
<p class="text-sm text-zinc-500 leading-relaxed">
Helping UK drivers save money at the pump since 2021. Real-time data, smarter choices.
</p>
<div class="flex gap-4">
<iconify-icon class="text-2xl text-zinc-500 hover:text-accent cursor-pointer transition-colors" icon="mdi:twitter"></iconify-icon>
<iconify-icon class="text-2xl text-zinc-500 hover:text-accent cursor-pointer transition-colors" icon="mdi:facebook"></iconify-icon>
<iconify-icon class="text-2xl text-zinc-500 hover:text-accent cursor-pointer transition-colors" icon="mdi:instagram"></iconify-icon>
</div>
</div>
<div class="space-y-4">
<h5 class="font-black uppercase text-xs text-zinc-800 tracking-widest">Product</h5>
<ul class="space-y-2 text-sm text-zinc-500">
<li><a class="hover:text-accent transition-colors" href="#pricing">Pricing</a></li>
<li><a class="hover:text-accent transition-colors" href="#features">Features</a></li>
<li><a class="hover:text-accent transition-colors" href="#">FuelAlert Pro</a></li>
<li><a class="hover:text-accent transition-colors" href="#">Enterprise API</a></li>
</ul>
</div>
<div class="space-y-4">
<h5 class="font-black uppercase text-xs text-zinc-800 tracking-widest">Resources</h5>
<ul class="space-y-2 text-sm text-zinc-500">
<li><a class="hover:text-accent transition-colors" href="#">Market Insights</a></li>
<li><a class="hover:text-accent transition-colors" href="#">How We Track</a></li>
<li><a class="hover:text-accent transition-colors" href="#">Help Center</a></li>
<li><a class="hover:text-accent transition-colors" href="#">Driver Safety</a></li>
</ul>
</div>
<div class="space-y-4">
<h5 class="font-black uppercase text-xs text-zinc-800 tracking-widest">Legal</h5>
<ul class="space-y-2 text-sm text-zinc-500">
<li><a class="hover:text-accent transition-colors" href="#">Privacy Policy</a></li>
<li><a class="hover:text-accent transition-colors" href="#">Terms of Service</a></li>
<li><a class="hover:text-accent transition-colors" href="#">Cookie Settings</a></li>
</ul>
</div>
</div>
<div class="max-w-7xl mx-auto pt-8 border-t border-zinc-300 flex flex-col md:flex-row justify-between items-center gap-4 text-[10px] font-bold uppercase tracking-widest text-zinc-500">
<p>© 2024 FuelAlert UK Limited. All Rights Reserved.</p>
<p>Data provided by official UK retail price transparency schemes.</p>
<p>Postcode data from <a class="underline hover:text-accent" href="https://geoportal.statistics.gov.uk/datasets/ons::onspd-online-latest-centroids-1/about" rel="noopener" target="_blank">ONS Postcode Directory</a>: contains OS data © Crown copyright &amp; database right, Royal Mail data © Royal Mail copyright &amp; database right, and National Statistics data © Crown copyright &amp; database right.</p>
</div>
</footer>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted, nextTick, defineAsyncComponent } 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 PostSearchFilters from '../components/PostSearchFilters.vue'
import StationList from '../components/StationList.vue'
const LeafletMap = defineAsyncComponent(() => import('../components/LeafletMap.vue'))
import LandingNav from '../components/landing/LandingNav.vue'
import LiveTicker from '../components/landing/LiveTicker.vue'
import VerdictCard from '../components/landing/VerdictCard.vue'
import HeroSearch from '../components/landing/HeroSearch.vue'
import StatsRow from '../components/landing/StatsRow.vue'
const { isAuthenticated, userTier } = useAuth()
const liveStats = ref({ stationCount: null, latestPriceAt: null })
onMounted(async () => {
try {
const { data } = await api.get('/stats/live')
liveStats.value = {
stationCount: data.station_count,
latestPriceAt: data.latest_price_at,
}
} catch {
// leave defaults; ticker degrades to "Live · updated …" only
}
})
const cadence = ref('monthly')
function ctaHref(tier) {
if (tier === 'free') {
return isAuthenticated.value ? '/dashboard' : '/register'
}
if (!isAuthenticated.value) {
return '/register?tier=' + tier + '&cadence=' + cadence.value
}
if (userTier.value === tier) {
return '/billing/portal'
}
return '/billing/checkout/' + tier + '/' + cadence.value
}
function ctaLabel(tier) {
if (tier === 'free') {
return isAuthenticated.value ? 'Go to dashboard' : 'Start free'
}
if (isAuthenticated.value && userTier.value === tier) {
return 'Manage subscription'
}
return {
basic: 'Choose Daily',
plus: 'Choose Smart',
pro: 'Choose Pro',
}[tier]
}
const PRICES = {
monthly: { basic: '£0.99', plus: '£2.49', pro: '£3.99' },
annual: { basic: '£9.90', plus: '£24.90', pro: '£39.90' },
}
const PRICE_SUFFIX = { monthly: '/mo', annual: '/yr' }
const { stations, meta, loading, error, search, reset } = useStations()
watch(loading, (isLoading) => {
if (!isLoading) return
nextTick(() => {
window.scrollBy({ top: 40, behavior: 'smooth' })
})
})
const searchOrigin = computed(() => {
if (meta.value?.lat != null && meta.value?.lng != null) {
return { lat: meta.value.lat, lng: meta.value.lng }
}
return null
})
const route = useRoute()
const router = useRouter()
const sort = ref('reliable')
const lastParams = ref(null)
const searchAttempted = ref(false)
const radiusMiles = ref(10)
const brandFilter = ref('')
const MAP_STORAGE_KEY = 'fuel-price:map-open'
function readSavedMapOpen() {
try {
const v = localStorage.getItem(MAP_STORAGE_KEY)
if (v === null) return true
return v === '1'
} catch {
return true
}
}
const mapOpen = ref(readSavedMapOpen())
watch(mapOpen, (v) => {
try {
localStorage.setItem(MAP_STORAGE_KEY, v ? '1' : '0')
} catch {
// ignore quota / privacy-mode errors
}
})
const availableBrands = computed(() => {
const brands = new Set()
stations.value.forEach(s => {
if (s.brand) brands.add(s.brand)
})
return [...brands].sort((a, b) => a.localeCompare(b))
})
const filteredStations = computed(() => {
if (!brandFilter.value) return stations.value
return stations.value.filter(s => s.brand === brandFilter.value)
})
watch(() => stations.value, () => {
if (brandFilter.value && !availableBrands.value.includes(brandFilter.value)) {
brandFilter.value = ''
}
})
const searchInitial = computed(() => ({
postcode: route.query.postcode ?? '',
lat: route.query.lat ? Number(route.query.lat) : null,
lng: route.query.lng ? Number(route.query.lng) : null,
fuelType: route.query.fuel_type ?? 'e10',
radius: route.query.radius ? Number(route.query.radius) : 10,
sort: route.query.sort ?? 'reliable',
}))
function paramsFromQuery(query) {
const hasPostcode = typeof query.postcode === 'string' && query.postcode.trim().length > 0
const hasCoords = query.lat && query.lng
if (!hasPostcode && !hasCoords) return null
return {
postcode: hasPostcode ? query.postcode.trim() : null,
lat: hasCoords ? Number(query.lat) : null,
lng: hasCoords ? Number(query.lng) : null,
fuelType: query.fuel_type ?? 'e10',
radius: query.radius ? Number(query.radius) : 10,
sort: query.sort ?? 'reliable',
}
}
function queryFromParams(params) {
const q = {
fuel_type: params.fuelType,
radius: String(params.radius),
sort: params.sort,
}
if (params.postcode) {
q.postcode = params.postcode
} else if (params.lat && params.lng) {
q.lat = String(params.lat)
q.lng = String(params.lng)
}
return q
}
async function runSearch(params) {
lastParams.value = params
sort.value = params.sort ?? sort.value
radiusMiles.value = params.radius ?? radiusMiles.value
searchAttempted.value = true
await search(params)
}
async function onSearch(params) {
await router.push({ query: queryFromParams(params) })
await runSearch(params)
}
watch(() => route.query, (query) => {
const params = paramsFromQuery(query)
if (!params) {
searchAttempted.value = false
lastParams.value = null
brandFilter.value = ''
reset()
return
}
const sameAsLast = lastParams.value
&& JSON.stringify(queryFromParams(lastParams.value)) === JSON.stringify(queryFromParams(params))
if (sameAsLast) return
runSearch(params)
}, { immediate: true })
</script>