fix(spa): complete checkout flow, UI standardization, and tailwind v4 styling fixes

This commit is contained in:
Your Name committed 2026-06-06 19:10:19 +07:00
1 parent 83df238bab
commit d8bff72e4e
24 files changed
+1169 -402

No files matched your search

+37 -37
View File
@@ -16,81 +16,81 @@ export default function CartDrawer() {
<div className="fixed inset-0 z-[100] flex justify-end">
{/* Backdrop */}
<div
className="absolute inset-0 bg-night-950/80 backdrop-blur-sm animate-fade-in"
className="absolute inset-0 bg-pop-black/60 backdrop-blur-sm animate-fade-in"
onClick={() => toggleCart(false)}
/>
{/* Drawer */}
<div className="relative w-full max-w-md h-full bg-night-900 shadow-2xl flex flex-col border-l border-white/10 animate-slide-left">
<div className="relative w-full max-w-md h-full bg-white shadow-2xl flex flex-col animate-slide-left">
{/* Header */}
<div className="p-6 border-b border-white/10 flex items-center justify-between bg-night-950/50">
<h2 className="text-xl font-bold flex items-center gap-2">
<ShoppingBag className="w-5 h-5 text-cyan-glow" />
<div className="p-6 border-b border-spa-stone/10 flex items-center justify-between bg-spa-light">
<h2 className="text-xl font-bold flex items-center gap-2 text-pop-black">
<ShoppingBag className="w-5 h-5 text-spa-forest" />
{t('cart.title')}
<span className="text-sm font-normal text-slate-400 bg-white/5 px-2 py-0.5 rounded-full ml-2">
<span className="text-sm font-normal text-spa-stone bg-spa-sage px-2 py-0.5 rounded-full ml-2">
{t('cart.items', { count: items.length })}
</span>
</h2>
<button
onClick={() => toggleCart(false)}
className="p-2 hover:bg-white/5 rounded-full transition-colors"
className="p-2 hover:bg-spa-sage rounded-full transition-colors"
>
<X className="w-5 h-5 text-slate-400 hover:text-white" />
<X className="w-5 h-5 text-spa-stone hover:text-pop-black" />
</button>
</div>
{/* Cart Items */}
<div className="flex-1 overflow-y-auto p-6 space-y-4">
{items.length === 0 ? (
<div className="h-full flex flex-col items-center justify-center text-slate-500 space-y-4">
<div className="h-full flex flex-col items-center justify-center text-spa-stone space-y-4">
<ShoppingBag className="w-16 h-16 opacity-20" />
<p>{t('cart.empty')}</p>
<button
onClick={() => toggleCart(false)}
className="text-cyan-glow hover:underline tracking-widest uppercase text-xs font-bold"
className="text-spa-forest hover:underline tracking-widest uppercase text-xs font-bold"
>
{t('cart.continueShopping')}
</button>
</div>
) : (
items.map((item) => (
<div key={item.cartItemId} className="flex gap-4 p-4 rounded-2xl bg-white/5 border border-white/5 group relative">
<div className="w-18 h-22 rounded-lg bg-night-950 overflow-hidden flex-shrink-0" style={{ width: '72px', height: '88px' }}>
<div key={item.cartItemId} className="flex gap-4 p-4 rounded-2xl bg-white border border-spa-stone/10 shadow-sm group relative">
<div className="w-18 h-22 rounded-lg bg-spa-sage/30 overflow-hidden flex-shrink-0" style={{ width: '72px', height: '88px' }}>
<img
src={item.productImage || '/images/oil_ozone.png'}
alt={item.productName}
className="w-full h-full object-cover opacity-80"
className="w-full h-full object-cover"
/>
</div>
<div className="flex-1 flex flex-col pt-1 space-y-1 relative min-w-0">
<h3 className="font-bold text-sm leading-tight pr-6 truncate">{item.productName}</h3>
<h3 className="font-bold text-sm text-spa-forest leading-tight pr-6 truncate">{item.productName}</h3>
{item.variantSize && (
<p className="text-xs text-slate-500">{item.variantSize}</p>
<p className="text-xs text-spa-stone">{item.variantSize}</p>
)}
{/* Price row */}
<div className="flex items-baseline gap-1.5 mt-0.5">
<span className="text-xs text-slate-500">{formatPrice(item.price)}</span>
<span className="text-xs text-slate-600">×</span>
<span className="text-xs text-slate-500">{item.quantity}</span>
<span className="text-xs text-slate-600">=</span>
<span className="text-sm font-bold text-cyan-glow">{formatPrice(item.price * item.quantity)}</span>
<span className="text-xs text-spa-stone/80">{formatPrice(item.price)}</span>
<span className="text-xs text-spa-stone/60">×</span>
<span className="text-xs text-spa-stone/80">{item.quantity}</span>
<span className="text-xs text-spa-stone/60">=</span>
<span className="text-sm font-bold text-rose-500">{formatPrice(item.price * item.quantity)}</span>
</div>
{/* Quantity controls */}
<div className="flex items-center gap-2 mt-1">
<div className="flex items-center gap-2 bg-night-950/70 rounded-lg border border-white/5">
<div className="flex items-center gap-2 bg-spa-sage/50 rounded-lg border border-spa-stone/5">
<button
onClick={() => updateQuantity(item.cartItemId, item.quantity - 1)}
className="p-1.5 hover:text-cyan-glow transition-colors"
className="p-1.5 text-spa-stone hover:text-spa-forest transition-colors"
>
<Minus className="w-3 h-3" />
</button>
<span className="w-5 text-center text-xs font-bold">{item.quantity}</span>
<span className="w-5 text-center text-xs font-bold text-pop-black">{item.quantity}</span>
<button
onClick={() => updateQuantity(item.cartItemId, item.quantity + 1)}
className="p-1.5 hover:text-cyan-glow transition-colors"
className="p-1.5 text-spa-stone hover:text-spa-forest transition-colors"
>
<Plus className="w-3 h-3" />
</button>
@@ -99,7 +99,7 @@ export default function CartDrawer() {
<button
onClick={() => removeFromCart(item.cartItemId)}
className="absolute top-0 right-0 p-1.5 text-slate-600 hover:text-rose-500 transition-colors"
className="absolute top-0 right-0 p-1.5 text-spa-stone/50 hover:text-rose-500 transition-colors"
title="Remove item"
>
<Trash2 className="w-3.5 h-3.5" />
@@ -159,42 +159,42 @@ function CartDrawerFooter({ subtotal, onClose }: { subtotal: number; onClose: ()
return (
<div className="p-5 border-t border-white/10 bg-night-950/60 space-y-4">
<div className="p-5 border-t border-spa-stone/10 bg-spa-light space-y-4">
{/* Totals */}
<div className="space-y-2 text-sm">
<div className="flex justify-between text-slate-400">
<div className="space-y-2 text-sm text-pop-black">
<div className="flex justify-between text-spa-stone">
<span>ราคาสินค้า</span>
<span>{formatPrice(subtotal)}</span>
</div>
{(discountAmount > 0 || isFreeShipping) && savedCoupon && (
<div className="flex justify-between text-cyan-glow">
<div className="flex justify-between text-spa-forest font-medium">
<span className="flex items-center gap-1.5">
ส่วนลดคูปอง
<span className="text-[10px] font-bold bg-cyan-glow/15 px-1.5 py-0.5 rounded tracking-widest">
<span className="text-[10px] font-bold bg-spa-sage px-1.5 py-0.5 rounded tracking-widest text-spa-forest">
{savedCoupon}
</span>
<button onClick={removeCoupon} className="text-slate-500 hover:text-rose-400 transition-colors" title="ยกเลิก">
<button onClick={removeCoupon} className="text-spa-stone/50 hover:text-rose-500 transition-colors" title="ยกเลิก">
<X className="w-3 h-3" />
</button>
</span>
<span>{isFreeShipping ? 'ส่งฟรี' : `- ${formatPrice(discountAmount)}`}</span>
<span className="text-rose-500">{isFreeShipping ? 'ส่งฟรี' : `- ${formatPrice(discountAmount)}`}</span>
</div>
)}
{savedCoupon && !couponValid && (
<div className="flex justify-between text-amber-400 text-xs">
<div className="flex justify-between text-amber-600 text-xs font-medium">
<span className="flex items-center gap-1.5">
<Tag className="w-3 h-3" />
คูปอง <span className="font-bold tracking-widest">{savedCoupon}</span> (ยังไม่ถึงยอดขั้นต่ำ)
<button onClick={removeCoupon} className="text-slate-500 hover:text-rose-400 transition-colors">
<button onClick={removeCoupon} className="text-spa-stone/50 hover:text-rose-500 transition-colors">
<X className="w-3 h-3" />
</button>
</span>
</div>
)}
<div className="flex justify-between font-bold text-base pt-2 border-t border-white/10">
<div className="flex justify-between font-bold text-base pt-2 border-t border-spa-stone/10 text-pop-black">
<span>รวมสุทธิ</span>
<span className="text-cyan-glow">{formatPrice(Math.max(0, subtotal - discountAmount))}</span>
<span className="text-rose-500">{formatPrice(Math.max(0, subtotal - discountAmount))}</span>
</div>
</div>
@@ -202,7 +202,7 @@ function CartDrawerFooter({ subtotal, onClose }: { subtotal: number; onClose: ()
<Link
to="/checkout"
onClick={onClose}
className="flex items-center justify-center gap-2 w-full py-4 bg-white text-night-900 font-bold rounded-xl uppercase tracking-widest text-sm hover:bg-cyan-glow hover:shadow-[0_0_25px_rgba(102,252,241,0.4)] transition-all duration-500 group"
className="flex items-center justify-center gap-2 w-full py-4 bg-spa-forest text-white font-bold rounded-xl uppercase tracking-widest text-sm hover:bg-spa-stone hover:shadow-lg transition-all duration-300 group"
>
<ShoppingBag className="w-4 h-4" />
ดำเนินการชำระเงิน
+40
View File
@@ -0,0 +1,40 @@
import { PackageOpen } from 'lucide-react'
import { Link } from 'react-router-dom'
interface EmptyStateProps {
title?: string
message?: string
actionText?: string
actionLink?: string
actionClick?: () => void
}
export default function EmptyState({
title = 'Nothing found',
message = 'We could not find any items here.',
actionText = 'Browse Shop',
actionLink,
actionClick
}: EmptyStateProps) {
const ButtonContent = () => (
<span className="inline-block mt-6 px-8 py-3 bg-spa-forest text-white rounded-xl text-sm font-bold uppercase tracking-widest hover:bg-spa-stone hover:shadow-lg transition-all cursor-pointer">
{actionText}
</span>
)
return (
<div className="flex flex-col items-center justify-center space-y-4 text-center p-12 bg-white border border-spa-stone/10 rounded-2xl shadow-sm max-w-lg mx-auto my-8">
<div className="w-16 h-16 bg-spa-sage/30 rounded-full flex items-center justify-center mb-2">
<PackageOpen className="w-8 h-8 text-spa-forest" />
</div>
<h3 className="text-xl font-bold text-pop-black">{title}</h3>
<p className="text-spa-stone">{message}</p>
{actionClick ? (
<div onClick={actionClick}><ButtonContent /></div>
) : actionLink ? (
<Link to={actionLink}><ButtonContent /></Link>
) : null}
</div>
)
}
+41
View File
@@ -0,0 +1,41 @@
import { AlertCircle } from 'lucide-react'
import { Link } from 'react-router-dom'
interface ErrorStateProps {
message?: string
fullScreen?: boolean
showHomeButton?: boolean
}
export default function ErrorState({
message = 'Something went wrong',
fullScreen = false,
showHomeButton = true
}: ErrorStateProps) {
const content = (
<div className="flex flex-col items-center justify-center space-y-4 text-center p-8 bg-white border border-rose-500/20 rounded-2xl shadow-sm max-w-md mx-auto">
<div className="w-12 h-12 bg-rose-500/10 rounded-full flex items-center justify-center">
<AlertCircle className="w-6 h-6 text-rose-500" />
</div>
<p className="text-pop-black font-semibold">{message}</p>
{showHomeButton && (
<Link
to="/"
className="mt-4 px-6 py-2 bg-spa-forest text-white rounded-lg text-sm font-bold uppercase tracking-widest hover:bg-spa-stone transition-colors"
>
Return Home
</Link>
)}
</div>
)
if (fullScreen) {
return (
<div className="min-h-[60vh] flex items-center justify-center bg-pop-bg px-4">
{content}
</div>
)
}
return content
}
+25
View File
@@ -0,0 +1,25 @@
import { Loader2 } from 'lucide-react'
interface LoadingStateProps {
message?: string
fullScreen?: boolean
}
export default function LoadingState({ message = 'Loading...', fullScreen = false }: LoadingStateProps) {
const content = (
<div className="flex flex-col items-center justify-center space-y-4 text-spa-stone p-8">
<Loader2 className="w-8 h-8 animate-spin text-spa-forest" />
<p className="text-sm font-semibold tracking-widest uppercase">{message}</p>
</div>
)
if (fullScreen) {
return (
<div className="min-h-[60vh] flex items-center justify-center bg-pop-bg">
{content}
</div>
)
}
return content
}
+61 -61
View File
@@ -1,7 +1,7 @@
import { Link, NavLink, useNavigate } from 'react-router-dom'
import { useState, useEffect } from 'react'
import type { Category } from '../types'
import { ShoppingBag, MapPin } from 'lucide-react'
import { ShoppingBag, MapPin, Menu as MenuIcon, X } from 'lucide-react'
import { useCartStore } from '../store/cartStore'
import { useAuthStore } from '../store/authStore'
import { logoutLiff, loginLiff } from '../lib/liff'
@@ -63,19 +63,19 @@ export default function Navbar({ categories = [] }: NavbarProps) {
}
const navLinkClass = ({ isActive }: { isActive: boolean }) =>
isActive ? 'text-cyan-glow' : 'text-slate-300 hover:text-white transition-colors'
isActive ? 'text-spa-forest font-semibold' : 'text-spa-stone hover:text-spa-forest transition-colors'
return (
<>
{/* ── NAVBAR BAR ── */}
<nav
className={`fixed top-0 w-full z-50 transition-all duration-300 px-4 md:px-6 py-3.5 md:py-4 flex justify-between items-center ${
scrolled ? 'bg-night-900/95 backdrop-blur-xl border-b border-white/10 shadow-xl' : 'glass backdrop-blur-xl border-b border-white/5'
className={`fixed top-0 w-full z-50 transition-all duration-300 px-4 md:px-6 py-3.5 md:py-4 flex justify-between items-center bg-white/80 backdrop-blur-xl border-b border-black/5 ${
scrolled ? 'shadow-lg shadow-black/5' : 'shadow-none'
}`}
>
{/* Logo */}
<Link to="/" onClick={() => setMobileOpen(false)} className="text-xl md:text-2xl font-bold tracking-widest text-white uppercase italic shrink-0">
HomAroma <span className="hidden md:inline text-cyan-glow text-xs md:text-sm not-italic font-light opacity-60 ml-1">Essential</span>
<Link to="/" onClick={() => setMobileOpen(false)} className="text-xl md:text-2xl font-bold tracking-widest text-spa-forest uppercase italic shrink-0">
HomAroma <span className="hidden md:inline text-spa-stone text-xs md:text-sm not-italic font-light ml-1">Essential</span>
</Link>
{/* ── DESKTOP NAV ── */}
@@ -87,19 +87,19 @@ export default function Navbar({ categories = [] }: NavbarProps) {
{/* Collections dropdown */}
{categories.length > 0 && (
<div className="relative group">
<button className="text-slate-300 hover:text-white transition-colors flex items-center gap-1 uppercase text-sm font-medium tracking-wide">
<button className="text-spa-stone hover:text-spa-forest transition-colors flex items-center gap-1 uppercase text-sm font-medium tracking-wide">
{t('nav.collections')}
<svg className="w-3 h-3 opacity-50 transition-transform duration-300 group-hover:rotate-180" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
<div className="absolute top-full left-1/2 -translate-x-1/2 mt-4 w-52 bg-night-900/95 backdrop-blur-xl border border-white/10 rounded-xl overflow-hidden opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-all duration-300 shadow-2xl">
<div className="absolute top-full left-1/2 -translate-x-1/2 mt-4 w-52 bg-white/95 backdrop-blur-xl border border-black/5 rounded-xl overflow-hidden opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-all duration-300 shadow-2xl">
<div className="p-2 space-y-0.5">
{categories.map((cat) => (
<Link
key={cat.id}
to={`/category/${cat.id}`}
className="flex items-center justify-between px-4 py-2.5 text-xs uppercase tracking-widest rounded-lg hover:bg-white/10 hover:text-cyan-glow transition-colors"
className="flex items-center justify-between px-4 py-2.5 text-xs uppercase tracking-widest text-spa-stone rounded-lg hover:bg-spa-sage hover:text-spa-forest transition-colors"
>
{cat.name}
<svg className="w-3 h-3 opacity-30" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@@ -120,11 +120,11 @@ export default function Navbar({ categories = [] }: NavbarProps) {
<div className="hidden md:flex items-center gap-3">
<Link
to="/track"
className="relative glass p-2.5 rounded-full hover:bg-white/10 transition-all group"
className="relative p-2.5 rounded-full text-spa-stone hover:bg-spa-sage transition-all group"
aria-label="Track Orders"
title="Track Orders"
>
<MapPin className="w-4 h-4 group-hover:text-cyan-glow transition-colors" />
<MapPin className="w-4 h-4 group-hover:text-spa-forest transition-colors" />
{localOrdersCount > 0 && (
<span className="absolute top-0 right-0 w-2 h-2 bg-rose-500 rounded-full animate-pulse" />
)}
@@ -132,36 +132,36 @@ export default function Navbar({ categories = [] }: NavbarProps) {
<button
onClick={() => setSearchOpen(true)}
className="glass p-2.5 rounded-full hover:bg-white/10 transition-all group"
className="p-2.5 rounded-full text-spa-stone hover:bg-spa-sage transition-all group"
aria-label="Search"
>
<svg className="w-4 h-4 group-hover:text-cyan-glow transition-colors" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<svg className="w-4 h-4 group-hover:text-spa-forest transition-colors" 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>
</button>
<button
onClick={() => toggleCart(true)}
className="relative glass p-2.5 rounded-full hover:bg-white/10 transition-all group"
className="relative p-2.5 rounded-full text-spa-stone hover:bg-spa-sage transition-all group"
aria-label="Cart"
>
<ShoppingBag className="w-4 h-4 group-hover:text-cyan-glow transition-colors" />
<ShoppingBag className="w-4 h-4 group-hover:text-spa-forest transition-colors" />
{cartCount > 0 && (
<span className="absolute -top-1 -right-1 bg-cyan-glow text-night-900 text-[9px] font-bold w-4 h-4 rounded-full flex items-center justify-center">
<span className="absolute -top-1 -right-1 bg-rose-500 text-white text-[9px] font-bold w-4 h-4 rounded-full flex items-center justify-center">
{cartCount}
</span>
)}
</button>
<Link
to="/shop"
className="glass px-4 py-2 rounded-full text-xs font-bold uppercase tracking-widest hover:bg-cyan-glow hover:text-night-900 transition-all duration-300"
className="px-4 py-2 rounded-full text-xs font-bold uppercase tracking-widest bg-spa-forest text-white hover:bg-spa-stone transition-all duration-300 shadow-sm"
>
{t('nav.shopNow')}
</Link>
<button
onClick={toggleLanguage}
className="glass px-3 py-2 rounded-full text-xs font-bold hover:bg-white/10 transition-all text-cyan-glow border border-cyan-glow/20"
className="px-3 py-2 rounded-full text-xs font-bold transition-all text-spa-forest border border-spa-forest/30 hover:bg-spa-sage bg-white"
>
{i18n.language.startsWith('en') ? 'EN' : 'TH'}
</button>
@@ -171,17 +171,17 @@ export default function Navbar({ categories = [] }: NavbarProps) {
<img
src={customer.pictureUrl || `https://ui-avatars.com/api/?name=${customer.displayName}`}
alt="Profile"
className="w-9 h-9 rounded-full border-2 border-cyan-glow/50 cursor-pointer object-cover"
className="w-9 h-9 rounded-full border-2 border-spa-forest/30 cursor-pointer object-cover"
/>
<div className="absolute top-full right-0 mt-4 w-32 bg-night-900/95 backdrop-blur-xl border border-white/10 rounded-xl overflow-hidden opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-all duration-300 shadow-2xl">
<div className="absolute top-full right-0 mt-4 w-32 bg-white/95 backdrop-blur-xl border border-spa-stone/10 rounded-xl overflow-hidden opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-all duration-300 shadow-xl">
<div className="p-1">
<div className="px-3 py-2 border-b border-white/10 mb-1">
<p className="text-[10px] text-slate-400 uppercase tracking-widest leading-tight truncate">Signed in as</p>
<p className="text-xs text-white font-bold truncate">{customer.displayName}</p>
<div className="px-3 py-2 border-b border-spa-stone/10 mb-1">
<p className="text-[10px] text-spa-stone/60 uppercase tracking-widest leading-tight truncate">Signed in as</p>
<p className="text-xs text-spa-forest font-bold truncate">{customer.displayName}</p>
</div>
<button
onClick={logoutLiff}
className="w-full text-left px-3 py-2 text-xs uppercase tracking-widest text-rose-400 hover:text-rose-300 hover:bg-rose-500/10 rounded-lg transition-colors"
className="w-full text-left px-3 py-2 text-xs uppercase tracking-widest text-rose-500 hover:text-rose-600 hover:bg-rose-50 rounded-lg transition-colors"
>
Logout
</button>
@@ -191,7 +191,7 @@ export default function Navbar({ categories = [] }: NavbarProps) {
) : (
<button
onClick={loginLiff}
className="glass px-4 py-2 rounded-full text-xs font-bold uppercase tracking-widest hover:bg-[#00B900] hover:text-white transition-all duration-300 border border-[#00B900]/50 text-[#00B900]"
className="px-4 py-2 rounded-full text-xs font-bold uppercase tracking-widest bg-[#00B900] text-white hover:bg-[#009900] transition-all duration-300 shadow-sm"
>
LINE Login
</button>
@@ -206,13 +206,13 @@ export default function Navbar({ categories = [] }: NavbarProps) {
src={customer.pictureUrl || `https://ui-avatars.com/api/?name=${customer.displayName}`}
alt="Profile"
onClick={() => setMobileOpen(true)}
className="w-7 h-7 rounded-full border-2 border-cyan-glow/50 object-cover cursor-pointer"
className="w-7 h-7 rounded-full border-2 border-spa-forest/30 object-cover cursor-pointer"
/>
</div>
) : (
<button
onClick={loginLiff}
className="p-1.5 rounded-full glass hover:bg-[#00B900]/20 transition-all border border-[#00B900]/50 text-[#00B900]"
className="p-1.5 rounded-full text-[#00B900] hover:bg-[#00B900]/10 transition-all border border-[#00B900]/30 bg-white"
aria-label="LINE Login"
>
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
@@ -224,7 +224,7 @@ export default function Navbar({ categories = [] }: NavbarProps) {
<Link
to="/track"
onClick={() => setMobileOpen(false)}
className="relative p-1.5 rounded-full glass hover:bg-white/10 transition-all"
className="relative p-1.5 rounded-full text-spa-stone hover:bg-spa-sage transition-all"
aria-label="Track Orders"
>
<MapPin className="w-4 h-4" />
@@ -234,7 +234,7 @@ export default function Navbar({ categories = [] }: NavbarProps) {
</Link>
<button
onClick={() => { setSearchOpen(true); setMobileOpen(false) }}
className="p-1.5 rounded-full glass hover:bg-white/10 transition-all"
className="p-1.5 rounded-full text-spa-stone hover:bg-spa-sage transition-all"
aria-label="Search"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@@ -244,12 +244,12 @@ export default function Navbar({ categories = [] }: NavbarProps) {
<button
onClick={() => { toggleCart(true); setMobileOpen(false) }}
className="relative p-1.5 rounded-full glass hover:bg-white/10 transition-all"
className="relative p-1.5 rounded-full text-spa-stone hover:bg-spa-sage transition-all"
aria-label="Cart"
>
<ShoppingBag className="w-4 h-4" />
{cartCount > 0 && (
<span className="absolute -top-1 -right-1 bg-cyan-glow text-night-900 text-[9px] font-bold w-4 h-4 rounded-full flex items-center justify-center">
<span className="absolute -top-1 -right-1 bg-rose-500 text-white text-[9px] font-bold w-4 h-4 rounded-full flex items-center justify-center">
{cartCount}
</span>
)}
@@ -257,14 +257,14 @@ export default function Navbar({ categories = [] }: NavbarProps) {
{/* Hamburger */}
<button
onClick={() => setMobileOpen(prev => !prev)}
className="p-1.5 rounded-full glass hover:bg-white/10 transition-all"
className="p-1.5 rounded-full text-spa-stone hover:bg-spa-sage transition-all"
aria-label={mobileOpen ? 'Close menu' : 'Open menu'}
>
<div className="w-4 h-4 flex flex-col justify-center gap-[4px] relative">
<span className={`block w-4 h-0.5 bg-white rounded-full transition-all duration-300 ${mobileOpen ? 'absolute rotate-45' : ''}`} />
<span className={`block h-0.5 bg-white rounded-full transition-all duration-300 ${mobileOpen ? 'opacity-0 w-0' : 'w-3'}`} />
<span className={`block h-0.5 bg-white rounded-full transition-all duration-300 ${mobileOpen ? 'absolute -rotate-45 w-4' : 'w-4'}`} />
</div>
{mobileOpen ? (
<X className="w-5 h-5" />
) : (
<MenuIcon className="w-5 h-5" />
)}
</button>
</div>
</nav>
@@ -275,24 +275,24 @@ export default function Navbar({ categories = [] }: NavbarProps) {
>
{/* Backdrop */}
<div
className={`absolute inset-0 bg-night-900/80 backdrop-blur-sm transition-opacity duration-300 ${mobileOpen ? 'opacity-100' : 'opacity-0'}`}
className={`absolute inset-0 bg-spa-stone/40 backdrop-blur-sm transition-opacity duration-300 ${mobileOpen ? 'opacity-100' : 'opacity-0'}`}
onClick={() => setMobileOpen(false)}
/>
{/* Drawer */}
<div
className={`absolute top-0 right-0 h-full w-72 bg-night-900/98 backdrop-blur-2xl border-l border-white/10 flex flex-col transition-transform duration-300 ${mobileOpen ? 'translate-x-0' : 'translate-x-full'}`}
className={`absolute top-0 right-0 h-full w-72 bg-white/98 backdrop-blur-2xl border-l border-spa-stone/10 flex flex-col transition-transform duration-300 shadow-2xl ${mobileOpen ? 'translate-x-0' : 'translate-x-full'}`}
>
{/* Drawer Header */}
<div className="flex items-center justify-between px-6 py-5 border-b border-white/5">
<span className="text-sm font-bold uppercase tracking-widest text-slate-300">{t('nav.menu')}</span>
<div className="flex items-center justify-between px-6 py-5 border-b border-spa-stone/10">
<span className="text-sm font-bold uppercase tracking-widest text-spa-forest">{t('nav.menu')}</span>
<div className="flex items-center gap-3">
{customer && (
<img src={customer.pictureUrl || ''} className="w-6 h-6 rounded-full border border-cyan-glow/50" />
<img src={customer.pictureUrl || ''} className="w-6 h-6 rounded-full border-2 border-spa-forest/30" />
)}
<button onClick={toggleLanguage} className="glass px-2.5 py-1 rounded-md text-[10px] font-bold text-cyan-glow border border-cyan-glow/20 uppercase">
<button onClick={toggleLanguage} className="px-2.5 py-1 rounded-md text-[10px] font-bold text-spa-forest border border-spa-forest/30 uppercase bg-spa-sage">
{i18n.language.startsWith('en') ? 'EN' : 'TH'}
</button>
<button onClick={() => setMobileOpen(false)} className="p-1.5 rounded-full glass hover:bg-white/10 transition-all">
<button onClick={() => setMobileOpen(false)} className="p-1.5 rounded-full hover:bg-spa-sage transition-all text-spa-stone">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
@@ -318,7 +318,7 @@ export default function Navbar({ categories = [] }: NavbarProps) {
onClick={() => setMobileOpen(false)}
className={({ isActive }) =>
`block px-4 py-3 rounded-xl text-sm font-medium tracking-wide transition-all duration-200 ${
isActive ? 'bg-cyan-glow/10 text-cyan-glow border border-cyan-glow/20' : 'hover:bg-white/5 text-slate-300 hover:text-white'
isActive ? 'bg-spa-forest text-white' : 'text-spa-stone hover:bg-spa-sage hover:text-spa-forest'
}`
}
>
@@ -329,16 +329,16 @@ export default function Navbar({ categories = [] }: NavbarProps) {
{/* Collections sub-section */}
{categories.length > 0 && (
<div className="pt-4">
<p className="text-[10px] uppercase tracking-widest text-slate-600 px-4 pb-2">{t('nav.collections')}</p>
<p className="text-[10px] uppercase tracking-widest text-spa-stone/60 px-4 pb-2 font-semibold">{t('nav.collections')}</p>
{categories.map(cat => (
<Link
key={cat.id}
to={`/category/${cat.id}`}
onClick={() => setMobileOpen(false)}
className="flex items-center justify-between px-4 py-2.5 rounded-xl text-sm text-slate-400 hover:text-white hover:bg-white/5 transition-all"
className="flex items-center justify-between px-4 py-2.5 rounded-xl text-sm text-spa-stone hover:text-spa-forest hover:bg-spa-sage transition-all"
>
{cat.name}
<svg className="w-3 h-3 opacity-30" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<svg className="w-3 h-3 text-spa-stone/40" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</Link>
@@ -348,18 +348,18 @@ export default function Navbar({ categories = [] }: NavbarProps) {
</div>
{/* Drawer Footer CTA */}
<div className="px-4 pb-8 pt-4 border-t border-white/5 space-y-3">
<div className="px-4 pb-8 pt-4 border-t border-spa-stone/10 space-y-3">
<Link
to="/shop"
onClick={() => setMobileOpen(false)}
className="block w-full py-3 bg-white text-night-900 font-bold text-center rounded-xl text-xs uppercase tracking-widest hover:bg-cyan-glow transition-colors duration-300"
className="block w-full py-3 bg-spa-forest text-white font-bold text-center rounded-xl text-xs uppercase tracking-widest hover:bg-spa-stone transition-colors duration-300"
>
{t('nav.shopNow')}
</Link>
<Link
to="/scent-guide"
onClick={() => setMobileOpen(false)}
className="block w-full py-3 glass text-center rounded-xl text-xs font-bold uppercase tracking-widest hover:bg-white/10 transition-colors border border-white/10"
className="block w-full py-3 text-center rounded-xl text-xs font-bold uppercase tracking-widest hover:bg-spa-sage transition-colors border-2 border-spa-forest text-spa-forest"
>
{t('nav.findMyScent')}
</Link>
@@ -386,13 +386,13 @@ export default function Navbar({ categories = [] }: NavbarProps) {
{/* ── SEARCH OVERLAY ── */}
{searchOpen && (
<div
className="fixed inset-0 z-[100] bg-night-900/95 backdrop-blur-xl flex flex-col items-center pt-24 md:pt-32 px-4 md:px-6"
className="fixed inset-0 z-[100] bg-spa-light/97 backdrop-blur-xl flex flex-col items-center pt-24 md:pt-32 px-4 md:px-6"
onClick={(e) => { if (e.target === e.currentTarget) setSearchOpen(false) }}
>
<div className="w-full max-w-2xl space-y-6 animate-fade-in">
<div className="flex justify-between items-center">
<p className="text-slate-400 text-xs uppercase tracking-widest">{t('nav.search.title')}</p>
<button onClick={() => setSearchOpen(false)} className="glass p-2 rounded-full hover:bg-white/10 transition-all">
<p className="text-spa-stone text-xs uppercase tracking-widest font-semibold">{t('nav.search.title')}</p>
<button onClick={() => setSearchOpen(false)} className="p-2 rounded-full hover:bg-spa-sage transition-all text-spa-stone">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
@@ -400,26 +400,26 @@ export default function Navbar({ categories = [] }: NavbarProps) {
</div>
<form onSubmit={handleSearch}>
<div className="flex glass border border-white/20 focus-within:border-cyan-glow/50 rounded-2xl overflow-hidden 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-2 border-spa-stone/20 focus-within:border-spa-forest rounded-2xl overflow-hidden transition-colors shadow-sm">
<svg className="w-5 h-5 text-spa-stone/50 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
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search scents, categories..."
className="bg-transparent border-none outline-none text-lg md:text-xl p-4 pl-0 w-full text-white placeholder-slate-600"
placeholder="ค้นหากลิ่น, หมวดหมู่..."
className="bg-transparent border-none outline-none text-lg md:text-xl p-4 pl-0 w-full text-spa-forest placeholder-spa-stone/40"
autoFocus
/>
<button type="submit" className="bg-cyan-glow text-night-900 px-4 md:px-6 font-bold text-xs uppercase tracking-widest shrink-0 hover:bg-white transition-colors">
<button type="submit" className="bg-spa-forest text-white px-4 md:px-6 font-bold text-xs uppercase tracking-widest shrink-0 hover:bg-spa-stone transition-colors">
Go
</button>
</div>
</form>
<div>
<p className="text-xs text-slate-500 uppercase tracking-widest mb-3">Quick Links</p>
<p className="text-xs text-spa-stone/60 uppercase tracking-widest mb-3 font-semibold">Quick Links</p>
<div className="flex flex-wrap gap-2">
{[
...categories.map(c => ({ label: c.name, to: `/category/${c.id}` })),
@@ -430,7 +430,7 @@ export default function Navbar({ categories = [] }: NavbarProps) {
key={item.to}
to={item.to}
onClick={() => setSearchOpen(false)}
className="glass px-4 py-1.5 rounded-full text-xs font-medium hover:bg-white/10 hover:text-cyan-glow transition-all border border-white/5"
className="px-4 py-1.5 rounded-full text-xs font-medium text-spa-stone hover:bg-spa-forest hover:text-white transition-all border border-spa-stone/20 bg-white"
>
{item.label}
</Link>
+23 -34
View File
@@ -8,56 +8,45 @@ interface ProductCardProps {
export default function ProductCard({ product }: ProductCardProps) {
return (
<div className="glass glass-hover rounded-2xl overflow-hidden flex flex-col group p-1.5 md:p-2">
<div className="h-40 md:h-64 overflow-hidden rounded-xl relative">
<Link
to={`/products/${product.id}`}
className="bg-white shadow-sm border border-spa-stone/10 rounded-2xl overflow-hidden flex flex-col group p-2 transition-all duration-300 hover:shadow-lg hover:border-spa-forest/30"
>
<div className="h-48 md:h-64 overflow-hidden rounded-xl relative bg-spa-sage/20">
<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}
/>
{/* Category badge */}
<div className="absolute top-4 left-4 glass px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-widest text-cyan-glow">
<div className="absolute top-3 left-3 bg-white/90 backdrop-blur-sm px-2.5 py-1 rounded-md text-[10px] font-bold uppercase tracking-widest text-spa-forest shadow-sm">
{product.category.name}
</div>
{/* Available badge */}
<div className="hidden md:block absolute top-4 right-4 glass px-3 py-1 rounded-full text-[10px] font-bold uppercase tracking-widest">
<div className="absolute top-3 right-3 bg-spa-forest/90 backdrop-blur-sm px-2.5 py-1 rounded-md text-[10px] font-bold uppercase tracking-widest text-white shadow-sm">
Available
</div>
</div>
<div className="p-3 md:p-6 space-y-2 md:space-y-4 flex flex-col flex-1">
<div className="flex flex-col md:flex-row md:justify-between md:items-start gap-1">
<div>
<h3 className="text-sm md:text-xl font-bold leading-tight line-clamp-1">
{product.scent.name}
</h3>
<p className="text-[10px] md:text-xs text-slate-400 mt-0.5 md:mt-1 uppercase tracking-widest line-clamp-1">
{product.category.name}
</p>
</div>
<span className="text-cyan-glow font-bold text-xs md:text-lg">
<div className="p-4 flex flex-col flex-1">
<div className="mb-4">
<h3 className="text-base md:text-lg font-bold text-pop-black leading-tight line-clamp-1 group-hover:text-spa-forest transition-colors">
{product.scent.name}
</h3>
<p className="text-[10px] md:text-xs text-spa-stone/60 mt-1 uppercase tracking-widest line-clamp-1 font-semibold">
{product.category.name}
</p>
</div>
<div className="mt-auto flex items-center justify-between">
<span className="text-rose-500 font-black text-sm md:text-lg">
{formatPrice(product.variants[0]?.price)}
</span>
<span className="text-[10px] md:text-xs font-bold text-spa-forest uppercase tracking-widest flex items-center gap-1 group-hover:translate-x-1 transition-transform">
View Details &rarr;
</span>
</div>
{/* Variants (sizes) */}
<div className="hidden md:flex flex-wrap gap-1.5 md:gap-2">
{product.variants.map((v) => (
<div key={v.id} className="glass px-1.5 py-0.5 md:px-2.5 md:py-1 rounded-lg text-[8px] md:text-[10px] uppercase tracking-wider">
{v.size}
</div>
))}
</div>
<div className="flex-1" />
<Link
to={`/products/${product.id}`}
className="block w-full py-2 md:py-3 text-center bg-white/10 hover:bg-white text-white hover:text-night-900 rounded-lg font-bold uppercase tracking-widest text-[10px] md:text-xs transition-all duration-300"
>
Discover
</Link>
</div>
</div>
</Link>
)
}