Files
fuel-price/docs/superpowers/specs/2026-04-05-station-search-page-design.md
2026-04-05 20:17:13 +01:00

4.4 KiB
Raw Permalink Blame History

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 120
  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:

'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