Redesign search UI with unified input, expandable filters, and integrated map controls
- Consolidate HeroSearch into single responsive form with inline geolocation button and submit actions - Transform SearchBar into pill-based filter bar with visual state indicators (active filters highlighted) - Move map toggle from separate component into SearchBar with open/close state management - Redesign StationList sort controls as pills with icons, move brand filter inline, add result count - Expand LeafletMap to full-width panel (96 viewport height) controlled by parent open state - Remove nested mobile/desktop layouts in HeroSearch in favor of single adaptive form - Add "Refine" and "Sort" labels to filter groups, implement clear-all filters button - Show verdict card only before first search on mobile, hide after results load - Position StatsRow within hero gradient, move results section into same gradient container - Update map initialization to only occur when panel is open, destroy on close - Add accessibility labels (aria-expanded, aria-controls) to map toggle button
This commit is contained in:
@@ -1,38 +1,28 @@
|
||||
<template>
|
||||
<div class="space-y-2">
|
||||
<button
|
||||
class="flex items-center gap-2 text-sm font-bold text-accent hover:text-accent-content transition-colors"
|
||||
@click="toggleMap"
|
||||
>
|
||||
<iconify-icon :icon="isOpen ? 'lucide:chevron-up' : 'lucide:chevron-down'"></iconify-icon>
|
||||
{{ isOpen ? 'Hide map' : 'Show map' }}
|
||||
</button>
|
||||
<div v-if="isOpen" id="leaflet-map-panel" class="space-y-2">
|
||||
<div
|
||||
ref="mapContainer"
|
||||
class="w-full h-96 md:h-160 rounded-2xl overflow-hidden border border-zinc-300 shadow-sm"
|
||||
></div>
|
||||
|
||||
<template v-if="isOpen">
|
||||
<div
|
||||
ref="mapContainer"
|
||||
class="w-full h-72 rounded-2xl overflow-hidden border border-zinc-300 shadow-sm"
|
||||
></div>
|
||||
|
||||
<div class="flex flex-wrap gap-3 text-xs text-zinc-500">
|
||||
<span class="flex items-center gap-1.5">
|
||||
<span class="inline-block size-3 rounded-full bg-green-500"></span>
|
||||
Current (<24h)
|
||||
</span>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<span class="inline-block size-3 rounded-full bg-slate-500"></span>
|
||||
Recent (24–48h)
|
||||
</span>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<span class="inline-block size-3 rounded-full bg-amber-500"></span>
|
||||
Stale (2–5 days)
|
||||
</span>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<span class="inline-block size-3 rounded-full bg-red-500"></span>
|
||||
Outdated (5+ days)
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="flex flex-wrap gap-3 text-xs text-zinc-500">
|
||||
<span class="flex items-center gap-1.5">
|
||||
<span class="inline-block size-3 rounded-full bg-green-500"></span>
|
||||
Current (<24h)
|
||||
</span>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<span class="inline-block size-3 rounded-full bg-slate-500"></span>
|
||||
Recent (24–48h)
|
||||
</span>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<span class="inline-block size-3 rounded-full bg-amber-500"></span>
|
||||
Stale (2–5 days)
|
||||
</span>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<span class="inline-block size-3 rounded-full bg-red-500"></span>
|
||||
Outdated (5+ days)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -87,13 +77,12 @@ function escHtml(str) {
|
||||
|
||||
const props = defineProps({
|
||||
stations: {type: Array, required: true},
|
||||
defaultOpen: {type: Boolean, default: false},
|
||||
isOpen: {type: Boolean, default: true},
|
||||
radiusMiles: {type: Number, default: 10},
|
||||
origin: {type: Object, default: null},
|
||||
})
|
||||
|
||||
const mapContainer = ref(null)
|
||||
const isOpen = ref(false)
|
||||
let mapInstance = null
|
||||
let markersLayer = null
|
||||
let userMarker = null
|
||||
@@ -102,8 +91,9 @@ function getZoomForRadius(radiusMiles) {
|
||||
if (radiusMiles <= 1) return 16
|
||||
if (radiusMiles <= 2) return 15
|
||||
if (radiusMiles <= 5) return 14
|
||||
if (radiusMiles <= 10) return 13
|
||||
if (radiusMiles <= 15) return 11
|
||||
if (radiusMiles <= 10) return 12
|
||||
if (radiusMiles <= 15) return 12
|
||||
if (radiusMiles <= 20) return 10
|
||||
if (radiusMiles <= 25) return 10
|
||||
if (radiusMiles <= 50) return 9
|
||||
return 8
|
||||
@@ -174,6 +164,10 @@ function initMap() {
|
||||
|
||||
markersLayer = L.layerGroup().addTo(mapInstance)
|
||||
|
||||
mapInstance.on('zoomend', () => {
|
||||
console.log('Map zoom:', mapInstance.getZoom())
|
||||
})
|
||||
|
||||
locateUser()
|
||||
}
|
||||
|
||||
@@ -238,47 +232,45 @@ function renderMarkers() {
|
||||
})
|
||||
|
||||
const zoom = getZoomForRadius(props.radiusMiles)
|
||||
const center = props.origin?.lat != null && props.origin?.lng != null
|
||||
? [props.origin.lat, props.origin.lng]
|
||||
: bounds[0]
|
||||
|
||||
if (bounds.length === 1) {
|
||||
mapInstance.setView(bounds[0], zoom)
|
||||
} else {
|
||||
mapInstance.fitBounds(bounds, {padding: [40, 40], maxZoom: zoom})
|
||||
}
|
||||
mapInstance.setView(center, zoom)
|
||||
}
|
||||
|
||||
async function toggleMap() {
|
||||
isOpen.value = !isOpen.value
|
||||
|
||||
if (isOpen.value) {
|
||||
await nextTick()
|
||||
initMap()
|
||||
mapInstance.invalidateSize()
|
||||
renderMarkers()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (props.defaultOpen) {
|
||||
isOpen.value = true
|
||||
await nextTick()
|
||||
initMap()
|
||||
mapInstance.invalidateSize()
|
||||
renderMarkers()
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.stations, () => {
|
||||
if (isOpen.value) {
|
||||
renderMarkers()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
function destroyMap() {
|
||||
if (mapInstance) {
|
||||
mapInstance.remove()
|
||||
mapInstance = null
|
||||
markersLayer = null
|
||||
userMarker = null
|
||||
}
|
||||
}
|
||||
|
||||
async function openMap() {
|
||||
await nextTick()
|
||||
initMap()
|
||||
mapInstance?.invalidateSize()
|
||||
renderMarkers()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.isOpen) openMap()
|
||||
})
|
||||
|
||||
watch(() => props.isOpen, (open) => {
|
||||
if (open) openMap()
|
||||
else destroyMap()
|
||||
})
|
||||
|
||||
watch(() => props.stations, () => {
|
||||
if (props.isOpen) {
|
||||
renderMarkers()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(destroyMap)
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
Reference in New Issue
Block a user