refactor: migrate from hardcoded hex colors to Tailwind CSS color tokens
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

Replace all hardcoded hex color values with semantic Tailwind design tokens:
- `#bb5b3e` → `accent`
- `#a34a31` → `accent-content` / `primary-dark`
- `#4a3f3b`, `#89726c` → `zinc-800`, `zinc-500`
- `#e5ded7`, `#faf6f3` → `zinc-300`, `zinc-50`
- `#8
This commit is contained in:
Ovidiu U
2026-04-11 16:26:34 +01:00
parent 02b004f381
commit 069a85cf11
15 changed files with 581 additions and 250 deletions

View File

@@ -2,7 +2,7 @@
<div class="space-y-2">
<button
@click="toggleMap"
class="flex items-center gap-2 text-sm font-bold text-[#bb5b3e] hover:text-[#a34a31] transition-colors"
class="flex items-center gap-2 text-sm font-bold text-accent hover:text-accent-content transition-colors"
>
<iconify-icon :icon="isOpen ? 'lucide:chevron-up' : 'lucide:chevron-down'"></iconify-icon>
{{ isOpen ? 'Hide map' : 'Show map' }}
@@ -11,7 +11,7 @@
<div
v-show="isOpen"
ref="mapContainer"
class="w-full h-72 rounded-2xl overflow-hidden border border-[#e5ded7] shadow-sm"
class="w-full h-72 rounded-2xl overflow-hidden border border-zinc-300 shadow-sm"
></div>
</div>
</template>

View File

@@ -5,11 +5,11 @@
v-if="!isPaidTier"
class="absolute inset-0 z-10 rounded-2xl backdrop-blur-sm bg-white/60 flex flex-col items-center justify-center gap-3 text-center px-6"
>
<iconify-icon icon="lucide:lock" class="text-[#bb5b3e] text-3xl"></iconify-icon>
<p class="font-bold text-[#4a3f3b]">Price predictions are available on paid plans</p>
<iconify-icon class="text-accent text-3xl" icon="lucide:lock"></iconify-icon>
<p class="font-bold text-zinc-800">Price predictions are available on paid plans</p>
<a
href="/pricing"
class="px-6 py-2 bg-[#bb5b3e] text-white rounded-full text-sm font-bold hover:bg-[#a34a31] transition-colors"
class="px-6 py-2 bg-accent text-white rounded-full text-sm font-bold hover:bg-accent-content transition-colors"
>
Upgrade from £0.99/mo
</a>
@@ -17,15 +17,15 @@
<!-- Card content (blurred for free users, fully visible for paid) -->
<div
:class="['p-6 bg-white rounded-2xl border border-[#e5ded7] space-y-4', !isPaidTier && 'select-none pointer-events-none']"
:class="['p-6 bg-white rounded-2xl border border-zinc-300 space-y-4', !isPaidTier && 'select-none pointer-events-none']"
>
<p class="text-xs font-bold uppercase tracking-widest text-[#89726c]">Price Prediction</p>
<p class="text-xs font-bold uppercase tracking-widest text-zinc-500">Price Prediction</p>
<!-- Loading state -->
<template v-if="loading">
<div class="animate-pulse space-y-2">
<div class="h-8 bg-[#e5ded7] rounded w-1/2"></div>
<div class="h-4 bg-[#e5ded7] rounded w-3/4"></div>
<div class="h-8 bg-zinc-300 rounded w-1/2"></div>
<div class="h-4 bg-zinc-300 rounded w-3/4"></div>
</div>
</template>
@@ -33,22 +33,22 @@
<template v-else-if="prediction">
<h3
class="text-2xl font-black"
:class="prediction.action === 'fill_now' ? 'text-[#8B4860]' : prediction.action === 'wait' ? 'text-[#4A7C7E]' : 'text-[#9B8B6B]'"
:class="prediction.action === 'fill_now' ? 'text-mauve' : prediction.action === 'wait' ? 'text-teal' : 'text-tan'"
>
{{ actionLabel }}
</h3>
<div class="w-full h-2 bg-[#eeeae5] rounded-full overflow-hidden">
<div class="w-full h-2 bg-zinc-200 rounded-full overflow-hidden">
<div
class="h-full rounded-full transition-all"
:class="prediction.action === 'fill_now' ? 'bg-[#8B4860]' : 'bg-[#4A7C7E]'"
:class="prediction.action === 'fill_now' ? 'bg-mauve' : 'bg-teal'"
:style="{ width: prediction.confidence_score + '%' }"
></div>
</div>
<p class="text-sm text-[#89726c] leading-relaxed">{{ prediction.reasoning }}</p>
<p class="text-sm text-zinc-500 leading-relaxed">{{ prediction.reasoning }}</p>
<div class="flex items-center gap-4 text-xs text-[#89726c] font-medium">
<div class="flex items-center gap-4 text-xs text-zinc-500 font-medium">
<span>Avg: {{ prediction.current_avg }}p</span>
<span>Confidence: {{ prediction.confidence_label }}</span>
<span v-if="prediction.predicted_change_pence">
@@ -59,9 +59,9 @@
<!-- Empty state (placeholder for gated view) -->
<template v-else>
<h3 class="text-2xl font-black text-[#8B4860]">Fill up now</h3>
<div class="h-2 bg-[#eeeae5] rounded-full"><div class="h-full bg-[#8B4860] w-4/5 rounded-full"></div></div>
<p class="text-sm text-[#89726c]">Prices in your area are rising best to fill up today.</p>
<h3 class="text-2xl font-black text-mauve">Fill up now</h3>
<div class="h-2 bg-zinc-200 rounded-full"><div class="h-full bg-mauve w-4/5 rounded-full"></div></div>
<p class="text-sm text-zinc-500">Prices in your area are rising best to fill up today.</p>
</template>
</div>
</div>

View File

@@ -1,7 +1,7 @@
<template>
<div class="relative flex flex-col sm:flex-row gap-3 max-w-md w-full">
<div class="relative flex-1">
<span class="absolute left-4 top-1/2 -translate-y-1/2 text-[#89726c]">
<span class="absolute left-4 top-1/2 -translate-y-1/2 text-zinc-500">
<iconify-icon icon="lucide:map-pin" style="font-size:1.25rem"></iconify-icon>
</span>
<input
@@ -9,13 +9,13 @@
@input="onInput"
type="text"
placeholder="Enter postcode, e.g. SW1A 1AA"
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-base"
class="w-full h-14 pl-12 pr-4 bg-white border border-zinc-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-accent shadow-inner text-base"
/>
</div>
<button
@click="emit('search', postcode)"
:disabled="!postcode.trim()"
class="h-14 px-8 bg-[#bb5b3e] text-white rounded-xl font-bold text-base shadow-xl hover:bg-[#a34a31] transition-all disabled:opacity-50 disabled:cursor-not-allowed"
class="h-14 px-8 bg-accent text-white rounded-xl font-bold text-base shadow-xl hover:bg-accent-content transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
Find Prices
</button>

View File

@@ -1,16 +1,16 @@
<template>
<div class="flex items-center justify-between p-4 bg-white rounded-xl border border-[#e5ded7] hover:border-[#bb5b3e] transition-colors">
<div class="flex items-center justify-between p-4 bg-white rounded-xl border border-zinc-300 hover:border-accent transition-colors">
<div class="flex items-center gap-3 min-w-0">
<div class="w-10 h-10 rounded-lg bg-[#bb5b3e]/10 flex items-center justify-center flex-shrink-0">
<div class="w-10 h-10 rounded-lg bg-accent/10 flex items-center justify-center flex-shrink-0">
<iconify-icon
:icon="station.is_supermarket ? 'lucide:shopping-cart' : 'lucide:fuel'"
style="font-size:1.25rem"
class="text-[#bb5b3e]"
class="text-accent"
></iconify-icon>
</div>
<div class="min-w-0">
<p class="font-bold text-[#4a3f3b] truncate">{{ station.name }}</p>
<p class="text-xs text-[#89726c]">{{ station.distance_km.toFixed(1) }} km away · Updated {{ updatedAgo }}</p>
<p class="font-bold text-zinc-800 truncate">{{ station.name }}</p>
<p class="text-xs text-zinc-500">{{ station.distance_km.toFixed(1) }} km away · Updated {{ updatedAgo }}</p>
</div>
</div>
<div class="text-right flex-shrink-0 ml-4">
@@ -28,10 +28,10 @@ const props = defineProps({
})
const priceColor = computed(() => {
if (!props.lowestPrice) return 'text-[#4a3f3b]'
if (props.station.price_pence === props.lowestPrice) return 'text-[#22c55e]'
if (props.station.price_pence > props.lowestPrice + 500) return 'text-[#ef4444]'
return 'text-[#4a3f3b]'
if (!props.lowestPrice) return 'text-zinc-800'
if (props.station.price_pence === props.lowestPrice) return 'text-status-good'
if (props.station.price_pence > props.lowestPrice + 500) return 'text-status-bad'
return 'text-zinc-800'
})
const updatedAgo = computed(() => {

View File

@@ -9,8 +9,8 @@
:class="[
'px-4 py-1.5 rounded-full text-sm font-bold transition-colors',
currentSort === option.value
? 'bg-[#bb5b3e] text-white'
: 'bg-white border border-[#e5ded7] text-[#89726c] hover:border-[#bb5b3e]'
? 'bg-accent text-white'
: 'bg-white border border-zinc-300 text-zinc-500 hover:border-accent'
]"
>
{{ option.label }}
@@ -18,7 +18,7 @@
</div>
<!-- Count -->
<p class="text-sm text-[#89726c] font-medium">
<p class="text-sm text-zinc-500 font-medium">
{{ stations.length }} station{{ stations.length !== 1 ? 's' : '' }} found
</p>

View File

@@ -1,149 +1,395 @@
<template>
<div class="min-h-screen bg-[#f5ede5]">
<div class="min-h-screen bg-zinc-100">
<!-- Navigation -->
<nav class="fixed top-0 w-full z-50 bg-[#faf6f3] border-b border-[#e5ded7] px-6 py-4 md:px-12">
<nav class="fixed top-0 w-full z-50 bg-zinc-50 border-b border-zinc-300 px-6 py-4 md:px-12">
<div class="max-w-7xl mx-auto flex items-center justify-between">
<RouterLink to="/" class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-[#bb5b3e] flex items-center justify-center shadow-md">
<iconify-icon icon="lucide:fuel" class="text-white text-xl"></iconify-icon>
<RouterLink class="flex items-center gap-3" to="/">
<div class="w-10 h-10 md:w-12 md:h-12 rounded-lg bg-accent 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 font-black tracking-tighter text-[#bb5b3e]">FuelAlert</span>
<span class="text-2xl md:text-3xl font-black font-display tracking-tighter text-accent">FuelAlert</span>
</RouterLink>
<div class="hidden md:flex items-center gap-10">
<a class="text-sm font-semibold text-zinc-500 hover:text-accent transition-colors" href="#how-it-works">How it Works</a>
<a class="text-sm font-semibold text-zinc-500 hover:text-accent transition-colors" href="#features">Features</a>
<a class="text-sm font-semibold text-zinc-500 hover:text-accent transition-colors" href="#pricing">Pricing</a>
</div>
<div class="flex items-center gap-4">
<template v-if="isAuthenticated">
<RouterLink to="/account" class="text-sm font-bold text-[#89726c] hover:text-[#4a3f3b]">Account</RouterLink>
<RouterLink class="text-sm font-bold text-zinc-500 hover:text-zinc-800" to="/dashboard">Dashboard</RouterLink>
</template>
<template v-else>
<a href="/login" class="text-sm font-bold text-[#89726c] hover:text-[#4a3f3b]">Login</a>
<a href="/register" class="bg-[#bb5b3e] text-white px-6 py-2.5 rounded-full text-sm font-bold shadow-lg hover:bg-[#a34a31] transition-all">Get Started</a>
<a class="text-sm font-bold text-zinc-500 hover:text-zinc-800" href="/login">Login</a>
<a class="bg-accent text-white px-6 py-2.5 rounded-full text-sm font-bold shadow-lg hover:bg-primary-dark transition-all transform hover:scale-105 active:scale-95" href="/register">Get Started</a>
</template>
</div>
</div>
</nav>
<!-- Hero -->
<section class="relative pt-36 pb-16 px-6">
<div class="max-w-2xl mx-auto text-center space-y-6">
<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-6xl font-black text-[#4a3f3b] leading-tight tracking-tighter">
Stop Overpaying <span class="text-[#bb5b3e]">for Fuel.</span>
</h1>
<p class="text-lg text-[#89726c] max-w-lg mx-auto">Find the cheapest petrol near you and know the best time to fill up.</p>
<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-accent/10 text-accent 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 font-display text-zinc-800 leading-[1.1] tracking-tighter">
Stop Overpaying <br><span class="text-accent">for Fuel.</span>
</h1>
<p class="text-xl text-zinc-500 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 justify-center">
<SearchBar @search="onSearch" />
<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-zinc-500 text-xl" icon="lucide:map-pin"></iconify-icon>
<input
class="w-full h-14 pl-12 pr-4 bg-white border border-zinc-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-accent shadow-inner text-lg"
placeholder="Enter Postcode"
type="text"
>
</div>
<button class="h-14 px-8 bg-accent text-white rounded-xl font-bold text-lg shadow-xl hover:bg-primary-dark transition-all">
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-zinc-500 font-medium italic">"Saved me £12 on my first tank!"</span>
</div>
</div>
<p v-if="stationError" class="text-sm text-red-500 font-medium">
{{ Object.values(stationError).flat().join(' ') }}
</p>
</div>
</section>
<!-- Results -->
<section v-if="hasSearched" class="px-6 pb-24">
<div class="max-w-4xl mx-auto space-y-6">
<!-- Fuel type selector -->
<div class="flex gap-2 flex-wrap">
<button
v-for="fuel in fuelOptions"
:key="fuel.value"
@click="changeFuelType(fuel.value)"
:class="[
'px-4 py-1.5 rounded-full text-sm font-bold transition-colors',
currentFuelType === fuel.value
? 'bg-[#4a3f3b] text-white'
: 'bg-white border border-[#e5ded7] text-[#89726c] hover:border-[#4a3f3b]'
]"
>
{{ fuel.label }}
</button>
</div>
<div class="grid lg:grid-cols-3 gap-6">
<!-- Map + List (2/3 width) -->
<div class="lg:col-span-2 space-y-4">
<LeafletMap :stations="stations" />
<template v-if="stationsLoading">
<div class="space-y-2">
<div v-for="i in 5" :key="i" class="h-16 bg-white rounded-xl animate-pulse border border-[#e5ded7]"></div>
<!-- Visual mockup card -->
<div class="relative">
<div class="absolute -inset-4 bg-accent/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-accent flex items-center justify-center">
<iconify-icon class="text-white" icon="lucide:fuel"></iconify-icon>
</div>
<span class="font-black text-accent">FuelAlert</span>
</div>
</template>
<template v-else>
<StationList
:stations="stations"
:current-sort="currentSort"
@sort="changeSort"
/>
</template>
</div>
<div class="text-xs font-bold text-zinc-500">SW1A 1AA</div>
</div>
<!-- Prediction (1/3 width) -->
<div>
<PredictionCard
:prediction="prediction"
:loading="predictionLoading"
:is-paid-tier="isPaidTier"
/>
<div class="bg-zinc-50 p-4 rounded-xl border border-zinc-300 shadow-sm">
<p class="text-[10px] font-bold uppercase tracking-widest text-zinc-500 mb-1">Recommendation</p>
<h3 class="text-2xl font-black font-display text-mauve">Fill up now</h3>
<div class="mt-2 h-1.5 w-full bg-zinc-200 rounded-full overflow-hidden">
<div class="h-full bg-mauve 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-zinc-200">
<span class="font-bold text-sm">Tesco Superstore</span>
<span class="font-black text-status-good">142.9p</span>
</div>
<div class="flex justify-between items-center p-3 bg-white rounded-lg border border-zinc-200">
<span class="font-bold text-sm">Shell V-Power</span>
<span class="font-black text-zinc-500">148.9p</span>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- How It Works -->
<section id="how-it-works" class="py-24 px-6 bg-zinc-50">
<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 font-display text-zinc-800">Smart Savings in 3 Steps</h2>
<p class="text-zinc-500 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-accent/10 text-accent 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 font-display">1. Search</h3>
<p class="text-zinc-500">Enter your postcode or location to find every forecourt within a 520 mile radius instantly.</p>
</div>
<div class="text-center space-y-4">
<div class="w-16 h-16 bg-accent/10 text-accent 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 font-display">2. Get Advice</h3>
<p class="text-zinc-500">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-accent/10 text-accent 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 font-display">3. Fill Up Smart</h3>
<p class="text-zinc-500">Navigate to the cheapest station and fill up with confidence knowing you've secured the best price.</p>
</div>
</div>
</div>
</section>
<!-- Features -->
<section id="features" class="py-24 px-6">
<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-zinc-50 border border-zinc-300 rounded-2xl space-y-3">
<iconify-icon class="text-3xl text-accent" icon="lucide:zap"></iconify-icon>
<h4 class="font-bold text-lg font-display">Real-Time Prices</h4>
<p class="text-sm text-zinc-500">Verified daily prices from thousands of UK forecourts.</p>
</div>
<div class="p-6 bg-zinc-50 border border-zinc-300 rounded-2xl space-y-3">
<iconify-icon class="text-3xl text-accent" icon="lucide:calendar"></iconify-icon>
<h4 class="font-bold text-lg font-display">Timing Predictions</h4>
<p class="text-sm text-zinc-500">Proprietary 14-day forecasts for petrol and diesel trends.</p>
</div>
<div class="p-6 bg-zinc-50 border border-zinc-300 rounded-2xl space-y-3">
<iconify-icon class="text-3xl text-accent" icon="lucide:shopping-bag"></iconify-icon>
<h4 class="font-bold text-lg font-display">Supermarket Anchors</h4>
<p class="text-sm text-zinc-500">Track local supermarkets to find the absolute lowest base price.</p>
</div>
<div class="p-6 bg-zinc-50 border border-zinc-300 rounded-2xl space-y-3">
<iconify-icon class="text-3xl text-accent" icon="lucide:bell-ring"></iconify-icon>
<h4 class="font-bold text-lg font-display">Smart Price Alerts</h4>
<p class="text-sm text-zinc-500">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 font-display text-zinc-800">The ultimate fuel companion.</h2>
<p class="text-lg text-zinc-500">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-accent" 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-accent" 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-accent" icon="lucide:check-circle-2"></iconify-icon>
Historic Price Benchmarking
</li>
</ul>
<button class="inline-flex items-center gap-2 text-accent font-black text-lg group">
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 id="pricing" class="py-24 px-6 bg-zinc-50">
<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>
</div>
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
<!-- Free -->
<div class="bg-white border border-zinc-300 p-8 rounded-3xl flex flex-col h-full">
<div class="mb-8">
<h4 class="text-xl font-bold font-display mb-2">Free</h4>
<div class="flex items-baseline gap-1">
<span class="text-4xl font-black">£0</span>
<span class="text-zinc-500 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-accent" icon="lucide:check"></iconify-icon> Basic Search</li>
<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>
</div>
<!-- Basic -->
<div class="bg-white border border-zinc-300 p-8 rounded-3xl flex flex-col h-full">
<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>
</div>
</div>
<ul class="space-y-4 mb-8 flex-1">
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> Ad-free Experience</li>
<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>
</div>
<!-- Plus -->
<div class="bg-white border-2 border-accent p-8 rounded-3xl flex flex-col h-full relative">
<div class="absolute -top-4 left-1/2 -translate-x-1/2 bg-accent text-white px-4 py-1 rounded-full text-[10px] font-black uppercase tracking-widest whitespace-nowrap">Most Popular</div>
<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>
</div>
</div>
<ul class="space-y-4 mb-8 flex-1">
<li class="text-sm flex gap-2 font-bold"><iconify-icon class="text-accent" icon="lucide:check"></iconify-icon> Supermarket Anchor</li>
<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>
</div>
<!-- Pro -->
<div class="bg-zinc-800 border border-zinc-800 p-8 rounded-3xl flex flex-col h-full text-white">
<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>
</div>
</div>
<ul class="space-y-4 mb-8 flex-1">
<li class="text-sm flex gap-2"><iconify-icon class="text-accent" icon="lucide:sparkles"></iconify-icon> AI Price Predictions</li>
<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>
</div>
</div>
</div>
</section>
<!-- Testimonials -->
<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 font-display text-zinc-800 mb-4">Loved by commuters.</h2>
<div class="flex items-center gap-1 text-status-warn 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-zinc-500">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-zinc-50 border border-zinc-300 rounded-2xl shadow-sm italic text-zinc-800">
"I used to just go to the station on my way home. Now I check FuelAlert and realise 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 alt="James R." 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-zinc-500 uppercase font-bold tracking-widest">Daily Commuter</p>
</div>
</div>
</div>
<div class="p-6 bg-zinc-50 border border-zinc-300 rounded-2xl shadow-sm italic text-zinc-800">
"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 alt="Sarah M." 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-zinc-500 uppercase font-bold tracking-widest">Delivery Driver</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- CTA -->
<section class="py-24 px-6 bg-accent text-white text-center">
<div class="max-w-3xl mx-auto space-y-8">
<h2 class="text-4xl md:text-5xl font-black font-display 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-accent px-10 py-4 rounded-xl text-lg font-black shadow-2xl hover:bg-zinc-100 transition-all" href="/register">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="#">Watch Demo Video</a>
</div>
</div>
</section>
<!-- Footer -->
<footer class="bg-zinc-50 border-t border-zinc-300 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">
<RouterLink class="flex items-center gap-2" to="/">
<div class="w-8 h-8 rounded bg-accent flex items-center justify-center">
<iconify-icon class="text-white" icon="lucide:fuel"></iconify-icon>
</div>
<span class="text-xl font-black font-display tracking-tighter text-accent">FuelAlert</span>
</RouterLink>
<p class="text-sm text-zinc-500 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-zinc-500 hover:text-accent cursor-pointer transition-colors" icon="mdi:twitter"></iconify-icon>
<iconify-icon class="text-2xl text-zinc-500 hover:text-accent cursor-pointer transition-colors" icon="mdi:facebook"></iconify-icon>
<iconify-icon class="text-2xl text-zinc-500 hover:text-accent cursor-pointer transition-colors" icon="mdi:instagram"></iconify-icon>
</div>
</div>
<div class="space-y-4">
<h5 class="font-black uppercase text-xs text-zinc-800 tracking-widest">Product</h5>
<ul class="space-y-2 text-sm text-zinc-500">
<li><a class="hover:text-accent transition-colors" href="#pricing">Pricing</a></li>
<li><a class="hover:text-accent transition-colors" href="#features">Features</a></li>
<li><a class="hover:text-accent transition-colors" href="#">FuelAlert Pro</a></li>
<li><a class="hover:text-accent transition-colors" href="#">Enterprise API</a></li>
</ul>
</div>
<div class="space-y-4">
<h5 class="font-black uppercase text-xs text-zinc-800 tracking-widest">Resources</h5>
<ul class="space-y-2 text-sm text-zinc-500">
<li><a class="hover:text-accent transition-colors" href="#">Market Insights</a></li>
<li><a class="hover:text-accent transition-colors" href="#">How We Track</a></li>
<li><a class="hover:text-accent transition-colors" href="#">Help Center</a></li>
<li><a class="hover:text-accent transition-colors" href="#">Driver Safety</a></li>
</ul>
</div>
<div class="space-y-4">
<h5 class="font-black uppercase text-xs text-zinc-800 tracking-widest">Legal</h5>
<ul class="space-y-2 text-sm text-zinc-500">
<li><a class="hover:text-accent transition-colors" href="#">Privacy Policy</a></li>
<li><a class="hover:text-accent transition-colors" href="#">Terms of Service</a></li>
<li><a class="hover:text-accent transition-colors" href="#">Cookie Settings</a></li>
</ul>
</div>
</div>
<div class="max-w-7xl mx-auto pt-8 border-t border-zinc-300 flex flex-col md:flex-row justify-between items-center gap-4 text-[10px] font-bold uppercase tracking-widest text-zinc-500">
<p>© 2024 FuelAlert UK Limited. All Rights Reserved.</p>
<p>Data provided by official UK retail price transparency schemes.</p>
</div>
</footer>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { RouterLink } from 'vue-router'
import { useAuth } from '../composables/useAuth.js'
import { useStations } from '../composables/useStations.js'
import { usePrediction } from '../composables/usePrediction.js'
import SearchBar from '../components/SearchBar.vue'
import LeafletMap from '../components/LeafletMap.vue'
import StationList from '../components/StationList.vue'
import PredictionCard from '../components/PredictionCard.vue'
const { isAuthenticated, isPaidTier } = useAuth()
const { stations, loading: stationsLoading, error: stationError, search } = useStations()
const { prediction, loading: predictionLoading, fetch: fetchPrediction } = usePrediction()
const hasSearched = ref(false)
const currentSort = ref('price')
const currentFuelType = ref('petrol')
const lastPostcode = ref('')
const fuelOptions = [
{ label: 'Petrol (E10)', value: 'petrol' },
{ label: 'Diesel', value: 'diesel' },
{ label: 'Premium Unleaded', value: 'e5' },
{ label: 'Premium Diesel', value: 'b7_premium' },
]
async function onSearch(postcode) {
lastPostcode.value = postcode
hasSearched.value = true
await Promise.all([
search({ postcode, fuelType: currentFuelType.value, sort: currentSort.value }),
fetchPrediction(),
])
}
async function changeSort(sort) {
currentSort.value = sort
if (lastPostcode.value) {
await search({ postcode: lastPostcode.value, fuelType: currentFuelType.value, sort })
}
}
async function changeFuelType(fuelType) {
currentFuelType.value = fuelType
if (lastPostcode.value) {
await search({ postcode: lastPostcode.value, fuelType, sort: currentSort.value })
}
}
const { isAuthenticated } = useAuth()
</script>

View File

@@ -1,16 +1,16 @@
<template>
<div class="min-h-screen bg-[#f5ede5] flex flex-col">
<div class="min-h-screen bg-zinc-100 flex flex-col">
<!-- Top nav -->
<nav class="fixed top-0 w-full z-50 bg-[#faf6f3] border-b border-[#e5ded7] px-6 py-4">
<nav class="fixed top-0 w-full z-50 bg-zinc-50 border-b border-zinc-300 px-6 py-4">
<div class="max-w-7xl mx-auto flex items-center justify-between">
<RouterLink to="/" class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-[#bb5b3e] flex items-center justify-center shadow-md">
<div class="w-10 h-10 rounded-lg bg-accent flex items-center justify-center shadow-md">
<iconify-icon icon="lucide:fuel" class="text-white text-xl"></iconify-icon>
</div>
<span class="text-2xl font-black tracking-tighter text-[#bb5b3e]">FuelAlert</span>
<span class="text-2xl font-black tracking-tighter text-accent">FuelAlert</span>
</RouterLink>
<div class="flex items-center gap-4">
<RouterLink to="/" class="text-sm font-bold text-[#89726c] hover:text-[#4a3f3b]">
<RouterLink class="text-sm font-bold text-zinc-500 hover:text-zinc-800" to="/">
Find fuel
</RouterLink>
@@ -18,7 +18,7 @@
<div ref="dropdownRef" class="relative">
<button
@click="dropdownOpen = !dropdownOpen"
class="w-9 h-9 rounded-full bg-[#bb5b3e] flex items-center justify-center text-white text-sm font-black hover:bg-[#a34a31] transition-colors"
class="w-9 h-9 rounded-full bg-accent flex items-center justify-center text-white text-sm font-black hover:bg-accent-content transition-colors"
:aria-expanded="dropdownOpen ? 'true' : 'false'"
aria-haspopup="true"
>
@@ -34,24 +34,24 @@
>
<div
v-show="dropdownOpen"
class="absolute right-0 top-full mt-2 w-64 bg-white border border-[#e5ded7] rounded-2xl shadow-lg overflow-hidden z-50"
class="absolute right-0 top-full mt-2 w-64 bg-white border border-zinc-300 rounded-2xl shadow-lg overflow-hidden z-50"
>
<div class="px-4 py-3 border-b border-[#e5ded7]">
<p class="text-sm font-black text-[#4a3f3b]">{{ user?.name }}</p>
<p class="text-xs text-[#89726c] truncate">{{ user?.email }}</p>
<div class="px-4 py-3 border-b border-zinc-300">
<p class="text-sm font-black text-zinc-800">{{ user?.name }}</p>
<p class="text-xs text-zinc-500 truncate">{{ user?.email }}</p>
</div>
<div class="py-1">
<RouterLink
to="/dashboard/settings"
@click="dropdownOpen = false"
class="flex items-center gap-3 px-4 py-2.5 text-sm font-bold text-[#89726c] hover:bg-[#faf6f3] hover:text-[#4a3f3b] transition-colors"
class="flex items-center gap-3 px-4 py-2.5 text-sm font-bold text-zinc-500 hover:bg-zinc-50 hover:text-zinc-800 transition-colors"
>
<iconify-icon icon="lucide:settings"></iconify-icon>
Settings
</RouterLink>
<button
@click="handleLogout"
class="w-full flex items-center gap-3 px-4 py-2.5 text-sm font-bold text-[#89726c] hover:bg-[#faf6f3] hover:text-[#4a3f3b] transition-colors"
class="w-full flex items-center gap-3 px-4 py-2.5 text-sm font-bold text-zinc-500 hover:bg-zinc-50 hover:text-zinc-800 transition-colors"
>
<iconify-icon icon="lucide:log-out"></iconify-icon>
Log out
@@ -74,8 +74,8 @@
:to="item.to"
class="flex items-center gap-3 px-4 py-2.5 rounded-xl text-sm font-bold transition-colors"
:class="isActive(item.to)
? 'bg-[#bb5b3e] text-white'
: 'text-[#89726c] hover:bg-white hover:text-[#4a3f3b]'"
? 'bg-accent text-white'
: 'text-zinc-500 hover:bg-white hover:text-zinc-800'"
>
<iconify-icon :icon="item.icon"></iconify-icon>
{{ item.label }}

View File

@@ -1,8 +1,8 @@
<template>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-black text-[#4a3f3b]">Welcome back{{ user ? ', ' + user.name : '' }}</h1>
<p class="text-[#89726c] mt-1">Your FuelAlert dashboard.</p>
<h1 class="text-2xl font-black text-zinc-800">Welcome back{{ user ? ', ' + user.name : '' }}</h1>
<p class="text-zinc-500 mt-1">Your FuelAlert dashboard.</p>
</div>
<div class="grid sm:grid-cols-3 gap-4">
@@ -10,18 +10,18 @@
v-for="item in quickLinks"
:key="item.to"
:to="item.to"
class="p-6 bg-white rounded-2xl border border-[#e5ded7] hover:border-[#bb5b3e] transition-colors space-y-3"
class="p-6 bg-white rounded-2xl border border-zinc-300 hover:border-accent transition-colors space-y-3"
>
<iconify-icon :icon="item.icon" class="text-[#bb5b3e] text-2xl"></iconify-icon>
<p class="font-bold text-[#4a3f3b]">{{ item.label }}</p>
<p class="text-sm text-[#89726c]">{{ item.description }}</p>
<iconify-icon :icon="item.icon" class="text-accent text-2xl"></iconify-icon>
<p class="font-bold text-zinc-800">{{ item.label }}</p>
<p class="text-sm text-zinc-500">{{ item.description }}</p>
</RouterLink>
</div>
<div class="p-6 bg-white rounded-2xl border border-[#e5ded7] space-y-2">
<p class="text-sm font-bold uppercase tracking-widest text-[#89726c]">Your plan</p>
<p class="text-xl font-black text-[#4a3f3b] capitalize">{{ userTier }}</p>
<a v-if="userTier === 'free'" href="/pricing" class="inline-block text-sm font-bold text-[#bb5b3e] hover:underline">
<div class="p-6 bg-white rounded-2xl border border-zinc-300 space-y-2">
<p class="text-sm font-bold uppercase tracking-widest text-zinc-500">Your plan</p>
<p class="text-xl font-black text-zinc-800 capitalize">{{ userTier }}</p>
<a v-if="userTier === 'free'" class="inline-block text-sm font-bold text-accent hover:underline" href="/pricing">
Upgrade for alerts + predictions
</a>
</div>

View File

@@ -1,13 +1,13 @@
<template>
<div class="space-y-6 max-w-lg">
<h1 class="text-2xl font-black text-[#4a3f3b]">Preferences</h1>
<h1 class="text-2xl font-black text-zinc-800">Preferences</h1>
<form @submit.prevent="save" class="space-y-5 p-6 bg-white rounded-2xl border border-[#e5ded7]">
<form class="space-y-5 p-6 bg-white rounded-2xl border border-zinc-300" @submit.prevent="save">
<div class="space-y-2">
<label class="text-sm font-bold text-[#4a3f3b]">Default fuel type</label>
<label class="text-sm font-bold text-zinc-800">Default fuel type</label>
<select
v-model="form.preferred_fuel_type"
class="w-full h-12 px-4 bg-[#faf6f3] border border-[#e5ded7] rounded-xl font-medium text-[#4a3f3b] focus:outline-none focus:ring-2 focus:ring-[#bb5b3e]"
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>
@@ -19,13 +19,13 @@
</div>
<div class="space-y-2">
<label class="text-sm font-bold text-[#4a3f3b]">Home postcode</label>
<label class="text-sm font-bold text-zinc-800">Home postcode</label>
<input
v-model="form.postcode"
type="text"
placeholder="e.g. SW1A 1AA"
maxlength="8"
class="w-full h-12 px-4 bg-[#faf6f3] border border-[#e5ded7] rounded-xl font-medium text-[#4a3f3b] focus:outline-none focus:ring-2 focus:ring-[#bb5b3e]"
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"
/>
</div>
@@ -33,7 +33,7 @@
<button
type="submit"
:disabled="saving"
class="px-8 py-3 bg-[#bb5b3e] text-white rounded-xl font-bold hover:bg-[#a34a31] transition-all disabled:opacity-50"
class="px-8 py-3 bg-accent text-white rounded-xl font-bold hover:bg-accent-content transition-all disabled:opacity-50"
>
{{ saving ? 'Saving…' : 'Save preferences' }}
</button>

View File

@@ -1,12 +1,12 @@
<template>
<div class="space-y-6">
<h1 class="text-2xl font-black text-[#4a3f3b]">Saved Stations</h1>
<h1 class="text-2xl font-black text-zinc-800">Saved Stations</h1>
<div v-if="loading" class="space-y-2">
<div v-for="i in 3" :key="i" class="h-16 bg-white rounded-xl animate-pulse border border-[#e5ded7]"></div>
<div v-for="i in 3" :key="i" class="h-16 bg-white rounded-xl animate-pulse border border-zinc-300"></div>
</div>
<div v-else-if="savedStations.length === 0" class="p-8 bg-white rounded-2xl border border-[#e5ded7] text-center text-[#89726c]">
<div v-else-if="savedStations.length === 0" class="p-8 bg-white rounded-2xl border border-zinc-300 text-center text-zinc-500">
<iconify-icon icon="lucide:bookmark" class="text-3xl mb-2"></iconify-icon>
<p class="font-medium">No saved stations yet.</p>
<p class="text-sm mt-1">Search for fuel and bookmark stations to see them here.</p>
@@ -16,11 +16,11 @@
<div
v-for="station in savedStations"
:key="station.station_id"
class="flex items-center justify-between p-4 bg-white rounded-xl border border-[#e5ded7]"
class="flex items-center justify-between p-4 bg-white rounded-xl border border-zinc-300"
>
<div>
<p class="font-bold text-[#4a3f3b]">{{ station.name }}</p>
<p class="text-sm text-[#89726c]">{{ station.postcode }}</p>
<p class="font-bold text-zinc-800">{{ station.name }}</p>
<p class="text-sm text-zinc-500">{{ station.postcode }}</p>
</div>
<button
@click="remove(station.station_id)"

View File

@@ -1,8 +1,8 @@
<template>
<div class="space-y-6 max-w-lg">
<div class="p-6 bg-white rounded-2xl border border-[#e5ded7] space-y-5">
<h2 class="text-lg font-black text-[#4a3f3b]">Appearance</h2>
<p class="text-sm text-[#89726c]">Choose how FuelAlert looks to you.</p>
<div class="p-6 bg-white rounded-2xl border border-zinc-300 space-y-5">
<h2 class="text-lg font-black text-zinc-800">Appearance</h2>
<p class="text-sm text-zinc-500">Choose how FuelAlert looks to you.</p>
<div class="grid grid-cols-3 gap-3">
<button
@@ -11,15 +11,15 @@
@click="setTheme(option.value)"
class="flex flex-col items-center gap-3 p-4 rounded-2xl border-2 transition-all"
:class="selectedTheme === option.value
? 'border-[#bb5b3e] bg-[#faf6f3]'
: 'border-[#e5ded7] hover:border-[#89726c]'"
? 'border-accent bg-zinc-50'
: 'border-zinc-300 hover:border-zinc-500'"
>
<iconify-icon :icon="option.icon" class="text-2xl text-[#bb5b3e]"></iconify-icon>
<span class="text-sm font-bold text-[#4a3f3b]">{{ option.label }}</span>
<iconify-icon :icon="option.icon" class="text-2xl text-accent"></iconify-icon>
<span class="text-sm font-bold text-zinc-800">{{ option.label }}</span>
</button>
</div>
<p class="text-xs text-[#89726c]">
<p class="text-xs text-zinc-500">
Current: <span class="font-bold capitalize">{{ selectedTheme }}</span>
</p>
</div>

View File

@@ -1,27 +1,27 @@
<template>
<div class="space-y-6 max-w-lg">
<!-- Profile form -->
<form @submit.prevent="saveProfile" class="p-6 bg-white rounded-2xl border border-[#e5ded7] space-y-5">
<h2 class="text-lg font-black text-[#4a3f3b]">Profile information</h2>
<form class="p-6 bg-white rounded-2xl border border-zinc-300 space-y-5" @submit.prevent="saveProfile">
<h2 class="text-lg font-black text-zinc-800">Profile information</h2>
<div class="space-y-2">
<label class="text-sm font-bold text-[#4a3f3b]">Full name</label>
<label class="text-sm font-bold text-zinc-800">Full name</label>
<input
v-model="profileForm.name"
type="text"
class="w-full h-12 px-4 bg-[#faf6f3] border rounded-xl font-medium text-[#4a3f3b] focus:outline-none focus:ring-2 focus:ring-[#bb5b3e]"
:class="profileErrors.name ? 'border-red-400' : 'border-[#e5ded7]'"
:class="profileErrors.name ? 'border-red-400' : 'border-zinc-300'"
class="w-full h-12 px-4 bg-zinc-50 border rounded-xl font-medium text-zinc-800 focus:outline-none focus:ring-2 focus:ring-accent"
/>
<p v-if="profileErrors.name" class="text-xs text-red-600">{{ profileErrors.name[0] }}</p>
</div>
<div class="space-y-2">
<label class="text-sm font-bold text-[#4a3f3b]">Email address</label>
<label class="text-sm font-bold text-zinc-800">Email address</label>
<input
v-model="profileForm.email"
type="email"
class="w-full h-12 px-4 bg-[#faf6f3] border rounded-xl font-medium text-[#4a3f3b] focus:outline-none focus:ring-2 focus:ring-[#bb5b3e]"
:class="profileErrors.email ? 'border-red-400' : 'border-[#e5ded7]'"
:class="profileErrors.email ? 'border-red-400' : 'border-zinc-300'"
class="w-full h-12 px-4 bg-zinc-50 border rounded-xl font-medium text-zinc-800 focus:outline-none focus:ring-2 focus:ring-accent"
/>
<p v-if="profileErrors.email" class="text-xs text-red-600">{{ profileErrors.email[0] }}</p>
</div>
@@ -32,7 +32,7 @@
<button
type="submit"
:disabled="profileSaving"
class="px-8 py-3 bg-[#bb5b3e] text-white rounded-xl font-bold hover:bg-[#a34a31] transition-all disabled:opacity-50"
class="px-8 py-3 bg-accent text-white rounded-xl font-bold hover:bg-accent-content transition-all disabled:opacity-50"
>
{{ profileSaving ? 'Saving…' : 'Save changes' }}
</button>
@@ -41,9 +41,9 @@
</form>
<!-- Delete account -->
<div class="p-6 bg-white rounded-2xl border border-[#e5ded7] space-y-4">
<h2 class="text-lg font-black text-[#4a3f3b]">Delete account</h2>
<p class="text-sm text-[#89726c]">Permanently delete your account and all associated data. This cannot be undone.</p>
<div class="p-6 bg-white rounded-2xl border border-zinc-300 space-y-4">
<h2 class="text-lg font-black text-zinc-800">Delete account</h2>
<p class="text-sm text-zinc-500">Permanently delete your account and all associated data. This cannot be undone.</p>
<button
@click="deleteModalOpen = true"
class="px-6 py-2.5 bg-red-600 text-white rounded-xl text-sm font-bold hover:bg-red-700 transition-colors"
@@ -59,16 +59,16 @@
@click.self="closeDeleteModal"
>
<div class="bg-white rounded-2xl p-6 w-full max-w-md mx-4 space-y-5">
<h3 class="text-lg font-black text-[#4a3f3b]">Are you sure?</h3>
<p class="text-sm text-[#89726c]">Enter your password to confirm account deletion.</p>
<h3 class="text-lg font-black text-zinc-800">Are you sure?</h3>
<p class="text-sm text-zinc-500">Enter your password to confirm account deletion.</p>
<div class="space-y-2">
<label class="text-sm font-bold text-[#4a3f3b]">Password</label>
<label class="text-sm font-bold text-zinc-800">Password</label>
<input
v-model="deletePassword"
type="password"
class="w-full h-12 px-4 bg-[#faf6f3] border rounded-xl font-medium text-[#4a3f3b] focus:outline-none focus:ring-2 focus:ring-red-400"
:class="deleteError ? 'border-red-400' : 'border-[#e5ded7]'"
:class="deleteError ? 'border-red-400' : 'border-zinc-300'"
class="w-full h-12 px-4 bg-zinc-50 border rounded-xl font-medium text-zinc-800 focus:outline-none focus:ring-2 focus:ring-red-400"
/>
<p v-if="deleteError" class="text-xs text-red-600">{{ deleteError }}</p>
</div>
@@ -76,7 +76,7 @@
<div class="flex gap-3">
<button
@click="closeDeleteModal"
class="flex-1 py-3 border border-[#e5ded7] rounded-xl text-sm font-bold text-[#89726c] hover:bg-[#faf6f3] transition-colors"
class="flex-1 py-3 border border-zinc-300 rounded-xl text-sm font-bold text-zinc-500 hover:bg-zinc-50 transition-colors"
>
Cancel
</button>

View File

@@ -1,38 +1,38 @@
<template>
<div class="space-y-6 max-w-lg">
<!-- Password change -->
<form @submit.prevent="savePassword" class="p-6 bg-white rounded-2xl border border-[#e5ded7] space-y-5">
<h2 class="text-lg font-black text-[#4a3f3b]">Change password</h2>
<form class="p-6 bg-white rounded-2xl border border-zinc-300 space-y-5" @submit.prevent="savePassword">
<h2 class="text-lg font-black text-zinc-800">Change password</h2>
<div class="space-y-2">
<label class="text-sm font-bold text-[#4a3f3b]">Current password</label>
<label class="text-sm font-bold text-zinc-800">Current password</label>
<input
v-model="passwordForm.current_password"
type="password"
class="w-full h-12 px-4 bg-[#faf6f3] border rounded-xl font-medium text-[#4a3f3b] focus:outline-none focus:ring-2 focus:ring-[#bb5b3e]"
:class="passwordErrors.current_password ? 'border-red-400' : 'border-[#e5ded7]'"
:class="passwordErrors.current_password ? 'border-red-400' : 'border-zinc-300'"
class="w-full h-12 px-4 bg-zinc-50 border rounded-xl font-medium text-zinc-800 focus:outline-none focus:ring-2 focus:ring-accent"
/>
<p v-if="passwordErrors.current_password" class="text-xs text-red-600">{{ passwordErrors.current_password[0] }}</p>
</div>
<div class="space-y-2">
<label class="text-sm font-bold text-[#4a3f3b]">New password</label>
<label class="text-sm font-bold text-zinc-800">New password</label>
<input
v-model="passwordForm.password"
type="password"
class="w-full h-12 px-4 bg-[#faf6f3] border rounded-xl font-medium text-[#4a3f3b] focus:outline-none focus:ring-2 focus:ring-[#bb5b3e]"
:class="passwordErrors.password ? 'border-red-400' : 'border-[#e5ded7]'"
:class="passwordErrors.password ? 'border-red-400' : 'border-zinc-300'"
class="w-full h-12 px-4 bg-zinc-50 border rounded-xl font-medium text-zinc-800 focus:outline-none focus:ring-2 focus:ring-accent"
/>
<p v-if="passwordErrors.password" class="text-xs text-red-600">{{ passwordErrors.password[0] }}</p>
</div>
<div class="space-y-2">
<label class="text-sm font-bold text-[#4a3f3b]">Confirm new password</label>
<label class="text-sm font-bold text-zinc-800">Confirm new password</label>
<input
v-model="passwordForm.password_confirmation"
type="password"
class="w-full h-12 px-4 bg-[#faf6f3] border rounded-xl font-medium text-[#4a3f3b] focus:outline-none focus:ring-2 focus:ring-[#bb5b3e]"
:class="passwordErrors.password_confirmation ? 'border-red-400' : 'border-[#e5ded7]'"
:class="passwordErrors.password_confirmation ? 'border-red-400' : 'border-zinc-300'"
class="w-full h-12 px-4 bg-zinc-50 border rounded-xl font-medium text-zinc-800 focus:outline-none focus:ring-2 focus:ring-accent"
/>
<p v-if="passwordErrors.password_confirmation" class="text-xs text-red-600">{{ passwordErrors.password_confirmation[0] }}</p>
</div>
@@ -43,7 +43,7 @@
<button
type="submit"
:disabled="passwordSaving"
class="px-8 py-3 bg-[#bb5b3e] text-white rounded-xl font-bold hover:bg-[#a34a31] transition-all disabled:opacity-50"
class="px-8 py-3 bg-accent text-white rounded-xl font-bold hover:bg-accent-content transition-all disabled:opacity-50"
>
{{ passwordSaving ? 'Updating…' : 'Update password' }}
</button>
@@ -52,37 +52,37 @@
</form>
<!-- Two-factor authentication -->
<div class="p-6 bg-white rounded-2xl border border-[#e5ded7] space-y-4">
<div class="p-6 bg-white rounded-2xl border border-zinc-300 space-y-4">
<div class="flex items-center justify-between">
<div>
<h2 class="text-lg font-black text-[#4a3f3b]">Two-factor authentication</h2>
<p class="text-sm text-[#89726c] mt-0.5">
<h2 class="text-lg font-black text-zinc-800">Two-factor authentication</h2>
<p class="text-sm text-zinc-500 mt-0.5">
{{ twoFactorEnabled ? 'Enabled — your account is extra secure.' : 'Add extra security to your account.' }}
</p>
</div>
<span
class="px-3 py-1 rounded-full text-xs font-black"
:class="twoFactorEnabled ? 'bg-green-100 text-green-700' : 'bg-[#faf6f3] text-[#89726c]'"
:class="twoFactorEnabled ? 'bg-green-100 text-green-700' : 'bg-zinc-50 text-zinc-500'"
>
{{ twoFactorEnabled ? 'ON' : 'OFF' }}
</span>
</div>
<!-- 2FA enabling flow -->
<div v-if="enablingTwoFactor" class="space-y-4 border-t border-[#e5ded7] pt-4">
<p class="text-sm font-bold text-[#4a3f3b]">Scan this QR code in your authenticator app:</p>
<div v-if="enablingTwoFactor" class="space-y-4 border-t border-zinc-300 pt-4">
<p class="text-sm font-bold text-zinc-800">Scan this QR code in your authenticator app:</p>
<div v-if="setupData" class="flex flex-col items-start gap-4">
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-html="setupData.svg" class="[&_svg]:w-40 [&_svg]:h-40"></div>
<div class="space-y-1">
<p class="text-xs font-bold text-[#89726c] uppercase tracking-widest">Or enter setup key manually</p>
<code class="text-xs bg-[#faf6f3] px-3 py-2 rounded-lg font-mono text-[#4a3f3b] break-all block">{{ setupData.secretKey }}</code>
<p class="text-xs font-bold text-zinc-500 uppercase tracking-widest">Or enter setup key manually</p>
<code class="text-xs bg-zinc-50 px-3 py-2 rounded-lg font-mono text-zinc-800 break-all block">{{ setupData.secretKey }}</code>
</div>
</div>
<div v-if="!twoFactorEnabled" class="space-y-2">
<label class="text-sm font-bold text-[#4a3f3b]">Enter the 6-digit code to confirm</label>
<label class="text-sm font-bold text-zinc-800">Enter the 6-digit code to confirm</label>
<div class="flex gap-3">
<input
v-model="confirmCode"
@@ -90,13 +90,13 @@
maxlength="6"
inputmode="numeric"
placeholder="000000"
class="w-36 h-12 px-4 bg-[#faf6f3] border rounded-xl font-mono text-center text-[#4a3f3b] focus:outline-none focus:ring-2 focus:ring-[#bb5b3e]"
:class="confirmError ? 'border-red-400' : 'border-[#e5ded7]'"
:class="confirmError ? 'border-red-400' : 'border-zinc-300'"
class="w-36 h-12 px-4 bg-zinc-50 border rounded-xl font-mono text-center text-zinc-800 focus:outline-none focus:ring-2 focus:ring-accent"
/>
<button
@click="confirmTwoFactor"
:disabled="confirmCode.length !== 6 || confirming"
class="px-6 py-3 bg-[#bb5b3e] text-white rounded-xl text-sm font-bold hover:bg-[#a34a31] transition-all disabled:opacity-50"
class="px-6 py-3 bg-accent text-white rounded-xl text-sm font-bold hover:bg-accent-content transition-all disabled:opacity-50"
>
{{ confirming ? 'Verifying…' : 'Confirm' }}
</button>
@@ -111,35 +111,35 @@
<button
v-if="!twoFactorEnabled"
@click="cancelEnable"
class="text-sm text-[#89726c] hover:text-[#4a3f3b]"
class="text-sm text-zinc-500 hover:text-zinc-800"
>
Cancel
</button>
</div>
<!-- Recovery codes (when 2FA enabled and not mid-setup) -->
<div v-if="twoFactorEnabled && !enablingTwoFactor" class="border-t border-[#e5ded7] pt-4 space-y-3">
<div v-if="twoFactorEnabled && !enablingTwoFactor" class="border-t border-zinc-300 pt-4 space-y-3">
<div class="flex items-center justify-between">
<p class="text-sm font-bold text-[#4a3f3b]">Recovery codes</p>
<p class="text-sm font-bold text-zinc-800">Recovery codes</p>
<button
@click="toggleRecoveryCodes"
class="text-xs font-bold text-[#bb5b3e] hover:underline"
class="text-xs font-bold text-accent hover:underline"
>
{{ showRecoveryCodes ? 'Hide' : 'Show' }}
</button>
</div>
<div v-if="showRecoveryCodes" class="space-y-2">
<div class="grid grid-cols-2 gap-1 bg-[#faf6f3] rounded-xl p-4">
<div class="grid grid-cols-2 gap-1 bg-zinc-50 rounded-xl p-4">
<code
v-for="code in recoveryCodes"
:key="code"
class="text-xs font-mono text-[#4a3f3b]"
class="text-xs font-mono text-zinc-800"
>{{ code }}</code>
</div>
<button
@click="regenRecoveryCodes"
:disabled="regenLoading"
class="text-xs font-bold text-[#89726c] hover:text-[#4a3f3b]"
class="text-xs font-bold text-zinc-500 hover:text-zinc-800"
>
{{ regenLoading ? 'Regenerating…' : 'Regenerate codes' }}
</button>
@@ -147,12 +147,12 @@
</div>
<!-- Action buttons -->
<div class="flex gap-3 border-t border-[#e5ded7] pt-4">
<div class="flex gap-3 border-t border-zinc-300 pt-4">
<button
v-if="!twoFactorEnabled && !enablingTwoFactor"
@click="enableTwoFactor"
:disabled="tfaActionLoading"
class="px-6 py-2.5 bg-[#bb5b3e] text-white rounded-xl text-sm font-bold hover:bg-[#a34a31] transition-colors disabled:opacity-50"
class="px-6 py-2.5 bg-accent text-white rounded-xl text-sm font-bold hover:bg-accent-content transition-colors disabled:opacity-50"
>
{{ tfaActionLoading ? 'Enabling…' : 'Enable 2FA' }}
</button>
@@ -160,7 +160,7 @@
v-if="twoFactorEnabled && !enablingTwoFactor"
@click="disableTwoFactor"
:disabled="tfaActionLoading"
class="px-6 py-2.5 bg-white border border-[#e5ded7] rounded-xl text-sm font-bold text-[#89726c] hover:border-red-300 hover:text-red-600 transition-colors disabled:opacity-50"
class="px-6 py-2.5 bg-white border border-zinc-300 rounded-xl text-sm font-bold text-zinc-500 hover:border-red-300 hover:text-red-600 transition-colors disabled:opacity-50"
>
{{ tfaActionLoading ? 'Disabling…' : 'Disable 2FA' }}
</button>

View File

@@ -1,13 +1,13 @@
<template>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-black text-[#4a3f3b]">Settings</h1>
<p class="text-[#89726c] mt-1">Manage your account and preferences.</p>
<h1 class="text-2xl font-black text-zinc-800">Settings</h1>
<p class="text-zinc-500 mt-1">Manage your account and preferences.</p>
</div>
<div class="flex gap-6">
<!-- Settings sub-nav -->
<aside class="w-44 flex-shrink-0">
<aside class="w-44 shrink-0">
<nav class="space-y-1">
<RouterLink
v-for="item in settingsNav"
@@ -15,8 +15,8 @@
:to="item.to"
class="flex items-center gap-3 px-3 py-2 rounded-xl text-sm font-bold transition-colors"
:class="$route.path === item.to
? 'bg-[#bb5b3e] text-white'
: 'text-[#89726c] hover:bg-white hover:text-[#4a3f3b]'"
? 'bg-accent text-white'
: 'text-zinc-500 hover:bg-white hover:text-zinc-800'"
>
<iconify-icon :icon="item.icon"></iconify-icon>
{{ item.label }}

View File

@@ -1,10 +1,95 @@
{
"version": 1,
"skills": {
"antfu": {
"source": "antfu/skills",
"sourceType": "github",
"computedHash": "6935586ef3ab02e18747a4477d396e692bc96e8f28b0d95b374ffc37fce74cda"
},
"nuxt": {
"source": "antfu/skills",
"sourceType": "github",
"computedHash": "9e8237ec92083db9a6f4c77d5e5bf4b1d9873d670671235bc75455ee1ca39c74"
},
"pinia": {
"source": "antfu/skills",
"sourceType": "github",
"computedHash": "897b6b0982956f41f5cc85128ca638b625e1b3c8bc619e303cd1e7661c95b7de"
},
"pnpm": {
"source": "antfu/skills",
"sourceType": "github",
"computedHash": "318f9fca2441a3e06fedc336e195dd18be5dbe901064e4fa71b76b40b096ab3a"
},
"slidev": {
"source": "antfu/skills",
"sourceType": "github",
"computedHash": "4973bc1baf08fa1303f99ef3abce8ec3aedfb2637e4315de30a2421746b610e3"
},
"superdesign": {
"source": "superdesigndev/superdesign-skill",
"sourceType": "github",
"computedHash": "7208b5e2e1145d76abcd698b2a9085952d986022ec2d5d8b9f972106b1b2447b"
},
"tsdown": {
"source": "antfu/skills",
"sourceType": "github",
"computedHash": "5d7a0732aa15849d7363d168c13eec9c4367c9e534397de3fc550d582ffea09b"
},
"turborepo": {
"source": "antfu/skills",
"sourceType": "github",
"computedHash": "bb531fab09e44812e3dc41e77f08708fbb67984ed9421d3c6f54f741fe70c2a0"
},
"unocss": {
"source": "antfu/skills",
"sourceType": "github",
"computedHash": "4b7a9f7b89994d5aef70b9d55cb7f65d6e80cea437d5786cd33bb5cf7231c7c4"
},
"vite": {
"source": "antfu/skills",
"sourceType": "github",
"computedHash": "867ff66238f3152e9c494339ad08dc432dd0df5bcf5cc7a00b61a72b580eb908"
},
"vitepress": {
"source": "antfu/skills",
"sourceType": "github",
"computedHash": "629d8ec4bcbf7cac86c15c3ca5f173933f01729bcb6396c0d74733755a896a41"
},
"vitest": {
"source": "antfu/skills",
"sourceType": "github",
"computedHash": "669c7275f7e1379b06b2bcfa593603aead4c983b951c7e8edbc09640aa38d0a1"
},
"vue": {
"source": "antfu/skills",
"sourceType": "github",
"computedHash": "9c241141e07e836e4f0537c63e8f929fba3767c2997250be38e699f19b75e3f2"
},
"vue-best-practices": {
"source": "antfu/skills",
"sourceType": "github",
"computedHash": "d7d22c8cb343583c3904692c4d1d7b50382945e433e4f6e053f4aabb9846cbc3"
},
"vue-router-best-practices": {
"source": "antfu/skills",
"sourceType": "github",
"computedHash": "e27384f4e6c8c70a612e76b74e4387efb8e291a6a1e3aa14a69102a4ce4b4654"
},
"vue-testing-best-practices": {
"source": "antfu/skills",
"sourceType": "github",
"computedHash": "18c7d8f42f350f927e37de055e34c97b8cfb9f79c12cf942f7f3d2a0821057b5"
},
"vueuse-functions": {
"source": "antfu/skills",
"sourceType": "github",
"computedHash": "fedfc78f035a88fe3e9d823f44725fe8981486c55b966c17fbd0d7c28339e49a"
},
"web-design-guidelines": {
"source": "antfu/skills",
"sourceType": "github",
"computedHash": "65a2e7d85753383ae0f88df15475d58b9e39723e9c4bb6891421d6144a85f79c"
}
}
}