docs: FuelFinder sub-component split design spec
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user