diff --git a/app/Livewire/Public/FuelFinder.php b/app/Livewire/Public/FuelFinder.php
index 5f0630a..94cb9e0 100644
--- a/app/Livewire/Public/FuelFinder.php
+++ b/app/Livewire/Public/FuelFinder.php
@@ -2,117 +2,13 @@
namespace App\Livewire\Public;
-use Illuminate\Http\Client\ConnectionException;
-use Illuminate\Support\Facades\Http;
use Illuminate\View\View;
use Livewire\Attributes\Layout;
-use Livewire\Attributes\Validate;
use Livewire\Component;
-#[Layout('layouts.guest')]
-class FuelFinder extends Component
+#[Layout('layouts.shell')]
+final class FuelFinder extends Component
{
- #[Validate('required|string', message: 'Please enter a postcode, town or city.')]
- public string $search = '';
-
- #[Validate('required|string', message: 'Please select a fuel type.')]
- public string $fuelType = 'petrol';
-
- #[Validate('required|integer|min:1|max:20')]
- public int $radius = 5;
-
- #[Validate('nullable|string|in:price,distance,updated,brand,reliable')]
- public string $sort = 'reliable';
-
- public array $results = [];
-
- public array $meta = [];
-
- public ?array $prediction = null;
-
- public ?string $apiError = null;
-
- public bool $hasSearched = false;
-
- public function updatedFuelType(): void
- {
- if ($this->hasSearched) {
- $this->findStations();
- }
- }
-
- public function updatedRadius(): void
- {
- if ($this->hasSearched) {
- $this->findStations();
- }
- }
-
- public function updatedSort(): void
- {
- if ($this->hasSearched) {
- $this->findStations();
- }
- }
-
- public function findStations(): void
- {
- $this->validate();
-
- $this->results = [];
- $this->meta = [];
- $this->prediction = null;
- $this->apiError = null;
- $this->hasSearched = false;
-
- $radiusKm = round($this->radius * 1.60934, 2);
-
- try {
- $response = Http::timeout(10)
- ->withHeaders(['X-Api-Key' => config('app.api_secret_key')])
- ->get(url('/api/stations'), [
- 'postcode' => $this->search,
- 'fuel_type' => $this->fuelType,
- 'radius' => $radiusKm,
- 'sort' => $this->sort,
- ]);
- } catch (ConnectionException) {
- $this->apiError = 'Unable to fetch stations. Please try again.';
-
- return;
- }
-
- if ($response->status() === 422) {
- $errors = $response->json('errors', []);
- $this->apiError = collect($errors)->flatten()->first()
- ?? $response->json('message', 'Validation error.');
-
- return;
- }
-
- if (! $response->successful()) {
- $this->apiError = 'Unable to fetch stations. Please try again.';
-
- return;
- }
-
- $this->results = $response->json('data', []);
- $this->meta = $response->json('meta', []);
- $this->hasSearched = true;
-
- try {
- $predictionResponse = Http::timeout(10)
- ->withHeaders(['X-Api-Key' => config('app.api_secret_key')])
- ->get(url('/api/prediction'));
-
- if ($predictionResponse->successful()) {
- $this->prediction = $predictionResponse->json();
- }
- } catch (ConnectionException) {
- // Prediction failure is silent — stations are more important
- }
- }
-
public function render(): View
{
return view('livewire.public.fuel-finder');
diff --git a/resources/views/livewire/public/fuel-finder.blade.php b/resources/views/livewire/public/fuel-finder.blade.php
index 3acece0..a088a49 100644
--- a/resources/views/livewire/public/fuel-finder.blade.php
+++ b/resources/views/livewire/public/fuel-finder.blade.php
@@ -1,201 +1,77 @@
-
+
-
+ {{-- HEADER --}}
+
- {{-- Scrollable main content, offset for fixed header (~112px) and footer (~80px) --}}
-
+
+
+
+
+ FuelAlert
+
- {{-- #search --}}
-
- {{-- Filter rows --}}
-
+ {{-- MAIN --}}
+
+
-
+
+
+
+
- @if ($apiError)
-
- {{ $apiError }}
-
- @endif
-
-
- {{-- #recommendation --}}
- {{-- @if ($prediction)
-
-
+
- @endif --}}
-
- {{-- #map --}}
-
-
- {{-- #stations --}}
- @if ($hasSearched)
-
- @if (! empty($meta))
-
-
Stations Nearby
-
- {{ $meta['count'] ?? 0 }} {{ str('Result')->plural($meta['count'] ?? 0) }}
-
-
- @endif
-
- @forelse ($results as $station)
-
-
-
- @empty
-
- No stations found within {{ $radius }} {{ str('mile')->plural($radius) }} of "{{ $search }}".
-
- @endforelse
-
- @endif
-
- {{-- #forecast --}}
-
+
-
+ {{-- BOTTOM TAB BAR --}}
+ @php
+ $tabs = [
+ ['label' => 'Prices', 'icon' => 'lucide:fuel', 'route' => 'home'],
+ ['label' => 'Alerts', 'icon' => 'lucide:bell', 'route' => null],
+ ['label' => 'Forecourts', 'icon' => 'lucide:map-pin', 'route' => null],
+ ['label' => 'Trends', 'icon' => 'lucide:trending-up', 'route' => null],
+ ];
+ @endphp
+
diff --git a/tests/Feature/Livewire/FuelFinderTest.php b/tests/Feature/Livewire/FuelFinderTest.php
index 42754fe..f2c861b 100644
--- a/tests/Feature/Livewire/FuelFinderTest.php
+++ b/tests/Feature/Livewire/FuelFinderTest.php
@@ -1,223 +1,9 @@
assertStatus(200)
- ->assertSeeHtml('name="search"');
-});
-
-it('has default property values', function () {
- Livewire::test(FuelFinder::class)
- ->assertSet('search', '')
- ->assertSet('fuelType', 'petrol')
- ->assertSet('radius', 5)
- ->assertSet('sort', 'reliable')
- ->assertSet('results', [])
- ->assertSet('meta', [])
- ->assertSet('prediction', null)
- ->assertSet('apiError', null)
- ->assertSet('hasSearched', false);
-});
-
-it('validates search is required', function () {
- Livewire::test(FuelFinder::class)
- ->call('findStations')
- ->assertHasErrors(['search' => 'required']);
-});
-
-it('validates fuelType is required', function () {
- Livewire::test(FuelFinder::class)
- ->set('search', 'SW1A 1AA')
- ->set('fuelType', '')
- ->call('findStations')
- ->assertHasErrors(['fuelType' => 'required']);
-});
-
-it('populates results, meta, and prediction on successful search', function () {
- Http::fake([
- '*/api/stations*' => Http::response([
- 'data' => [
- [
- 'station_id' => 'abc123',
- 'name' => 'BP Garage',
- 'brand' => 'BP',
- 'is_supermarket' => false,
- 'address' => '1 High Street',
- 'postcode' => 'SW1A 1AA',
- 'lat' => 51.5074,
- 'lng' => -0.1278,
- 'distance_km' => 1.5,
- 'fuel_type' => 'e10',
- 'price_pence' => 14390,
- 'price' => 143.9,
- 'price_updated_at' => '2026-04-05T08:00:00.000Z',
- 'price_classification' => 'current',
- 'price_classification_label' => 'Current',
- ],
- ],
- 'meta' => [
- 'count' => 1,
- 'lowest_pence' => 14390,
- 'avg_pence' => 14390.0,
- ],
- ], 200),
- '*/api/prediction*' => Http::response([
- 'action' => 'fill_now',
- 'confidence_score' => 80.0,
- 'confidence_label' => 'high',
- 'reasoning' => 'Prices rising.',
- 'predicted_direction' => 'up',
- 'predicted_change_pence' => 3.5,
- ], 200),
- ]);
-
- Livewire::test(FuelFinder::class)
- ->set('search', 'SW1A 1AA')
- ->set('fuelType', 'petrol')
- ->call('findStations')
- ->assertSet('hasSearched', true)
- ->assertSet('apiError', null)
- ->assertSet('results', fn (array $r) => count($r) === 1 && $r[0]['name'] === 'BP Garage')
- ->assertSet('meta', fn (array $m) => $m['count'] === 1)
- ->assertSet('prediction', fn (?array $p) => $p !== null && $p['action'] === 'fill_now');
-});
-
-it('sets apiError from 422 station response and leaves prediction null', function () {
- Http::fake([
- '*/api/stations*' => Http::response([
- 'errors' => ['postcode' => ['Postcode not found.']],
- ], 422),
- ]);
-
- Livewire::test(FuelFinder::class)
- ->set('search', 'ZZ99 9ZZ')
- ->set('fuelType', 'petrol')
- ->call('findStations')
- ->assertSet('results', [])
- ->assertSet('meta', [])
- ->assertSet('prediction', null)
- ->assertSet('hasSearched', false)
- ->assertSet('apiError', 'Postcode not found.');
-});
-
-it('sets generic apiError on server error', function () {
- Http::fake([
- '*/api/stations*' => Http::response([], 500),
- ]);
-
- Livewire::test(FuelFinder::class)
- ->set('search', 'SW1A 1AA')
- ->set('fuelType', 'petrol')
- ->call('findStations')
- ->assertSet('apiError', 'Unable to fetch stations. Please try again.');
-});
-
-it('converts radius from miles to km in the outgoing stations request', function () {
- Http::fake([
- '*/api/stations*' => Http::response(['data' => [], 'meta' => ['count' => 0]], 200),
- '*/api/prediction*' => Http::response(['action' => 'no_signal', 'confidence_score' => 0, 'confidence_label' => 'low', 'reasoning' => '', 'predicted_direction' => 'stable', 'predicted_change_pence' => 0], 200),
- ]);
-
- Livewire::test(FuelFinder::class)
- ->set('search', 'SW1A 1AA')
- ->set('fuelType', 'petrol')
- ->set('radius', 5)
- ->call('findStations');
-
- Http::assertSent(function ($request) {
- if (! str_contains($request->url(), 'api/stations')) {
- return false;
- }
- $data = $request->data();
-
- return isset($data['radius']) && abs((float) $data['radius'] - 8.05) < 0.01;
- });
-});
-
-it('resets state before each new search', function () {
- Http::fake([
- '*/api/stations*' => Http::response(['data' => [], 'meta' => ['count' => 0]], 200),
- '*/api/prediction*' => Http::response(['action' => 'no_signal', 'confidence_score' => 0, 'confidence_label' => 'low', 'reasoning' => '', 'predicted_direction' => 'stable', 'predicted_change_pence' => 0], 200),
- ]);
-
- Livewire::test(FuelFinder::class)
- ->set('search', 'SW1A 1AA')
- ->set('fuelType', 'petrol')
- ->set('results', [['name' => 'Old Result']])
- ->set('apiError', 'Old error')
- ->call('findStations')
- ->assertSet('apiError', null)
- ->assertSet('results', []);
-});
-
-it('does not call findStations on updatedFuelType if not yet searched', function () {
- Http::fake();
-
- Livewire::test(FuelFinder::class)
- ->set('fuelType', 'diesel');
-
- Http::assertNothingSent();
-});
-
-it('re-runs findStations on updatedFuelType when already searched', function () {
- Http::fake([
- '*/api/stations*' => Http::response(['data' => [], 'meta' => ['count' => 0]], 200),
- '*/api/prediction*' => Http::response(['action' => 'no_signal', 'confidence_score' => 0, 'confidence_label' => 'low', 'reasoning' => '', 'predicted_direction' => 'stable', 'predicted_change_pence' => 0], 200),
- ]);
-
- Livewire::test(FuelFinder::class)
- ->set('hasSearched', true)
- ->set('search', 'SW1A 1AA')
- ->set('fuelType', 'diesel');
-
- Http::assertSentCount(2); // stations + prediction
-});
-
-it('re-runs findStations on updatedRadius when already searched', function () {
- Http::fake([
- '*/api/stations*' => Http::response(['data' => [], 'meta' => ['count' => 0]], 200),
- '*/api/prediction*' => Http::response(['action' => 'no_signal', 'confidence_score' => 0, 'confidence_label' => 'low', 'reasoning' => '', 'predicted_direction' => 'stable', 'predicted_change_pence' => 0], 200),
- ]);
-
- Livewire::test(FuelFinder::class)
- ->set('hasSearched', true)
- ->set('search', 'SW1A 1AA')
- ->set('radius', 10);
-
- Http::assertSentCount(2);
-});
-
-it('re-runs findStations on updatedSort when already searched', function () {
- Http::fake([
- '*/api/stations*' => Http::response(['data' => [], 'meta' => ['count' => 0]], 200),
- '*/api/prediction*' => Http::response(['action' => 'no_signal', 'confidence_score' => 0, 'confidence_label' => 'low', 'reasoning' => '', 'predicted_direction' => 'stable', 'predicted_change_pence' => 0], 200),
- ]);
-
- Livewire::test(FuelFinder::class)
- ->set('hasSearched', true)
- ->set('search', 'SW1A 1AA')
- ->set('sort', 'price');
-
- Http::assertSentCount(2);
-});
-
-it('prediction remains null when prediction api fails', function () {
- Http::fake([
- '*/api/stations*' => Http::response(['data' => [], 'meta' => ['count' => 0]], 200),
- '*/api/prediction*' => Http::response([], 500),
- ]);
-
- Livewire::test(FuelFinder::class)
- ->set('search', 'SW1A 1AA')
- ->set('fuelType', 'petrol')
- ->call('findStations')
- ->assertSet('hasSearched', true)
- ->assertSet('prediction', null);
+ ->assertStatus(200);
});