diff --git a/code-style.md b/.claude/rules/code-style.md similarity index 100% rename from code-style.md rename to .claude/rules/code-style.md diff --git a/docs/theme.md b/docs/theme.md new file mode 100644 index 0000000..cddfd96 --- /dev/null +++ b/docs/theme.md @@ -0,0 +1,41 @@ +# FuelAlert — Colour Palette + +## Primary — Burnt Sienna +| Token | Hex | Usage | +|--------------|-----------|------------------------------| +| primary | `#bb5b3e` | CTA buttons, focus rings, brand | +| primary-dark | `#a34a31` | Hover / pressed state | + +## Neutrals — Warm Brown +| Token | Hex | Usage | +|-----------|-----------|------------------------------| +| text-base | `#4a3f3b` | Body text, headings | +| text-muted| `#89726c` | Secondary text, icons | +| text-dim | `#6b5a55` | Tertiary / placeholder text | + +## Surfaces — Cream / Linen +| Token | Hex | Usage | +|----------------|-----------|------------------------------| +| surface | `#faf6f3` | Input & card background | +| surface-page | `#f5ede5` | Page / app background | +| surface-subtle | `#eeeae5` | Subtle surface / hover | +| border | `#e5ded7` | Borders & dividers | + +## Accents +| Token | Hex | Usage | +|--------|-----------|------------------------------| +| teal | `#4A7C7E` | Secondary accent (minor) | +| mauve | `#8B4860` | Tertiary accent (minor) | +| tan | `#9B8B6B` | Warm neutral accent | + +## Status +| Token | Hex | Usage | +|---------|-----------|------------------------------| +| success | `#22c55e` | Price current / good signal | +| warning | `#f59e0b` | Stale price / weak signal | +| error | `#ef4444` | Outdated price / error state | + +## Notes +- Core brand feel: warm terracotta on a cream/linen base. +- Teal and mauve are currently used sparingly — confirm role in DaisyUI theme before promoting to named tokens. +- Never use cold grays — all neutrals should lean warm. diff --git a/resources/js/maps/station-map.js b/resources/js/maps/station-map.js index 2200675..9b335d4 100644 --- a/resources/js/maps/station-map.js +++ b/resources/js/maps/station-map.js @@ -26,15 +26,38 @@ const CLASSIFICATION_COLOURS = { }; const UK_CENTRE = [54.0, -2.0]; -const UK_ZOOM = 6; +const UK_ZOOM = 7; -export function stationMap(results) { +const USER_MARKER_CSS = ` +@keyframes fuelalert-pulse { + 0% { transform: scale(1); opacity: 0.6; } + 70% { transform: scale(2.8); opacity: 0; } + 100% { transform: scale(1); opacity: 0; } +} +.fuelalert-user-marker { position: relative; width: 16px; height: 16px; } +.fuelalert-user-dot { position: absolute; inset: 0; border-radius: 50%; background: #3b82f6; border: 2px solid #fff; box-shadow: 0 0 0 2px #3b82f6; } +.fuelalert-user-ring { position: absolute; inset: 0; border-radius: 50%; background: #3b82f6; animation: fuelalert-pulse 2s ease-out infinite; } +`; + +function injectUserMarkerStyles() { + if (document.getElementById('fuelalert-user-marker-styles')) return; + const style = document.createElement('style'); + style.id = 'fuelalert-user-marker-styles'; + style.textContent = USER_MARKER_CSS; + document.head.appendChild(style); +} + +export function stationMap(results, meta, radius) { return { results, + meta, + radius, _map: null, _markers: [], + _userMarker: null, init() { + injectUserMarkerStyles(); this._map = L.map(this.$el, { zoomControl: true }).setView(UK_CENTRE, UK_ZOOM); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { @@ -47,6 +70,18 @@ export function stationMap(results) { } this.$watch('results', () => this._plotMarkers()); + this.locateUser(); + }, + + getZoomForRadius(radiusMiles) { + if (radiusMiles <= 1) return 15; + if (radiusMiles <= 2) return 14; + if (radiusMiles <= 5) return 12; + if (radiusMiles <= 10) return 11; + if (radiusMiles <= 15) return 10; + if (radiusMiles <= 25) return 9; + if (radiusMiles <= 50) return 8; + return 7; }, _clearMarkers() { @@ -54,11 +89,61 @@ export function stationMap(results) { this._markers = []; }, + addUserMarker(lat, lng) { + if (this._userMarker) { + this._userMarker.remove(); + } + + const icon = L.divIcon({ + className: '', + html: '
', + iconSize: [16, 16], + iconAnchor: [8, 8], + }); + + this._userMarker = L.marker([lat, lng], { icon, zIndexOffset: 1000 }) + .bindPopup('Your location') + .addTo(this._map); + + console.log(`[stationMap] user marker lat=${lat} lng=${lng}`); + }, + + locateUser() { + if (!navigator.geolocation) { + console.warn('[stationMap] Geolocation not supported'); + return; + } + + const ipFallback = () => { + fetch('https://ipapi.co/json/') + .then((r) => r.json()) + .then((d) => d.latitude && d.longitude && this.addUserMarker(d.latitude, d.longitude)) + .catch(() => {}); + }; + + // Quick low-accuracy fix first — places the marker immediately. + navigator.geolocation.getCurrentPosition( + (pos) => { + this.addUserMarker(pos.coords.latitude, pos.coords.longitude); + + // Then refine with high accuracy if GPS is available. + navigator.geolocation.getCurrentPosition( + (precise) => this.addUserMarker(precise.coords.latitude, precise.coords.longitude), + () => {}, + { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 }, + ); + }, + () => ipFallback(), + { enableHighAccuracy: false, timeout: 5000, maximumAge: 60000 }, + ); + }, + destroy() { if (this._map) { this._map.remove(); this._map = null; this._markers = []; + this._userMarker = null; } }, @@ -72,8 +157,6 @@ export function stationMap(results) { return; } - const bounds = []; - this.results.forEach((station) => { const colour = escHtml(CLASSIFICATION_COLOURS[station.price_classification] ?? '#64748b'); const miles = (station.distance_km * 0.621371).toFixed(1); @@ -101,14 +184,18 @@ export function stationMap(results) { marker.addTo(this._map); this._markers.push(marker); - bounds.push([station.lat, station.lng]); }); - if (bounds.length === 1) { - this._map.setView(bounds[0], 14); - } else { - this._map.fitBounds(bounds, { padding: [40, 40], maxZoom: 14 }); - } + const map = this._map; + const lat = this.meta?.lat; + const lng = this.meta?.lng; + const zoom = this.getZoomForRadius(this.radius); + + setTimeout(() => { + map.invalidateSize(); + map.setView([lat, lng], zoom, { animate: true, duration: 0.5 }); + console.log(`[stationMap] setView lat=${lat} lng=${lng} zoom=${zoom} (radius=${this.radius}mi)`); + }, 50); }, }; } diff --git a/resources/views/components/fuel/station-map.blade.php b/resources/views/components/fuel/station-map.blade.php index fd4d69e..27ffd7e 100644 --- a/resources/views/components/fuel/station-map.blade.php +++ b/resources/views/components/fuel/station-map.blade.php @@ -1,6 +1,6 @@ @props(['results' => []])
diff --git a/resources/views/livewire/public/fuel-finder.blade.php b/resources/views/livewire/public/fuel-finder.blade.php index f51d656..3acece0 100644 --- a/resources/views/livewire/public/fuel-finder.blade.php +++ b/resources/views/livewire/public/fuel-finder.blade.php @@ -2,53 +2,148 @@ - {{-- Scrollable main content, offset for fixed header (~80px) and footer (~80px) --}} + {{-- Scrollable main content, offset for fixed header (~112px) and footer (~80px) --}}
{{-- #search --}}
-
+ +
+ {{-- Right-side controls --}} +
+ {{-- Clear --}} + + {{-- Near me pill --}} + + {{-- Search --}} + +
+ {{-- IP fallback nudge --}} +
+
+

+ Showing approximate location. + Enter your postcode above for exact results. +

+
+
@error('search')

{{ $message }}

@enderror - {{-- Filter pills (scrollable row) --}} -
-
- + {{-- Filter rows --}} +
+
+
+ +
+
+ +
-
- -
-
- +
+
+ +
- @if ($apiError) @@ -59,14 +154,14 @@
{{-- #recommendation --}} - @if ($prediction) + {{-- @if ($prediction)
- @endif + @endif --}} {{-- #map --}} -
+
diff --git a/resources/views/mobile.blade.php b/resources/views/mobile.blade.php new file mode 100644 index 0000000..000f2af --- /dev/null +++ b/resources/views/mobile.blade.php @@ -0,0 +1,620 @@ + + + + + + FuelAlert | Stop Overpaying for Fuel + + + + + + + @vite(['resources/css/app.css', 'resources/js/app.js']) + + + + +{{-- Mobile App Layout (hidden on desktop) --}} +
+ + {{-- Mobile Header --}} +
+
+
+ +
+ FuelAlert +
+ +
+ + {{-- Mobile Scrollable Main --}} +
+ + {{-- Search & Filters --}} +
+
+ + +
+
+ + + +
+
+ + {{-- Recommendation Card --}} +
+
+
+
+

Recommendation

+

Fill up now

+
+
+
+ + + + + 80% +
+ Confidence +
+
+

+ Local prices are at a 30-day low. Regional wholesale trends indicate a 3p/litre increase starting Monday. Securing fuel today is highly advised. +

+
+
+ + {{-- Map Section --}} +
+ {{-- Simulated map grid --}} +
+
+ + {{-- Map Markers --}} +
+
142.9p
+ +
+
+
145.7p
+ +
+
+
148.9p
+ +
+ + {{-- Legend --}} +
+
+ + Current +
+
+ + Recent +
+
+ + Stale +
+
+
+ + {{-- Nearby Stations --}} +
+
+

Stations Nearby

+ 26 Results +
+
+
+

Tesco Superstore

+
142.9p
+
+
+

Sainsbury's Fuel

+
143.1p
+
+
+

BP Connect

+
145.7p
+
+
+

Shell V-Power

+
148.9p
+
+
+

Esso Express

+
151.2p
+
+
+
+ + {{-- 14-Day Forecast (Pro) --}} +
+
+
+
+

14-Day Forecast

+
+ + Pro +
+
+
+ + + +
+ +
+
+
+
+
+ +
+ + {{-- Mobile Tab Bar --}} + + +
{{-- end mobile layout --}} + +{{-- Desktop Layout (hidden on mobile) --}} +{{-- end desktop layout --}} + + + diff --git a/routes/web.php b/routes/web.php index 6f1a0dd..b31c511 100644 --- a/routes/web.php +++ b/routes/web.php @@ -4,7 +4,11 @@ use App\Livewire\Public\FuelFinder; use App\Livewire\Public\StationSearch; use Illuminate\Support\Facades\Route; -Route::get('/', FuelFinder::class)->name('home'); +//Route::get('/', FuelFinder::class)->name('home'); + +Route::view('/', 'homepage')->name('home'); + +Route::get('/fuel-finder', FuelFinder::class)->name('fuel-finder'); Route::get('/stations', StationSearch::class)->name('stations.search');