Files
fuel-price/docs/superpowers/specs/2026-04-07-mobile-landing-fuelfinder-design.md
Ovidiu U 6a80c11f38
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled
feat: add LLM prediction providers with structured output support
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 14:42:44 +01:00

203 lines
6.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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: 120 |
| `$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 AZ.
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` (0100)
- `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 |
|---|---|---|
| `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)