Files
fuel-price/resources/js/components/SearchBar.vue
Ovidiu U 8335f49fd6
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
Redesign station cards with compact layout, improved typography, and expandable details
- Reduce header size and weight, convert all-caps brand names to title case
- Replace address line with distance-only in collapsed state, move brand label to expanded section
- Apply monospace font to pricing, reduce size and weight across labels
- Move badge list and full details into expandable section
- Normalize font weights throughout (semibold for headings, medium for labels)
- Create `.pill` component class with `.is-active` state for consistent filter styling
- Apply pill styling to SearchBar filters, StationList sort buttons, and brand filter
- Add `name` attributes to fuel type and radius selects
- Update package dependencies (@tailwindcss/node, @tailwindcss/oxide, @rolldown/*)
2026-04-22 11:23:05 +01:00

146 lines
5.0 KiB
Vue

<template>
<div class="flex flex-wrap items-center gap-2 md:gap-2.5">
<!-- Leading label -->
<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>
<iconify-icon class="text-sm opacity-50 group-hover:opacity-100" icon="lucide:chevron-down"></iconify-icon>
<select
v-model="fuelType"
aria-label="Fuel type"
class="absolute inset-0 opacity-0 cursor-pointer"
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>
<iconify-icon class="text-sm opacity-50 group-hover:opacity-100" icon="lucide:chevron-down"></iconify-icon>
<select
v-model.number="radius"
aria-label="Search radius"
class="absolute inset-0 opacity-0 cursor-pointer"
name="radius"
>
<option :value="5">5 miles</option>
<option :value="10">10 miles</option>
<option :value="20">20 miles</option>
</select>
</label>
<!-- Show / hide map -->
<button
:class="{ 'is-active': mapOpen }"
class="pill"
:aria-expanded="mapOpen"
aria-controls="leaflet-map-panel"
type="button"
@click="emit('toggle-map')"
>
<iconify-icon :icon="mapOpen ? 'lucide:map' : 'lucide:map-off'" class="text-sm opacity-70"></iconify-icon>
<span class="text-sm font-medium">{{ mapOpen ? 'Hide map' : 'Show map' }}</span>
<iconify-icon
:class="mapOpen ? 'rotate-180' : ''"
class="text-sm opacity-60 transition-transform duration-200"
icon="lucide:chevron-down"
></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"
>
<iconify-icon class="text-sm" icon="lucide:x"></iconify-icon>
Clear
</button>
</template>
</div>
</template>
<script setup>
import { ref, computed, watch, nextTick } from 'vue'
import { FUEL_TYPES } from '../constants/fuelTypes.js'
const DEFAULTS = Object.freeze({
fuelType: 'e10',
radius: 10,
sort: 'reliable',
})
const props = defineProps({
initial: { type: Object, default: () => ({}) },
resultCount: { type: Number, default: null },
mapOpen: { type: Boolean, default: true },
})
const emit = defineEmits(['search', 'toggle-map'])
const postcode = ref('')
const coords = ref(null)
const fuelType = ref(DEFAULTS.fuelType)
const radius = ref(DEFAULTS.radius)
const sort = ref(DEFAULTS.sort)
let hydrating = false
watch(() => props.initial, (v) => {
if (!v) return
hydrating = true
if (typeof v.postcode === 'string') postcode.value = v.postcode
if (v.lat != null && v.lng != null) coords.value = { lat: v.lat, lng: v.lng }
if (v.fuelType) fuelType.value = v.fuelType
if (v.radius != null) radius.value = Number(v.radius)
if (v.sort) sort.value = v.sort
nextTick(() => { hydrating = false })
}, { immediate: true, deep: true })
watch([fuelType, radius, sort], () => {
if (hydrating) return
if (postcode.value.trim() || coords.value) emitSearch()
})
const fuelLabel = computed(() => {
return FUEL_TYPES.find(f => f.value === fuelType.value)?.label ?? 'Fuel'
})
const hasActive = computed(() => (
fuelType.value !== DEFAULTS.fuelType
|| radius.value !== DEFAULTS.radius
|| sort.value !== DEFAULTS.sort
))
function resetFilters() {
fuelType.value = DEFAULTS.fuelType
radius.value = DEFAULTS.radius
sort.value = DEFAULTS.sort
}
function emitSearch() {
const hasPostcode = postcode.value.trim().length > 0
const hasCoords = coords.value !== null
if (!hasPostcode && !hasCoords) return
emit('search', {
postcode: hasPostcode ? postcode.value.trim() : null,
lat: hasCoords ? coords.value.lat : null,
lng: hasCoords ? coords.value.lng : null,
fuelType: fuelType.value,
radius: radius.value,
sort: sort.value,
})
}
</script>