feat: strip FuelFinder to layout shell, wire sub-components
This commit is contained in:
@@ -1,201 +1,77 @@
|
||||
<div class="flex h-dvh flex-col bg-[#f5ede5]">
|
||||
<div class="flex h-dvh flex-col bg-surface-page">
|
||||
|
||||
<x-mobile-header />
|
||||
{{-- HEADER --}}
|
||||
<header class="shrink-0 z-50 bg-surface border-b border-border shadow-sm
|
||||
flex items-center justify-between px-5 pb-4 md:px-8"
|
||||
style="padding-top: max(1rem, env(safe-area-inset-top))">
|
||||
|
||||
{{-- Scrollable main content, offset for fixed header (~112px) and footer (~80px) --}}
|
||||
<main
|
||||
class="flex-1 overflow-y-auto pt-28 pb-20"
|
||||
style="-ms-overflow-style:none;scrollbar-width:none;"
|
||||
>
|
||||
<a href="{{ route('home') }}" class="flex items-center gap-2.5">
|
||||
<div class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-primary shadow-md md:h-10 md:w-10">
|
||||
<iconify-icon icon="lucide:fuel" class="text-lg text-white md:text-xl"></iconify-icon>
|
||||
</div>
|
||||
<span class="text-xl font-black tracking-tighter text-primary md:text-2xl">FuelAlert</span>
|
||||
</a>
|
||||
|
||||
{{-- #search --}}
|
||||
<section class="space-y-3 px-5 pt-5 pb-4">
|
||||
<form wire:submit="findStations">
|
||||
<nav class="hidden items-center gap-8 md:flex">
|
||||
<a href="#" class="text-sm font-semibold text-text-muted transition-colors hover:text-primary">Prices</a>
|
||||
<a href="#" class="text-sm font-semibold text-text-muted transition-colors hover:text-primary">Alerts</a>
|
||||
<a href="#" class="text-sm font-semibold text-text-muted transition-colors hover:text-primary">Trends</a>
|
||||
</nav>
|
||||
|
||||
<div
|
||||
x-data="{
|
||||
query: @js($search),
|
||||
locatingUser: false,
|
||||
_usedIpFallback: false,
|
||||
async _postcodeFromLatLng(lat, lng) {
|
||||
const res = await fetch(`https://api.postcodes.io/postcodes?lon=${lng}&lat=${lat}&limit=1&radius=1000`);
|
||||
const data = await res.json();
|
||||
return data?.result?.[0]?.postcode ?? null;
|
||||
},
|
||||
async locateUser() {
|
||||
this.locatingUser = true;
|
||||
this._usedIpFallback = false;
|
||||
try {
|
||||
let lat, lng;
|
||||
try {
|
||||
const pos = await new Promise((resolve, reject) =>
|
||||
navigator.geolocation.getCurrentPosition(resolve, reject, { enableHighAccuracy: false, timeout: 5000, maximumAge: 60000 })
|
||||
);
|
||||
lat = pos.coords.latitude;
|
||||
lng = pos.coords.longitude;
|
||||
} catch (e) {
|
||||
const d = await fetch('https://ipapi.co/json/').then(r => r.json());
|
||||
lat = d.latitude;
|
||||
lng = d.longitude;
|
||||
this._usedIpFallback = true;
|
||||
}
|
||||
const postcode = await this._postcodeFromLatLng(lat, lng);
|
||||
if (postcode) {
|
||||
this.query = postcode;
|
||||
this.$wire.set('search', postcode);
|
||||
this.$wire.findStations();
|
||||
}
|
||||
} catch (e) {
|
||||
// silent
|
||||
} finally {
|
||||
this.locatingUser = false;
|
||||
}
|
||||
}
|
||||
}"
|
||||
class="relative mb-3"
|
||||
>
|
||||
<iconify-icon
|
||||
icon="lucide:map-pin"
|
||||
class="absolute left-4 top-1/2 -translate-y-1/2 text-xl text-[#89726c] pointer-events-none"
|
||||
></iconify-icon>
|
||||
<input
|
||||
wire:model="search"
|
||||
x-model="query"
|
||||
x-ref="searchInput"
|
||||
type="text"
|
||||
name="search"
|
||||
@focus="_usedIpFallback = false"
|
||||
placeholder="Postcode, town or city"
|
||||
class="h-14 w-full rounded-xl border border-[#e5ded7] bg-[#faf6f3] pl-12 pr-36 text-base font-semibold text-[#4a3f3b] focus:outline-none focus:ring-2 focus:ring-[#bb5b3e] focus:border-transparent"
|
||||
/>
|
||||
{{-- Right-side controls --}}
|
||||
<div class="absolute inset-y-0 right-0 flex items-center gap-2 pr-3">
|
||||
{{-- Clear --}}
|
||||
<button
|
||||
x-show="query.length > 0"
|
||||
x-cloak
|
||||
type="button"
|
||||
@click="query = ''; $wire.set('search', ''); _usedIpFallback = false"
|
||||
class="text-[#89726c] hover:text-[#4a3f3b]"
|
||||
>
|
||||
<iconify-icon icon="lucide:x" class="text-base"></iconify-icon>
|
||||
</button>
|
||||
{{-- Near me pill --}}
|
||||
<button
|
||||
type="button"
|
||||
@click="locateUser()"
|
||||
:disabled="locatingUser"
|
||||
class="flex items-center gap-1.5 rounded-full bg-[#e5ded7] px-3 py-1.5 text-sm font-semibold text-[#4a3f3b] disabled:opacity-40"
|
||||
>
|
||||
<iconify-icon x-show="!locatingUser" icon="lucide:locate-fixed" class="text-sm"></iconify-icon>
|
||||
<iconify-icon x-show="locatingUser" icon="lucide:loader-circle" class="text-sm animate-spin"></iconify-icon>
|
||||
<span x-text="locatingUser ? 'Finding...' : 'Near me'">Near me</span>
|
||||
</button>
|
||||
{{-- Search --}}
|
||||
<button
|
||||
type="submit"
|
||||
wire:loading.attr="disabled"
|
||||
class="text-[#89726c] disabled:opacity-60"
|
||||
>
|
||||
<iconify-icon wire:loading.remove wire:target="findStations" icon="lucide:search" class="text-xl"></iconify-icon>
|
||||
<iconify-icon wire:loading wire:target="findStations" icon="lucide:loader-circle" class="text-xl animate-spin"></iconify-icon>
|
||||
</button>
|
||||
</div>
|
||||
{{-- IP fallback nudge --}}
|
||||
<div
|
||||
x-show="_usedIpFallback"
|
||||
x-cloak
|
||||
x-transition:enter="transition ease-out duration-300 delay-500"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2"
|
||||
x-transition:enter-end="opacity-100 translate-y-0"
|
||||
x-transition:leave="transition ease-in duration-200"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="absolute top-full left-0 right-0 z-40 mt-2"
|
||||
>
|
||||
<div
|
||||
@click="$refs.searchInput.focus(); _usedIpFallback = false"
|
||||
class="cursor-pointer rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 transition-colors hover:bg-amber-100"
|
||||
>
|
||||
<p class="text-center text-xs text-amber-800">
|
||||
<span class="font-medium">Showing approximate location.</span>
|
||||
<span class="underline">Enter your postcode above</span> for exact results.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@error('search')
|
||||
<p class="mb-2 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
@auth
|
||||
<a href="{{ route('dashboard') }}"
|
||||
class="flex h-9 w-9 items-center justify-center rounded-full border border-border bg-surface-subtle">
|
||||
<iconify-icon icon="lucide:user" class="text-base text-text-muted"></iconify-icon>
|
||||
</a>
|
||||
@else
|
||||
<a href="{{ route('login') }}"
|
||||
class="flex h-9 w-9 items-center justify-center rounded-full border border-border bg-surface-subtle">
|
||||
<iconify-icon icon="lucide:user" class="text-base text-text-muted"></iconify-icon>
|
||||
</a>
|
||||
@endauth
|
||||
</header>
|
||||
|
||||
{{-- Filter rows --}}
|
||||
<div class="space-y-2 mb-3">
|
||||
<div class="flex gap-2">
|
||||
<div class="shrink-0">
|
||||
<x-fuel.type-select wire:model.live="fuelType" />
|
||||
</div>
|
||||
<div class="shrink-0">
|
||||
<x-fuel.sort-select wire:model.live="sort" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<div class="shrink-0">
|
||||
<x-fuel.radius-select wire:model.live="radius" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{-- MAIN --}}
|
||||
<main class="flex-1 overflow-y-auto" style="-ms-overflow-style:none;scrollbar-width:none;">
|
||||
<div class="md:mx-auto md:max-w-3xl">
|
||||
|
||||
</form>
|
||||
<livewire:public.fuel.search />
|
||||
<livewire:public.fuel.recommendation />
|
||||
<livewire:public.fuel.map />
|
||||
<livewire:public.fuel.station-list />
|
||||
|
||||
@if ($apiError)
|
||||
<div class="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
|
||||
{{ $apiError }}
|
||||
</div>
|
||||
@endif
|
||||
</section>
|
||||
|
||||
{{-- #recommendation --}}
|
||||
{{-- @if ($prediction)
|
||||
<section class="px-5 pb-5">
|
||||
<x-fuel.recommendation :prediction="$prediction" />
|
||||
<section class="px-5 pb-8">
|
||||
<x-fuel.forecast />
|
||||
</section>
|
||||
@endif --}}
|
||||
|
||||
{{-- #map --}}
|
||||
<section class="mb-4" wire:ignore>
|
||||
<x-fuel.station-map :results="$results" />
|
||||
</section>
|
||||
|
||||
{{-- #stations --}}
|
||||
@if ($hasSearched)
|
||||
<section class="px-5 pb-5">
|
||||
@if (! empty($meta))
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<h3 class="text-base font-bold text-[#4a3f3b]">Stations Nearby</h3>
|
||||
<span class="text-[10px] font-bold uppercase tracking-widest text-[#89726c]">
|
||||
{{ $meta['count'] ?? 0 }} {{ str('Result')->plural($meta['count'] ?? 0) }}
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@forelse ($results as $station)
|
||||
<div class="mb-2">
|
||||
<x-fuel.station-card :station="$station" />
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-sm text-[#89726c]">
|
||||
No stations found within {{ $radius }} {{ str('mile')->plural($radius) }} of "{{ $search }}".
|
||||
</p>
|
||||
@endforelse
|
||||
</section>
|
||||
@endif
|
||||
|
||||
{{-- #forecast --}}
|
||||
<section class="px-5 pb-8">
|
||||
<x-fuel.forecast />
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<x-mobile-footer />
|
||||
{{-- BOTTOM TAB BAR --}}
|
||||
@php
|
||||
$tabs = [
|
||||
['label' => 'Prices', 'icon' => 'lucide:fuel', 'route' => 'home'],
|
||||
['label' => 'Alerts', 'icon' => 'lucide:bell', 'route' => null],
|
||||
['label' => 'Forecourts', 'icon' => 'lucide:map-pin', 'route' => null],
|
||||
['label' => 'Trends', 'icon' => 'lucide:trending-up', 'route' => null],
|
||||
];
|
||||
@endphp
|
||||
<nav class="shrink-0 border-t border-border bg-surface md:hidden"
|
||||
style="padding-bottom: max(0.5rem, env(safe-area-inset-bottom))">
|
||||
<div class="flex pt-3">
|
||||
@foreach ($tabs as $tab)
|
||||
@php $active = $tab['route'] && request()->routeIs($tab['route']); @endphp
|
||||
<div class="flex flex-1 flex-col items-center gap-1">
|
||||
<iconify-icon
|
||||
icon="{{ $tab['icon'] }}"
|
||||
class="text-xl {{ $active ? 'text-primary' : 'text-text-muted' }}"
|
||||
></iconify-icon>
|
||||
<span class="text-[10px] font-bold uppercase tracking-wide {{ $active ? 'text-primary' : 'text-text-muted' }}">
|
||||
{{ $tab['label'] }}
|
||||
</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user