# 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 ├── — search form, filters, geolocation, API calls ├── — relays stations-found to Alpine/Leaflet ├── — renders station cards ├── — renders prediction card └── — 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 `` 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 `` 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
``` --- ### 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 `` 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 `` 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