From a969c1b347a158ba5e9d6c7165db7c97670f43c8 Mon Sep 17 00:00:00 2001 From: Ovidiu U Date: Sat, 11 Apr 2026 20:51:07 +0100 Subject: [PATCH] feat: add fuel price classification markers and responsive search UI improvements - Move map pin icon to right side of input with adjusted spacing - Change button styling from accent to primary color --- app/Http/Middleware/VerifyApiKey.php | 5 + resources/css/app.css | 9 +- resources/js/components/LeafletMap.vue | 131 +++++++++++++++++++++---- resources/js/components/SearchBar.vue | 22 +++-- resources/js/views/Home.vue | 8 +- 5 files changed, 136 insertions(+), 39 deletions(-) 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' }} -
+ @@ -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 @@