diff --git a/app/Http/Middleware/VerifyApiKey.php b/app/Http/Middleware/VerifyApiKey.php
index 115c606..b7fb0d9 100644
--- a/app/Http/Middleware/VerifyApiKey.php
+++ b/app/Http/Middleware/VerifyApiKey.php
@@ -5,6 +5,7 @@ namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
+use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful;
use Symfony\Component\HttpFoundation\Response;
final class VerifyApiKey
@@ -20,6 +21,10 @@ final class VerifyApiKey
return $next($request);
}
+ if (EnsureFrontendRequestsAreStateful::fromFrontend($request)) {
+ return $next($request);
+ }
+
if ($request->header('X-Api-Key') !== config('app.api_secret_key')) {
abort(403);
}
diff --git a/resources/css/app.css b/resources/css/app.css
index 75c48d9..905b963 100644
--- a/resources/css/app.css
+++ b/resources/css/app.css
@@ -6,7 +6,7 @@
@source '../../vendor/livewire/flux-pro/stubs/**/*.blade.php';
@source '../../vendor/livewire/flux/stubs/**/*.blade.php';
-@custom-variant dark (&:where(.dark, .dark *));
+@custom-variant dark (&:where(.dark-mode-disabled));
@theme {
--font-sans: 'Inter', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
@@ -53,13 +53,6 @@
--font-display: 'Manrope', ui-sans-serif, system-ui, sans-serif;
}
-@layer theme {
- .dark {
- --color-accent: var(--color-white);
- --color-accent-content: var(--color-white);
- --color-accent-foreground: var(--color-neutral-800);
- }
-}
@layer utilities {
.hero-gradient {
diff --git a/resources/js/components/LeafletMap.vue b/resources/js/components/LeafletMap.vue
index 9287101..9b2b69a 100644
--- a/resources/js/components/LeafletMap.vue
+++ b/resources/js/components/LeafletMap.vue
@@ -8,11 +8,31 @@
{{ isOpen ? 'Hide map' : 'Show map' }}
-
+
+
+
+
+
+
+ Current (<24h)
+
+
+
+ Recent (24–48h)
+
+
+
+ Stale (2–5 days)
+
+
+
+ Outdated (5+ days)
+
+
+
@@ -21,13 +41,61 @@ import { ref, watch, onMounted, onUnmounted, nextTick } from 'vue'
import L from 'leaflet'
import 'leaflet/dist/leaflet.css'
-// Fix Leaflet default marker icon path broken by Vite
-delete L.Icon.Default.prototype._getIconUrl
-L.Icon.Default.mergeOptions({
- iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png',
- iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png',
- shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
-})
+const CLASSIFICATION_COLOURS = {
+ current: '#22c55e',
+ recent: '#64748b',
+ stale: '#f59e0b',
+ outdated: '#ef4444',
+}
+
+const CLASSIFICATION_BORDER_COLOURS = {
+ current: '#16a34a',
+ recent: '#475569',
+ stale: '#d97706',
+ outdated: '#dc2626',
+}
+
+function buildMarkerHtml(station, index, colour, borderColour) {
+ const isFirst = index === 0
+ const w = isFirst ? 63 : 59
+ const h = isFirst ? 58 : 51
+ const bw = isFirst ? 56 : 52
+ const bh = isFirst ? 43 : 38
+ const br = isFirst ? 17 : 15
+ const tailTop = isFirst ? 45 : 40
+ const tailW = isFirst ? 9 : 7
+ const tailH = isFirst ? 11 : 9
+ const badgeSize = isFirst ? 18 : 16
+ const badgeFontSize = isFirst ? 10 : 8
+ const priceFontSize = isFirst ? 12 : 11
+
+ const initial = escHtml((station.brand || station.name || '?')[0].toUpperCase())
+
+ const badge = isFirst
+ ? `★
`
+ : `${index + 1}
`
+
+ return `
+ ${badge}
+
+
+ ${initial}
+
+
+ ${Number(station.price).toFixed(1)}p
+
+
+
+
`
+}
+
+function escHtml(str) {
+ return String(str ?? '')
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+}
const props = defineProps({
stations: { type: Array, required: true },
@@ -60,15 +128,44 @@ function renderMarkers() {
const bounds = []
- props.stations.forEach(station => {
- const marker = L.marker([station.lat, station.lng])
- .bindPopup(`${station.name}
${station.price}p`)
+ props.stations.forEach((station, index) => {
+ const colour = CLASSIFICATION_COLOURS[station.price_classification] ?? '#64748b'
+ const borderColour = CLASSIFICATION_BORDER_COLOURS[station.price_classification] ?? '#475569'
+ const miles = ((station.distance_km ?? 0) * 0.621371).toFixed(1)
+ const supermarketTag = station.is_supermarket
+ ? 'Supermarket'
+ : ''
+
+ const popup = `
+
+ ${escHtml(station.name)}${supermarketTag}
+ ${Number(station.price).toFixed(1)}p
+ ${escHtml(miles)} miles away
+ ${escHtml(station.address)}, ${escHtml(station.postcode)}
+
+ `
+
+ const isFirst = index === 0
+ const w = isFirst ? 63 : 59
+ const h = isFirst ? 58 : 51
+
+ const icon = L.divIcon({
+ className: '',
+ iconSize: [w, h],
+ iconAnchor: [w / 2, h],
+ html: buildMarkerHtml(station, index, colour, borderColour),
+ })
+
+ const marker = L.marker([station.lat, station.lng], { icon }).bindPopup(popup)
+
markersLayer.addLayer(marker)
bounds.push([station.lat, station.lng])
})
- if (bounds.length) {
- mapInstance.fitBounds(bounds, { padding: [30, 30] })
+ if (bounds.length === 1) {
+ mapInstance.setView(bounds[0], 14)
+ } else {
+ mapInstance.fitBounds(bounds, { padding: [40, 40], maxZoom: 14 })
}
}
diff --git a/resources/js/components/SearchBar.vue b/resources/js/components/SearchBar.vue
index 894a6da..d518040 100644
--- a/resources/js/components/SearchBar.vue
+++ b/resources/js/components/SearchBar.vue
@@ -1,10 +1,12 @@
-
+
-
+