# Homepage Search Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Wire up the homepage `SearchBar` to perform a live station search and render a map + station list inline below the hero section.

**Architecture:** `SearchBar` emits `{ postcode, fuelType, radius }` on button click. `Home.vue` calls `useStations.search()` with those params and renders `LeafletMap` (open by default) + `StationList` in a full-width section below the hero. `LeafletMap` gains a `defaultOpen` prop to auto-initialise without user toggle.

**Tech Stack:** Vue 3 Composition API (`<script setup>`), Leaflet.js, `useStations` composable, Tailwind CSS v4

---

## File Map

| File | Change |
|---|---|
| `resources/js/components/SearchBar.vue` | Add fuel type + radius selects; change emit to object; remove debounce |
| `resources/js/components/LeafletMap.vue` | Add `defaultOpen` prop; init map on mount when true |
| `resources/js/views/Home.vue` | Wire `useStations`; hold `lastParams`/`sort`; render results section |

---

## Task 1: Update SearchBar — params + layout

**Files:**
- Modify: `resources/js/components/SearchBar.vue`

- [ ] **Step 1: Replace SearchBar.vue with the new implementation**

```vue
<template>
    <div class="flex flex-col gap-3 max-w-md w-full">
        <!-- Row 1: postcode + button -->
        <div class="relative flex flex-col sm:flex-row gap-3">
            <div class="relative flex-1">
                <span class="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-500">
                    <iconify-icon icon="lucide:map-pin" style="font-size:1.25rem"></iconify-icon>
                </span>
                <input
                    v-model="postcode"
                    type="text"
                    placeholder="Enter postcode, e.g. SW1A 1AA"
                    class="w-full h-14 pl-12 pr-4 bg-white border border-zinc-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-accent shadow-inner text-base"
                />
            </div>
            <button
                @click="onSearch"
                :disabled="!postcode.trim()"
                class="h-14 px-8 bg-accent text-white rounded-xl font-bold text-base shadow-xl hover:bg-accent-content transition-all disabled:opacity-50 disabled:cursor-not-allowed"
            >
                Find Prices
            </button>
        </div>

        <!-- Row 2: fuel type + radius -->
        <div class="flex gap-3">
            <select
                v-model="fuelType"
                class="flex-1 h-10 px-3 bg-white border border-zinc-300 rounded-xl text-sm font-medium text-zinc-700 focus:outline-none focus:ring-2 focus:ring-accent"
            >
                <option value="e10">Petrol (E10)</option>
                <option value="e5">Premium Petrol (E5)</option>
                <option value="b7_standard">Diesel (B7)</option>
                <option value="b7_premium">Premium Diesel (B7)</option>
                <option value="b10">Diesel (B10)</option>
                <option value="hvo">HVO</option>
            </select>
            <select
                v-model="radius"
                class="w-32 h-10 px-3 bg-white border border-zinc-300 rounded-xl text-sm font-medium text-zinc-700 focus:outline-none focus:ring-2 focus:ring-accent"
            >
                <option :value="2">2 miles</option>
                <option :value="5">5 miles</option>
                <option :value="10">10 miles</option>
                <option :value="20">20 miles</option>
            </select>
        </div>
    </div>
</template>

<script setup>
import { ref } from 'vue'

const emit = defineEmits(['search'])

const postcode = ref('')
const fuelType = ref('e10')
const radius = ref(10)

function onSearch() {
    if (!postcode.value.trim()) return
    emit('search', {
        postcode: postcode.value.trim(),
        fuelType: fuelType.value,
        radius: radius.value,
    })
}
</script>
```

- [ ] **Step 2: Commit**

```bash
git add resources/js/components/SearchBar.vue
git commit -m "feat: add fuel type and radius selects to SearchBar"
```

---

## Task 2: Update LeafletMap — defaultOpen prop

**Files:**
- Modify: `resources/js/components/LeafletMap.vue`

- [ ] **Step 1: Add `defaultOpen` prop and auto-init on mount**

Replace the `<script setup>` block in `resources/js/components/LeafletMap.vue`:

