# Station Search Page — Design Spec **Date:** 2026-04-05 **Status:** Approved --- ## Overview A public-facing Livewire page that lets anyone search for nearby petrol stations by postcode, town, or city. The user selects a fuel type and search radius in miles. On submit, results are fetched server-side from `/api/stations` and displayed as a list. --- ## Routing - Route: `GET /stations` — public, no auth middleware - Registered in `routes/web.php` as a full-page Livewire component - Uses the existing `x-layouts::app` (sidebar) layout --- ## Component **Class:** `app/Livewire/Public/StationSearch.php` **View:** `resources/views/livewire/public/station-search.blade.php` ### Public properties | Property | Type | Default | Notes | |---|---|---|---| | `$search` | string | `''` | Postcode, outcode, town or city | | `$fuelType` | string | `''` | API fuel type alias (e.g. `petrol`, `diesel`) | | `$radius` | int | `5` | In miles — converted to km before API call | | `$results` | array | `[]` | Populated from API `data` array | | `$meta` | array | `[]` | Populated from API `meta` object | | `$apiError` | string\|null | `null` | Human-readable error from API or network failure | ### Method: `findStations()` 1. Validate: `$search` required, `$fuelType` required, `$radius` integer between 1–20 2. Reset `$results`, `$meta`, `$apiError` 3. Convert radius: `$radiusKm = $this->radius * 1.60934` 4. Call `Http::timeout(10)->withHeaders(['X-Api-Key' => config('services.fuelalert.api_key')])->get(url('/api/stations'), ['postcode' => $this->search, 'fuel_type' => $this->fuelType, 'radius' => $radiusKm, 'sort' => 'price'])` 5. On 422 — extract `errors.postcode[0]` or `message` and set `$apiError` 6. On other non-2xx — set `$apiError` to a generic message 7. On success — set `$results` and `$meta` from response JSON ### Configuration - API key read from `config('services.fuelalert.api_key')` - `.env` key: `FUELALERT_API_KEY` - Add to `config/services.php` under `fuelalert` --- ## Form UI Three fields in a horizontal row (stacks vertically on mobile): 1. **Search** — text input, placeholder "Postcode, town or city" 2. **Fuel type** — select with options: - Petrol (E10) → `petrol` - Super Unleaded (E5) → `e5` - Diesel → `diesel` - Premium Diesel → `b7_premium` - B10 Biodiesel → `b10` - HVO → `hvo` 3. **Radius** — select: 1, 2, 5, 10, 20 miles (default 5) Submit button with `wire:loading` spinner and disabled state while request is in flight. Inline validation errors shown below each field using `@error`. --- ## Results List Shown below the form after a successful search. **Meta bar** (above results): "{count} stations found · Cheapest: {lowest}p · Average: {avg}p" **Each station row:** - Station name + brand (badge if supermarket) - Address / postcode - Distance in miles (convert `distance_km` from response: `× 0.621371`, rounded to 1dp) - Price: formatted as `{price}p` per litre (e.g. `143.9p`) - Last updated: human-readable relative time (e.g. "2 hours ago") using `Carbon::parse(...)->diffForHumans()` Sorted cheapest first (default). No client-side re-sorting in v1. --- ## States | State | UI | |---|---| | Initial | Form only, no results area | | Loading | Submit button shows spinner + disabled, no results change | | Results | Meta bar + station list below form | | API error | Error alert above results (e.g. "Postcode not found.") | | Empty results | "No stations found within {radius} miles of {search}." | --- ## Data Flow ``` User fills form → submit → findStations() → Http::get /api/stations → success → $results + $meta populated → Livewire re-renders list → 422 → $apiError = postcode error message → other → $apiError = "Unable to fetch stations. Please try again." ``` --- ## Config Add to `config/services.php`: ```php 'fuelalert' => [ 'api_key' => env('FUELALERT_API_KEY'), ], ``` Add to `.env` and `.env.example`: ``` FUELALERT_API_KEY= ``` --- ## Testing Feature test: `tests/Feature/Livewire/StationSearchTest.php` Test cases: - Form renders with empty state - Validation: search required, fuel type required, radius required - Successful search populates `$results` and `$meta` (Http::fake) - 422 response sets `$apiError` - Network failure sets `$apiError` - Distance converted correctly from km to miles - Radius converted correctly from miles to km in outgoing request