# FuelFinder Sub-Component Split

**Date:** 2026-04-07
**Status:** Approved
**Scope:** Split the monolithic `FuelFinder` Livewire component into focused sub-components communicating via browser events.

---

## Motivation

`FuelFinder` currently owns search state, API calls, map data, station results, and prediction — all in one component. Every search re-renders the entire page including the map container. Splitting into focused components allows the map, station list, and recommendation to update independently without re-mounting Leaflet or triggering unnecessary re-renders.

---

## Architecture

```
FuelFinder (shell)                   — layout only, no state
├── <livewire:fuel.search />         — search form, filters, geolocation, API calls
├── <livewire:fuel.map />            — relays stations-found to Alpine/Leaflet
├── <livewire:fuel.station-list />   — renders station cards
├── <livewire:fuel.recommendation /> — renders prediction card
└── <x-fuel.forecast />             — Blade component, unchanged
```

---

## Components

### FuelFinder (shell)

**Class:** `App\Livewire\Public\FuelFinder`
**View:** `resources/views/livewire/public/fuel-finder.blade.php`

Retains `#[Layout('layouts.shell')]`. Holds no state and no methods. The view contains only the page chrome (header, main scroll area, bottom nav) and the four `<livewire:fuel.*>` tags.

---

### fuel.search

**Class:** `App\Livewire\Public\Fuel\Search`
**View:** `resources/views/livewire/public/fuel/search.blade.php`

Owns all search state and logic migrated from `FuelFinder`:

**Properties:**
- `search: string` — postcode/town input
- `fuelType: string` — default `'petrol'`
- `radius: int` — default `5`
- `sort: string` — default `'reliable'`
- `apiError: ?string`
- `hasSearched: bool`

**Methods:**
- `findStations()` — validates, calls `/api/stations`, calls `/api/prediction`, dispatches `stations-found`
- `updatedFuelType()`, `updatedRadius()`, `updatedSort()` — re-run `findStations()` if `hasSearched`

**Dispatches:**
```php
$this->dispatch('stations-found',
    results: $results,
    meta: $meta,
    prediction: $prediction,
    radius: $this->radius,  // needed by Map for Leaflet zoom level
);
```

The view contains the search input, geolocation Alpine component, filter selects, and error display — extracted from the current `fuel-finder.blade.php`.

---

### fuel.map

**Class:** `App\Livewire\Public\Fuel\Map`
**View:** `resources/views/livewire/public/fuel/map.blade.php`

Thin relay. Listens for `stations-found` on the PHP side and re-dispatches as a browser event for Alpine/Leaflet to consume. No re-render required.

```php
#[On('stations-found')]
public function handle(array $results, array $meta, int $radius = 5, ?array $prediction = null): void
{
    $this->dispatch('map-update', results: $results, meta: $meta, radius: $radius);
}
```

The view contains the `<x-fuel.station-map>` component wrapped in `wire:ignore`. The Alpine `stationMap` function in `station-map.js` is updated to listen for the `map-update` browser event instead of `$watch`-ing an entangled Livewire property.

**station-map.js change:**
```js
init() {
    // ...existing Leaflet init...
    window.addEventListener('map-update', (e) => {
        this.results = e.detail.results;
        this.meta    = e.detail.meta;
        this.radius  = e.detail.radius;
        this._plotMarkers();
    });
}
```

`station-map.blade.php` props change from `@entangle(...)` to static defaults — `Map` holds no state:
```blade
<div x-data="stationMap([], {}, 5)" class="h-56 w-full ..."></div>
```

---

### fuel.station-list

**Class:** `App\Livewire\Public\Fuel\StationList`
**View:** `resources/views/livewire/public/fuel/station-list.blade.php`

Listens for `stations-found`, stores results, re-renders station cards.

**Properties:**
- `results: array`
- `meta: array`
- `hasSearched: bool`

**Methods:**
```php
#[On('stations-found')]
public function handle(array $results, array $meta): void
{
    $this->results    = $results;
    $this->meta       = $meta;
    $this->hasSearched = true;
}
```

The view contains the "Stations Nearby" heading, result count, `@forelse` station card loop, and the empty-state message — extracted from `fuel-finder.blade.php`.

---

### fuel.recommendation

**Class:** `App\Livewire\Public\Fuel\Recommendation`
**View:** `resources/views/livewire/public/fuel/recommendation.blade.php`

Listens for `stations-found`, stores prediction, re-renders.

**Properties:**
- `prediction: ?array`

**Methods:**
```php
#[On('stations-found')]
public function handle(array $results, array $meta, ?array $prediction = null): void
{
    $this->prediction = $prediction;
}
```

The view renders `<x-fuel.recommendation :prediction="$prediction" />` when prediction is set. Currently commented out in the parent view — this component makes it easy to uncomment when the scoring engine is wired.

---

## Event Contract

**Event name:** `stations-found`

**Payload:**
```php
[
    'results'    => array,   // station records from /api/stations
    'meta'       => array,   // lat, lng, count from API response
    'prediction' => ?array,  // from /api/prediction, nullable
    'radius'     => int,     // user-selected radius in miles, for Leaflet zoom
]
```

All listener components must accept `?array $prediction = null` and `int $radius = 5` as defaults to avoid errors when optional fields are absent.

---

## File Changes

**New files:**
- `app/Livewire/Public/Fuel/Search.php`
- `app/Livewire/Public/Fuel/Map.php`
- `app/Livewire/Public/Fuel/StationList.php`
- `app/Livewire/Public/Fuel/Recommendation.php`
- `resources/views/livewire/public/fuel/search.blade.php`
- `resources/views/livewire/public/fuel/map.blade.php`
- `resources/views/livewire/public/fuel/station-list.blade.php`
- `resources/views/livewire/public/fuel/recommendation.blade.php`

**Modified files:**
- `app/Livewire/Public/FuelFinder.php` — strip all state/methods, keep layout attribute
- `resources/views/livewire/public/fuel-finder.blade.php` — replace inline sections with `<livewire:fuel.*>` tags
- `resources/js/maps/station-map.js` — replace `$watch` with `map-update` event listener
- `resources/views/components/fuel/station-map.blade.php` — remove `@entangle` props

**Moved tests:**
- `FuelFinderTest` search/API logic tests → `SearchTest`

**New tests:**
- `SearchTest` — validates, dispatches `stations-found`, handles API errors
- `StationListTest` — responds to `stations-found`, renders cards, handles empty state
- `RecommendationTest` — responds to `stations-found`, renders when prediction set, hidden when null

---

## What Does Not Change

- `x-fuel.forecast` — remains a Blade component
- `station-map.js` map logic — only the data-binding mechanism changes
- All `fuel/` Blade components (`station-card`, `recommendation`, `forecast`, etc.)
- The visual layout in `fuel-finder.blade.php`
- Route definition
