Remove obsolete Livewire fuel search components and consolidate pricing tiers
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
tests / ci (8.5) (push) Has been cancelled

- Delete unused Livewire Search test and fuel type select Blade component
- Move subscription webhook listener from EventServiceProvider to AppServiceProvider
- Add FUEL_TYPES global config to app layout for client-side use
- Add Billable trait to User model and include email_verified_at in fillable
- Implement monthly/annual cadence toggle with pricing display and smart CTA routing on homepage
- Update VerifyApiKeyMiddlewareTest to use e10 instead of petrol
- Refactor PollFuelPrices to auto-refresh stale stations based on last_seen_at
- Add incremental polling with cached timestamp and effective-start-timestamp param to FuelPriceService
- Normalize amenities/fuel_types from API objects to flat arrays, skip stations missing required fields
- Log response body on API failures in ApiLogger
- Default homepage sort to 'reliable' instead of 'price'
This commit is contained in:
Ovidiu U
2026-04-20 14:12:15 +01:00
parent aec547cd86
commit 5acb99c9e3
33 changed files with 739 additions and 391 deletions

View File

@@ -220,7 +220,25 @@
<div class="max-w-7xl mx-auto">
<div class="text-center mb-16">
<h2 class="text-4xl md:text-5xl font-black font-display text-zinc-800 mb-4">Pricing for every driver</h2>
<p class="text-zinc-500 text-lg">Save hundreds for less than the cost of a coffee.</p>
<p class="text-zinc-500 text-lg mb-8">Save hundreds for less than the cost of a coffee.</p>
<div class="inline-flex items-center gap-1 p-1 bg-white border border-zinc-300 rounded-full">
<button
:class="cadence === 'monthly' ? 'bg-accent text-white' : 'text-zinc-500'"
class="px-5 py-2 rounded-full text-sm font-bold transition-colors"
type="button"
@click="cadence = 'monthly'"
>
Monthly
</button>
<button
:class="cadence === 'annual' ? 'bg-accent text-white' : 'text-zinc-500'"
class="px-5 py-2 rounded-full text-sm font-bold transition-colors"
type="button"
@click="cadence = 'annual'"
>
Annual <span class="text-[10px] opacity-80">(save 17%)</span>
</button>
</div>
</div>
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
@@ -238,7 +256,7 @@
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> Daily Updates</li>
<li class="text-sm flex gap-2 text-zinc-500"><iconify-icon class="text-zinc-300" icon="lucide:x"></iconify-icon> No Alerts</li>
</ul>
<a class="w-full py-3 px-4 border border-zinc-300 rounded-xl text-center font-bold hover:bg-zinc-100 transition-colors" href="/register">Get Started</a>
<a :href="ctaHref('free')" class="w-full py-3 px-4 border border-zinc-300 rounded-xl text-center font-bold hover:bg-zinc-100 transition-colors">{{ ctaLabel('free') }}</a>
</div>
<!-- Basic -->
@@ -246,8 +264,8 @@
<div class="mb-8">
<h4 class="text-xl font-bold font-display mb-2">Basic</h4>
<div class="flex items-baseline gap-1">
<span class="text-4xl font-black">£0.99</span>
<span class="text-zinc-500 text-sm">/mo</span>
<span class="text-4xl font-black">{{ PRICES[cadence].basic }}</span>
<span class="text-zinc-500 text-sm">{{ PRICE_SUFFIX[cadence] }}</span>
</div>
</div>
<ul class="space-y-4 mb-8 flex-1">
@@ -255,7 +273,7 @@
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> 14-day Trend Data</li>
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> 3 Daily Price Alerts</li>
</ul>
<a class="w-full py-3 px-4 border border-zinc-300 rounded-xl text-center font-bold hover:bg-zinc-100 transition-colors" href="/register">Select Basic</a>
<a :href="ctaHref('basic')" class="w-full py-3 px-4 border border-zinc-300 rounded-xl text-center font-bold hover:bg-zinc-100 transition-colors">{{ ctaLabel('basic') }}</a>
</div>
<!-- Plus -->
@@ -264,8 +282,8 @@
<div class="mb-8">
<h4 class="text-xl font-bold font-display mb-2">Plus</h4>
<div class="flex items-baseline gap-1">
<span class="text-4xl font-black text-accent">£2.49</span>
<span class="text-zinc-500 text-sm">/mo</span>
<span class="text-4xl font-black text-accent">{{ PRICES[cadence].plus }}</span>
<span class="text-zinc-500 text-sm">{{ PRICE_SUFFIX[cadence] }}</span>
</div>
</div>
<ul class="space-y-4 mb-8 flex-1">
@@ -273,7 +291,7 @@
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> Priority Price Alerts</li>
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> Multi-location tracking</li>
</ul>
<a class="w-full py-3 px-4 bg-accent text-white rounded-xl text-center font-bold shadow-lg hover:bg-primary-dark transition-all" href="/register">Join Plus</a>
<a :href="ctaHref('plus')" class="w-full py-3 px-4 bg-accent text-white rounded-xl text-center font-bold shadow-lg hover:bg-primary-dark transition-all">{{ ctaLabel('plus') }}</a>
</div>
<!-- Pro -->
@@ -281,8 +299,8 @@
<div class="mb-8">
<h4 class="text-xl font-bold font-display mb-2">Pro</h4>
<div class="flex items-baseline gap-1">
<span class="text-4xl font-black">£3.99</span>
<span class="text-zinc-400 text-sm">/mo</span>
<span class="text-4xl font-black">{{ PRICES[cadence].pro }}</span>
<span class="text-zinc-400 text-sm">{{ PRICE_SUFFIX[cadence] }}</span>
</div>
</div>
<ul class="space-y-4 mb-8 flex-1">
@@ -290,7 +308,7 @@
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> Multi-Vehicle Fleet</li>
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> Exportable Price History</li>
</ul>
<a class="w-full py-3 px-4 bg-white text-zinc-800 rounded-xl text-center font-bold hover:bg-zinc-100 transition-colors" href="/register">Go Pro</a>
<a :href="ctaHref('pro')" class="w-full py-3 px-4 bg-white text-zinc-800 rounded-xl text-center font-bold hover:bg-zinc-100 transition-colors">{{ ctaLabel('pro') }}</a>
</div>
</div>
</div>
@@ -417,10 +435,45 @@ import SearchBar from '../components/SearchBar.vue'
import LeafletMap from '../components/LeafletMap.vue'
import StationList from '../components/StationList.vue'
const { isAuthenticated } = useAuth()
const { isAuthenticated, userTier } = useAuth()
const cadence = ref('monthly')
function ctaHref(tier) {
if (tier === 'free') {
return isAuthenticated.value ? '/dashboard' : '/register'
}
if (!isAuthenticated.value) {
return '/register?tier=' + tier + '&cadence=' + cadence.value
}
if (userTier.value === tier) {
return '/billing/portal'
}
return '/billing/checkout/' + tier + '/' + cadence.value
}
function ctaLabel(tier) {
if (tier === 'free') {
return isAuthenticated.value ? 'Go to dashboard' : 'Get started'
}
if (isAuthenticated.value && userTier.value === tier) {
return 'Manage subscription'
}
return {
basic: 'Select Basic',
plus: 'Join Plus',
pro: 'Go Pro',
}[tier]
}
const PRICES = {
monthly: { basic: '£0.99', plus: '£2.49', pro: '£3.99' },
annual: { basic: '£9.90', plus: '£24.90', pro: '£39.90' },
}
const PRICE_SUFFIX = { monthly: '/mo', annual: '/yr' }
const { stations, loading, error, search } = useStations()
const sort = ref('price')
const sort = ref('reliable')
const lastParams = ref(null)
const searchAttempted = ref(false)
const radiusMiles = ref(10)

