Rename SearchBar to PostSearchFilters, add sort controls and brand filter, relocate station count display
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

- Move SearchBar.vue to PostSearchFilters.vue and expand to include sort buttons, brand filter dropdown, and station count
- Integrate sort controls (Reliable/Price/Distance/Updated) with icons into filter bar
- Add brand filter dropdown with dynamic brand list from parent, emit update events
- Move station count from StationList to PostSearchFilters, display as "X station(s) found"
- Remove sort tabs and brand filter from StationList component
- Add force-new-line div for mobile layout between Refine and Sort groups
- Include brand filter in hasActive check and resetFilters function
- Update Home.vue to pass brands/brandFilter props and handle brandFilter updates
- Add reset() method to useStations composable to clear state on empty query
- Clear search state when route query is empty instead of attempting search
- Update Fuel Finder API base URL to include /api/v1 path
- Adjust map zoom levels for 10-15 mile radius range
- Update API token request to use retry and increase timeout to 60s
This commit is contained in:
Ovidiu U
2026-04-22 11:50:59 +01:00
parent 8335f49fd6
commit b4bd78ab4c
7 changed files with 96 additions and 95 deletions

View File

@@ -91,8 +91,8 @@ function getZoomForRadius(radiusMiles) {
if (radiusMiles <= 1) return 16
if (radiusMiles <= 2) return 15
if (radiusMiles <= 5) return 14
if (radiusMiles <= 10) return 12
if (radiusMiles <= 15) return 12
if (radiusMiles <= 10) return 11
if (radiusMiles <= 15) return 11
if (radiusMiles <= 20) return 10
if (radiusMiles <= 25) return 10
if (radiusMiles <= 50) return 9

View File

@@ -1,11 +1,10 @@
<template>
<div class="flex flex-wrap items-center gap-2 md:gap-2.5">
<!-- Leading label -->
<div class="flex flex-wrap items-center gap-2 md:gap-2.5 py-3 border-b border-zinc-200">
<!-- Refine group -->
<span class="hidden md:inline text-xs font-mono uppercase tracking-widest text-zinc-400 mr-1">
Refine
</span>
<!-- Fuel type -->
<label :class="{ 'is-active': fuelType !== DEFAULTS.fuelType }" class="pill group">
<iconify-icon class="text-sm opacity-70" icon="lucide:fuel"></iconify-icon>
<span class="text-sm font-medium">{{ fuelLabel }}</span>
@@ -14,13 +13,12 @@
v-model="fuelType"
aria-label="Fuel type"
class="absolute inset-0 opacity-0 cursor-pointer"
name="fuelType"
name="fuelType"
>
<option v-for="fuel in FUEL_TYPES" :key="fuel.value" :value="fuel.value">{{ fuel.label }}</option>
</select>
</label>
<!-- Radius -->
<label :class="{ 'is-active': radius !== DEFAULTS.radius }" class="pill group">
<iconify-icon class="text-sm opacity-70" icon="lucide:circle-dot"></iconify-icon>
<span class="text-sm font-medium">{{ radius }} miles</span>
@@ -29,7 +27,7 @@
v-model.number="radius"
aria-label="Search radius"
class="absolute inset-0 opacity-0 cursor-pointer"
name="radius"
name="radius"
>
<option :value="5">5 miles</option>
<option :value="10">10 miles</option>
@@ -37,12 +35,11 @@
</select>
</label>
<!-- Show / hide map -->
<button
:class="{ 'is-active': mapOpen }"
class="pill"
:aria-expanded="mapOpen"
:class="{ 'is-active': mapOpen }"
aria-controls="leaflet-map-panel"
class="pill"
type="button"
@click="emit('toggle-map')"
>
@@ -55,18 +52,58 @@
></iconify-icon>
</button>
<!-- Divider + clear (only when any active) -->
<template v-if="hasActive">
<span class="hidden md:inline h-5 w-px bg-zinc-200 mx-1"></span>
<button
class="inline-flex items-center gap-1 h-10 px-3 text-sm text-zinc-500 hover:text-zinc-900 transition-colors"
type="button"
@click="resetFilters"
<button
v-if="hasActive"
class="inline-flex items-center gap-1 h-10 px-3 text-sm text-zinc-500 hover:text-zinc-900 transition-colors cursor-pointer"
type="button"
@click="resetFilters"
>
<iconify-icon class="text-sm" icon="lucide:x"></iconify-icon>
Clear
</button>
<!-- Force Sort to a new line on mobile only -->
<div aria-hidden="true" class="basis-full md:hidden"></div>
<!-- Sort group -->
<span class="hidden md:inline text-xs font-mono uppercase tracking-widest text-zinc-400 mx-1">
Sort
</span>
<button
v-for="option in sortOptions"
:key="option.value"
:class="{ 'is-active': sort === option.value }"
class="pill"
type="button"
@click="sort = option.value"
>
<iconify-icon :icon="option.icon" class="text-sm opacity-70"></iconify-icon>
<span class="text-sm font-medium">{{ option.label }}</span>
</button>
<label
v-if="brands.length > 1"
:class="{ 'is-active': brandFilter }"
class="pill group"
>
<iconify-icon class="text-sm opacity-70" icon="lucide:tag"></iconify-icon>
<span class="text-sm font-medium">{{ brandFilter || 'All brands' }}</span>
<iconify-icon class="text-sm opacity-50 group-hover:opacity-100" icon="lucide:chevron-down"></iconify-icon>
<select
:value="brandFilter"
aria-label="Filter by brand"
class="absolute inset-0 opacity-0 cursor-pointer"
@change="emit('update:brandFilter', $event.target.value)"
>
<iconify-icon class="text-sm" icon="lucide:x"></iconify-icon>
Clear
</button>
</template>
<option value="">All brands</option>
<option v-for="brand in brands" :key="brand" :value="brand">{{ brand }}</option>
</select>
</label>
<span class="ml-auto text-sm text-zinc-500 font-medium">
{{ stationCount }} station{{ stationCount !== 1 ? 's' : '' }} found
</span>
</div>
</template>
@@ -80,13 +117,22 @@ const DEFAULTS = Object.freeze({
sort: 'reliable',
})
const sortOptions = [
{ label: 'Reliable', value: 'reliable', icon: 'lucide:shield-check' },
{ label: 'Price', value: 'price', icon: 'lucide:pound-sterling' },
{ label: 'Distance', value: 'distance', icon: 'lucide:map-pin' },
{ label: 'Updated', value: 'updated', icon: 'lucide:clock' },
]
const props = defineProps({
initial: { type: Object, default: () => ({}) },
resultCount: { type: Number, default: null },
brands: { type: Array, default: () => [] },
brandFilter: { type: String, default: '' },
mapOpen: { type: Boolean, default: true },
stationCount: { type: Number, default: 0 },
})
const emit = defineEmits(['search', 'toggle-map'])
const emit = defineEmits(['search', 'toggle-map', 'update:brandFilter'])
const postcode = ref('')
const coords = ref(null)
@@ -120,12 +166,14 @@ const hasActive = computed(() => (
fuelType.value !== DEFAULTS.fuelType
|| radius.value !== DEFAULTS.radius
|| sort.value !== DEFAULTS.sort
|| Boolean(props.brandFilter)
))
function resetFilters() {
fuelType.value = DEFAULTS.fuelType
radius.value = DEFAULTS.radius
sort.value = DEFAULTS.sort
if (props.brandFilter) emit('update:brandFilter', '')
}
function emitSearch() {

View File

@@ -1,47 +1,5 @@
<template>
<div class="space-y-3">
<!-- Sort tabs -->
<div class="flex flex-wrap items-center gap-2 md:gap-2.5 py-3 border-b border-zinc-200">
<span class="hidden md:inline text-xs font-mono uppercase tracking-widest text-zinc-400 mr-1">
Sort
</span>
<button
v-for="option in sortOptions"
:key="option.value"
:class="{ 'is-active': currentSort === option.value }"
class="pill"
type="button"
@click="emit('sort', option.value)"
>
<iconify-icon :icon="option.icon" class="text-sm opacity-70"></iconify-icon>
<span class="text-sm font-medium">{{ option.label }}</span>
</button>
<!-- Brand filter -->
<label
v-if="brands.length > 1"
:class="{ 'is-active': brandFilter }"
class="pill group"
>
<iconify-icon class="text-sm opacity-70" icon="lucide:tag"></iconify-icon>
<span class="text-sm font-medium">{{ brandFilter || 'All brands' }}</span>
<iconify-icon class="text-sm opacity-50 group-hover:opacity-100" icon="lucide:chevron-down"></iconify-icon>
<select
:value="brandFilter"
aria-label="Filter by brand"
class="absolute inset-0 opacity-0 cursor-pointer"
@change="emit('update:brandFilter', $event.target.value)"
>
<option value="">All brands</option>
<option v-for="brand in brands" :key="brand" :value="brand">{{ brand }}</option>
</select>
</label>
<span class="ml-auto text-sm text-zinc-500 font-medium">
{{ stations.length }} station{{ stations.length !== 1 ? 's' : '' }} found
</span>
</div>
<!-- Grouped results when sorting by reliability -->
<template v-if="currentSort === 'reliable'">
<section v-if="reliable.length" class="space-y-2">
@@ -121,19 +79,8 @@ const props = defineProps({
stations: { type: Array, required: true },
currentSort: { type: String, default: 'reliable' },
origin: { type: Object, default: null },
brands: { type: Array, default: () => [] },
brandFilter: { type: String, default: '' },
})
const emit = defineEmits(['sort', 'update:brandFilter'])
const sortOptions = [
{ label: 'Reliable', value: 'reliable', icon: 'lucide:shield-check' },
{ label: 'Price', value: 'price', icon: 'lucide:pound-sterling' },
{ label: 'Distance', value: 'distance', icon: 'lucide:map-pin' },
{ label: 'Updated', value: 'updated', icon: 'lucide:clock' },
]
const reliable = computed(() => props.stations.filter(s => s.reliability === 'reliable'))
const stale = computed(() => props.stations.filter(s => s.reliability === 'stale'))
const outdated = computed(() => props.stations.filter(s => s.reliability === 'outdated'))