Files
fuel-price/resources/js/components/landing/HeroSearch.vue
Ovidiu U d822b77fb0
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
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
2026-04-20 20:27:02 +01:00

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>