diff --git a/CLAUDE.md b/CLAUDE.md index 5ce6614..06888d4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,4 +1,4 @@ -# FuelAlert — Claude Code Instructions +# Fuel Price — Claude Code Instructions UK fuel price intelligence app. Subscribers receive fill-up timing recommendations based on local price trends. Built solo by a PHP/Laravel developer. diff --git a/app/Http/Controllers/Api/StationController.php b/app/Http/Controllers/Api/StationController.php index 70bfe5e..49c830a 100644 --- a/app/Http/Controllers/Api/StationController.php +++ b/app/Http/Controllers/Api/StationController.php @@ -74,7 +74,6 @@ class StationController extends Controller : $filtered->sortBy(match ($sort) { 'price' => fn ($s) => (int) $s->price_pence, 'updated' => fn ($s) => $s->price_effective_at ? -strtotime($s->price_effective_at) : PHP_INT_MAX, - 'brand' => fn ($s) => strtolower((string) $s->brand_name), default => fn ($s) => (float) $s->distance_km, })->values(); diff --git a/app/Http/Controllers/Api/StatsController.php b/app/Http/Controllers/Api/StatsController.php index 71d8b3b..9702f35 100644 --- a/app/Http/Controllers/Api/StatsController.php +++ b/app/Http/Controllers/Api/StatsController.php @@ -4,11 +4,33 @@ namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; use App\Models\Search; +use App\Models\Station; +use App\Models\StationPrice; +use Carbon\CarbonImmutable; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Cache; class StatsController extends Controller { + public function live(): JsonResponse + { + $payload = Cache::remember('api:stats:live', now()->addMinutes(5), function (): array { + $stationCount = Station::query() + ->where('permanent_closure', false) + ->count(); + + $latestPriceAt = StationPrice::query()->max('recorded_at'); + + return [ + 'station_count' => $stationCount, + 'latest_price_at' => $latestPriceAt ? CarbonImmutable::parse($latestPriceAt)->toIso8601String() : null, + ]; + }); + + return response()->json($payload); + } + public function searches(Request $request): JsonResponse { $period = $request->input('period', 'week'); diff --git a/app/Http/Resources/Api/StationResource.php b/app/Http/Resources/Api/StationResource.php index 605645b..366e2c5 100644 --- a/app/Http/Resources/Api/StationResource.php +++ b/app/Http/Resources/Api/StationResource.php @@ -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; + } } diff --git a/package-lock.json b/package-lock.json index 1bf0f9c..613b055 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,9 @@ "vue": "^3.5.32", "vue-router": "^4.6.4" }, + "devDependencies": { + "@iconify-json/lucide": "^1.2.102" + }, "optionalDependencies": { "@rollup/rollup-linux-x64-gnu": "4.9.5", "@tailwindcss/oxide-linux-x64-gnu": "^4.0.1", @@ -101,6 +104,16 @@ "tslib": "^2.4.0" } }, + "node_modules/@iconify-json/lucide": { + "version": "1.2.102", + "resolved": "https://registry.npmjs.org/@iconify-json/lucide/-/lucide-1.2.102.tgz", + "integrity": "sha512-Dm3EEqu5NrmzyDMB2U1+8yroEj2/dB9V4KlH0m/szwwF/ofSf0cPaGTZqkd1aExXjCor+vU53ttRMCGuXf+/cg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@iconify/types": "*" + } + }, "node_modules/@iconify/types": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", diff --git a/package.json b/package.json index 1487d07..7bb6bd8 100644 --- a/package.json +++ b/package.json @@ -24,5 +24,8 @@ "@rollup/rollup-linux-x64-gnu": "4.9.5", "@tailwindcss/oxide-linux-x64-gnu": "^4.0.1", "lightningcss-linux-x64-gnu": "^1.29.1" + }, + "devDependencies": { + "@iconify-json/lucide": "^1.2.102" } } diff --git a/resources/js/app.js b/resources/js/app.js index 1e61e4b..38f8dad 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1,6 +1,10 @@ import 'iconify-icon' +import { addCollection } from 'iconify-icon' +import lucideIcons from '@iconify-json/lucide/icons.json' import { createApp } from 'vue' import App from './App.vue' import router from './router/index.js' +addCollection(lucideIcons) + createApp(App).use(router).mount('#app') diff --git a/resources/js/components/LeafletMap.vue b/resources/js/components/LeafletMap.vue index fc1aa92..a657366 100644 --- a/resources/js/components/LeafletMap.vue +++ b/resources/js/components/LeafletMap.vue @@ -63,20 +63,17 @@ function buildDirectionsUrl(station, origin) { return base } -function buildMarkerHtml(station, index, colour, borderColour, origin) { +function buildMarkerHtml(station, index, colour, borderColour) { const isFirst = index === 0 + const w = isFirst ? 46 : 40 const h = isFirst ? 20 : 18 const fontSize = isFirst ? 11 : 10 - const iconSize = isFirst ? 11 : 10 const star = isFirst ? `★` : '' - const directionsUrl = escHtml(buildDirectionsUrl(station, origin)) - const navSvg = `` - - return `
-
+ {{ brandLabel }}
+
+
+
{{ statusLabel }}
++ {{ priceDelta }} +
+ {{ fullAddress }} · Updated {{ updatedAgo }} +
+- {{ stations.length }} station{{ stations.length !== 1 ? 's' : '' }} found + {{ filteredStations.length }} station{{ filteredStations.length !== 1 ? 's' : '' }} + matching {{ brandFilter }} + found
@@ -33,6 +45,7 @@
Join 50,000+ UK drivers using real-time insights to find the cheapest petrol and time their fill-ups perfectly.
@@ -46,14 +50,14 @@