fix(spa): complete checkout flow, UI standardization, and tailwind v4 styling fixes
This commit is contained in:
1 parent
83df238bab
commit
d8bff72e4e
24 files changed
+1169
-402
No files matched your search
@@ -85,26 +85,27 @@ export default function CategoryPage() {
|
||||
alt={category?.name}
|
||||
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-night-900 via-night-900/60 to-transparent" />
|
||||
<div className="absolute inset-0 bg-night-900/40" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-night-900/90 via-night-900/10 to-transparent" />
|
||||
<div className="absolute bottom-0 left-0 right-0 px-6 pb-10 pt-20">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Breadcrumb */}
|
||||
<div className="flex items-center gap-2 text-xs text-slate-400 uppercase tracking-widest mb-4">
|
||||
<div className="flex items-center gap-2 text-xs text-spa-sage/80 uppercase tracking-widest mb-4 font-semibold">
|
||||
<Link to="/" className="hover:text-white transition-colors">Home</Link>
|
||||
<span>/</span>
|
||||
<Link to="/shop" className="hover:text-white transition-colors">Shop</Link>
|
||||
<span>/</span>
|
||||
<span className="text-cyan-glow">{category?.name ?? '...'}</span>
|
||||
<span className="text-white">{category?.name ?? '...'}</span>
|
||||
</div>
|
||||
<h1 className="text-4xl md:text-5xl font-bold">
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-white">
|
||||
{loading ? (
|
||||
<div className="h-10 w-48 bg-white/10 animate-pulse rounded-lg" />
|
||||
<div className="h-10 w-48 bg-white/20 animate-pulse rounded-lg" />
|
||||
) : (
|
||||
category?.name
|
||||
)}
|
||||
</h1>
|
||||
{!loading && category?._count && (
|
||||
<p className="text-slate-400 mt-2">
|
||||
<p className="text-spa-sage mt-2">
|
||||
{category._count.products} products available
|
||||
</p>
|
||||
)}
|
||||
@@ -120,10 +121,10 @@ export default function CategoryPage() {
|
||||
<Link
|
||||
key={cat.id}
|
||||
to={`/category/${cat.id}`}
|
||||
className={`px-4 py-1.5 rounded-full text-xs font-bold uppercase tracking-widest transition-all duration-300 ${
|
||||
className={`px-4 py-1.5 rounded-full text-xs font-bold uppercase tracking-widest transition-all duration-300 border ${
|
||||
cat.id === category?.id
|
||||
? 'bg-cyan-glow text-night-900 shadow-[0_0_15px_rgba(102,252,241,0.3)]'
|
||||
: 'glass hover:bg-white/10'
|
||||
? 'bg-spa-forest text-white border-spa-forest shadow-md'
|
||||
: 'bg-white text-spa-stone border-spa-stone/20 hover:border-spa-forest hover:text-spa-forest shadow-sm'
|
||||
}`}
|
||||
>
|
||||
{cat.name}
|
||||
@@ -134,19 +135,19 @@ export default function CategoryPage() {
|
||||
{error ? (
|
||||
<div className="text-center py-24 space-y-3">
|
||||
<p className="text-4xl">⚠️</p>
|
||||
<p className="text-slate-400">{error}</p>
|
||||
<Link to="/shop" className="text-cyan-glow text-sm hover:underline">
|
||||
<p className="text-spa-stone font-medium">{error}</p>
|
||||
<Link to="/shop" className="text-spa-forest font-bold text-sm hover:underline">
|
||||
Back to Shop
|
||||
</Link>
|
||||
</div>
|
||||
) : loading ? (
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 md:gap-8">
|
||||
{Array.from({ length: 9 }).map((_, i) => (
|
||||
<div key={i} className="rounded-2xl bg-white/5 animate-pulse h-96" />
|
||||
<div key={i} className="rounded-2xl bg-spa-stone/10 animate-pulse h-96" />
|
||||
))}
|
||||
</div>
|
||||
) : products.length === 0 ? (
|
||||
<div className="text-center py-24 text-slate-500 space-y-3">
|
||||
<div className="text-center py-24 text-spa-stone/60 space-y-3 font-medium">
|
||||
<p className="text-4xl">🌿</p>
|
||||
<p>No products in this category yet.</p>
|
||||
</div>
|
||||
@@ -161,7 +162,7 @@ export default function CategoryPage() {
|
||||
{/* Invisible target for IntersectionObserver */}
|
||||
{displayCount < products.length && (
|
||||
<div ref={loaderRef} className="py-12 flex justify-center">
|
||||
<div className="w-8 h-8 rounded-full border-2 border-cyan-glow border-t-transparent animate-spin" />
|
||||
<div className="w-8 h-8 rounded-full border-2 border-spa-forest border-t-transparent animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -219,61 +219,67 @@ export default function CheckoutPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<div className="min-h-screen bg-pop-bg">
|
||||
<Navbar categories={categories} />
|
||||
|
||||
<main className="max-w-6xl mx-auto px-6 pt-32 pb-24">
|
||||
<main className="max-w-6xl mx-auto px-6 pt-32 pb-24 text-pop-black">
|
||||
<h1 className="text-4xl font-bold mb-12">Checkout</h1>
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-12">
|
||||
{/* Form Section */}
|
||||
<div className="flex-1 space-y-8">
|
||||
<form id="checkout-form" onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="bg-night-900/50 p-8 rounded-2xl border border-white/5 space-y-6">
|
||||
<h2 className="text-xl font-bold uppercase tracking-widest text-cyan-glow border-b border-white/10 pb-4">Shipping Information</h2>
|
||||
<div className="bg-white p-8 rounded-2xl border border-spa-stone/10 space-y-6 shadow-sm">
|
||||
<h2 className="text-xl font-bold uppercase tracking-widest text-spa-forest border-b border-spa-stone/10 pb-4">Shipping Information</h2>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs uppercase tracking-widest text-slate-400">Full Name *</label>
|
||||
<label htmlFor="checkout-name" className="text-xs uppercase tracking-widest text-spa-stone/80 font-bold">Full Name *</label>
|
||||
<input
|
||||
id="checkout-name"
|
||||
type="text"
|
||||
required
|
||||
value={formData.name}
|
||||
onChange={e => setFormData({ ...formData, name: e.target.value })}
|
||||
className="w-full bg-night-950 border border-white/10 rounded-lg p-3 text-white focus:border-cyan-glow focus:ring-1 focus:ring-cyan-glow outline-none transition-all"
|
||||
className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-3 text-pop-black focus:border-spa-forest focus:ring-1 focus:ring-spa-forest outline-none transition-all placeholder:text-spa-stone/50"
|
||||
placeholder="Jane Doe"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs uppercase tracking-widest text-slate-400">Phone Number *</label>
|
||||
<label htmlFor="checkout-phone" className="text-xs uppercase tracking-widest text-spa-stone/80 font-bold">Phone Number *</label>
|
||||
<input
|
||||
id="checkout-phone"
|
||||
type="tel"
|
||||
required
|
||||
pattern="[0-9]{9,10}"
|
||||
title="Please enter a valid phone number (9-10 digits)"
|
||||
value={formData.phone}
|
||||
onChange={e => setFormData({ ...formData, phone: e.target.value })}
|
||||
className="w-full bg-night-950 border border-white/10 rounded-lg p-3 text-white focus:border-cyan-glow focus:ring-1 focus:ring-cyan-glow outline-none transition-all"
|
||||
className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-3 text-pop-black focus:border-spa-forest focus:ring-1 focus:ring-spa-forest outline-none transition-all placeholder:text-spa-stone/50"
|
||||
placeholder="0812345678"
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-2 space-y-2">
|
||||
<label className="text-xs uppercase tracking-widest text-slate-400">Email Address *</label>
|
||||
<label htmlFor="checkout-email" className="text-xs uppercase tracking-widest text-spa-stone/80 font-bold">Email Address *</label>
|
||||
<input
|
||||
id="checkout-email"
|
||||
type="email"
|
||||
required
|
||||
value={formData.email}
|
||||
onChange={e => setFormData({ ...formData, email: e.target.value })}
|
||||
className="w-full bg-night-950 border border-white/10 rounded-lg p-3 text-white focus:border-cyan-glow focus:ring-1 focus:ring-cyan-glow outline-none transition-all"
|
||||
className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-3 text-pop-black focus:border-spa-forest focus:ring-1 focus:ring-spa-forest outline-none transition-all placeholder:text-spa-stone/50"
|
||||
placeholder="jane@example.com"
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-2 space-y-2">
|
||||
<label className="text-xs uppercase tracking-widest text-slate-400">Shipping Address *</label>
|
||||
<label htmlFor="checkout-address" className="text-xs uppercase tracking-widest text-spa-stone/80 font-bold">Shipping Address *</label>
|
||||
<textarea
|
||||
id="checkout-address"
|
||||
required
|
||||
rows={3}
|
||||
value={formData.address}
|
||||
onChange={e => setFormData({ ...formData, address: e.target.value })}
|
||||
className="w-full bg-night-950 border border-white/10 rounded-lg p-3 text-white focus:border-cyan-glow focus:ring-1 focus:ring-cyan-glow outline-none transition-all resize-none"
|
||||
className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-3 text-pop-black focus:border-spa-forest focus:ring-1 focus:ring-spa-forest outline-none transition-all resize-none placeholder:text-spa-stone/50"
|
||||
placeholder="123 Serenity Street..."
|
||||
/>
|
||||
</div>
|
||||
@@ -284,84 +290,84 @@ export default function CheckoutPage() {
|
||||
|
||||
{/* Summary Section */}
|
||||
<div className="lg:w-96 space-y-6">
|
||||
<div className="bg-night-900/50 p-8 rounded-2xl border border-white/5 sticky top-32">
|
||||
<h2 className="text-xl font-bold uppercase tracking-widest text-cyan-glow border-b border-white/10 pb-4 mb-6">Order Summary</h2>
|
||||
<div className="bg-white p-8 rounded-2xl border border-spa-stone/10 shadow-sm sticky top-32">
|
||||
<h2 className="text-xl font-bold uppercase tracking-widest text-spa-forest border-b border-spa-stone/10 pb-4 mb-6">Order Summary</h2>
|
||||
|
||||
{/* Coupon Section */}
|
||||
<div className="mb-6 pb-6 border-b border-white/10">
|
||||
<div className="mb-6 pb-6 border-b border-spa-stone/10">
|
||||
{!appliedCoupon ? (
|
||||
<div className="space-y-2">
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-500">Discount Code</label>
|
||||
<label className="text-[10px] uppercase font-bold tracking-widest text-spa-stone/80">Discount Code</label>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={couponCode}
|
||||
onChange={e => setCouponCode(e.target.value.toUpperCase())}
|
||||
placeholder="e.g. SUMMER2026"
|
||||
className="flex-1 bg-night-950 border border-white/10 rounded-lg px-3 text-sm text-white focus:border-cyan-glow outline-none uppercase tracking-widest"
|
||||
className="flex-1 bg-spa-light border border-spa-stone/20 rounded-lg px-3 text-sm text-pop-black focus:border-spa-forest outline-none uppercase tracking-widest placeholder:text-spa-stone/50"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleApplyCoupon}
|
||||
disabled={couponLoading || !couponCode.trim()}
|
||||
className="px-4 bg-white/5 hover:bg-white/10 border border-white/10 rounded-lg text-xs font-bold uppercase tracking-widest disabled:opacity-50 transition-all"
|
||||
className="px-4 bg-spa-stone/10 hover:bg-spa-stone/20 border border-spa-stone/10 rounded-lg text-xs font-bold uppercase tracking-widest disabled:opacity-50 transition-all text-pop-black"
|
||||
>
|
||||
{couponLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : 'Apply'}
|
||||
</button>
|
||||
</div>
|
||||
{couponError && <p className="text-xs text-rose-500 mt-1">{couponError}</p>}
|
||||
{couponError && <p className="text-xs text-rose-500 mt-1 font-medium">{couponError}</p>}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-between bg-cyan-glow/10 border border-cyan-glow/20 rounded-lg px-4 py-3">
|
||||
<div className="flex items-center justify-between bg-spa-sage/30 border border-spa-forest/20 rounded-lg px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="w-4 h-4 text-cyan-glow" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<svg className="w-4 h-4 text-spa-forest" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span className="text-sm font-bold tracking-widest text-cyan-glow">{appliedCoupon}</span>
|
||||
<span className="text-sm font-bold tracking-widest text-spa-forest">{appliedCoupon}</span>
|
||||
</div>
|
||||
<button type="button" onClick={removeCoupon} className="text-xs text-slate-400 hover:text-rose-500 uppercase tracking-widest font-bold">Remove</button>
|
||||
<button type="button" onClick={removeCoupon} className="text-xs text-spa-stone/80 hover:text-rose-500 uppercase tracking-widest font-bold">Remove</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 mb-6">
|
||||
{items.map(item => (
|
||||
<div key={item.cartItemId} className="flex justify-between items-start gap-4 text-sm">
|
||||
<div key={item.cartItemId} className="flex justify-between items-start gap-4 text-sm text-pop-black">
|
||||
<div className="flex-1">
|
||||
<p className="font-bold">{item.productName}</p>
|
||||
<p className="text-slate-400 text-xs mt-0.5">{item.variantSize} x {item.quantity}</p>
|
||||
<p className="text-spa-stone/80 text-xs mt-0.5">{item.variantSize} x {item.quantity}</p>
|
||||
</div>
|
||||
<p className="font-medium text-cyan-glow">{formatPrice(item.price * item.quantity)}</p>
|
||||
<p className="font-bold text-rose-500">{formatPrice(item.price * item.quantity)}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-white/10 pt-4 space-y-3 font-medium">
|
||||
<div className="flex justify-between text-sm text-slate-300">
|
||||
<div className="border-t border-spa-stone/10 pt-4 space-y-3 font-medium text-pop-black">
|
||||
<div className="flex justify-between text-sm text-spa-stone">
|
||||
<span>Subtotal</span>
|
||||
<span>{formatPrice(subtotal)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm text-slate-300">
|
||||
<div className="flex justify-between text-sm text-spa-stone">
|
||||
<span>Shipping Fee</span>
|
||||
{loadingSettings ? <Loader2 className="w-4 h-4 animate-spin" /> :
|
||||
<span className={isFreeShipping ? "line-through text-slate-500" : ""}>{formatPrice(shippingFee)}</span>
|
||||
<span className={isFreeShipping ? "line-through text-spa-stone/50" : ""}>{formatPrice(shippingFee)}</span>
|
||||
}
|
||||
</div>
|
||||
{isFreeShipping && (
|
||||
<div className="flex justify-between text-sm text-emerald-400">
|
||||
<div className="flex justify-between text-sm text-spa-forest">
|
||||
<span>Shipping Discount</span>
|
||||
<span>- {formatPrice(shippingFee)}</span>
|
||||
</div>
|
||||
)}
|
||||
{discountAmount > 0 && (
|
||||
<div className="flex justify-between text-sm text-cyan-glow">
|
||||
<div className="flex justify-between text-sm text-rose-500">
|
||||
<span>Discount</span>
|
||||
<span>- {formatPrice(discountAmount)}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="border-t border-white/10 pt-3 mt-3 flex justify-between text-lg font-bold">
|
||||
<div className="border-t border-spa-stone/10 pt-3 mt-3 flex justify-between text-lg font-bold text-pop-black">
|
||||
<span>Total</span>
|
||||
<span className="text-cyan-glow">{formatPrice(total)}</span>
|
||||
<span className="text-rose-500">{formatPrice(total)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -369,7 +375,7 @@ export default function CheckoutPage() {
|
||||
form="checkout-form"
|
||||
type="submit"
|
||||
disabled={loading || loadingSettings}
|
||||
className="w-full mt-8 py-4 bg-white text-night-900 font-bold rounded-lg tracking-widest uppercase hover:bg-cyan-glow hover:shadow-[0_0_25px_rgba(102,252,241,0.4)] transition-all duration-500 disabled:opacity-50 disabled:cursor-not-allowed flex justify-center items-center gap-2"
|
||||
className="w-full mt-8 py-4 bg-spa-forest text-white font-bold rounded-lg tracking-widest uppercase hover:bg-spa-stone hover:shadow-lg transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed flex justify-center items-center gap-2"
|
||||
>
|
||||
{loading ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Confirm Order'}
|
||||
</button>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import Navbar from '../components/Navbar'
|
||||
import Footer from '../components/Footer'
|
||||
import EmptyState from '../components/EmptyState'
|
||||
import { API_BASE, type Category } from '../types'
|
||||
import { Ticket, Copy, CheckCircle2, ArrowRight } from 'lucide-react'
|
||||
|
||||
@@ -61,15 +62,15 @@ export default function CouponsPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<div className="min-h-screen bg-pop-bg">
|
||||
<Navbar categories={categories} />
|
||||
|
||||
<main className="max-w-6xl mx-auto px-6 pt-32 pb-24 min-h-[70vh]">
|
||||
<div className="text-center space-y-4 mb-16">
|
||||
<h1 className="text-4xl md:text-5xl font-bold bg-gradient-to-r from-white via-cyan-glow to-blue-500 bg-clip-text text-transparent">
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-pop-black">
|
||||
Exclusive Offers
|
||||
</h1>
|
||||
<p className="text-slate-400 max-w-2xl mx-auto">
|
||||
<p className="text-spa-stone max-w-2xl mx-auto">
|
||||
คูปองส่วนลดพิเศษสำหรับคุณ เลือกใช้คูปองที่คุณต้องการแล้วไปช้อปกันเลย!
|
||||
</p>
|
||||
</div>
|
||||
@@ -77,63 +78,58 @@ export default function CouponsPage() {
|
||||
{loading ? (
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{[1, 2, 3].map(i => (
|
||||
<div key={i} className="h-64 bg-white/5 rounded-3xl animate-pulse" />
|
||||
<div key={i} className="h-64 bg-spa-stone/10 rounded-3xl animate-pulse" />
|
||||
))}
|
||||
</div>
|
||||
) : coupons.length === 0 ? (
|
||||
<div className="text-center py-20 bg-night-900/50 rounded-3xl border border-white/5">
|
||||
<Ticket className="w-16 h-16 text-slate-600 mx-auto mb-4" />
|
||||
<h2 className="text-xl font-bold text-white mb-2">ยังไม่มีคูปองส่วนลดในขณะนี้</h2>
|
||||
<p className="text-slate-400 mb-6">กรุณากลับมาตรวจสอบโปรโมชั่นใหม่ในภายหลัง</p>
|
||||
<button
|
||||
onClick={() => navigate('/shop')}
|
||||
className="px-8 py-3 bg-cyan-glow/10 text-cyan-glow hover:bg-cyan-glow hover:text-night-900 font-bold rounded-xl transition-all"
|
||||
>
|
||||
ไปช้อปปิ้งกันก่อน
|
||||
</button>
|
||||
</div>
|
||||
<EmptyState
|
||||
title="ยังไม่มีคูปองส่วนลดในขณะนี้"
|
||||
message="กรุณากลับมาตรวจสอบโปรโมชั่นใหม่ในภายหลัง"
|
||||
actionText="ไปช้อปปิ้งกันก่อน"
|
||||
actionLink="/shop"
|
||||
/>
|
||||
) : (
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{coupons.map(coupon => (
|
||||
<div
|
||||
key={coupon.id}
|
||||
className="group relative bg-night-900 border border-white/10 rounded-3xl overflow-hidden hover:border-cyan-glow/50 transition-all duration-500"
|
||||
className="group relative bg-white border border-spa-stone/10 rounded-3xl overflow-hidden hover:border-spa-forest transition-all duration-500 shadow-sm hover:shadow-md"
|
||||
>
|
||||
{/* Decoration */}
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-cyan-glow/10 blur-3xl rounded-full -translate-y-1/2 translate-x-1/2" />
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-spa-sage/20 blur-3xl rounded-full -translate-y-1/2 translate-x-1/2" />
|
||||
|
||||
<div className="p-8 space-y-6 relative z-10">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="w-12 h-12 rounded-xl bg-cyan-glow/10 text-cyan-glow flex items-center justify-center border border-cyan-glow/20">
|
||||
<div className="w-12 h-12 rounded-xl bg-spa-sage/30 text-spa-forest flex items-center justify-center border border-spa-stone/10">
|
||||
<Ticket className="w-6 h-6" />
|
||||
</div>
|
||||
{coupon.expiresAt && (
|
||||
<span className="text-[10px] font-bold text-amber-400 bg-amber-500/10 px-2 py-1 rounded-md uppercase tracking-wider">
|
||||
<span className="text-[10px] font-bold text-rose-500 bg-rose-500/10 px-2 py-1 rounded-md uppercase tracking-wider">
|
||||
หมดเขต {new Date(coupon.expiresAt).toLocaleDateString('th-TH')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-3xl font-black text-white mb-1">
|
||||
<h3 className="text-3xl font-black text-pop-black mb-1">
|
||||
{formatDiscount(coupon.type, coupon.value)}
|
||||
</h3>
|
||||
<p className="text-sm text-slate-400">
|
||||
<p className="text-sm text-spa-stone">
|
||||
เมื่อซื้อครบ {coupon.minPurchase} บาท
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 bg-night-950 p-1 pl-4 rounded-xl border border-white/5">
|
||||
<span className="flex-1 font-mono text-cyan-glow font-bold tracking-widest uppercase">
|
||||
<div className="flex items-center gap-2 bg-spa-light p-1 pl-4 rounded-xl border border-spa-stone/20">
|
||||
<span className="flex-1 font-mono text-spa-forest font-bold tracking-widest uppercase">
|
||||
{coupon.code}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => handleCopy(coupon.code)}
|
||||
className="p-3 hover:bg-white/5 rounded-lg text-slate-400 hover:text-white transition-all group/btn"
|
||||
className="p-3 hover:bg-spa-stone/10 rounded-lg text-spa-stone hover:text-spa-forest transition-all group/btn"
|
||||
title="Copy Code"
|
||||
>
|
||||
{copiedCode === coupon.code ? (
|
||||
<CheckCircle2 className="w-5 h-5 text-emerald-400" />
|
||||
<CheckCircle2 className="w-5 h-5 text-emerald-500" />
|
||||
) : (
|
||||
<Copy className="w-5 h-5 group-hover/btn:scale-110 transition-transform" />
|
||||
)}
|
||||
@@ -142,7 +138,7 @@ export default function CouponsPage() {
|
||||
|
||||
<button
|
||||
onClick={() => handleUseNow(coupon.code)}
|
||||
className="w-full py-4 bg-white/5 hover:bg-cyan-glow text-white hover:text-night-900 font-bold rounded-xl flex items-center justify-center gap-2 transition-all duration-300"
|
||||
className="w-full py-4 bg-spa-forest text-white hover:bg-spa-stone font-bold rounded-xl flex items-center justify-center gap-2 transition-all duration-300 shadow-sm"
|
||||
>
|
||||
ใช้คูปองนี้เลย
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
@@ -150,8 +146,8 @@ export default function CouponsPage() {
|
||||
</div>
|
||||
|
||||
{/* Scalloped edge effect */}
|
||||
<div className="absolute left-0 top-1/2 -translate-y-1/2 -translate-x-1/2 w-6 h-6 bg-[#0a0a0a] rounded-full border-r border-white/10" />
|
||||
<div className="absolute right-0 top-1/2 -translate-y-1/2 translate-x-1/2 w-6 h-6 bg-[#0a0a0a] rounded-full border-l border-white/10" />
|
||||
<div className="absolute left-0 top-1/2 -translate-y-1/2 -translate-x-1/2 w-6 h-6 bg-pop-bg rounded-full border-r border-spa-stone/10" />
|
||||
<div className="absolute right-0 top-1/2 -translate-y-1/2 translate-x-1/2 w-6 h-6 bg-pop-bg rounded-full border-l border-spa-stone/10" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -42,31 +42,33 @@ export default function HomePage() {
|
||||
<Navbar categories={categories} />
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative w-full min-h-[90vh] md:min-h-0 md:aspect-[21/9] flex items-center justify-center overflow-hidden bg-white">
|
||||
<div className="absolute inset-0 z-0 opacity-40">
|
||||
<section className="relative w-full min-h-[90vh] md:min-h-0 md:aspect-[21/9] flex items-center justify-center overflow-hidden bg-night-900">
|
||||
<div className="absolute inset-0 z-0">
|
||||
<img
|
||||
src={hero?.imageUrl || "/images/hero.png"}
|
||||
className="w-full h-full object-cover scale-[1.02] filter contrast-125 saturate-150"
|
||||
className="w-full h-full object-cover scale-[1.02]"
|
||||
alt="Hero Background"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = 'none'
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-pop-bg/80 to-transparent"></div>
|
||||
{/* Dark gradient overlay for premium vibe */}
|
||||
<div className="absolute inset-0 bg-night-900/40"></div>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-night-900/90 via-night-900/10 to-night-900/30"></div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 text-center md:text-left space-y-6 px-8 animate-pop-in max-w-6xl w-full mx-auto">
|
||||
<p className="text-pop-pink font-semibold tracking-[0.2em] text-sm uppercase">
|
||||
<div className="relative z-10 text-center space-y-6 px-8 animate-fade-in max-w-4xl w-full mx-auto mt-16 md:mt-0">
|
||||
<p className="text-spa-sage font-semibold tracking-[0.3em] text-xs md:text-sm uppercase drop-shadow-md">
|
||||
{hero?.tagline || "Vibrant Elegance Collection"}
|
||||
</p>
|
||||
<h1 className="text-5xl md:text-7xl font-bold text-pop-black leading-[1.1] tracking-tight max-w-3xl">
|
||||
<h1 className="text-5xl md:text-7xl lg:text-8xl font-light text-white leading-tight tracking-wide drop-shadow-md font-serif">
|
||||
{hero ? (
|
||||
hero.title.split('[').map((part, i) => {
|
||||
if (part.includes(']')) {
|
||||
const [accent, rest] = part.split(']')
|
||||
return (
|
||||
<span key={i}>
|
||||
<span className="text-pop-pink font-semibold">
|
||||
<span className="text-spa-gold font-medium italic">
|
||||
{accent}
|
||||
</span>
|
||||
{rest}
|
||||
@@ -77,25 +79,28 @@ export default function HomePage() {
|
||||
})
|
||||
) : (
|
||||
<>
|
||||
Inhale <span className="text-pop-pink">Serenity</span>
|
||||
Inhale <span className="text-spa-gold font-medium italic">Serenity</span>
|
||||
</>
|
||||
)}
|
||||
</h1>
|
||||
<p className="text-gray-600 max-w-xl text-lg md:text-xl font-normal pt-4 pb-6">
|
||||
<p className="text-spa-light/90 max-w-2xl mx-auto text-lg md:text-xl font-light pt-4 pb-8 drop-shadow-md">
|
||||
{hero?.description || "Crafting premium sensory experiences that transform your space into a sanctuary of peace."}
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 items-center md:justify-start justify-center">
|
||||
<div className="flex flex-col sm:flex-row gap-6 items-center justify-center">
|
||||
{/* White solid primary button */}
|
||||
<Link
|
||||
to={hero?.buttonLink || "/shop"}
|
||||
className="brutal-button shadow-lg shadow-pop-pink/30"
|
||||
className="px-8 py-4 bg-white text-spa-forest rounded-full font-bold uppercase tracking-widest text-sm hover:bg-spa-sage hover:shadow-xl transition-all duration-300"
|
||||
>
|
||||
{hero?.buttonText || "Explore Collection"}
|
||||
</Link>
|
||||
{/* Outline secondary link */}
|
||||
<a
|
||||
href="#categories"
|
||||
className="px-8 py-4 text-pop-black font-medium hover:text-pop-pink transition-colors"
|
||||
className="px-8 py-4 text-white font-medium uppercase tracking-widest text-sm hover:text-spa-gold flex items-center gap-2 group transition-colors"
|
||||
>
|
||||
Learn More
|
||||
Learn More
|
||||
<span className="group-hover:translate-x-1 transition-transform">→</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -73,10 +73,10 @@ export default function OrderSuccessPage() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<div className="min-h-screen bg-pop-bg">
|
||||
<Navbar categories={categories} />
|
||||
<div className="flex items-center justify-center pt-40">
|
||||
<Loader2 className="w-8 h-8 text-cyan-glow animate-spin" />
|
||||
<Loader2 className="w-8 h-8 text-spa-forest animate-spin" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -84,119 +84,119 @@ export default function OrderSuccessPage() {
|
||||
|
||||
if (!order || (order as any).error) {
|
||||
return (
|
||||
<div className="min-h-screen text-center pt-40">
|
||||
<div className="min-h-screen bg-pop-bg text-center pt-40">
|
||||
<Navbar categories={categories} />
|
||||
<h1 className="text-2xl font-bold mb-4">Order Not Found</h1>
|
||||
<Link to="/shop" className="text-cyan-glow hover:underline">Return to Shop</Link>
|
||||
<h1 className="text-2xl font-bold mb-4 text-pop-black">Order Not Found</h1>
|
||||
<Link to="/shop" className="text-spa-forest hover:underline font-bold">Return to Shop</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<div className="min-h-screen bg-pop-bg">
|
||||
<Navbar categories={categories} />
|
||||
|
||||
<main className="max-w-3xl mx-auto px-6 pt-32 pb-24 space-y-12 animate-fade-in">
|
||||
<main className="max-w-3xl mx-auto px-6 pt-32 pb-24 space-y-12 animate-fade-in text-pop-black">
|
||||
|
||||
{/* Header */}
|
||||
<div className="text-center space-y-6">
|
||||
<div className="w-20 h-20 bg-cyan-glow/10 rounded-full flex items-center justify-center mx-auto border border-cyan-glow/20">
|
||||
<CheckCircle2 className="w-10 h-10 text-cyan-glow" />
|
||||
<div className="w-20 h-20 bg-spa-sage/30 rounded-full flex items-center justify-center mx-auto border border-spa-stone/10 shadow-sm">
|
||||
<CheckCircle2 className="w-10 h-10 text-spa-forest" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold">Order Received</h1>
|
||||
<p className="text-slate-400 mt-2">Thank you! Your order #{order.id.toString().padStart(4, '0')} has been placed.</p>
|
||||
<h1 className="text-4xl font-bold text-pop-black">Order Received</h1>
|
||||
<p className="text-spa-stone mt-2">Thank you! Your order #{order.id.toString().padStart(4, '0')} has been placed.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Order Summary */}
|
||||
<div className="bg-night-900/50 p-8 rounded-2xl border border-white/5 space-y-6">
|
||||
<h2 className="text-sm font-bold uppercase tracking-widest text-cyan-glow border-b border-white/10 pb-4">Order Items</h2>
|
||||
<div className="bg-white p-8 rounded-2xl border border-spa-stone/10 space-y-6 shadow-sm">
|
||||
<h2 className="text-sm font-bold uppercase tracking-widest text-spa-forest border-b border-spa-stone/10 pb-4">Order Items</h2>
|
||||
<div className="space-y-4">
|
||||
{order.items.map(item => (
|
||||
<div key={item.id} className="flex items-center gap-4 text-sm">
|
||||
<div className="w-12 h-12 rounded-lg bg-white/5 flex-shrink-0 overflow-hidden">
|
||||
<div className="w-12 h-12 rounded-lg bg-spa-stone/10 flex-shrink-0 overflow-hidden border border-spa-stone/10">
|
||||
<img src={item.productImage || '/images/oil_ozone.png'} alt="Product" className="w-full h-full object-cover" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<p className="font-bold">{item.productName}</p>
|
||||
<p className="text-slate-400 text-xs mt-0.5">{item.variantSize} x {item.quantity}</p>
|
||||
<p className="font-bold text-pop-black">{item.productName}</p>
|
||||
<p className="text-spa-stone/80 text-xs mt-0.5">{item.variantSize} x {item.quantity}</p>
|
||||
</div>
|
||||
<p className="font-medium text-cyan-glow">{formatPrice(item.priceAtPurchase * item.quantity)}</p>
|
||||
<p className="font-bold text-rose-500">{formatPrice(item.priceAtPurchase * item.quantity)}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="border-t border-white/10 pt-4 space-y-3 font-medium text-sm text-slate-300">
|
||||
<div className="flex justify-between">
|
||||
<div className="border-t border-spa-stone/10 pt-4 space-y-3 font-medium text-sm text-pop-black">
|
||||
<div className="flex justify-between text-spa-stone">
|
||||
<span>Subtotal</span>
|
||||
<span>{formatPrice(order.totalAmount - order.shippingFee)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex justify-between text-spa-stone">
|
||||
<span>Shipping Fee</span>
|
||||
<span>{formatPrice(order.shippingFee)}</span>
|
||||
</div>
|
||||
<div className="border-t border-white/10 pt-3 mt-3 flex justify-between text-lg font-bold text-white">
|
||||
<div className="border-t border-spa-stone/10 pt-3 mt-3 flex justify-between text-lg font-bold text-pop-black">
|
||||
<span>Total</span>
|
||||
<span className="text-cyan-glow">{formatPrice(order.totalAmount)}</span>
|
||||
<span className="text-rose-500">{formatPrice(order.totalAmount)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Banking Details */}
|
||||
{settings && order.status === 'PENDING_PAYMENT' && (
|
||||
<div className="bg-night-900/50 p-8 rounded-2xl border border-white/5 text-center space-y-6 relative overflow-hidden">
|
||||
<div className="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-transparent via-cyan-glow to-transparent opacity-50" />
|
||||
<div className="bg-white p-8 rounded-2xl border border-spa-stone/10 text-center space-y-6 relative overflow-hidden shadow-sm">
|
||||
<div className="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-transparent via-spa-forest to-transparent opacity-50" />
|
||||
|
||||
<h2 className="text-xs uppercase font-bold tracking-widest text-slate-400">Payment Required</h2>
|
||||
<div className="text-3xl font-bold text-cyan-glow">
|
||||
<h2 className="text-xs uppercase font-bold tracking-widest text-spa-stone/80">Payment Required</h2>
|
||||
<div className="text-3xl font-bold text-spa-forest">
|
||||
{formatPrice(order.totalAmount)}
|
||||
</div>
|
||||
|
||||
<div className="bg-night-950 p-6 rounded-xl border border-white/5 inline-block text-left w-full max-w-md mx-auto space-y-4">
|
||||
<div className="bg-spa-light p-6 rounded-xl border border-spa-stone/20 inline-block text-left w-full max-w-md mx-auto space-y-4 shadow-inner">
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 uppercase tracking-widest">Bank</p>
|
||||
<p className="font-bold">{settings.bankName}</p>
|
||||
<p className="text-xs text-spa-stone/80 uppercase tracking-widest font-bold">Bank</p>
|
||||
<p className="font-bold text-pop-black">{settings.bankName}</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 uppercase tracking-widest">Account Number</p>
|
||||
<p className="font-mono text-xl font-bold tracking-widest text-cyan-glow">{settings.bankAccountNumber}</p>
|
||||
<p className="text-xs text-spa-stone/80 uppercase tracking-widest font-bold">Account Number</p>
|
||||
<p className="font-mono text-xl font-bold tracking-widest text-spa-forest">{settings.bankAccountNumber}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={copyToClipboard}
|
||||
className="p-2 glass rounded-lg hover:bg-white/10 transition-colors shrink-0"
|
||||
className="p-2 bg-white rounded-lg hover:bg-spa-stone/10 border border-spa-stone/20 transition-colors shrink-0 shadow-sm"
|
||||
title="Copy account number"
|
||||
>
|
||||
{copied ? <CheckCircle2 className="w-5 h-5 text-emerald-400" /> : <Copy className="w-5 h-5 text-slate-300" />}
|
||||
{copied ? <CheckCircle2 className="w-5 h-5 text-emerald-500" /> : <Copy className="w-5 h-5 text-spa-stone/80" />}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 uppercase tracking-widest">Account Name</p>
|
||||
<p className="font-bold">{settings.bankAccountName}</p>
|
||||
<p className="text-xs text-spa-stone/80 uppercase tracking-widest font-bold">Account Name</p>
|
||||
<p className="font-bold text-pop-black">{settings.bankAccountName}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500">Please transfer the exact amount and upload your slip below.</p>
|
||||
<p className="text-xs text-spa-stone font-medium">Please transfer the exact amount and upload your slip below.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Upload Status */}
|
||||
<div className="bg-night-900/50 p-8 rounded-2xl border border-white/5 space-y-6">
|
||||
<h2 className="text-sm font-bold uppercase tracking-widest text-cyan-glow border-b border-white/10 pb-4">Payment Slip Verification</h2>
|
||||
<div className="bg-white p-8 rounded-2xl border border-spa-stone/10 space-y-6 shadow-sm">
|
||||
<h2 className="text-sm font-bold uppercase tracking-widest text-spa-forest border-b border-spa-stone/10 pb-4">Payment Slip Verification</h2>
|
||||
|
||||
{order.paymentSlipUrl ? (
|
||||
<div className="flex flex-col items-center justify-center space-y-4 py-4">
|
||||
<div className="w-16 h-16 bg-emerald-500/10 rounded-full flex items-center justify-center">
|
||||
<CheckCircle2 className="w-8 h-8 text-emerald-400" />
|
||||
<div className="w-16 h-16 bg-emerald-500/10 rounded-full flex items-center justify-center border border-emerald-500/20">
|
||||
<CheckCircle2 className="w-8 h-8 text-emerald-500" />
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="font-bold text-lg text-emerald-400">Slip Uploaded Successfully</p>
|
||||
<p className="text-sm text-slate-400 mt-1">Status: {order.status}</p>
|
||||
<p className="text-xs mt-2 text-slate-500">We will verify your payment and process your order shortly.</p>
|
||||
<p className="font-bold text-lg text-emerald-600">Slip Uploaded Successfully</p>
|
||||
<p className="text-sm text-spa-stone mt-1 font-medium">Status: {order.status}</p>
|
||||
<p className="text-xs mt-2 text-spa-stone/80">We will verify your payment and process your order shortly.</p>
|
||||
</div>
|
||||
<img src={order.paymentSlipUrl} alt="Slip" className="w-48 rounded-xl border border-white/10 mt-4 opacity-50 hover:opacity-100 transition-opacity" />
|
||||
<img src={order.paymentSlipUrl} alt="Slip" className="w-48 rounded-xl border border-spa-stone/20 mt-4 opacity-80 hover:opacity-100 transition-opacity shadow-sm" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative border-2 border-dashed border-white/10 rounded-xl p-10 hover:border-cyan-glow/50 transition-colors group cursor-pointer overflow-hidden">
|
||||
<div className="relative border-2 border-dashed border-spa-stone/30 bg-spa-light rounded-xl p-10 hover:border-spa-forest transition-colors group cursor-pointer overflow-hidden">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
@@ -206,13 +206,13 @@ export default function OrderSuccessPage() {
|
||||
/>
|
||||
<div className="text-center space-y-4">
|
||||
{uploading ? (
|
||||
<Loader2 className="w-12 h-12 text-cyan-glow animate-spin mx-auto" />
|
||||
<Loader2 className="w-12 h-12 text-spa-forest animate-spin mx-auto" />
|
||||
) : (
|
||||
<UploadCloud className="w-12 h-12 text-slate-500 group-hover:text-cyan-glow transition-colors mx-auto" />
|
||||
<UploadCloud className="w-12 h-12 text-spa-stone/60 group-hover:text-spa-forest transition-colors mx-auto" />
|
||||
)}
|
||||
<div>
|
||||
<p className="font-bold">{uploading ? 'Uploading...' : 'Click or drag to upload slip'}</p>
|
||||
<p className="text-xs text-slate-500 mt-1">Supports JPG, PNG (Max 5MB)</p>
|
||||
<p className="font-bold text-pop-black">{uploading ? 'Uploading...' : 'Click or drag to upload slip'}</p>
|
||||
<p className="text-xs text-spa-stone font-medium mt-1">Supports JPG, PNG (Max 5MB)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -50,16 +50,16 @@ export default function ProductDetailPage() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<div className="min-h-screen bg-pop-bg">
|
||||
<Navbar categories={categories} />
|
||||
<div className="pt-32 px-6 max-w-7xl mx-auto animate-pulse space-y-8">
|
||||
<div className="h-6 w-48 bg-white/5 rounded" />
|
||||
<div className="h-6 w-48 bg-spa-stone/10 rounded" />
|
||||
<div className="grid md:grid-cols-2 gap-12">
|
||||
<div className="h-96 bg-white/5 rounded-2xl" />
|
||||
<div className="h-96 bg-spa-stone/10 rounded-2xl" />
|
||||
<div className="space-y-4">
|
||||
<div className="h-8 w-3/4 bg-white/5 rounded" />
|
||||
<div className="h-4 w-1/2 bg-white/5 rounded" />
|
||||
<div className="h-12 w-1/3 bg-white/5 rounded" />
|
||||
<div className="h-8 w-3/4 bg-spa-stone/10 rounded" />
|
||||
<div className="h-4 w-1/2 bg-spa-stone/10 rounded" />
|
||||
<div className="h-12 w-1/3 bg-spa-stone/10 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -70,12 +70,12 @@ export default function ProductDetailPage() {
|
||||
|
||||
if (error || !product) {
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<div className="min-h-screen bg-pop-bg">
|
||||
<Navbar categories={categories} />
|
||||
<div className="pt-40 text-center space-y-4">
|
||||
<p className="text-4xl">⚠️</p>
|
||||
<p className="text-slate-400">{error ?? 'Product not found'}</p>
|
||||
<Link to="/shop" className="text-cyan-glow hover:underline text-sm">
|
||||
<p className="text-spa-stone font-medium">{error ?? 'Product not found'}</p>
|
||||
<Link to="/shop" className="text-spa-forest hover:underline text-sm font-bold">
|
||||
Back to Shop
|
||||
</Link>
|
||||
</div>
|
||||
@@ -100,35 +100,35 @@ export default function ProductDetailPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen pb-24 md:pb-0">
|
||||
<div className="min-h-screen pb-24 md:pb-0 bg-pop-bg">
|
||||
<Navbar categories={categories} />
|
||||
|
||||
<div className="max-w-7xl mx-auto px-6 pt-32 pb-24 space-y-24">
|
||||
<div className="max-w-7xl mx-auto px-6 pt-24 md:pt-32 pb-24 space-y-12 md:space-y-24">
|
||||
{/* Breadcrumb */}
|
||||
<div className="flex items-center gap-2 text-xs text-slate-400 uppercase tracking-widest">
|
||||
<Link to="/" className="hover:text-white transition-colors">Home</Link>
|
||||
<div className="flex items-center flex-wrap gap-2 text-[10px] md:text-xs text-spa-stone/60 uppercase tracking-widest font-semibold mb-6 md:mb-0">
|
||||
<Link to="/" className="hover:text-spa-forest transition-colors">Home</Link>
|
||||
<span>/</span>
|
||||
<Link to="/shop" className="hover:text-white transition-colors">Shop</Link>
|
||||
<Link to="/shop" className="hover:text-spa-forest transition-colors">Shop</Link>
|
||||
<span>/</span>
|
||||
<Link to={`/category/${product.category.id}`} className="hover:text-white transition-colors">
|
||||
<Link to={`/category/${product.category.id}`} className="hover:text-spa-forest transition-colors">
|
||||
{product.category.name}
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<span className="text-cyan-glow">{product.scent.name}</span>
|
||||
<span className="text-spa-forest">{product.scent.name}</span>
|
||||
</div>
|
||||
|
||||
{/* Product Detail */}
|
||||
<div className="grid md:grid-cols-2 gap-16 items-center">
|
||||
<div className="grid md:grid-cols-2 gap-8 md:gap-16 items-start">
|
||||
{/* Image */}
|
||||
<div className="relative rounded-2xl overflow-hidden h-[480px] group">
|
||||
<div className="relative rounded-2xl overflow-hidden h-[480px] group bg-white shadow-sm border border-spa-stone/10">
|
||||
<img
|
||||
src={product.images?.[0]?.url || getCategoryImage(product.category.name)}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
|
||||
alt={product.scent.name}
|
||||
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-night-900/50 to-transparent" />
|
||||
<div className="absolute top-6 left-6 glass px-3 py-1.5 rounded-full text-[10px] font-bold uppercase tracking-widest text-cyan-glow border border-cyan-glow/20">
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/10 to-transparent" />
|
||||
<div className="absolute top-6 left-6 bg-white/90 backdrop-blur-sm px-3 py-1.5 rounded-full text-[10px] font-bold uppercase tracking-widest text-spa-forest shadow-sm">
|
||||
{product.category.name}
|
||||
</div>
|
||||
</div>
|
||||
@@ -136,34 +136,34 @@ export default function ProductDetailPage() {
|
||||
{/* Info */}
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-2">
|
||||
<p className="text-cyan-glow uppercase tracking-[0.3em] text-xs font-semibold opacity-80">
|
||||
<p className="text-spa-forest uppercase tracking-[0.3em] text-xs font-bold opacity-80">
|
||||
{product.category.name}
|
||||
</p>
|
||||
<h1 className="text-5xl font-bold leading-tight">
|
||||
<h1 className="text-5xl font-bold leading-tight text-pop-black">
|
||||
{product.scent.name}
|
||||
</h1>
|
||||
<p className="text-slate-400 text-sm uppercase tracking-widest">
|
||||
<p className="text-spa-stone/80 text-sm uppercase tracking-widest font-semibold">
|
||||
Premium Collection
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{product.description && (
|
||||
<p className="text-slate-300 leading-relaxed">{product.description}</p>
|
||||
<p className="text-spa-stone leading-relaxed">{product.description}</p>
|
||||
)}
|
||||
|
||||
{/* Price */}
|
||||
<div className="flex items-baseline gap-3">
|
||||
<span className="text-4xl font-bold text-cyan-glow">
|
||||
<span className="text-4xl font-bold text-rose-500">
|
||||
{formatPrice(currentVariant?.price)}
|
||||
</span>
|
||||
{currentVariant && (
|
||||
<span className="text-slate-400 text-sm">/ {currentVariant.size}</span>
|
||||
<span className="text-spa-stone/60 text-sm font-semibold">/ {currentVariant.size}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Variant Selector */}
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs uppercase tracking-widest text-slate-400 font-semibold">
|
||||
<p className="text-xs uppercase tracking-widest text-spa-stone/80 font-bold">
|
||||
Select Size
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
@@ -171,14 +171,14 @@ export default function ProductDetailPage() {
|
||||
<button
|
||||
key={v.id}
|
||||
onClick={() => setSelectedVariant(i)}
|
||||
className={`px-5 py-2.5 rounded-lg text-sm font-bold uppercase tracking-widest transition-all duration-300 ${
|
||||
className={`px-5 py-2.5 rounded-lg text-sm font-bold uppercase tracking-widest transition-all duration-300 border ${
|
||||
selectedVariant === i
|
||||
? 'bg-cyan-glow text-night-900 shadow-[0_0_15px_rgba(102,252,241,0.4)]'
|
||||
: 'glass hover:bg-white/10'
|
||||
? 'bg-spa-forest text-white border-spa-forest shadow-md'
|
||||
: 'bg-white text-spa-stone border-spa-stone/20 hover:border-spa-forest hover:text-spa-forest shadow-sm'
|
||||
}`}
|
||||
>
|
||||
{v.size}
|
||||
<span className={`block text-[10px] mt-0.5 ${selectedVariant === i ? 'text-night-800' : 'text-slate-400'}`}>
|
||||
<span className={`block text-[10px] mt-0.5 ${selectedVariant === i ? 'text-white/80' : 'text-spa-stone/60'}`}>
|
||||
{formatPrice(v.price)}
|
||||
</span>
|
||||
</button>
|
||||
@@ -187,14 +187,14 @@ export default function ProductDetailPage() {
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-4">
|
||||
<div className="hidden md:flex gap-4">
|
||||
<button
|
||||
onClick={handleAddToCart}
|
||||
className="flex-1 py-4 bg-white text-night-900 font-bold rounded-lg tracking-widest uppercase hover:bg-cyan-glow hover:shadow-[0_0_25px_rgba(102,252,241,0.4)] transition-all duration-500"
|
||||
className="flex-1 py-4 bg-spa-forest text-white font-bold rounded-lg tracking-widest uppercase hover:bg-spa-stone hover:shadow-lg transition-all duration-300"
|
||||
>
|
||||
Add to Cart
|
||||
</button>
|
||||
<button className="glass p-4 rounded-lg hover:bg-white/10 transition-all">
|
||||
<button className="bg-white border border-spa-stone/20 text-spa-stone p-4 rounded-lg hover:border-spa-forest hover:text-spa-forest shadow-sm transition-all">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
|
||||
</svg>
|
||||
@@ -202,16 +202,16 @@ export default function ProductDetailPage() {
|
||||
</div>
|
||||
|
||||
{/* Meta */}
|
||||
<div className="border-t border-white/5 pt-6 grid grid-cols-2 gap-4 text-sm text-slate-400">
|
||||
<div className="border-t border-spa-stone/10 pt-6 grid grid-cols-2 gap-4 text-sm text-spa-stone/80">
|
||||
<div>
|
||||
<span className="text-xs uppercase tracking-widest block mb-1">Scent</span>
|
||||
<span className="text-white font-medium">{product.scent.name}</span>
|
||||
<span className="text-xs uppercase tracking-widest block mb-1 font-semibold text-spa-stone/60">Scent</span>
|
||||
<span className="text-pop-black font-bold">{product.scent.name}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-xs uppercase tracking-widest block mb-1">Category</span>
|
||||
<span className="text-xs uppercase tracking-widest block mb-1 font-semibold text-spa-stone/60">Category</span>
|
||||
<Link
|
||||
to={`/category/${product.category.id}`}
|
||||
className="text-white font-medium hover:text-cyan-glow transition-colors"
|
||||
className="text-pop-black font-bold hover:text-spa-forest transition-colors"
|
||||
>
|
||||
{product.category.name}
|
||||
</Link>
|
||||
@@ -222,10 +222,10 @@ export default function ProductDetailPage() {
|
||||
|
||||
{/* Full Details Section */}
|
||||
{product.details && (
|
||||
<div className="pt-16 border-t border-white/5 space-y-6">
|
||||
<h2 className="text-3xl font-bold">Product Details</h2>
|
||||
<div className="pt-16 border-t border-spa-stone/10 space-y-6">
|
||||
<h2 className="text-3xl font-bold text-pop-black">Product Details</h2>
|
||||
<div
|
||||
className="prose prose-invert prose-lg max-w-4xl product-editorial"
|
||||
className="prose prose-spa prose-lg max-w-4xl product-editorial text-spa-stone"
|
||||
dangerouslySetInnerHTML={{ __html: product.details }}
|
||||
/>
|
||||
</div>
|
||||
@@ -233,17 +233,17 @@ export default function ProductDetailPage() {
|
||||
|
||||
{/* Related Products */}
|
||||
{related.length > 0 && (
|
||||
<div className="space-y-8 border-t border-white/5 pt-16">
|
||||
<div className="space-y-8 border-t border-spa-stone/10 pt-16">
|
||||
<div className="flex justify-between items-end">
|
||||
<div>
|
||||
<p className="text-cyan-glow uppercase tracking-[0.3em] text-xs font-semibold opacity-70 mb-2">
|
||||
<p className="text-spa-forest uppercase tracking-[0.3em] text-xs font-bold opacity-80 mb-2">
|
||||
More {product.category.name}
|
||||
</p>
|
||||
<h2 className="text-3xl font-bold">Related Products</h2>
|
||||
<h2 className="text-3xl font-bold text-pop-black">Related Products</h2>
|
||||
</div>
|
||||
<Link
|
||||
to={`/category/${product.category.id}`}
|
||||
className="text-cyan-glow text-sm font-bold uppercase tracking-widest hover:underline"
|
||||
className="text-spa-forest text-sm font-bold uppercase tracking-widest hover:underline"
|
||||
>
|
||||
View All →
|
||||
</Link>
|
||||
@@ -260,7 +260,7 @@ export default function ProductDetailPage() {
|
||||
<Footer categories={categories} />
|
||||
|
||||
{/* Mobile Sticky Add to Cart Bar */}
|
||||
<div className="md:hidden fixed bottom-0 left-0 right-0 p-4 bg-night-950/95 backdrop-blur-xl border-t border-white/10 z-40 animate-fade-in flex flex-col gap-3 shadow-[0_-10px_30px_rgba(0,0,0,0.5)]">
|
||||
<div className="md:hidden fixed bottom-0 left-0 right-0 p-4 bg-white/95 backdrop-blur-xl border-t border-spa-stone/10 z-40 animate-fade-in flex flex-col gap-3 shadow-[0_-10px_30px_rgba(0,0,0,0.05)]">
|
||||
|
||||
{/* Mobile Variant Selector */}
|
||||
{product.variants && product.variants.length > 1 && (
|
||||
@@ -271,8 +271,8 @@ export default function ProductDetailPage() {
|
||||
onClick={() => setSelectedVariant(i)}
|
||||
className={`flex-shrink-0 px-4 py-2 rounded-lg text-[10px] font-bold uppercase tracking-widest transition-all border whitespace-nowrap ${
|
||||
selectedVariant === i
|
||||
? 'bg-cyan-glow/10 border-cyan-glow text-cyan-glow'
|
||||
: 'bg-white/5 border-white/10 text-slate-400'
|
||||
? 'bg-spa-forest border-spa-forest text-white'
|
||||
: 'bg-white border-spa-stone/20 text-spa-stone'
|
||||
}`}
|
||||
>
|
||||
{v.size}
|
||||
@@ -282,17 +282,17 @@ export default function ProductDetailPage() {
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] uppercase tracking-widest text-slate-400">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] uppercase tracking-widest text-spa-stone/60 font-semibold">
|
||||
{product.variants && product.variants.length > 1 ? currentVariant?.size : 'Total Price'}
|
||||
</span>
|
||||
<span className="text-xl font-bold text-cyan-glow">
|
||||
<span className="text-xl font-bold text-rose-500">
|
||||
{formatPrice(currentVariant?.price)}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleAddToCart}
|
||||
className="flex-1 py-3.5 bg-cyan-glow text-night-900 font-bold rounded-lg tracking-widest uppercase hover:brightness-110 shadow-[0_0_15px_rgba(102,252,241,0.3)] transition-all flex justify-center items-center gap-2"
|
||||
className="flex-1 py-3.5 bg-spa-forest text-white font-bold rounded-lg tracking-widest uppercase hover:bg-spa-stone shadow-sm transition-all flex justify-center items-center gap-2"
|
||||
>
|
||||
Add to Cart
|
||||
</button>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useSearchParams, Link } from 'react-router-dom'
|
||||
import Navbar from '../components/Navbar'
|
||||
import Footer from '../components/Footer'
|
||||
import ProductCard from '../components/ProductCard'
|
||||
import EmptyState from '../components/EmptyState'
|
||||
import { API_BASE, type Category, type Product } from '../types'
|
||||
|
||||
export default function SearchPage() {
|
||||
@@ -46,20 +47,20 @@ export default function SearchPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<div className="min-h-screen bg-pop-bg">
|
||||
<Navbar categories={categories} />
|
||||
|
||||
{/* Search Header */}
|
||||
<div className="pt-32 pb-16 px-6 bg-night-800/20 border-b border-white/5">
|
||||
<div className="pt-32 pb-16 px-6 bg-spa-light border-b border-spa-stone/10">
|
||||
<div className="max-w-3xl mx-auto space-y-6 text-center">
|
||||
<p className="text-cyan-glow uppercase tracking-[0.4em] text-xs font-semibold opacity-70">
|
||||
<p className="text-spa-forest uppercase tracking-[0.4em] text-xs font-bold opacity-80">
|
||||
Discover
|
||||
</p>
|
||||
<h1 className="text-5xl font-bold">Search Products</h1>
|
||||
<h1 className="text-5xl font-bold text-pop-black">Search Products</h1>
|
||||
|
||||
<form onSubmit={handleSubmit} className="relative">
|
||||
<div className="flex glass border border-white/10 rounded-2xl overflow-hidden focus-within:border-cyan-glow/40 transition-colors">
|
||||
<svg className="w-5 h-5 text-slate-400 m-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<div className="flex bg-white border border-spa-stone/20 rounded-2xl overflow-hidden shadow-sm focus-within:border-spa-forest focus-within:ring-1 focus-within:ring-spa-forest transition-all">
|
||||
<svg className="w-5 h-5 text-spa-stone m-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
<input
|
||||
@@ -67,12 +68,12 @@ export default function SearchPage() {
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
placeholder="Search by scent name, category..."
|
||||
className="bg-transparent border-none outline-none text-base p-4 pl-0 w-full text-white placeholder-slate-500"
|
||||
className="bg-transparent border-none outline-none text-base p-4 pl-0 w-full text-pop-black placeholder-spa-stone/60"
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-white text-night-900 px-8 font-bold text-xs uppercase tracking-widest hover:bg-cyan-glow transition-colors shrink-0"
|
||||
className="bg-spa-forest text-white px-8 font-bold text-xs uppercase tracking-widest hover:bg-spa-stone transition-colors shrink-0"
|
||||
>
|
||||
Search
|
||||
</button>
|
||||
@@ -85,7 +86,7 @@ export default function SearchPage() {
|
||||
<Link
|
||||
key={cat.id}
|
||||
to={`/search?q=${encodeURIComponent(cat.name)}`}
|
||||
className="glass px-4 py-1.5 rounded-full text-xs font-medium hover:border-cyan-glow/30 hover:text-cyan-glow transition-all duration-300"
|
||||
className="bg-white border border-spa-stone/20 px-4 py-1.5 rounded-full text-xs font-medium text-spa-stone hover:border-spa-forest hover:text-spa-forest shadow-sm transition-all duration-300"
|
||||
>
|
||||
{cat.name}
|
||||
</Link>
|
||||
@@ -97,34 +98,32 @@ export default function SearchPage() {
|
||||
{/* Results */}
|
||||
<div className="max-w-7xl mx-auto px-6 py-16 space-y-8">
|
||||
{!searched && !query && (
|
||||
<div className="text-center py-20 space-y-3 text-slate-500">
|
||||
<div className="text-center py-20 space-y-3 text-spa-stone">
|
||||
<p className="text-5xl">🔍</p>
|
||||
<p className="text-lg text-slate-400">Type something to discover our collection</p>
|
||||
<p className="text-lg font-medium text-pop-black">Type something to discover our collection</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{searched && (
|
||||
<p className="text-slate-400 text-sm">
|
||||
<p className="text-spa-stone text-sm font-medium">
|
||||
{loading ? 'Searching...' : `${products.length} result${products.length !== 1 ? 's' : ''} for `}
|
||||
{!loading && <span className="text-white font-semibold">"{query}"</span>}
|
||||
{!loading && <span className="text-pop-black font-bold">"{query}"</span>}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="rounded-2xl bg-white/5 animate-pulse h-96" />
|
||||
<div key={i} className="rounded-2xl bg-spa-stone/10 animate-pulse h-96" />
|
||||
))}
|
||||
</div>
|
||||
) : searched && products.length === 0 ? (
|
||||
<div className="text-center py-20 space-y-4">
|
||||
<p className="text-5xl">🌿</p>
|
||||
<p className="text-xl text-slate-400">No results found</p>
|
||||
<p className="text-slate-500 text-sm">Try a different keyword or browse our categories</p>
|
||||
<Link to="/shop" className="inline-block mt-4 px-8 py-3 bg-white/10 hover:bg-white hover:text-night-900 text-white font-bold rounded-lg uppercase tracking-widest text-xs transition-all duration-300">
|
||||
Browse All Products
|
||||
</Link>
|
||||
</div>
|
||||
<EmptyState
|
||||
title="No results found"
|
||||
message="Try a different keyword or browse our categories"
|
||||
actionText="Browse All Products"
|
||||
actionLink="/shop"
|
||||
/>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{products.map((p) => <ProductCard key={p.id} product={p} />)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Link, useLocation } from 'react-router-dom'
|
||||
import Navbar from '../components/Navbar'
|
||||
import Footer from '../components/Footer'
|
||||
import ProductCard from '../components/ProductCard'
|
||||
import EmptyState from '../components/EmptyState'
|
||||
import { API_BASE, type Category, type Product } from '../types'
|
||||
|
||||
export default function ShopPage() {
|
||||
@@ -99,19 +100,19 @@ export default function ShopPage() {
|
||||
<Navbar categories={categories} />
|
||||
|
||||
{/* Page Header */}
|
||||
<div className="pt-32 pb-12 px-6 text-center space-y-4 border-b border-white/5 bg-night-800/30">
|
||||
<p className="text-cyan-glow uppercase tracking-[0.4em] text-xs font-semibold opacity-70">
|
||||
<div className="pt-32 pb-12 px-6 text-center space-y-4 border-b border-spa-stone/10 bg-spa-light">
|
||||
<p className="text-spa-forest uppercase tracking-[0.4em] text-xs font-bold opacity-80">
|
||||
All Products
|
||||
</p>
|
||||
<h1 className="text-5xl font-bold tracking-tight">The Collection</h1>
|
||||
<p className="text-slate-400 max-w-md mx-auto">
|
||||
<h1 className="text-5xl font-bold tracking-tight text-pop-black">The Collection</h1>
|
||||
<p className="text-spa-stone max-w-md mx-auto">
|
||||
Browse our full range of premium aroma products — crafted to elevate every space.
|
||||
</p>
|
||||
|
||||
{/* Search */}
|
||||
<div className="max-w-md mx-auto mt-6">
|
||||
<div className="flex glass border border-white/10 rounded-xl overflow-hidden">
|
||||
<svg className="w-4 h-4 text-slate-400 m-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<div className="flex bg-white border border-spa-stone/20 rounded-xl overflow-hidden shadow-sm focus-within:border-spa-forest focus-within:ring-1 focus-within:ring-spa-forest transition-all">
|
||||
<svg className="w-4 h-4 text-spa-stone m-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
<input
|
||||
@@ -119,7 +120,7 @@ export default function ShopPage() {
|
||||
placeholder="Search by scent or category..."
|
||||
value={searchInput}
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
className="bg-transparent border-none outline-none text-sm p-2 w-full text-white placeholder-slate-500"
|
||||
className="bg-transparent border-none outline-none text-sm p-2 w-full text-pop-black placeholder-spa-stone/60"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -130,10 +131,10 @@ export default function ShopPage() {
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<button
|
||||
onClick={() => setActiveCategory(null)}
|
||||
className={`px-5 py-2 rounded-full text-xs font-bold uppercase tracking-widest transition-all duration-300 ${
|
||||
className={`px-5 py-2 rounded-full text-xs font-bold uppercase tracking-widest transition-all duration-300 border ${
|
||||
activeCategory === null
|
||||
? 'bg-cyan-glow text-night-900 shadow-[0_0_15px_rgba(102,252,241,0.3)]'
|
||||
: 'glass hover:bg-white/10'
|
||||
? 'bg-spa-forest text-white border-spa-forest shadow-md'
|
||||
: 'bg-white text-spa-stone border-spa-stone/20 hover:border-spa-forest hover:text-spa-forest shadow-sm'
|
||||
}`}
|
||||
>
|
||||
All
|
||||
@@ -142,15 +143,15 @@ export default function ShopPage() {
|
||||
<button
|
||||
key={cat.id}
|
||||
onClick={() => setActiveCategory(cat.id)}
|
||||
className={`px-5 py-2 rounded-full text-xs font-bold uppercase tracking-widest transition-all duration-300 ${
|
||||
className={`px-5 py-2 rounded-full text-xs font-bold uppercase tracking-widest transition-all duration-300 border ${
|
||||
activeCategory === cat.id
|
||||
? 'bg-cyan-glow text-night-900 shadow-[0_0_15px_rgba(102,252,241,0.3)]'
|
||||
: 'glass hover:bg-white/10'
|
||||
? 'bg-spa-forest text-white border-spa-forest shadow-md'
|
||||
: 'bg-white text-spa-stone border-spa-stone/20 hover:border-spa-forest hover:text-spa-forest shadow-sm'
|
||||
}`}
|
||||
>
|
||||
{cat.name}
|
||||
{cat._count && (
|
||||
<span className="ml-2 opacity-60">({cat._count.products})</span>
|
||||
<span className={`ml-2 ${activeCategory === cat.id ? 'opacity-80' : 'opacity-60'}`}>({cat._count.products})</span>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
@@ -159,13 +160,13 @@ export default function ShopPage() {
|
||||
{/* Results count */}
|
||||
{(!loading) && (
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-slate-500 text-sm">
|
||||
<p className="text-spa-stone font-semibold text-sm">
|
||||
{productsLoading ? 'Loading...' : `${totalProducts} products`}
|
||||
</p>
|
||||
{activeCategory && (
|
||||
<Link
|
||||
to={`/category/${activeCategory}`}
|
||||
className="text-cyan-glow text-xs font-bold uppercase tracking-widest hover:underline"
|
||||
className="text-spa-forest text-xs font-bold uppercase tracking-widest hover:underline"
|
||||
>
|
||||
View Category Page →
|
||||
</Link>
|
||||
@@ -177,15 +178,20 @@ export default function ShopPage() {
|
||||
{loading || productsLoading ? (
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 md:gap-8">
|
||||
{Array.from({ length: 9 }).map((_, i) => (
|
||||
<div key={i} className="rounded-2xl bg-white/5 animate-pulse h-96" />
|
||||
<div key={i} className="rounded-2xl bg-spa-stone/10 animate-pulse h-96" />
|
||||
))}
|
||||
</div>
|
||||
) : products.length === 0 ? (
|
||||
<div className="text-center py-24 text-slate-500 space-y-3">
|
||||
<p className="text-4xl">🌿</p>
|
||||
<p className="text-lg">No products found</p>
|
||||
<p className="text-sm">Try adjusting your search or filter</p>
|
||||
</div>
|
||||
<EmptyState
|
||||
title="No products found"
|
||||
message="Try adjusting your search or filter"
|
||||
actionText="Clear Filters"
|
||||
actionClick={() => {
|
||||
setActiveCategory(null);
|
||||
setSearchInput('');
|
||||
setSearchQuery('');
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 md:gap-8">
|
||||
@@ -197,7 +203,7 @@ export default function ShopPage() {
|
||||
{/* Invisible target for IntersectionObserver */}
|
||||
{displayCount < products.length && (
|
||||
<div ref={loaderRef} className="py-12 flex justify-center">
|
||||
<div className="w-8 h-8 rounded-full border-2 border-cyan-glow border-t-transparent animate-spin" />
|
||||
<div className="w-8 h-8 rounded-full border-2 border-spa-forest border-t-transparent animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -64,55 +64,55 @@ export default function TrackOrderPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<div className="min-h-screen flex flex-col bg-pop-bg">
|
||||
<Navbar categories={categories} />
|
||||
|
||||
<main className="flex-1 max-w-4xl w-full mx-auto px-6 flex flex-col justify-center pt-32 pb-24 space-y-12 animate-fade-in">
|
||||
|
||||
{/* Header */}
|
||||
<div className="text-center space-y-4">
|
||||
<div className="w-16 h-16 bg-cyan-glow/10 rounded-full flex items-center justify-center mx-auto border border-cyan-glow/20">
|
||||
<MapPin className="w-8 h-8 text-cyan-glow" />
|
||||
<div className="w-16 h-16 bg-spa-sage/30 rounded-full flex items-center justify-center mx-auto border border-spa-stone/10">
|
||||
<MapPin className="w-8 h-8 text-spa-forest" />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold tracking-tight">Track Your Status</h1>
|
||||
<p className="text-slate-400">Enter your order details below to check shipping status or upload payment slips.</p>
|
||||
<h1 className="text-4xl font-bold tracking-tight text-pop-black">Track Your Status</h1>
|
||||
<p className="text-spa-stone">Enter your order details below to check shipping status or upload payment slips.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-12">
|
||||
|
||||
{/* Tracking Form */}
|
||||
<div className="bg-night-900/50 p-8 rounded-2xl border border-white/5 space-y-6 self-start">
|
||||
<h2 className="text-lg font-bold uppercase tracking-widest border-b border-white/10 pb-4 flex items-center gap-2 text-cyan-glow">
|
||||
<div className="bg-white p-8 rounded-2xl border border-spa-stone/10 space-y-6 self-start shadow-sm">
|
||||
<h2 className="text-lg font-bold uppercase tracking-widest border-b border-spa-stone/10 pb-4 flex items-center gap-2 text-spa-forest">
|
||||
<Search className="w-5 h-5" /> Manual Search
|
||||
</h2>
|
||||
|
||||
<form onSubmit={handleTrackForm} className="space-y-5">
|
||||
{error && (
|
||||
<div className="p-4 bg-rose-500/10 border border-rose-500/20 text-rose-400 rounded-lg text-sm text-center">
|
||||
<div className="p-4 bg-rose-500/10 border border-rose-500/20 text-rose-500 font-medium rounded-lg text-sm text-center">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs uppercase font-bold tracking-widest text-slate-500">Order Number (ID)</label>
|
||||
<label className="text-xs uppercase font-bold tracking-widest text-spa-stone/80">Order Number (ID)</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={orderId}
|
||||
onChange={e => setOrderId(e.target.value)}
|
||||
className="w-full bg-night-950 border border-white/10 rounded-lg p-3.5 text-white focus:border-cyan-glow focus:ring-1 focus:ring-cyan-glow outline-none transition-all placeholder:text-slate-600"
|
||||
className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-3.5 text-pop-black focus:border-spa-forest focus:ring-1 focus:ring-spa-forest outline-none transition-all placeholder:text-spa-stone/50"
|
||||
placeholder="e.g. 1024"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs uppercase font-bold tracking-widest text-slate-500">Phone Number</label>
|
||||
<label className="text-xs uppercase font-bold tracking-widest text-spa-stone/80">Phone Number</label>
|
||||
<input
|
||||
type="tel"
|
||||
required
|
||||
value={phone}
|
||||
onChange={e => setPhone(e.target.value)}
|
||||
className="w-full bg-night-950 border border-white/10 rounded-lg p-3.5 text-white focus:border-cyan-glow focus:ring-1 focus:ring-cyan-glow outline-none transition-all placeholder:text-slate-600"
|
||||
className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-3.5 text-pop-black focus:border-spa-forest focus:ring-1 focus:ring-spa-forest outline-none transition-all placeholder:text-spa-stone/50"
|
||||
placeholder="The phone number used during checkout"
|
||||
/>
|
||||
</div>
|
||||
@@ -120,7 +120,7 @@ export default function TrackOrderPage() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full py-4 bg-white text-night-900 font-bold rounded-lg tracking-widest uppercase hover:bg-cyan-glow hover:shadow-[0_0_20px_rgba(102,252,241,0.3)] transition-all duration-500 disabled:opacity-50 flex items-center justify-center gap-2"
|
||||
className="w-full py-4 bg-spa-forest text-white font-bold rounded-lg tracking-widest uppercase hover:bg-spa-stone hover:shadow-lg transition-all duration-300 disabled:opacity-50 flex items-center justify-center gap-2"
|
||||
>
|
||||
{loading ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Find Order'}
|
||||
</button>
|
||||
@@ -129,14 +129,14 @@ export default function TrackOrderPage() {
|
||||
|
||||
{/* Local Order History */}
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-lg font-bold uppercase tracking-widest border-b border-white/10 pb-4 flex items-center gap-2 text-white">
|
||||
<Package className="w-5 h-5 text-slate-400" /> Recent Device Orders
|
||||
<h2 className="text-lg font-bold uppercase tracking-widest border-b border-spa-stone/10 pb-4 flex items-center gap-2 text-pop-black">
|
||||
<Package className="w-5 h-5 text-spa-stone/60" /> Recent Device Orders
|
||||
</h2>
|
||||
|
||||
{localOrders.length === 0 ? (
|
||||
<div className="text-center py-12 bg-white/5 rounded-2xl border border-dashed border-white/10 space-y-3">
|
||||
<p className="text-slate-500 text-sm">No recent orders found on this device.</p>
|
||||
<p className="text-[10px] uppercase text-slate-600 tracking-widest">Use manual search if you ordered elsewhere</p>
|
||||
<div className="text-center py-12 bg-spa-light rounded-2xl border border-dashed border-spa-stone/20 space-y-3">
|
||||
<p className="text-spa-stone text-sm font-semibold">No recent orders found on this device.</p>
|
||||
<p className="text-[10px] uppercase text-spa-stone/60 tracking-widest font-bold">Use manual search if you ordered elsewhere</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
@@ -144,14 +144,14 @@ export default function TrackOrderPage() {
|
||||
<Link
|
||||
key={id}
|
||||
to={`/order/${id}`}
|
||||
className="flex justify-between items-center p-5 bg-white/5 rounded-xl border border-white/5 hover:bg-white/10 hover:border-cyan-glow/50 transition-all group"
|
||||
className="flex justify-between items-center p-5 bg-white rounded-xl border border-spa-stone/10 shadow-sm hover:border-spa-forest transition-all group"
|
||||
>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 uppercase tracking-widest mb-1">Order</p>
|
||||
<p className="text-xl font-bold font-mono text-cyan-glow">#{id.toString().padStart(4, '0')}</p>
|
||||
<p className="text-xs text-spa-stone/80 uppercase tracking-widest mb-1 font-semibold">Order</p>
|
||||
<p className="text-xl font-bold font-mono text-spa-forest">#{id.toString().padStart(4, '0')}</p>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-full bg-night-950 flex items-center justify-center group-hover:bg-cyan-glow transition-colors">
|
||||
<ChevronRight className="w-5 h-5 text-slate-500 group-hover:text-night-900" />
|
||||
<div className="w-10 h-10 rounded-full bg-spa-light flex items-center justify-center group-hover:bg-spa-forest transition-colors">
|
||||
<ChevronRight className="w-5 h-5 text-spa-stone/80 group-hover:text-white" />
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
Reference in new issue
Block a user