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:
45
resources/js/components/landing/LiveTicker.vue
Normal file
45
resources/js/components/landing/LiveTicker.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div class="inline-flex items-center gap-2 font-mono text-[11px] uppercase tracking-[0.15em] text-zinc-600">
|
||||
<span class="size-1.5 rounded-full bg-status-good live-dot"></span>
|
||||
<span class="font-medium">Live</span>
|
||||
<span v-if="stationCount" aria-hidden="true">·</span>
|
||||
<span v-if="stationCount">{{ formattedStationCount }} UK stations</span>
|
||||
<span aria-hidden="true">·</span>
|
||||
<span>updated {{ updatedAgo || '…' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
stationCount: { type: Number, default: null },
|
||||
latestPriceAt: { type: String, default: null },
|
||||
})
|
||||
|
||||
const now = ref(Date.now())
|
||||
let ticker = null
|
||||
|
||||
const formattedStationCount = computed(() => {
|
||||
return props.stationCount == null ? '' : props.stationCount.toLocaleString('en-GB')
|
||||
})
|
||||
|
||||
const updatedAgo = computed(() => {
|
||||
if (!props.latestPriceAt) return ''
|
||||
const diffMin = Math.floor((now.value - new Date(props.latestPriceAt).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(() => {
|
||||
ticker = setInterval(() => { now.value = Date.now() }, 60000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (ticker) clearInterval(ticker)
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user