```vue
<script setup>
import { ref, watch, onMounted, onUnmounted, nextTick } from 'vue'
import L from 'leaflet'
import 'leaflet/dist/leaflet.css'

// Fix Leaflet default marker icon path broken by Vite
delete L.Icon.Default.prototype._getIconUrl
L.Icon.Default.mergeOptions({
    iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png',
    iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png',
    shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
})

const props = defineProps({
    stations: { type: Array, required: true },
    defaultOpen: { type: Boolean, default: false },
})

const mapContainer = ref(null)
const isOpen = ref(false)
let mapInstance = null
let markersLayer = null

function initMap() {
    if (mapInstance || !mapContainer.value) return

    mapInstance = L.map(mapContainer.value)

    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: '© OpenStreetMap contributors',
    }).addTo(mapInstance)

    markersLayer = L.layerGroup().addTo(mapInstance)
}

function renderMarkers() {
    if (!mapInstance || !markersLayer) return

    markersLayer.clearLayers()

    if (!props.stations.length) return

    const bounds = []

    props.stations.forEach(station => {
        const marker = L.marker([station.lat, station.lng])
            .bindPopup(`<strong>${station.name}</strong><br>${station.price}p`)
        markersLayer.addLayer(marker)
        bounds.push([station.lat, station.lng])
    })

    if (bounds.length) {
        mapInstance.fitBounds(bounds, { padding: [30, 30] })
    }
}

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(() => {
    if (mapInstance) {
        mapInstance.remove()
        mapInstance = null
    }
})
</script>
```

- [ ] **Step 2: Commit**

```bash
git add resources/js/components/LeafletMap.vue
git commit -m "feat: add defaultOpen prop to LeafletMap"
```

---

## Task 3: Wire up Home.vue — results section

**Files:**
- Modify: `resources/js/views/Home.vue`

- [ ] **Step 1: Update the script block**

Replace the `<script setup>` block at the bottom of `resources/js/views/Home.vue`:

```vue
<script setup>
import { ref } from 'vue'
import { RouterLink } from 'vue-router'
import { useAuth } from '../composables/useAuth.js'
import { useStations } from '../composables/useStations.js'
import SearchBar from '../components/SearchBar.vue'
import LeafletMap from '../components/LeafletMap.vue'
import StationList from '../components/StationList.vue'

const { isAuthenticated } = useAuth()
const { stations, loading, error, search } = useStations()

const sort = ref('price')
const lastParams = ref(null)
const searchAttempted = ref(false)

async function onSearch(params) {
    lastParams.value = params
    searchAttempted.value = true
    await search({ ...params, sort: sort.value })
}

async function onSort(newSort) {
    sort.value = newSort
    if (lastParams.value) {
        await search({ ...lastParams.value, sort: newSort })
    }
}
</script>
```

- [ ] **Step 2: Add the results section to the template**

Add this new `<section>` block directly after the closing `</section>` tag of the hero section (after line `</section>` that closes `id="hero"`), before the `<!-- How It Works -->` section:

```html
<!-- Search Results -->
<section v-if="searchAttempted" class="px-6 py-10 bg-zinc-100">
    <div class="max-w-7xl mx-auto space-y-6">

        <!-- Loading -->
        <div v-if="loading" class="flex items-center justify-center py-16">
            <div class="flex items-center gap-3 text-zinc-500">
                <iconify-icon icon="lucide:loader-circle" class="animate-spin text-2xl text-accent"></iconify-icon>
                <span class="font-medium">Finding stations near you…</span>
            </div>
        </div>

        <!-- Error -->
        <div v-else-if="error" class="flex items-center gap-3 p-4 bg-white border border-zinc-300 rounded-xl text-status-bad">
            <iconify-icon icon="lucide:circle-alert" style="font-size:1.25rem"></iconify-icon>
            <span class="font-medium">{{ error.general?.[0] ?? 'Unable to load stations. Please try again.' }}</span>
        </div>

        <!-- Results -->
        <template v-else>
            <LeafletMap :stations="stations" :default-open="true" />
            <StationList :stations="stations" :current-sort="sort" @sort="onSort" />
        </template>

    </div>
</section>
```

- [ ] **Step 3: Verify the `<SearchBar>` tag in the hero still has `@search="onSearch"`**

In the hero section, the tag should read:
```html
<SearchBar @search="onSearch" />
```

- [ ] **Step 4: Commit**

```bash
git add resources/js/views/Home.vue
git commit -m "feat: wire up homepage search with map and station list"
```

---

## Task 4: Manual browser verification

- [ ] **Step 1: Start the dev server** (if not already running)

```bash
npm run dev
```

- [ ] **Step 2: Open the homepage** at the URL from `herd` (e.g. `https://fuel-price.test`)

- [ ] **Step 3: Verify the search bar has three controls** — postcode input, fuel type select, radius select

- [ ] **Step 4: Enter a valid UK postcode** (e.g. `SW1A 1AA`) and click "Find Prices"

- [ ] **Step 5: Verify the results section appears below the hero** with:
  - Loading spinner shown briefly
  - Map opens automatically with markers at each station
  - Station list renders below the map with sort tabs (Price / Distance / Updated)
  - Cheapest station price is highlighted in green

- [ ] **Step 6: Click a sort tab** (e.g. Distance) and verify the list re-fetches and reorders

- [ ] **Step 7: Click "Hide map" toggle** and verify the map collapses; click "Show map" and verify it reopens

- [ ] **Step 8: Enter an invalid postcode** and verify an error message appears instead of the results
