6.6 KiB
FuelFinder Mobile Landing Page — Design Spec
Date: 2026-04-07
Scope: Replace static mobile homepage with a fully functional Livewire-powered landing page, backed by reusable Blade components shared with the desktop search.
Goals
- Mobile landing page (
/) becomes the working app — search, recommendation, map, stations list, forecast - All interactive sections are driven by a single new Livewire component (
FuelFinder) - Presentation elements extracted into reusable Blade components consumed by both
FuelFinderand the existingStationSearch - As lean as possible — no duplicated logic, one station repeater, one map component
Route Change
// routes/web.php
Route::get('/', FuelFinder::class)->name('home');
/stations (StationSearch) is kept as-is for now. Migration to shared components happens separately.
Livewire Component: FuelFinder
File: app/Livewire/Public/FuelFinder.php
View: resources/views/livewire/public/fuel-finder.blade.php
Properties
| Property | Type | Default | Notes |
|---|---|---|---|
$search |
string |
'' |
Validated: required |
$fuelType |
string |
'petrol' |
Validated: required |
$radius |
int |
5 |
Validated: 1–20 |
$sort |
string |
'reliable' |
Validated: in allowed list |
$results |
array |
[] |
Populated from /api/stations |
$meta |
array |
[] |
Count, lowest/avg price |
$prediction |
?array |
null |
Populated from /api/prediction |
$apiError |
?string |
null |
Surface API/connection errors |
$hasSearched |
bool |
false |
Controls section visibility |
Methods
findStations()
- Validate all properties
- Reset results, meta, prediction, apiError, hasSearched
- Call
GET /api/stationswith postcode, fuel_type, radius (km), sort - On success: populate
$results,$meta, set$hasSearched = true - Call
GET /api/predictionwithfuel_type(no lat/lng — national fallback) - On success: populate
$prediction - On any failure: set
$apiError
updatedFuelType() / updatedRadius() / updatedSort()
Re-run findStations() if $hasSearched is true (live filter refresh).
Blade Components
All components live under resources/views/components/fuel/ and are namespaced as x-fuel.*.
x-fuel.type-select
Fuel type <select>. Accepts wire:model passthrough via $attributes->whereStartsWith('wire:').
Options: Petrol (E10), Super Unleaded (E5), Diesel, Premium Diesel, B10 Biodiesel, HVO.
Reused in: FuelFinder, StationSearch.
x-fuel.radius-select
Radius <select>. Options: 1, 2, 5, 10, 20 miles.
Accepts wire:model passthrough.
x-fuel.sort-select
Sort <select>. Options: Best price (reliable), Cheapest first, Nearest first, Recently updated, Brand A–Z.
Accepts wire:model passthrough.
x-fuel.station-card :station
Single station row. Props from /api/stations response:
- Name, address, postcode, distance (km → miles)
- Price in pence (formatted to 1dp)
- Price colour:
current→ green,recent→ slate,stale→ amber,outdated→ red is_supermarket→ shows "Supermarket" badge
x-fuel.station-map :results
Leaflet map wrapper using Alpine x-data="stationMap(@entangle('results'))" pattern.
- Default centre: UK (
54.0, -2.0), zoom 6 — shown before search - After search: re-centres to fit result bounds automatically (handled in
station-map.js) - Height:
h-56mobile /h-96desktop
x-fuel.recommendation :prediction
Recommendation card driven by /api/prediction response.
- Shows
actionas headline:fill_now→ "Fill up now",wait→ "Wait", else "No signal" - Confidence ring (SVG) from
confidence_score(0–100) reasoningtext below- Confidence label badge (
confidence_label: low / medium / high) - Hidden when
$predictionis null
x-fuel.forecast
Static 14-day forecast Pro upsell card.
- SVG squiggle line chart (decorative)
- Blurred/locked overlay with "Unlock Forecast" CTA button
- "Pro" badge
x-mobile-header
Mobile app header: FuelAlert logo + user icon button (links to login/dashboard based on auth state).
pt-14 for safe area, fixed positioning.
x-mobile-footer
Mobile tab bar: Prices, Alerts, Forecourts, Trends. Sticky bottom, pb-8 for safe area.
Active tab highlight for current route.
View: fuel-finder.blade.php
Mobile layout only (desktop layout handled by existing homepage / StationSearch).
Structure:
<x-mobile-header />
<main> {{-- scrollable --}}
{{-- #search --}}
Search input + x-fuel.type-select + x-fuel.radius-select + x-fuel.sort-select
Displayed as pill-style filter buttons (scroll horizontally on mobile)
Submit triggers findStations()
{{-- #recommendation --}}
<x-fuel.recommendation :prediction="$prediction" />
Hidden (@if $prediction) until searched
{{-- #map --}}
<x-fuel.station-map :results="$results" />
Always visible — UK centre until search
{{-- #stations --}}
@if $hasSearched
@forelse $results as $station
<x-fuel.station-card :station="$station" />
@empty
"No stations found" message
@endforelse
@endif
{{-- #forecast --}}
<x-fuel.forecast />
</main>
<x-mobile-footer />
API Contracts
/api/stations
| Param | Type | Notes |
|---|---|---|
postcode |
string | Full postcode, outcode, or place name |
fuel_type |
string | e.g. petrol, diesel, e5 |
radius |
float | km (convert from miles: × 1.60934) |
sort |
string | reliable, price, distance, updated, brand |
Response shape: { data: Station[], meta: { count, lowest_pence, avg_pence } }
/api/prediction
| Param | Type | Notes |
|---|---|---|
lat |
float? | Optional — falls back to national |
lng |
float? | Optional |
Key response fields used: action, confidence_score, confidence_label, reasoning, predicted_direction, predicted_change_pence.
Data Flow
User types postcode → submits → findStations()
→ GET /api/stations → $results, $meta, $hasSearched=true
→ GET /api/prediction → $prediction
→ blade re-renders:
#recommendation shows with prediction data
#map re-centres to results
#stations lists results
Filter change (fuel type / radius / sort) → updated*() → findStations() if $hasSearched
Not in scope
- Desktop layout changes (StationSearch at
/stationsunchanged) - Migrating StationSearch to use shared Blade components (follow-up)
- lat/lng resolution for
/api/prediction(uses national fallback for now) - Tab bar navigation routes (Alerts, Forecourts, Trends pages don't exist yet)