feat: redesign homepage with responsive hero, verdict card preview, and modular landing components
- 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
This commit is contained in:
127
resources/js/components/landing/HeroSearch.vue
Normal file
127
resources/js/components/landing/HeroSearch.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user