feat: add Laravel Fortify skill, condense API data rules, add homepage mockup
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

This commit is contained in:
Ovidiu U
2026-04-09 14:19:04 +01:00
parent 1848c070da
commit 19d5c6eb0b
7 changed files with 753 additions and 394 deletions

View File

@@ -0,0 +1,420 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title>FuelAlert | Save Smart on Petrol & Diesel</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js"></script>
<link href="https://fonts.googleapis.com" rel="preconnect">
<link crossorigin href="https://fonts.gstatic.com" rel="preconnect">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Manrope:wght@600;700;800;900&display=swap" rel="stylesheet">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Manrope:wght@600;700;800;900&display=swap');
:root {
--primary: #bb5b3e;
--bg-main: #f5ede5;
--bg-card: #faf6f3;
--text-main: #4a3f3b;
--neutral: #89726c;
--rec-now: #8B4860;
--rec-wait: #4A7C7E;
--rec-hold: #9B8B6B;
}
body {
font-family: 'Inter', sans-serif;
color: var(--text-main);
margin: 0;
}
h1, h2, h3, h4 {
font-family: 'Manrope', sans-serif;
letter-spacing: -0.02em;
}
.glass-card {
background: rgba(250, 246, 243, 0.8);
backdrop-filter: blur(12px);
border: 1px solid rgba(229, 222, 215, 0.5);
}
.hero-gradient {
background: radial-gradient(circle at top right, #bb5b3e15, transparent 50%),
radial-gradient(circle at bottom left, #bb5b3e10, transparent 40%);
}
</style>
</head>
<body>
<div class="min-h-screen bg-[#f5ede5]">
<!-- Navigation -->
<nav class="fixed top-0 w-full z-50 bg-[#faf6f3] border-b border-[#e5ded7] px-6 py-4 md:px-12">
<div class="max-w-7xl mx-auto flex items-center justify-between">
<a class="flex items-center gap-3" href="#" id="nav-logo-link">
<div class="w-10 h-10 md:w-12 md:h-12 rounded-lg bg-[#bb5b3e] flex items-center justify-center shadow-md">
<iconify-icon class="text-white text-xl md:text-2xl" icon="lucide:fuel"></iconify-icon>
</div>
<span class="text-2xl md:text-3xl font-black tracking-tighter text-[#bb5b3e]">FuelAlert</span>
</a>
<div class="hidden md:flex items-center gap-10">
<a class="text-sm font-semibold text-[#89726c] hover:text-[#bb5b3e] transition-colors" href="#how-it-works" id="nav-how-link">How it Works</a>
<a class="text-sm font-semibold text-[#89726c] hover:text-[#bb5b3e] transition-colors" href="#features" id="nav-features-link">Features</a>
<a class="text-sm font-semibold text-[#89726c] hover:text-[#bb5b3e] transition-colors" href="#pricing" id="nav-pricing-link">Pricing</a>
</div>
<div class="flex items-center gap-4">
<a class="text-sm font-bold text-[#89726c] hover:text-[#4a3f3b]" href="#" id="nav-login-link">Login</a>
<a class="bg-[#bb5b3e] text-white px-6 py-2.5 rounded-full text-sm font-bold shadow-lg hover:bg-[#a34a31] transition-all transform hover:scale-105 active:scale-95" href="#" id="nav-cta-link">Get Started</a>
</div>
</div>
</nav>
<!-- Hero Section -->
<section class="relative pt-40 pb-24 px-6 hero-gradient overflow-hidden">
<div class="max-w-7xl mx-auto grid lg:grid-cols-2 gap-16 items-center">
<div class="space-y-8">
<div class="inline-flex items-center gap-2 px-3 py-1 bg-[#bb5b3e]/10 text-[#bb5b3e] rounded-full text-xs font-bold uppercase tracking-wider">
<iconify-icon icon="lucide:sparkles"></iconify-icon>
Save up to £250/year on fuel
</div>
<h1 class="text-5xl md:text-7xl font-black text-[#4a3f3b] leading-[1.1] tracking-tighter">
Stop Overpaying <br/> <span class="text-[#bb5b3e]">for Fuel.</span>
</h1>
<p class="text-xl text-[#89726c] max-w-lg leading-relaxed">
Join 50,000+ UK drivers using real-time insights to find the cheapest petrol and time their fill-ups perfectly.
</p>
<div class="flex flex-col sm:flex-row gap-3 max-w-md">
<div class="relative flex-1">
<iconify-icon class="absolute left-4 top-1/2 -translate-y-1/2 text-[#89726c] text-xl" icon="lucide:map-pin"></iconify-icon>
<input class="w-full h-14 pl-12 pr-4 bg-white border border-[#e5ded7] rounded-xl focus:outline-none focus:ring-2 focus:ring-[#bb5b3e] shadow-inner text-lg" id="hero-postcode-input" placeholder="Enter Postcode" type="text">
</div>
<button class="h-14 px-8 bg-[#bb5b3e] text-white rounded-xl font-bold text-lg shadow-xl hover:bg-[#a34a31] transition-all" id="hero-search-btn">Find Prices</button>
</div>
<div class="flex items-center gap-4 pt-4">
<div class="flex -space-x-2">
<img alt="User" class="w-8 h-8 rounded-full border-2 border-white" src="https://api.dicebear.com/7.x/avataaars/svg?seed=1">
<img alt="User" class="w-8 h-8 rounded-full border-2 border-white" src="https://api.dicebear.com/7.x/avataaars/svg?seed=2">
<img alt="User" class="w-8 h-8 rounded-full border-2 border-white" src="https://api.dicebear.com/7.x/avataaars/svg?seed=3">
</div>
<span class="text-sm text-[#89726c] font-medium italic">"Saved me £12 on my first tank!"</span>
</div>
</div>
<!-- Visual Mockup -->
<div class="relative">
<div class="absolute -inset-4 bg-[#bb5b3e]/5 rounded-[2.5rem] blur-2xl"></div>
<div class="relative glass-card p-6 rounded-[2rem] shadow-2xl space-y-4 max-w-md mx-auto transform rotate-2">
<div class="flex justify-between items-center mb-4">
<div class="flex items-center gap-2">
<div class="w-8 h-8 rounded bg-[#bb5b3e] flex items-center justify-center"><iconify-icon class="text-white" icon="lucide:fuel"></iconify-icon></div>
<span class="font-black text-[#bb5b3e]">FuelAlert</span>
</div>
<div class="text-xs font-bold text-[#89726c]">SW1A 1AA</div>
</div>
<div class="bg-[#faf6f3] p-4 rounded-xl border border-[#e5ded7] shadow-sm">
<p class="text-[10px] font-bold uppercase tracking-widest text-[#89726c] mb-1">Recommendation</p>
<h3 class="text-2xl font-black text-[#8B4860]">Fill up now</h3>
<div class="mt-2 h-1.5 w-full bg-[#eeeae5] rounded-full overflow-hidden">
<div class="h-full bg-[#8B4860] w-[80%]"></div>
</div>
</div>
<div class="space-y-2">
<div class="flex justify-between items-center p-3 bg-white rounded-lg border border-[#eeeae5]">
<span class="font-bold text-sm">Tesco Superstore</span>
<span class="font-black text-[#22c55e]">142.9p</span>
</div>
<div class="flex justify-between items-center p-3 bg-white rounded-lg border border-[#eeeae5]">
<span class="font-bold text-sm">Shell V-Power</span>
<span class="font-black text-[#89726c]">148.9p</span>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- How It Works -->
<section class="py-24 px-6 bg-[#faf6f3]" id="how-it-works">
<div class="max-w-7xl mx-auto">
<div class="text-center mb-16 space-y-4">
<h2 class="text-4xl md:text-5xl font-black text-[#4a3f3b]">Smart Savings in 3 Steps</h2>
<p class="text-[#89726c] text-lg max-w-2xl mx-auto">Stop guessing when to fill up. Our engine analyzes thousands of data points daily to save you money.</p>
</div>
<div class="grid md:grid-cols-3 gap-12">
<div class="text-center space-y-4">
<div class="w-16 h-16 bg-[#bb5b3e]/10 text-[#bb5b3e] rounded-2xl flex items-center justify-center mx-auto text-3xl">
<iconify-icon icon="lucide:search"></iconify-icon>
</div>
<h3 class="text-2xl font-bold">1. Search</h3>
<p class="text-[#89726c]">Enter your postcode or location to find every forecourt within a 5-20 mile radius instantly.</p>
</div>
<div class="text-center space-y-4">
<div class="w-16 h-16 bg-[#bb5b3e]/10 text-[#bb5b3e] rounded-2xl flex items-center justify-center mx-auto text-3xl">
<iconify-icon icon="lucide:trending-up"></iconify-icon>
</div>
<h3 class="text-2xl font-bold">2. Get Advice</h3>
<p class="text-[#89726c]">Our AI compares local prices against national wholesale trends to give you a Fill Up/Wait recommendation.</p>
</div>
<div class="text-center space-y-4">
<div class="w-16 h-16 bg-[#bb5b3e]/10 text-[#bb5b3e] rounded-2xl flex items-center justify-center mx-auto text-3xl">
<iconify-icon icon="lucide:wallet"></iconify-icon>
</div>
<h3 class="text-2xl font-bold">3. Fill Up Smart</h3>
<p class="text-[#89726c]">Navigate to the cheapest station and fill up with confidence knowing you've secured the best price.</p>
</div>
</div>
</div>
</section>
<!-- Features Section -->
<section class="py-24 px-6" id="features">
<div class="max-w-7xl mx-auto">
<div class="grid lg:grid-cols-2 gap-20 items-center">
<div class="order-2 lg:order-1">
<div class="grid grid-cols-2 gap-6">
<div class="p-6 bg-[#faf6f3] border border-[#e5ded7] rounded-2xl space-y-3">
<iconify-icon class="text-3xl text-[#bb5b3e]" icon="lucide:zap"></iconify-icon>
<h4 class="font-bold text-lg">Real-Time Prices</h4>
<p class="text-sm text-[#89726c]">Verified daily prices from thousands of UK forecourts.</p>
</div>
<div class="p-6 bg-[#faf6f3] border border-[#e5ded7] rounded-2xl space-y-3">
<iconify-icon class="text-3xl text-[#bb5b3e]" icon="lucide:calendar"></iconify-icon>
<h4 class="font-bold text-lg">Timing Predictions</h4>
<p class="text-sm text-[#89726c]">Proprietary 14-day forecasts for petrol and diesel trends.</p>
</div>
<div class="p-6 bg-[#faf6f3] border border-[#e5ded7] rounded-2xl space-y-3">
<iconify-icon class="text-3xl text-[#bb5b3e]" icon="lucide:shopping-bag"></iconify-icon>
<h4 class="font-bold text-lg">Supermarket Anchors</h4>
<p class="text-sm text-[#89726c]">Track local supermarkets to find the absolute lowest base price.</p>
</div>
<div class="p-6 bg-[#faf6f3] border border-[#e5ded7] rounded-2xl space-y-3">
<iconify-icon class="text-3xl text-[#bb5b3e]" icon="lucide:bell-ring"></iconify-icon>
<h4 class="font-bold text-lg">Smart Price Alerts</h4>
<p class="text-sm text-[#89726c]">Get notified when local prices drop below your set target.</p>
</div>
</div>
</div>
<div class="order-1 lg:order-2 space-y-8">
<h2 class="text-4xl md:text-5xl font-black text-[#4a3f3b]">The ultimate fuel companion.</h2>
<p class="text-lg text-[#89726c]">Whether you're a daily commuter, a delivery professional, or just planning a weekend road trip, FuelAlert gives you the edge at the pump.</p>
<ul class="space-y-4">
<li class="flex items-center gap-3 font-bold">
<iconify-icon class="text-[#bb5b3e]" icon="lucide:check-circle-2"></iconify-icon>
Coverage for 98% of UK Forecourts
</li>
<li class="flex items-center gap-3 font-bold">
<iconify-icon class="text-[#bb5b3e]" icon="lucide:check-circle-2"></iconify-icon>
Hyper-local Map Visualization
</li>
<li class="flex items-center gap-3 font-bold">
<iconify-icon class="text-[#bb5b3e]" icon="lucide:check-circle-2"></iconify-icon>
Historic Price Benchmarking
</li>
</ul>
<button class="inline-flex items-center gap-2 text-[#bb5b3e] font-black text-lg group" id="features-cta-btn">
Explore all features
<iconify-icon class="group-hover:translate-x-1 transition-transform" icon="lucide:arrow-right"></iconify-icon>
</button>
</div>
</div>
</div>
</section>
<!-- Pricing Section -->
<section class="py-24 px-6 bg-[#faf6f3]" id="pricing">
<div class="max-w-7xl mx-auto">
<div class="text-center mb-16">
<h2 class="text-4xl md:text-5xl font-black text-[#4a3f3b] mb-4">Pricing for every driver</h2>
<p class="text-[#89726c] text-lg">Save hundreds for less than the cost of a coffee.</p>
</div>
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
<!-- Free -->
<div class="bg-white border border-[#e5ded7] p-8 rounded-3xl flex flex-col h-full">
<div class="mb-8">
<h4 class="text-xl font-bold mb-2">Free</h4>
<div class="flex items-baseline gap-1">
<span class="text-4xl font-black">£0</span>
<span class="text-[#89726c] text-sm">/mo</span>
</div>
</div>
<ul class="space-y-4 mb-8 flex-1">
<li class="text-sm flex gap-2"><iconify-icon class="text-[#bb5b3e]" icon="lucide:check"></iconify-icon> Basic Search</li>
<li class="text-sm flex gap-2"><iconify-icon class="text-[#bb5b3e]" icon="lucide:check"></iconify-icon> Daily Updates</li>
<li class="text-sm flex gap-2 text-[#89726c]"><iconify-icon class="text-gray-300" icon="lucide:x"></iconify-icon> No Alerts</li>
</ul>
<a class="w-full py-3 px-4 border border-[#e5ded7] rounded-xl text-center font-bold hover:bg-[#faf6f3] transition-colors" href="#" id="plan-free-btn">Get Started</a>
</div>
<!-- Basic -->
<div class="bg-white border border-[#e5ded7] p-8 rounded-3xl flex flex-col h-full">
<div class="mb-8">
<h4 class="text-xl font-bold mb-2">Basic</h4>
<div class="flex items-baseline gap-1">
<span class="text-4xl font-black">£0.99</span>
<span class="text-[#89726c] text-sm">/mo</span>
</div>
</div>
<ul class="space-y-4 mb-8 flex-1">
<li class="text-sm flex gap-2"><iconify-icon class="text-[#bb5b3e]" icon="lucide:check"></iconify-icon> Ad-free Experience</li>
<li class="text-sm flex gap-2"><iconify-icon class="text-[#bb5b3e]" icon="lucide:check"></iconify-icon> 14-day Trend Data</li>
<li class="text-sm flex gap-2"><iconify-icon class="text-[#bb5b3e]" icon="lucide:check"></iconify-icon> 3 Daily Price Alerts</li>
</ul>
<a class="w-full py-3 px-4 border border-[#e5ded7] rounded-xl text-center font-bold hover:bg-[#faf6f3] transition-colors" href="#" id="plan-basic-btn">Select Basic</a>
</div>
<!-- Plus -->
<div class="bg-white border-2 border-[#bb5b3e] p-8 rounded-3xl flex flex-col h-full relative">
<div class="absolute -top-4 left-1/2 -translate-x-1/2 bg-[#bb5b3e] text-white px-4 py-1 rounded-full text-[10px] font-black uppercase tracking-widest">Most Popular</div>
<div class="mb-8">
<h4 class="text-xl font-bold mb-2">Plus</h4>
<div class="flex items-baseline gap-1">
<span class="text-4xl font-black text-[#bb5b3e]">£2.49</span>
<span class="text-[#89726c] text-sm">/mo</span>
</div>
</div>
<ul class="space-y-4 mb-8 flex-1">
<li class="text-sm flex gap-2 font-bold"><iconify-icon class="text-[#bb5b3e]" icon="lucide:check"></iconify-icon> Supermarket Anchor</li>
<li class="text-sm flex gap-2"><iconify-icon class="text-[#bb5b3e]" icon="lucide:check"></iconify-icon> Priority Price Alerts</li>
<li class="text-sm flex gap-2"><iconify-icon class="text-[#bb5b3e]" icon="lucide:check"></iconify-icon> Multi-location tracking</li>
</ul>
<a class="w-full py-3 px-4 bg-[#bb5b3e] text-white rounded-xl text-center font-bold shadow-lg hover:bg-[#a34a31] transition-all" href="#" id="plan-plus-btn">Join Plus</a>
</div>
<!-- Pro -->
<div class="bg-[#4a3f3b] border border-[#4a3f3b] p-8 rounded-3xl flex flex-col h-full text-white">
<div class="mb-8">
<h4 class="text-xl font-bold mb-2">Pro</h4>
<div class="flex items-baseline gap-1">
<span class="text-4xl font-black">£3.99</span>
<span class="text-gray-300 text-sm">/mo</span>
</div>
</div>
<ul class="space-y-4 mb-8 flex-1">
<li class="text-sm flex gap-2"><iconify-icon class="text-[#bb5b3e]" icon="lucide:sparkles"></iconify-icon> AI Price Predictions</li>
<li class="text-sm flex gap-2"><iconify-icon class="text-[#bb5b3e]" icon="lucide:check"></iconify-icon> Multi-Vehicle Fleet</li>
<li class="text-sm flex gap-2"><iconify-icon class="text-[#bb5b3e]" icon="lucide:check"></iconify-icon> Exportable Price History</li>
</ul>
<a class="w-full py-3 px-4 bg-white text-[#4a3f3b] rounded-xl text-center font-bold hover:bg-gray-100 transition-colors" href="#" id="plan-pro-btn">Go Pro</a>
</div>
</div>
</div>
</section>
<!-- Social Proof -->
<section class="py-24 px-6">
<div class="max-w-7xl mx-auto">
<div class="flex flex-col md:flex-row gap-12 items-center">
<div class="md:w-1/3">
<h2 class="text-4xl font-black text-[#4a3f3b] mb-4">Loved by commuters.</h2>
<div class="flex items-center gap-1 text-[#f59e0b] mb-4 text-xl">
<iconify-icon icon="lucide:star"></iconify-icon>
<iconify-icon icon="lucide:star"></iconify-icon>
<iconify-icon icon="lucide:star"></iconify-icon>
<iconify-icon icon="lucide:star"></iconify-icon>
<iconify-icon icon="lucide:star"></iconify-icon>
</div>
<p class="text-[#89726c]">Join thousands of UK drivers saving every single month.</p>
</div>
<div class="md:w-2/3 grid sm:grid-cols-2 gap-6">
<div class="p-6 bg-[#faf6f3] border border-[#e5ded7] rounded-2xl shadow-sm italic text-[#4a3f3b]">
"I used to just go to the station on my way home. Now I check FuelAlert and realize there's a station 2 miles away that's 5p cheaper! Over a month, it adds up to a free tank per year."
<div class="mt-4 flex items-center gap-3 not-italic">
<img class="w-10 h-10 rounded-full" src="https://api.dicebear.com/7.x/avataaars/svg?seed=John">
<div>
<p class="font-bold text-sm">James R.</p>
<p class="text-[10px] text-[#89726c] uppercase font-bold tracking-widest">Daily Commuter</p>
</div>
</div>
</div>
<div class="p-6 bg-[#faf6f3] border border-[#e5ded7] rounded-2xl shadow-sm italic text-[#4a3f3b]">
"The predictions are eerily accurate. I was going to fill up Friday, but FuelAlert said 'Hold on' for Monday. Sure enough, prices dropped at my local Tesco by 3p. Brilliant."
<div class="mt-4 flex items-center gap-3 not-italic">
<img class="w-10 h-10 rounded-full" src="https://api.dicebear.com/7.x/avataaars/svg?seed=Sarah">
<div>
<p class="font-bold text-sm">Sarah M.</p>
<p class="text-[10px] text-[#89726c] uppercase font-bold tracking-widest">Delivery Driver</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- CTA Footer -->
<section class="py-24 px-6 bg-[#bb5b3e] text-white text-center">
<div class="max-w-3xl mx-auto space-y-8">
<h2 class="text-4xl md:text-5xl font-black leading-tight">Ready to outsmart the pumps?</h2>
<p class="text-xl text-white/80">Sign up for free today and never pay over the odds for fuel again.</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a class="bg-white text-[#bb5b3e] px-10 py-4 rounded-xl text-lg font-black shadow-2xl hover:bg-gray-100 transition-all" href="#" id="final-cta-btn">Create Free Account</a>
<a class="bg-transparent border-2 border-white/30 text-white px-10 py-4 rounded-xl text-lg font-bold hover:bg-white/10 transition-all" href="#" id="demo-btn">Watch Demo Video</a>
</div>
</div>
</section>
<!-- Main Footer -->
<footer class="bg-[#faf6f3] border-t border-[#e5ded7] pt-16 pb-8 px-6">
<div class="max-w-7xl mx-auto grid grid-cols-2 md:grid-cols-4 gap-12 mb-12">
<div class="col-span-2 md:col-span-1 space-y-4">
<a class="flex items-center gap-2" href="#" id="footer-logo-link">
<div class="w-8 h-8 rounded bg-[#bb5b3e] flex items-center justify-center"><iconify-icon class="text-white" icon="lucide:fuel"></iconify-icon></div>
<span class="text-xl font-black tracking-tighter text-[#bb5b3e]">FuelAlert</span>
</a>
<p class="text-sm text-[#89726c] leading-relaxed">
Helping UK drivers save money at the pump since 2021. Real-time data, smarter choices.
</p>
<div class="flex gap-4">
<iconify-icon class="text-2xl text-[#89726c] hover:text-[#bb5b3e] cursor-pointer" icon="mdi:twitter"></iconify-icon>
<iconify-icon class="text-2xl text-[#89726c] hover:text-[#bb5b3e] cursor-pointer" icon="mdi:facebook"></iconify-icon>
<iconify-icon class="text-2xl text-[#89726c] hover:text-[#bb5b3e] cursor-pointer" icon="mdi:instagram"></iconify-icon>
</div>
</div>
<div class="space-y-4">
<h5 class="font-black uppercase text-xs text-[#4a3f3b] tracking-widest">Product</h5>
<ul class="space-y-2 text-sm text-[#89726c]">
<li><a class="hover:text-[#bb5b3e]" href="#" id="footer-pricing-link">Pricing</a></li>
<li><a class="hover:text-[#bb5b3e]" href="#" id="footer-features-link">Features</a></li>
<li><a class="hover:text-[#bb5b3e]" href="#" id="footer-pro-link">FuelAlert Pro</a></li>
<li><a class="hover:text-[#bb5b3e]" href="#" id="footer-api-link">Enterprise API</a></li>
</ul>
</div>
<div class="space-y-4">
<h5 class="font-black uppercase text-xs text-[#4a3f3b] tracking-widest">Resources</h5>
<ul class="space-y-2 text-sm text-[#89726c]">
<li><a class="hover:text-[#bb5b3e]" href="#" id="footer-blog-link">Market Insights</a></li>
<li><a class="hover:text-[#bb5b3e]" href="#" id="footer-how-link">How We Track</a></li>
<li><a class="hover:text-[#bb5b3e]" href="#" id="footer-help-link">Help Center</a></li>
<li><a class="hover:text-[#bb5b3e]" href="#" id="footer-safety-link">Driver Safety</a></li>
</ul>
</div>
<div class="space-y-4">
<h5 class="font-black uppercase text-xs text-[#4a3f3b] tracking-widest">Legal</h5>
<ul class="space-y-2 text-sm text-[#89726c]">
<li><a class="hover:text-[#bb5b3e]" href="#" id="footer-privacy-link">Privacy Policy</a></li>
<li><a class="hover:text-[#bb5b3e]" href="#" id="footer-terms-link">Terms of Service</a></li>
<li><a class="hover:text-[#bb5b3e]" href="#" id="footer-cookies-link">Cookie Settings</a></li>
</ul>
</div>
</div>
<div class="max-w-7xl mx-auto pt-8 border-t border-[#e5ded7] flex flex-col md:flex-row justify-between items-center gap-4 text-[10px] font-bold uppercase tracking-widest text-[#89726c]">
<p>© 2024 FuelAlert UK Limited. All Rights Reserved.</p>
<p>Data provided by official UK retail price transparency schemes.</p>
</div>
</footer>
</div>
</body>
</html>

View File

@@ -34,344 +34,25 @@ Content-Type: application/json
- Use the `refresh_token` to regenerate before expiry if needed
- Include token in every API request: `Authorization: Bearer {token}`
Fuel Finder REST API
The Fuel Finder API is a REST API that gives a simple, consistent way to request, create and update data. REST stands for Representational State Transfer which is an architectural software style in which standard HTTP request methods are used to retrieve and modify representations of data. This is identical to the process of retrieving a web page or submitting a web form.
Representational State Transfer (REST) web services
In a RESTful API, each data resource has a unique URL and is manipulated using standard HTTP verbs such as:
GET to request a resource
POST to create a resource (not used for read-only endpoints)
PUT to change a resource (not used for read-only endpoints)
DELETE to remove a resource (not used for read-only endpoints)
Example: request a price resource
GET: https://api.fuelfinder.service.gov.uk/v1/prices/GB-12345 HTTP/1.1
The request uses GET and does not include a request body.
In a RESTful API, a resource is modified by POSTing a revised resource representation, in this case JSON, to the same resource URL:
POST: https://api.fuelfinder.service.gov.uk/v1/<endpoint>
Content-Type: text/json
{
"CustomerName": "Joe Bloggs",
"Address": "",
"etc": etc
}
REST builds on the features of HTTP. Because each resource has a globally unique URL and can be fetched with GET, REST APIs can benefit from existing network components such as caches and proxies.
The JSON data format
Responses use JSON (JavaScript Object Notation). JSON is a compact, widely used format for storing and exchanging data. Most programming languages support JSON, which makes it well suited to HTTP-based API services.
#### Endpoints
- Endpoints
- Method Endpoint
- GET Fetch all PFS fuel prices
- GET Fetch incremental PFS fuel prices
- GET Fetch PFS information
- GET Fetch incremental PFS information
- `GET /api/v1/pfs/fuel-prices?batch-number` — all/incremental station prices
- `GET /api/v1/pfs?batch-number` — all/incremental station metadata
**Fuel prices response fields** (array of stations):
- `node_id` — station identifier
- `trading_name` — station name
- `fuel_prices[]` — array of `{fuel_type, price, price_last_updated, price_change_effective_timestamp}`
- Fuel types: `E5`, `E10`, `B7_STANDARD`, `B7_PREMIUM`, `B10`, `HVO`
- Price is a float (e.g. `159.9` = 159.9p) — multiply × 100 and store as integer pence
```
https://www.fuel-finder.service.gov.uk/api/v1/pfs/fuel-prices?batch-number
[
{
"node_id": "0028acef5f3afc41c7e7d56fb285a940dfb64d6fea01cb4accd79c148321112d",
"public_phone_number": null,
"trading_name": "Alex Fuel Station",
"fuel_prices": [
{
"fuel_type": "E5",
"price": 159.9,
"price_last_updated": "2026-02-17T16:03:04.938Z",
"price_change_effective_timestamp": "2026-02-17T16:00:00.000Z"
},
{
"fuel_type": "E10",
"price": 132.9,
"price_last_updated": "2026-02-17T16:03:04.938Z",
"price_change_effective_timestamp": "2026-02-17T16:00:00.000Z"
},
{
"fuel_type": "B7_STANDARD",
"price": 141.9,
"price_last_updated": "2026-02-17T16:03:04.938Z",
"price_change_effective_timestamp": "2026-02-17T16:00:00.000Z"
}
]
},
{
"node_id": "01da92125c3751767044d06b202f45da5933f0e16e256fa3e98a16af8386308d",
"public_phone_number": "",
"trading_name": "Star Garage",
"fuel_prices": [
{
"fuel_type": "E5",
"price": 159.9,
"price_last_updated": "2026-02-17T16:03:04.938Z",
"price_change_effective_timestamp": "2026-02-17T16:00:00.000Z"
}
]
},
{
"node_id": "020592cd81196efdb61ab2135f837ddf3d2bee4e64346810270f0b088b4c09d8",
"public_phone_number": null,
"trading_name": "Blue Hills Fuel Station",
"fuel_prices": [
{
"fuel_type": "E5",
"price": 159.9,
"price_last_updated": "2026-02-17T16:03:04.938Z",
"price_change_effective_timestamp": "2026-02-17T16:00:00.000Z"
},
{
"fuel_type": "B7_STANDARD",
"price": 141.9,
"price_last_updated": "2026-02-17T16:03:04.938Z",
"price_change_effective_timestamp": "2026-02-17T16:00:00.000Z"
}
]
}
]
```
```
https://www.fuel-finder.service.gov.uk/api/v1/pfs?batch-number=1
[
{
"node_id": "9b275ab576eeba3c6677984be15ee22a74e54fdfe8e5ea700e84a03178dc4ac1",
"public_phone_number": null,
"trading_name": "TEST",
"is_same_trading_and_brand_name": true,
"brand_name": "TEST",
"temporary_closure": false,
"permanent_closure": false,
"permanent_closure_date": null,
"is_motorway_service_station": false,
"is_supermarket_service_station": false,
"location": {
"address_line_1": "HALL & WOODHOUSE, TAPLOW BOATYARD, MILL LANE, TAPLOW, MAIDENHEAD, SL6 0AA",
"address_line_2": null,
"city": "MAIDENHEAD",
"country": "England",
"county": null,
"postcode": "SL6 0AA",
"latitude": 51.5268585,
"longitude": -0.700361
},
"amenities": [
"water_filling"
],
"opening_times": {
"usual_days": {
"monday": {
"open": "00:00:00",
"close": "00:00:00",
"is_24_hours": false
},
"tuesday": {
"open": "00:00:00",
"close": "00:00:00",
"is_24_hours": false
},
"wednesday": {
"open": "00:00:00",
"close": "00:00:00",
"is_24_hours": false
},
"thursday": {
"open": "00:00:00",
"close": "00:00:00",
"is_24_hours": false
},
"friday": {
"open": "00:00:00",
"close": "00:00:00",
"is_24_hours": false
},
"saturday": {
"open": "00:00:00",
"close": "00:00:00",
"is_24_hours": false
},
"sunday": {
"open": "00:00:00",
"close": "23:59:00",
"is_24_hours": true
}
},
"bank_holiday": {
"type": "bank holiday",
"open_time": "00:00:00",
"close_time": "00:00:00",
"is_24_hours": false
}
},
"fuel_types": [
"E10",
"E5",
"HVO",
"B10"
]
},
{
"node_id": "4fd9a4c6b48358b9b5c95989fba100fdcbb87c9e909ed4ce1ad96f64ffb8b56a",
"public_phone_number": "+44 7723608248",
"trading_name": "TEST FORECOURT 1",
"is_same_trading_and_brand_name": true,
"brand_name": "TEXACO ONE",
"temporary_closure": false,
"permanent_closure": null,
"permanent_closure_date": null,
"is_motorway_service_station": false,
"is_supermarket_service_station": false,
"location": {
"address_line_1": "NEWPORT",
"address_line_2": "",
"city": "BROUGH",
"country": "ENGLAND",
"county": "EAST YORKSHIRE",
"postcode": "HU15 2RD",
"latitude": 51.258503,
"longitude": -3.417567
},
"amenities": [
"adblue_packaged",
"adblue_pumps",
"car_wash",
"customer_toilets"
],
"opening_times": {
"usual_days": {
"monday": {
"open": "06:00:01",
"close": "23:00:01",
"is_24_hours": false
},
"tuesday": {
"open": "06:00:01",
"close": "23:00:01",
"is_24_hours": false
},
"wednesday": {
"open": "06:00:01",
"close": "23:00:01",
"is_24_hours": false
},
"thursday": {
"open": "06:00:01",
"close": "23:00:01",
"is_24_hours": false
},
"friday": {
"open": "06:00:01",
"close": "23:00:01",
"is_24_hours": false
},
"saturday": {
"open": "06:00:01",
"close": "23:00:01",
"is_24_hours": false
},
"sunday": {
"open": "06:00:01",
"close": "23:00:01",
"is_24_hours": false
}
},
"bank_holiday": {
"type": "standard",
"open_time": "06:00:01",
"close_time": "23:00:01",
"is_24_hours": false
}
},
"fuel_types": [
"B10"
]
},
{
"node_id": "91bdda1c07fa05110a31639cc66932f9ed8bd388d4f6be542a423365bcfd53e1",
"public_phone_number": "+442071930000",
"trading_name": "SUPERFUEL LOUGHBOROUGH 12",
"is_same_trading_and_brand_name": true,
"brand_name": "SUPERFUEL STATION 4",
"temporary_closure": false,
"permanent_closure": null,
"permanent_closure_date": null,
"is_motorway_service_station": false,
"is_supermarket_service_station": false,
"location": {
"address_line_1": "14 LONDON ROAD",
"address_line_2": "FUELVILLE",
"city": "LOUGHBOROUGH",
"country": "ENGLAND",
"county": "LEICESTERSHIRE",
"postcode": "LE11 9AA",
"latitude": 50.503343,
"longitude": -2.12444
},
"amenities": [
"adblue_packaged",
"adblue_pumps",
"car_wash",
"customer_toilets",
"water_filling"
],
"opening_times": {
"usual_days": {
"monday": {
"open": "06:00:00",
"close": "22:00:00",
"is_24_hours": false
},
"tuesday": {
"open": "06:00:00",
"close": "22:00:00",
"is_24_hours": false
},
"wednesday": {
"open": "06:00:00",
"close": "22:00:00",
"is_24_hours": false
},
"thursday": {
"open": "06:00:00",
"close": "22:00:00",
"is_24_hours": false
},
"friday": {
"open": "06:00:00",
"close": "22:00:00",
"is_24_hours": false
},
"saturday": {
"open": "06:00:00",
"close": "22:00:00",
"is_24_hours": false
},
"sunday": {
"open": "06:00:00",
"close": "22:00:00",
"is_24_hours": false
}
},
"bank_holiday": {
"type": "standard",
"open_time": "08:00:00",
"close_time": "20:00:00",
"is_24_hours": false
}
},
"fuel_types": [
"E5",
"HVO",
"B10",
"B7_PREMIUM",
"B7_STANDARD"
]
}
]
```
**Station metadata response fields** (array of stations):
- `node_id`, `trading_name`, `brand_name`
- `is_supermarket_service_station`, `is_motorway_service_station`
- `temporary_closure`, `permanent_closure`
- `location``{address_line_1, city, postcode, latitude, longitude}`
- `amenities` — string array (e.g. `car_wash`, `adblue_pumps`)
- `fuel_types` — string array of available fuel types
- `opening_times` — per-day open/close times (not used in scoring)
### FuelPriceService responsibilities
1. Fetch OAuth token (cache it)

View File

@@ -8,52 +8,30 @@ Never guess — stay silent (no_signal) when signals conflict or data is insuffi
## The 5 signals (in priority order)
### Signal 1 — Local price trend (HIGHEST WEIGHT)
- Query `station_prices` for user's nearest 5 stations (within 5km of user lat/lng)
- Use last 14 days of history for `e10` (or user's preferred fuel type)
- **Use linear regression, not rolling averages:**
- Run least-squares regression on `(recorded_at, price_pence)` pairs
- Calculate slope (pence/day) and R² (goodness of fit, 01)
- Only use the regression result if R² ≥ 0.5 — below that, data is too noisy
- Use adaptive lookback: try 5 days first (best signal on sharp moves), fall back to 14 days if R² < 0.5
- **Falling**: slope ≤ -0.3p/day AND R² ≥ 0.5 → wait signal, points scale with slope magnitude
- **Rising**: slope ≥ +0.3p/day AND R² ≥ 0.5 → fill_up signal
- **Flat / noisy**: |slope| < 0.3 OR R² < 0.5 no signal from this source
- Store slope, R², lookback_days, and data_points in signal output
- Weight: 40 points max
### Signal 1 — Local price trend (40 pts max)
- Nearest 5 stations within 5km; user's preferred fuel type
- Least-squares regression on `(recorded_at, price_pence)`; adaptive lookback: 5 days first, fall back to 14 if R² < 0.5
- Slope ≤ -0.3p/day AND R² ≥ 0.5 → wait; slope ≥ +0.3p/day AND R² ≥ 0.5 → fill_up; otherwise no signal
- Store: slope, R², lookback_days, data_points
### Signal 2 — Supermarket anchor effect (HIGH WEIGHT)
- Find nearest supermarket station (is_supermarket = 1) within 10km
- Check if supermarket cut price in last 48 hours (> 1p drop)
- Check if nearest non-supermarket stations have NOT yet followed
- If supermarket cut AND independents haven't moved → strong wait signal
- Also check the inverse: if supermarket RAISED and independents haven't → mild fill_up
- Weight: 35 points max
### Signal 2 — Supermarket anchor effect (35 pts max)
- Nearest supermarket (is_supermarket = 1) within 10km
- Supermarket cut > 1p in last 48h AND independents haven't followed → wait
- Inverse (supermarket raised, independents haven't) → mild fill_up
### Signal 3 — Day-of-week pattern (MEDIUM WEIGHT — needs 8+ weeks data)
- Per station: average price by day-of-week over last 90 days
- Only activate if station has 56+ days of history
- If today is statistically 1.5p+ cheaper than weekly average → mild fill_up
- If today is statistically 1.5p+ more expensive → mild wait
- Weight: 15 points max
### Signal 3 — Day-of-week pattern (15 pts max)
- Requires 56+ days of station history; average price by day-of-week over last 90 days
- Today 1.5p+ below weekly average → mild fill_up; 1.5p+ above → mild wait
### Signal 4 — Brent crude direction (LOW WEIGHT)
- Read from `price_predictions` table never query `brent_prices` directly in scoring
- `OilPriceService::generatePrediction()` runs daily at 7am and writes the prediction
- LLM (`source = 'llm'`) is preferred; EWMA (`source = 'ewma'`) is the fallback
- Direction `rising` → mild fill_up pressure; `falling` → mild wait; `flat` → no signal
- Points awarded proportionally to confidence: `(confidence / 100) * 10`
- Weight: 10 points max
### Signal 4 — Brent crude direction (10 pts max)
- Read from `price_predictions` table only (never query `brent_prices` in scoring)
- LLM (`source='llm'`) preferred; EWMA fallback. Points = `(confidence / 100) * 10`
- `rising` → fill_up; `falling` → wait; `flat` → no signal
### Signal 5 — Price stickiness (CONFIDENCE MODIFIER)
- Per station: calculate average hold duration (days between price changes) from history
- Requires 30+ days of history to activate
- Use as a confidence modifier, not a directional signal:
- avg hold < 2 days reduce overall confidence by 5 points (volatile, hard to predict)
- avg hold 24 days → neutral, no adjustment
- avg hold > 5 days → increase overall confidence by 5 points (predictable, sticky)
- Store avg_hold_days and data_points in signal output
- Applied after all other signals are summed (±5 points)
### Signal 5 — Price stickiness (confidence modifier, ±5 pts)
- Requires 30+ days history. Applied after all signals are summed.
- avg hold < 2 days -5 pts; 24 days 0; > 5 days → +5 pts
- Store: avg_hold_days, data_points
## Confidence thresholds
@@ -99,18 +77,9 @@ Reason strings are stored in `scoring_results.signals` JSON and shown in the UI
## Data quality — anomaly rejection
The Fuel Finder API contains dirty data (live example: 1369.0p/litre in national index).
Reject a price record before storing or scoring if:
- `price_pence > 25000` (over 250p/litre — physically implausible for UK pump prices)
- `price_pence < 10000` (under 100p/litre — almost certainly a decimal entry error)
- Price changed by more than 20p in a single update from the same station
(flag for review, do not use in scoring)
Log rejected records to an `anomalous_prices` table for monitoring.
Never let a dirty data point skew the regression slope or collapse R².
Reject before storing or scoring: `price_pence > 25000` or `< 10000`, or single-update change > 20p (flag for review).
Log to `anomalous_prices` table. Never let dirty data skew regression slope or collapse R².
## Accuracy self-tracking
After 3 days, check if `wait` recommendation was correct (prices did fall further).
Store outcome in `scoring_results` for future display:
"This signal has been right X% of the time in your area."
After 3 days, check if `wait` was correct (prices fell further). Store outcome in `scoring_results` for display.

View File

@@ -0,0 +1,131 @@
---
name: fortify-development
description: 'ACTIVATE when the user works on authentication in Laravel. This includes login, registration, password reset, email verification, two-factor authentication (2FA/TOTP/QR codes/recovery codes), profile updates, password confirmation, or any auth-related routes and controllers. Activate when the user mentions Fortify, auth, authentication, login, register, signup, forgot password, verify email, 2FA, or references app/Actions/Fortify/, CreateNewUser, UpdateUserProfileInformation, FortifyServiceProvider, config/fortify.php, or auth guards. Fortify is the frontend-agnostic authentication backend for Laravel that registers all auth routes and controllers. Also activate when building SPA or headless authentication, customizing login redirects, overriding response contracts like LoginResponse, or configuring login throttling. Do NOT activate for Laravel Passport (OAuth2 API tokens), Socialite (OAuth social login), or non-auth Laravel features.'
license: MIT
metadata:
author: laravel
---
# Laravel Fortify Development
Fortify is a headless authentication backend that provides authentication routes and controllers for Laravel applications.
## Documentation
Use `search-docs` for detailed Laravel Fortify patterns and documentation.
## Usage
- **Routes**: Use `list-routes` with `only_vendor: true` and `action: "Fortify"` to see all registered endpoints
- **Actions**: Check `app/Actions/Fortify/` for customizable business logic (user creation, password validation, etc.)
- **Config**: See `config/fortify.php` for all options including features, guards, rate limiters, and username field
- **Contracts**: Look in `Laravel\Fortify\Contracts\` for overridable response classes (`LoginResponse`, `LogoutResponse`, etc.)
- **Views**: All view callbacks are set in `FortifyServiceProvider::boot()` using `Fortify::loginView()`, `Fortify::registerView()`, etc.
## Available Features
Enable in `config/fortify.php` features array:
- `Features::registration()` - User registration
- `Features::resetPasswords()` - Password reset via email
- `Features::emailVerification()` - Requires User to implement `MustVerifyEmail`
- `Features::updateProfileInformation()` - Profile updates
- `Features::updatePasswords()` - Password changes
- `Features::twoFactorAuthentication()` - 2FA with QR codes and recovery codes
> Use `search-docs` for feature configuration options and customization patterns.
## Setup Workflows
### Two-Factor Authentication Setup
```
- [ ] Add TwoFactorAuthenticatable trait to User model
- [ ] Enable feature in config/fortify.php
- [ ] If the `*_add_two_factor_columns_to_users_table.php` migration is missing, publish via `php artisan vendor:publish --tag=fortify-migrations` and migrate
- [ ] Set up view callbacks in FortifyServiceProvider
- [ ] Create 2FA management UI
- [ ] Test QR code and recovery codes
```
> Use `search-docs` for TOTP implementation and recovery code handling patterns.
### Email Verification Setup
```
- [ ] Enable emailVerification feature in config
- [ ] Implement MustVerifyEmail interface on User model
- [ ] Set up verifyEmailView callback
- [ ] Add verified middleware to protected routes
- [ ] Test verification email flow
```
> Use `search-docs` for MustVerifyEmail implementation patterns.
### Password Reset Setup
```
- [ ] Enable resetPasswords feature in config
- [ ] Set up requestPasswordResetLinkView callback
- [ ] Set up resetPasswordView callback
- [ ] Define password.reset named route (if views disabled)
- [ ] Test reset email and link flow
```
> Use `search-docs` for custom password reset flow patterns.
### SPA Authentication Setup
```
- [ ] Set 'views' => false in config/fortify.php
- [ ] Install and configure Laravel Sanctum for session-based SPA authentication
- [ ] Use the 'web' guard in config/fortify.php (required for session-based authentication)
- [ ] Set up CSRF token handling
- [ ] Test XHR authentication flows
```
> Use `search-docs` for integration and SPA authentication patterns.
#### Two-Factor Authentication in SPA Mode
When `views` is set to `false`, Fortify returns JSON responses instead of redirects.
If a user attempts to log in and two-factor authentication is enabled, the login request will return a JSON response indicating that a two-factor challenge is required:
```json
{
"two_factor": true
}
```
## Best Practices
### Custom Authentication Logic
Override authentication behavior using `Fortify::authenticateUsing()` for custom user retrieval or `Fortify::authenticateThrough()` to customize the authentication pipeline. Override response contracts in `AppServiceProvider` for custom redirects.
### Registration Customization
Modify `app/Actions/Fortify/CreateNewUser.php` to customize user creation logic, validation rules, and additional fields.
### Rate Limiting
Configure via `fortify.limiters.login` in config. Default configuration throttles by username + IP combination.
## Key Endpoints
| Feature | Method | Endpoint |
|------------------------|----------|---------------------------------------------|
| Login | POST | `/login` |
| Logout | POST | `/logout` |
| Register | POST | `/register` |
| Password Reset Request | POST | `/forgot-password` |
| Password Reset | POST | `/reset-password` |
| Email Verify Notice | GET | `/email/verify` |
| Resend Verification | POST | `/email/verification-notification` |
| Password Confirm | POST | `/user/confirm-password` |
| Enable 2FA | POST | `/user/two-factor-authentication` |
| Confirm 2FA | POST | `/user/confirmed-two-factor-authentication` |
| 2FA Challenge | POST | `/two-factor-challenge` |
| Get QR Code | GET | `/user/two-factor-qr-code` |
| Recovery Codes | GET/POST | `/user/two-factor-recovery-codes` |

155
CLAUDE.md
View File

@@ -51,9 +51,11 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for
This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions.
- php - 8.4
- filament/filament (FILAMENT) - v5
- laravel/fortify (FORTIFY) - v1
- laravel/framework (LARAVEL) - v13
- laravel/prompts (PROMPTS) - v0
- laravel/sanctum (SANCTUM) - v4
- livewire/flux (FLUXUI_FREE) - v2
- livewire/livewire (LIVEWIRE) - v4
- laravel/boost (BOOST) - v2
@@ -74,6 +76,7 @@ This project has domain-specific skills available. You MUST activate the relevan
- `livewire-development` — Use for any task or question involving Livewire. Activate if user mentions Livewire, wire: directives, or Livewire-specific concepts like wire:model, wire:click, wire:sort, or islands, invoke this skill. Covers building new components, debugging reactivity issues, real-time form validation, drag-and-drop, loading states, migrating from Livewire 3 to 4, converting component formats (SFC/MFC/class-based), and performance optimization. Do not use for non-Livewire reactive UI (React, Vue, Alpine-only, Inertia.js) or standard Laravel forms without Livewire.
- `pest-testing` — Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code.
- `tailwindcss-development` — Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS.
- `fortify-development` — ACTIVATE when the user works on authentication in Laravel. This includes login, registration, password reset, email verification, two-factor authentication (2FA/TOTP/QR codes/recovery codes), profile updates, password confirmation, or any auth-related routes and controllers. Activate when the user mentions Fortify, auth, authentication, login, register, signup, forgot password, verify email, 2FA, or references app/Actions/Fortify/, CreateNewUser, UpdateUserProfileInformation, FortifyServiceProvider, config/fortify.php, or auth guards. Fortify is the frontend-agnostic authentication backend for Laravel that registers all auth routes and controllers. Also activate when building SPA or headless authentication, customizing login redirects, overriding response contracts like LoginResponse, or configuring login throttling. Do NOT activate for Laravel Passport (OAuth2 API tokens), Socialite (OAuth social login), or non-auth Laravel features.
## Conventions
@@ -148,7 +151,7 @@ This project has domain-specific skills available. You MUST activate the relevan
- Always use curly braces for control structures, even for single-line bodies.
- Use PHP 8 constructor property promotion: `public function __construct(public GitHub $github) { }`. Do not leave empty zero-parameter `__construct()` methods unless the constructor is private.
- Use explicit return type declarations and type hints for all method parameters: `function isAccessible(User $user, ?string $path = null): bool`
- Use TitleCase for Enum keys: `FavoritePerson`, `BestLake`, `Monthly`.
- Follow existing application Enum naming conventions.
- Prefer PHPDoc blocks over inline comments. Only add inline comments for exceptionally complex logic.
- Use array shape type definitions in PHPDoc blocks.
@@ -219,4 +222,154 @@ This project has domain-specific skills available. You MUST activate the relevan
- Run tests: `php artisan test --compact` or filter: `php artisan test --compact --filter=testName`.
- Do NOT delete tests without approval.
=== filament/filament rules ===
## Filament
- Filament is used by this application. Follow the existing conventions for how and where it is implemented.
- Filament is a Server-Driven UI (SDUI) framework for Laravel that lets you define user interfaces in PHP using structured configuration objects. Built on Livewire, Alpine.js, and Tailwind CSS.
- Use the `search-docs` tool for official documentation on Artisan commands, code examples, testing, relationships, and idiomatic practices. If `search-docs` is unavailable, refer to https://filamentphp.com/docs.
### Artisan
- Always use Filament-specific Artisan commands to create files. Find available commands with the `list-artisan-commands` tool, or run `php artisan --help`.
- Always inspect required options before running a command, and always pass `--no-interaction`.
### Patterns
Always use static `make()` methods to initialize components. Most configuration methods accept a `Closure` for dynamic values.
Use `Get $get` to read other form field values for conditional logic:
<code-snippet name="Conditional form field visibility" lang="php">
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Schemas\Components\Utilities\Get;
Select::make('type')
->options(CompanyType::class)
->required()
->live(),
TextInput::make('company_name')
->required()
->visible(fn (Get $get): bool => $get('type') === 'business'),
</code-snippet>
Use `state()` with a `Closure` to compute derived column values:
<code-snippet name="Computed table column value" lang="php">
use Filament\Tables\Columns\TextColumn;
TextColumn::make('full_name')
->state(fn (User $record): string => "{$record->first_name} {$record->last_name}"),
</code-snippet>
Actions encapsulate a button with an optional modal form and logic:
<code-snippet name="Action with modal form" lang="php">
use Filament\Actions\Action;
use Filament\Forms\Components\TextInput;
Action::make('updateEmail')
->schema([
TextInput::make('email')
->email()
->required(),
])
->action(fn (array $data, User $record) => $record->update($data))
</code-snippet>
### Testing
Always authenticate before testing panel functionality. Filament uses Livewire, so use `Livewire::test()` or `livewire()` (available when `pestphp/pest-plugin-livewire` is in `composer.json`):
<code-snippet name="Table test" lang="php">
use function Pest\Livewire\livewire;
livewire(ListUsers::class)
->assertCanSeeTableRecords($users)
->searchTable($users->first()->name)
->assertCanSeeTableRecords($users->take(1))
->assertCanNotSeeTableRecords($users->skip(1));
</code-snippet>
<code-snippet name="Create resource test" lang="php">
use function Pest\Laravel\assertDatabaseHas;
use function Pest\Livewire\livewire;
livewire(CreateUser::class)
->fillForm([
'name' => 'Test',
'email' => 'test@example.com',
])
->call('create')
->assertNotified()
->assertRedirect();
assertDatabaseHas(User::class, [
'name' => 'Test',
'email' => 'test@example.com',
]);
</code-snippet>
<code-snippet name="Testing validation" lang="php">
use function Pest\Livewire\livewire;
livewire(CreateUser::class)
->fillForm([
'name' => null,
'email' => 'invalid-email',
])
->call('create')
->assertHasFormErrors([
'name' => 'required',
'email' => 'email',
])
->assertNotNotified();
</code-snippet>
<code-snippet name="Calling actions in pages" lang="php">
use Filament\Actions\DeleteAction;
use function Pest\Livewire\livewire;
livewire(EditUser::class, ['record' => $user->id])
->callAction(DeleteAction::class)
->assertNotified()
->assertRedirect();
</code-snippet>
<code-snippet name="Calling actions in tables" lang="php">
use Filament\Actions\Testing\TestAction;
use function Pest\Livewire\livewire;
livewire(ListUsers::class)
->callAction(TestAction::make('promote')->table($user), [
'role' => 'admin',
])
->assertNotified();
</code-snippet>
### Correct Namespaces
- Form fields (`TextInput`, `Select`, etc.): `Filament\Forms\Components\`
- Infolist entries (`TextEntry`, `IconEntry`, etc.): `Filament\Infolists\Components\`
- Layout components (`Grid`, `Section`, `Fieldset`, `Tabs`, `Wizard`, etc.): `Filament\Schemas\Components\`
- Schema utilities (`Get`, `Set`, etc.): `Filament\Schemas\Components\Utilities\`
- Actions (`DeleteAction`, `CreateAction`, etc.): `Filament\Actions\`. Never use `Filament\Tables\Actions\`, `Filament\Forms\Actions\`, or any other sub-namespace for actions.
- Icons: `Filament\Support\Icons\Heroicon` enum (e.g., `Heroicon::PencilSquare`)
### Common Mistakes
- **Never assume public file visibility.** File visibility is `private` by default. Always use `->visibility('public')` when public access is needed.
- **Never assume full-width layout.** `Grid`, `Section`, and `Fieldset` do not span all columns by default. Explicitly set column spans when needed.
</laravel-boost-guidelines>

View File

@@ -5,12 +5,17 @@
"guidelines": true,
"mcp": true,
"nightwatch_mcp": false,
"packages": [
"filament/filament",
"laravel/fortify"
],
"sail": false,
"skills": [
"laravel-best-practices",
"fluxui-development",
"livewire-development",
"pest-testing",
"tailwindcss-development"
"tailwindcss-development",
"fortify-development"
]
}

View File

@@ -1,5 +1,5 @@
<x-layouts::app.sidebar :title="$title ?? null">
<flux:main>
<x-layouts::app.header :title="$title ?? null">
<flux:main container>
{{ $slot }}
</flux:main>
</x-layouts::app.sidebar>
</x-layouts::app.header>