feat: expand station cards with detailed information and add live statistics endpoint
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

- Add `/stats/live` endpoint returning station count and latest price timestamp with 5-minute cache
- Transform StationCard into expandable component with click/keyboard interaction showing full details
- Display brand label, badges (24h/Supermarket/Motorway), fuel types, amenities, opening hours, and price delta vs average
- Add brand filter dropdown to StationList with dynamic brand extraction from results
- Calculate and display price comparison against filtered stations average
- Redesign map markers to simpler price display; move directions link to popup alongside station details
- Add "locate-me" button to SearchBar for geolocation trigger
- Show "Live" indicator with station count and last-update time on homepage hero
- Remove standalone directions link from marker HTML; consolidate in popup with click propagation handling
- Persist `avgPence` calculation across StationList and pass to cards for delta display
- Add `@iconify-json/lucide` dev dependency and register collection on app mount
- Stop click propagation on card action buttons (directions, remove)
This commit is contained in:
Ovidiu U
2026-04-20 18:58:13 +01:00
parent c2466e5a61
commit 831637380c
14 changed files with 438 additions and 48 deletions

View File

@@ -20,12 +20,16 @@ class StationResource extends JsonResource
'name' => $this->trading_name,
'brand' => $this->brand_name,
'is_supermarket' => (bool) $this->is_supermarket,
'is_motorway' => (bool) $this->is_motorway_service_station,
'address' => implode(', ', array_filter([$this->address_line_1, $this->city])),
'postcode' => $this->postcode,
'lat' => (float) $this->lat,
'lng' => (float) $this->lng,
'distance_km' => round((float) $this->distance_km, 2),
'fuel_type' => $this->fuel_type,
'fuel_types_available' => $this->fuel_types ?? [],
'amenities' => $this->amenities ?? [],
'open_today' => $this->openTodayPayload(),
'price_pence' => (int) $this->price_pence,
'price' => round((int) $this->price_pence / 100, 2),
'price_updated_at' => $this->price_effective_at
@@ -37,4 +41,50 @@ class StationResource extends JsonResource
'reliability_label' => $reliability->label(),
];
}
/**
* @return array{is_24_hours: bool, open: ?string, close: ?string, is_open_now: bool}|null
*/
private function openTodayPayload(): ?array
{
$times = $this->opening_times;
if (! is_array($times) || empty($times['usual_days'])) {
return null;
}
$now = Carbon::now('Europe/London');
$dayKey = strtolower($now->format('l'));
$today = $times['usual_days'][$dayKey] ?? null;
if (! is_array($today)) {
return null;
}
$is24 = (bool) ($today['is_24_hours'] ?? false);
$open = $today['open'] ?? null;
$close = $today['close'] ?? null;
return [
'is_24_hours' => $is24,
'open' => $open ? substr($open, 0, 5) : null,
'close' => $close ? substr($close, 0, 5) : null,
'is_open_now' => $this->computeIsOpenNow($is24, $open, $close, $now),
];
}
private function computeIsOpenNow(bool $is24, ?string $open, ?string $close, Carbon $now): bool
{
if ($is24) {
return true;
}
if (! $open || ! $close) {
return false;
}
$current = $now->format('H:i:s');
return $current >= $open && $current < $close;
}
}