docs: add FuelFinder mobile landing page design spec
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
# 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 `FuelFinder` and the existing `StationSearch`
|
||||
- As lean as possible — no duplicated logic, one station repeater, one map component
|
||||
|
||||
---
|
||||
|
||||
## Route Change
|
||||
|
||||
```php
|
||||
// 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()`**
|
||||
1. Validate all properties
|
||||
2. Reset results, meta, prediction, apiError, hasSearched
|
||||
3. Call `GET /api/stations` with postcode, fuel_type, radius (km), sort
|
||||
4. On success: populate `$results`, `$meta`, set `$hasSearched = true`
|
||||
5. Call `GET /api/prediction` with `fuel_type` (no lat/lng — national fallback)
|
||||
6. On success: populate `$prediction`
|
||||
7. 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-56` mobile / `h-96` desktop
|
||||
|
||||
### `x-fuel.recommendation :prediction`
|
||||
Recommendation card driven by `/api/prediction` response.
|
||||
- Shows `action` as headline: `fill_now` → "Fill up now", `wait` → "Wait", else "No signal"
|
||||
- Confidence ring (SVG) from `confidence_score` (0–100)
|
||||
- `reasoning` text below
|
||||
- Confidence label badge (`confidence_label`: low / medium / high)
|
||||
- Hidden when `$prediction` is 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 |
|
||||
|---|---|---|
|
||||
| `fuel_type` | string | e.g. `petrol`, `diesel` |
|
||||
| `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 `/stations` unchanged)
|
||||
- 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)
|
||||
Reference in New Issue
Block a user