212 lines
6.8 KiB
Markdown
212 lines
6.8 KiB
Markdown
# 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
|