From f4a958a76c674cad024e02b480fd809a16ff3be9 Mon Sep 17 00:00:00 2001 From: Ovidiu U Date: Tue, 7 Apr 2026 21:27:56 +0100 Subject: [PATCH] docs: FuelFinder sub-component split design spec --- ...07-fuelfinder-subcomponent-split-design.md | 211 ++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-07-fuelfinder-subcomponent-split-design.md diff --git a/docs/superpowers/specs/2026-04-07-fuelfinder-subcomponent-split-design.md b/docs/superpowers/specs/2026-04-07-fuelfinder-subcomponent-split-design.md new file mode 100644 index 0000000..d52ae61 --- /dev/null +++ b/docs/superpowers/specs/2026-04-07-fuelfinder-subcomponent-split-design.md @@ -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 +├── — 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