- Extract LandingNav, LiveTicker, StatsRow, VerdictCard, and HeroSearch into reusable landing components - Implement responsive two-layout strategy: mobile stacked (hero search + verdict card + CTA) vs desktop inline pill input with verdict card sidebar - Add serif/mono font tokens and live-dot pulse animation to CSS - Move verdict card above search input on mobile, to right sidebar on desktop - Replace hero "fill up now" mockup with dynamic VerdictCard showing top stations, pricing, and recommendation - Simplify navigation with uppercase tracking, add Fleet anchor, and gate CTA by auth state - Lazy-load LeafletMap with defineAsyncComponent to reduce initial bundle - Relocate SearchBar below hero on search attempt for persistent filter UI - Add meta description for SEO
128 lines
5.2 KiB
Vue
128 lines
5.2 KiB
Vue
<template>
|
|
<div class="w-full max-w-xl">
|
|
<!-- Mobile layout: stacked input + full-width geolocation CTA -->
|
|
<div class="md:hidden space-y-3">
|
|
<label class="relative block">
|
|
<span class="sr-only">Postcode</span>
|
|
<iconify-icon
|
|
class="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-500"
|
|
icon="lucide:map-pin"
|
|
style="font-size:18px;"
|
|
></iconify-icon>
|
|
<input
|
|
v-model="postcode"
|
|
class="w-full h-[52px] pl-11 pr-4 bg-white border border-zinc-300 rounded-xl text-base text-zinc-800 placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-accent/40 focus:border-accent"
|
|
placeholder="Enter a UK postcode"
|
|
type="text"
|
|
@keyup.enter="submitPostcode"
|
|
/>
|
|
</label>
|
|
<button
|
|
:disabled="locating"
|
|
class="w-full h-14 bg-accent text-white rounded-xl font-semibold text-base flex items-center justify-center gap-2 shadow-lg hover:bg-primary-dark transition-all disabled:opacity-70"
|
|
type="button"
|
|
@click="useMyLocation"
|
|
>
|
|
<iconify-icon :class="{ 'animate-spin': locating }" :icon="locating ? 'lucide:loader-circle' : 'lucide:locate-fixed'" style="font-size:20px;"></iconify-icon>
|
|
{{ locating ? 'Getting location…' : 'Use my location' }}
|
|
</button>
|
|
<p class="font-mono text-[11px] text-zinc-500 text-center">Free · no signup to try</p>
|
|
</div>
|
|
|
|
<!-- Desktop layout: inline postcode pill + geo link below -->
|
|
<div class="hidden md:block">
|
|
<div class="flex items-stretch bg-white border border-zinc-300 rounded-full h-[60px] pl-5 pr-1.5 shadow-sm focus-within:ring-2 focus-within:ring-accent/40 focus-within:border-accent">
|
|
<iconify-icon
|
|
class="self-center text-zinc-500 mr-3"
|
|
icon="lucide:map-pin"
|
|
style="font-size:18px;"
|
|
></iconify-icon>
|
|
<input
|
|
v-model="postcode"
|
|
class="flex-1 bg-transparent text-base text-zinc-800 placeholder:text-zinc-500 focus:outline-none"
|
|
placeholder="Enter a UK postcode"
|
|
type="text"
|
|
@keyup.enter="submitPostcode"
|
|
/>
|
|
<button
|
|
:disabled="!postcode.trim()"
|
|
class="my-1.5 px-6 bg-accent text-white rounded-full font-semibold text-sm hover:bg-primary-dark transition-all disabled:opacity-40 disabled:cursor-not-allowed"
|
|
type="button"
|
|
@click="submitPostcode"
|
|
>
|
|
Check prices
|
|
</button>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-3 mt-3 text-[13px]">
|
|
<button
|
|
:disabled="locating"
|
|
class="inline-flex items-center gap-1.5 font-medium text-zinc-700 hover:text-accent transition-colors disabled:opacity-70"
|
|
type="button"
|
|
@click="useMyLocation"
|
|
>
|
|
<iconify-icon :class="{ 'animate-spin': locating }" :icon="locating ? 'lucide:loader-circle' : 'lucide:locate-fixed'" style="font-size:14px;"></iconify-icon>
|
|
{{ locating ? 'Getting location…' : 'Use my location' }}
|
|
</button>
|
|
<span aria-hidden="true" class="text-zinc-400">·</span>
|
|
<span class="font-mono text-zinc-500">Try SW1A 1AA · M1 1AD · EH1 1YZ</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, watch } from 'vue'
|
|
|
|
const props = defineProps({
|
|
initial: { type: Object, default: () => ({}) },
|
|
fuelType: { type: String, default: 'e10' },
|
|
radius: { type: Number, default: 10 },
|
|
sort: { type: String, default: 'reliable' },
|
|
})
|
|
|
|
const emit = defineEmits(['search'])
|
|
|
|
const postcode = ref('')
|
|
const locating = ref(false)
|
|
|
|
watch(() => props.initial, (v) => {
|
|
if (!v) return
|
|
if (typeof v.postcode === 'string') postcode.value = v.postcode
|
|
}, { immediate: true, deep: true })
|
|
|
|
function submitPostcode() {
|
|
const trimmed = postcode.value.trim()
|
|
if (!trimmed) return
|
|
emit('search', {
|
|
postcode: trimmed,
|
|
lat: null,
|
|
lng: null,
|
|
fuelType: props.fuelType,
|
|
radius: props.radius,
|
|
sort: props.sort,
|
|
})
|
|
}
|
|
|
|
function useMyLocation() {
|
|
if (!navigator.geolocation) return
|
|
locating.value = true
|
|
navigator.geolocation.getCurrentPosition(
|
|
({ coords }) => {
|
|
locating.value = false
|
|
postcode.value = ''
|
|
emit('search', {
|
|
postcode: null,
|
|
lat: coords.latitude,
|
|
lng: coords.longitude,
|
|
fuelType: props.fuelType,
|
|
radius: props.radius,
|
|
sort: props.sort,
|
|
})
|
|
},
|
|
() => { locating.value = false },
|
|
{ timeout: 8000, enableHighAccuracy: false, maximumAge: 30000 },
|
|
)
|
|
}
|
|
</script>
|