View File

@@ -9,12 +9,9 @@
v-model="form.preferred_fuel_type"
class="w-full h-12 px-4 bg-zinc-50 border border-zinc-300 rounded-xl font-medium text-zinc-800 focus:outline-none focus:ring-2 focus:ring-accent"
>
<option value="petrol">Petrol (E10)</option>
<option value="diesel">Diesel (B7)</option>
<option value="e5">Premium Unleaded (E5)</option>
<option value="b7_premium">Premium Diesel</option>
<option value="b10">B10 Biodiesel</option>
<option value="hvo">HVO</option>
<option v-for="fuel in FUEL_TYPES" :key="fuel.value" :value="fuel.value">
{{ fuel.label }}
</option>
</select>
</div>
@@ -46,14 +43,15 @@
<script setup>
import { ref, onMounted } from 'vue'
import api from '../../axios.js'
import { FUEL_TYPES } from '../../constants/fuelTypes.js'
const form = ref({ preferred_fuel_type: 'petrol', postcode: '' })
const form = ref({ preferred_fuel_type: 'e10', postcode: '' })
const saving = ref(false)
const saved = ref(false)
onMounted(async () => {
const response = await api.get('/user/preferences')
form.value.preferred_fuel_type = response.data.preferred_fuel_type ?? 'petrol'
form.value.preferred_fuel_type = response.data.preferred_fuel_type ?? 'e10'
form.value.postcode = response.data.postcode ?? ''
})