Rename ssr to website, website to spa, add spa nginx config
This commit is contained in:
1 parent
fc6bf0218e
commit
8514342c2f
103 files changed
+801
-768
No files matched your search
@@ -0,0 +1,146 @@
|
||||
"use client";
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { getCategoryImage, formatPrice, type Product } from '@homaroma/types'
|
||||
import { useCartStore } from '../store/cartStore'
|
||||
|
||||
interface Props {
|
||||
product: Product
|
||||
}
|
||||
|
||||
export default function AddToCartSection({ product }: Props) {
|
||||
const [selectedVariant, setSelectedVariant] = useState(0)
|
||||
const { addToCart, toggleCart } = useCartStore()
|
||||
|
||||
const currentVariant = product.variants[selectedVariant]
|
||||
|
||||
const handleAddToCart = () => {
|
||||
if (!currentVariant) return
|
||||
addToCart({
|
||||
productId: product.id,
|
||||
variantId: currentVariant.id,
|
||||
quantity: 1,
|
||||
price: currentVariant.price || product.price || 0,
|
||||
productName: product.scent.name,
|
||||
variantSize: currentVariant.size,
|
||||
productImage: product.images?.[0]?.url || product.category.imageUrl || getCategoryImage(product.category.name),
|
||||
})
|
||||
toggleCart(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Desktop Info Panel */}
|
||||
<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">
|
||||
{product.category.name}
|
||||
</p>
|
||||
<h1 className="text-5xl font-bold leading-tight">{product.scent.name}</h1>
|
||||
<p className="text-slate-400 text-sm uppercase tracking-widest">Premium Collection</p>
|
||||
</div>
|
||||
|
||||
{product.description && (
|
||||
<p className="text-slate-300 leading-relaxed">{product.description}</p>
|
||||
)}
|
||||
|
||||
{/* Price */}
|
||||
<div className="flex items-baseline gap-3">
|
||||
<span className="text-4xl font-bold text-cyan-glow">
|
||||
{formatPrice(currentVariant?.price)}
|
||||
</span>
|
||||
{currentVariant && (
|
||||
<span className="text-slate-400 text-sm">/ {currentVariant.size}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Variant Selector */}
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs uppercase tracking-widest text-slate-400 font-semibold">Select Size</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{product.variants.map((v, i) => (
|
||||
<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 ${
|
||||
selectedVariant === i
|
||||
? 'bg-cyan-glow text-night-900 shadow-[0_0_15px_rgba(102,252,241,0.4)]'
|
||||
: 'glass hover:bg-white/10'
|
||||
}`}
|
||||
>
|
||||
{v.size}
|
||||
<span className={`block text-[10px] mt-0.5 ${selectedVariant === i ? 'text-night-800' : 'text-slate-400'}`}>
|
||||
{formatPrice(v.price)}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="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"
|
||||
>
|
||||
Add to Cart
|
||||
</button>
|
||||
<button className="glass p-4 rounded-lg hover:bg-white/10 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>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Meta */}
|
||||
<div className="border-t border-white/5 pt-6 grid grid-cols-2 gap-4 text-sm text-slate-400">
|
||||
<div>
|
||||
<span className="text-xs uppercase tracking-widest block mb-1">Scent</span>
|
||||
<span className="text-white font-medium">{product.scent.name}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-xs uppercase tracking-widest block mb-1">Category</span>
|
||||
<Link href={`/category/${product.category.id}`} className="text-white font-medium hover:text-cyan-glow transition-colors">
|
||||
{product.category.name}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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)]">
|
||||
{product.variants.length > 1 && (
|
||||
<div className="flex gap-2 overflow-x-auto pb-1" style={{ scrollbarWidth: 'none' }}>
|
||||
{product.variants.map((v, i) => (
|
||||
<button
|
||||
key={`mobile-v-${v.id}`}
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
{v.size}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[10px] uppercase tracking-widest text-slate-400">
|
||||
{product.variants.length > 1 ? currentVariant?.size : 'Total Price'}
|
||||
</span>
|
||||
<span className="text-xl font-bold text-cyan-glow">{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"
|
||||
>
|
||||
Add to Cart
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
import React from 'react'
|
||||
import { X, ShoppingBag, Plus, Minus, Trash2, Tag, ArrowRight } from 'lucide-react'
|
||||
import { Link } from 'react-router-dom'
|
||||
"use client";
|
||||
import { X, ShoppingBag, Plus, Minus, Trash2 } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useCartStore } from '../store/cartStore'
|
||||
import { formatPrice } from '@homaroma/types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { API_BASE } from '../types'
|
||||
|
||||
export default function CartDrawer() {
|
||||
const { t } = useTranslation()
|
||||
@@ -40,7 +39,7 @@ export default function CartDrawer() {
|
||||
</div>
|
||||
|
||||
{/* Cart Items */}
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-4">
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-6">
|
||||
{items.length === 0 ? (
|
||||
<div className="h-full flex flex-col items-center justify-center text-slate-500 space-y-4">
|
||||
<ShoppingBag className="w-16 h-16 opacity-20" />
|
||||
@@ -55,7 +54,7 @@ export default function CartDrawer() {
|
||||
) : (
|
||||
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 className="w-20 h-24 rounded-lg bg-night-950 overflow-hidden flex-shrink-0">
|
||||
<img
|
||||
src={item.productImage || '/images/oil_ozone.png'}
|
||||
alt={item.productName}
|
||||
@@ -63,46 +62,34 @@ export default function CartDrawer() {
|
||||
/>
|
||||
</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>
|
||||
<div className="flex-1 flex flex-col pt-1 space-y-1 relative">
|
||||
<h3 className="font-bold text-sm leading-tight pr-6">{item.productName}</h3>
|
||||
{item.variantSize && (
|
||||
<p className="text-xs text-slate-500">{item.variantSize}</p>
|
||||
<p className="text-xs text-slate-400">{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>
|
||||
</div>
|
||||
<p className="text-cyan-glow text-sm font-bold mt-auto">{formatPrice(item.price)}</p>
|
||||
|
||||
{/* 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">
|
||||
<button
|
||||
onClick={() => updateQuantity(item.cartItemId, item.quantity - 1)}
|
||||
className="p-1.5 hover:text-cyan-glow transition-colors"
|
||||
>
|
||||
<Minus className="w-3 h-3" />
|
||||
</button>
|
||||
<span className="w-5 text-center text-xs font-bold">{item.quantity}</span>
|
||||
<button
|
||||
onClick={() => updateQuantity(item.cartItemId, item.quantity + 1)}
|
||||
className="p-1.5 hover:text-cyan-glow transition-colors"
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mt-2 bg-night-950/50 rounded-lg w-max border border-white/5">
|
||||
<button
|
||||
onClick={() => updateQuantity(item.cartItemId, item.quantity - 1)}
|
||||
className="p-1 hover:text-cyan-glow transition-colors"
|
||||
>
|
||||
<Minus className="w-4 h-4" />
|
||||
</button>
|
||||
<span className="w-4 text-center text-xs font-bold">{item.quantity}</span>
|
||||
<button
|
||||
onClick={() => updateQuantity(item.cartItemId, item.quantity + 1)}
|
||||
className="p-1 hover:text-cyan-glow transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => removeFromCart(item.cartItemId)}
|
||||
className="absolute top-0 right-0 p-1.5 text-slate-600 hover:text-rose-500 transition-colors"
|
||||
title="Remove item"
|
||||
className="absolute top-1 right-0 p-1 text-slate-500 hover:text-rose-500 transition-colors opacity-0 group-hover:opacity-100"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -112,103 +99,22 @@ export default function CartDrawer() {
|
||||
|
||||
{/* Footer */}
|
||||
{items.length > 0 && (
|
||||
<CartDrawerFooter subtotal={getCartTotal()} onClose={() => toggleCart(false)} t={t} />
|
||||
<div className="p-6 border-t border-white/10 bg-night-950/50 space-y-6">
|
||||
<div className="flex items-center justify-between text-lg font-bold">
|
||||
<span>{t('cart.subtotal')}</span>
|
||||
<span className="text-cyan-glow">{formatPrice(getCartTotal())}</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 text-center">{t('cart.shippingNote')}</p>
|
||||
<Link
|
||||
href="/checkout"
|
||||
onClick={() => toggleCart(false)}
|
||||
className="btn-primary w-full py-4 text-center block uppercase tracking-widest text-sm"
|
||||
>
|
||||
{t('cart.proceed')}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CartDrawerFooter({ subtotal, onClose }: { subtotal: number; onClose: () => void; t?: any }) {
|
||||
const [savedCoupon, setSavedCoupon] = React.useState<string | null>(null)
|
||||
const [discountAmount, setDiscountAmount] = React.useState<number>(0)
|
||||
const [isFreeShipping, setIsFreeShipping] = React.useState<boolean>(false)
|
||||
const [couponValid, setCouponValid] = React.useState<boolean>(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
const code = localStorage.getItem('irelax_active_coupon')
|
||||
setSavedCoupon(code)
|
||||
if (code && subtotal > 0) {
|
||||
fetch(`${API_BASE}/coupons/validate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ code, cartTotal: subtotal })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.valid) {
|
||||
setCouponValid(true)
|
||||
setDiscountAmount(data.discountAmount || 0)
|
||||
setIsFreeShipping(data.isFreeShipping || false)
|
||||
} else {
|
||||
setCouponValid(false)
|
||||
}
|
||||
})
|
||||
.catch(() => setCouponValid(false))
|
||||
}
|
||||
}, [subtotal])
|
||||
|
||||
const removeCoupon = () => {
|
||||
localStorage.removeItem('irelax_active_coupon')
|
||||
setSavedCoupon(null)
|
||||
setDiscountAmount(0)
|
||||
setIsFreeShipping(false)
|
||||
setCouponValid(false)
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="p-5 border-t border-white/10 bg-night-950/60 space-y-4">
|
||||
|
||||
{/* Totals */}
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between text-slate-400">
|
||||
<span>ราคาสินค้า</span>
|
||||
<span>{formatPrice(subtotal)}</span>
|
||||
</div>
|
||||
{(discountAmount > 0 || isFreeShipping) && savedCoupon && (
|
||||
<div className="flex justify-between text-cyan-glow">
|
||||
<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">
|
||||
{savedCoupon}
|
||||
</span>
|
||||
<button onClick={removeCoupon} className="text-slate-500 hover:text-rose-400 transition-colors" title="ยกเลิก">
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</span>
|
||||
<span>{isFreeShipping ? 'ส่งฟรี' : `- ${formatPrice(discountAmount)}`}</span>
|
||||
</div>
|
||||
)}
|
||||
{savedCoupon && !couponValid && (
|
||||
<div className="flex justify-between text-amber-400 text-xs">
|
||||
<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">
|
||||
<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">
|
||||
<span>รวมสุทธิ</span>
|
||||
<span className="text-cyan-glow">{formatPrice(Math.max(0, subtotal - discountAmount))}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Checkout button */}
|
||||
<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"
|
||||
>
|
||||
<ShoppingBag className="w-4 h-4" />
|
||||
ดำเนินการชำระเงิน
|
||||
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
"use client";
|
||||
import '../lib/i18n';
|
||||
import CartDrawer from './CartDrawer';
|
||||
import ScrollToTop from './ScrollToTop';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export default function ClientProviders({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<ScrollToTop />
|
||||
{children}
|
||||
<CartDrawer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import type { Category } from '../types'
|
||||
"use client";
|
||||
import Link from 'next/link'
|
||||
import type { Category } from '@homaroma/types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface FooterProps {
|
||||
@@ -30,12 +31,12 @@ export default function Footer({ categories = [] }: FooterProps) {
|
||||
<div className="space-y-4">
|
||||
<h4 className="font-bold uppercase tracking-widest text-xs text-white">{t('footer.explore')}</h4>
|
||||
<div className="flex flex-col space-y-2 text-sm text-slate-400">
|
||||
<Link to="/" className="hover:text-white transition-colors">{t('nav.home')}</Link>
|
||||
<Link to="/shop" className="hover:text-white transition-colors">{t('nav.shop')}</Link>
|
||||
<Link to="/scent-guide" className="hover:text-white transition-colors">{t('nav.scentGuide')}</Link>
|
||||
<Link to="/about" className="hover:text-white transition-colors">{t('nav.about')}</Link>
|
||||
<Link to="/contact" className="hover:text-white transition-colors">{t('nav.contact')}</Link>
|
||||
<Link to="/faq" className="hover:text-white transition-colors">{t('nav.faq')}</Link>
|
||||
<Link href="/" className="hover:text-white transition-colors">{t('nav.home')}</Link>
|
||||
<Link href="/shop" className="hover:text-white transition-colors">{t('nav.shop')}</Link>
|
||||
<Link href="/scent-guide" className="hover:text-white transition-colors">{t('nav.scentGuide')}</Link>
|
||||
<Link href="/about" className="hover:text-white transition-colors">{t('nav.about')}</Link>
|
||||
<Link href="/contact" className="hover:text-white transition-colors">{t('nav.contact')}</Link>
|
||||
<Link href="/faq" className="hover:text-white transition-colors">{t('nav.faq')}</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -43,7 +44,7 @@ export default function Footer({ categories = [] }: FooterProps) {
|
||||
<h4 className="font-bold uppercase tracking-widest text-xs text-white">{t('nav.collections')}</h4>
|
||||
<div className="flex flex-col space-y-2 text-sm text-slate-400">
|
||||
{categories.map((c) => (
|
||||
<Link key={c.id} to={`/category/${c.id}`} className="hover:text-white transition-colors">
|
||||
<Link key={c.id} href={`/category/${c.id}`} className="hover:text-white transition-colors">
|
||||
{c.name}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Link, NavLink, useNavigate } from 'react-router-dom'
|
||||
"use client";
|
||||
import Link from 'next/link'
|
||||
import { useRouter, usePathname } from 'next/navigation'
|
||||
import { useState, useEffect } from 'react'
|
||||
import type { Category } from '../types'
|
||||
import type { Category } from '@homaroma/types'
|
||||
import { ShoppingBag, MapPin } from 'lucide-react'
|
||||
import { useCartStore } from '../store/cartStore'
|
||||
import { useAuthStore } from '../store/authStore'
|
||||
import { logoutLiff, loginLiff } from '../lib/liff'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface NavbarProps {
|
||||
@@ -17,11 +17,11 @@ export default function Navbar({ categories = [] }: NavbarProps) {
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [scrolled, setScrolled] = useState(false)
|
||||
const [localOrdersCount, setLocalOrdersCount] = useState(0)
|
||||
const navigate = useNavigate()
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
|
||||
const { toggleCart, getCartCount } = useCartStore()
|
||||
const cartCount = getCartCount()
|
||||
const { customer } = useAuthStore()
|
||||
const { t, i18n } = useTranslation()
|
||||
|
||||
const toggleLanguage = () => {
|
||||
@@ -45,7 +45,7 @@ export default function Navbar({ categories = [] }: NavbarProps) {
|
||||
}, [])
|
||||
|
||||
// Close mobile menu on route change
|
||||
useEffect(() => { setMobileOpen(false) }, [navigate])
|
||||
useEffect(() => { setMobileOpen(false) }, [pathname])
|
||||
// Prevent body scroll when overlays open
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = (searchOpen || mobileOpen) ? 'hidden' : ''
|
||||
@@ -55,15 +55,15 @@ export default function Navbar({ categories = [] }: NavbarProps) {
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (searchQuery.trim()) {
|
||||
navigate(`/search?q=${encodeURIComponent(searchQuery.trim())}`)
|
||||
router.push(`/search?q=${encodeURIComponent(searchQuery.trim())}`)
|
||||
setSearchOpen(false)
|
||||
setMobileOpen(false)
|
||||
setSearchQuery('')
|
||||
}
|
||||
}
|
||||
|
||||
const navLinkClass = ({ isActive }: { isActive: boolean }) =>
|
||||
isActive ? 'text-cyan-glow' : 'text-slate-300 hover:text-white transition-colors'
|
||||
const navLinkClass = (path: string) =>
|
||||
pathname === path ? 'text-cyan-glow' : 'text-slate-300 hover:text-white transition-colors'
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -74,15 +74,15 @@ export default function Navbar({ categories = [] }: NavbarProps) {
|
||||
}`}
|
||||
>
|
||||
{/* Logo */}
|
||||
<Link to="/" onClick={() => setMobileOpen(false)} className="text-xl md:text-2xl font-bold tracking-widest text-white uppercase italic shrink-0">
|
||||
iRelax <span className="hidden md:inline text-cyan-glow text-xs md:text-sm not-italic font-light opacity-60 ml-1">Essential</span>
|
||||
<Link href="/" onClick={() => setMobileOpen(false)} className="text-xl md:text-2xl font-bold tracking-widest text-white uppercase italic shrink-0">
|
||||
iRelax <span className="text-cyan-glow text-xs md:text-sm not-italic font-light opacity-60">Essential</span>
|
||||
</Link>
|
||||
|
||||
{/* ── DESKTOP NAV ── */}
|
||||
<div className="hidden md:flex space-x-7 text-sm font-medium tracking-wide uppercase">
|
||||
<NavLink to="/" end className={navLinkClass}>{t('nav.home')}</NavLink>
|
||||
<NavLink to="/shop" className={navLinkClass}>{t('nav.shop')}</NavLink>
|
||||
<NavLink to="/coupons" className={navLinkClass}>🔥 {t('nav.promotions') || 'Promotions'}</NavLink>
|
||||
<Link href="/" className={navLinkClass('/')}>{t('nav.home')}</Link>
|
||||
<Link href="/shop" className={navLinkClass('/shop')}>{t('nav.shop')}</Link>
|
||||
<Link href="/coupons" className={navLinkClass('/coupons')}>🔥 {t('nav.promotions') || 'Promotions'}</Link>
|
||||
|
||||
{/* Collections dropdown */}
|
||||
{categories.length > 0 && (
|
||||
@@ -98,7 +98,7 @@ export default function Navbar({ categories = [] }: NavbarProps) {
|
||||
{categories.map((cat) => (
|
||||
<Link
|
||||
key={cat.id}
|
||||
to={`/category/${cat.id}`}
|
||||
href={`/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"
|
||||
>
|
||||
{cat.name}
|
||||
@@ -112,14 +112,14 @@ export default function Navbar({ categories = [] }: NavbarProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<NavLink to="/scent-guide" className={navLinkClass}>{t('nav.scentGuide')}</NavLink>
|
||||
<NavLink to="/about" className={navLinkClass}>{t('nav.about')}</NavLink>
|
||||
<Link href="/scent-guide" className={navLinkClass('/scent-guide')}>{t('nav.scentGuide')}</Link>
|
||||
<Link href="/about" className={navLinkClass('/about')}>{t('nav.about')}</Link>
|
||||
</div>
|
||||
|
||||
{/* ── DESKTOP RIGHT ACTIONS ── */}
|
||||
<div className="hidden md:flex items-center gap-3">
|
||||
<Link
|
||||
to="/track"
|
||||
href="/track"
|
||||
className="relative glass p-2.5 rounded-full hover:bg-white/10 transition-all group"
|
||||
aria-label="Track Orders"
|
||||
title="Track Orders"
|
||||
@@ -153,7 +153,7 @@ export default function Navbar({ categories = [] }: NavbarProps) {
|
||||
)}
|
||||
</button>
|
||||
<Link
|
||||
to="/shop"
|
||||
href="/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"
|
||||
>
|
||||
{t('nav.shopNow')}
|
||||
@@ -165,91 +165,39 @@ export default function Navbar({ categories = [] }: NavbarProps) {
|
||||
>
|
||||
{i18n.language.startsWith('en') ? 'EN' : 'TH'}
|
||||
</button>
|
||||
|
||||
{customer ? (
|
||||
<div className="relative group flex items-center">
|
||||
<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"
|
||||
/>
|
||||
<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="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>
|
||||
<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"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<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]"
|
||||
>
|
||||
LINE Login
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── MOBILE RIGHT ACTIONS ── */}
|
||||
<div className="flex md:hidden items-center gap-1.5">
|
||||
{customer ? (
|
||||
<div className="relative">
|
||||
<img
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={loginLiff}
|
||||
className="p-1.5 rounded-full glass hover:bg-[#00B900]/20 transition-all border border-[#00B900]/50 text-[#00B900]"
|
||||
aria-label="LINE Login"
|
||||
>
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M24 10.304c0-5.369-5.383-9.738-12-9.738-6.616 0-12 4.369-12 9.738 0 4.814 3.932 8.946 9.539 9.615.375.056 1.155.176 1.321.603.149.387.049.991.023 1.258-.035.342-.165 1.011 1.05 1.503 1.216.492 4.417-2.602 6.012-4.423C21.841 16.208 24 13.524 24 10.304z"/>
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="flex md:hidden items-center gap-2">
|
||||
<Link
|
||||
to="/track"
|
||||
href="/track"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className="relative p-1.5 rounded-full glass hover:bg-white/10 transition-all"
|
||||
className="relative p-2 rounded-full glass hover:bg-white/10 transition-all"
|
||||
aria-label="Track Orders"
|
||||
>
|
||||
<MapPin className="w-4 h-4" />
|
||||
{localOrdersCount > 0 && (
|
||||
<span className="absolute top-0 right-0 w-2 h-2 bg-rose-500 rounded-full" />
|
||||
<span className="absolute top-1 right-1 w-2 h-2 bg-rose-500 rounded-full" />
|
||||
)}
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => { setSearchOpen(true); setMobileOpen(false) }}
|
||||
className="p-1.5 rounded-full glass hover:bg-white/10 transition-all"
|
||||
className="p-2 rounded-full glass hover:bg-white/10 transition-all"
|
||||
aria-label="Search"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<svg className="w-5 h-5" 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); setMobileOpen(false) }}
|
||||
className="relative p-1.5 rounded-full glass hover:bg-white/10 transition-all"
|
||||
className="relative p-2 rounded-full glass hover:bg-white/10 transition-all"
|
||||
aria-label="Cart"
|
||||
>
|
||||
<ShoppingBag className="w-4 h-4" />
|
||||
<ShoppingBag className="w-5 h-5" />
|
||||
{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-0 right-0 bg-cyan-glow text-night-900 text-[9px] font-bold w-4 h-4 rounded-full flex items-center justify-center translate-x-1/4 -translate-y-1/4">
|
||||
{cartCount}
|
||||
</span>
|
||||
)}
|
||||
@@ -257,13 +205,13 @@ 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-2 rounded-full glass hover:bg-white/10 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 className="w-5 h-5 flex flex-col justify-center gap-[5px] relative">
|
||||
<span className={`block w-5 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-4'}`} />
|
||||
<span className={`block h-0.5 bg-white rounded-full transition-all duration-300 ${mobileOpen ? 'absolute -rotate-45 w-5' : 'w-5'}`} />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
@@ -286,9 +234,6 @@ export default function Navbar({ categories = [] }: NavbarProps) {
|
||||
<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 gap-3">
|
||||
{customer && (
|
||||
<img src={customer.pictureUrl || ''} className="w-6 h-6 rounded-full border border-cyan-glow/50" />
|
||||
)}
|
||||
<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">
|
||||
{i18n.language.startsWith('en') ? 'EN' : 'TH'}
|
||||
</button>
|
||||
@@ -303,7 +248,7 @@ export default function Navbar({ categories = [] }: NavbarProps) {
|
||||
{/* Drawer Nav Links */}
|
||||
<div className="flex-1 overflow-y-auto px-4 py-6 space-y-1">
|
||||
{[
|
||||
{ to: '/', label: t('nav.home'), end: true },
|
||||
{ to: '/', label: t('nav.home') },
|
||||
{ to: '/shop', label: t('nav.shop') },
|
||||
{ to: '/coupons', label: `🔥 ${t('nav.promotions') || 'Promotions'}` },
|
||||
{ to: '/scent-guide', label: `✨ ${t('nav.scentGuide')}` },
|
||||
@@ -311,19 +256,18 @@ export default function Navbar({ categories = [] }: NavbarProps) {
|
||||
{ to: '/faq', label: t('nav.faq') },
|
||||
{ to: '/contact', label: t('nav.contact') },
|
||||
].map(item => (
|
||||
<NavLink
|
||||
<Link
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
end={item.end}
|
||||
href={item.to}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className={({ isActive }) =>
|
||||
className={
|
||||
`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'
|
||||
pathname === item.to ? 'bg-cyan-glow/10 text-cyan-glow border border-cyan-glow/20' : 'hover:bg-white/5 text-slate-300 hover:text-white'
|
||||
}`
|
||||
}
|
||||
>
|
||||
{item.label}
|
||||
</NavLink>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
{/* Collections sub-section */}
|
||||
@@ -333,7 +277,7 @@ export default function Navbar({ categories = [] }: NavbarProps) {
|
||||
{categories.map(cat => (
|
||||
<Link
|
||||
key={cat.id}
|
||||
to={`/category/${cat.id}`}
|
||||
href={`/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"
|
||||
>
|
||||
@@ -350,35 +294,19 @@ export default function Navbar({ categories = [] }: NavbarProps) {
|
||||
{/* Drawer Footer CTA */}
|
||||
<div className="px-4 pb-8 pt-4 border-t border-white/5 space-y-3">
|
||||
<Link
|
||||
to="/shop"
|
||||
href="/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"
|
||||
>
|
||||
{t('nav.shopNow')}
|
||||
</Link>
|
||||
<Link
|
||||
to="/scent-guide"
|
||||
href="/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"
|
||||
>
|
||||
{t('nav.findMyScent')}
|
||||
</Link>
|
||||
|
||||
{customer ? (
|
||||
<button
|
||||
onClick={() => { logoutLiff(); setMobileOpen(false); }}
|
||||
className="block w-full py-3 glass text-center rounded-xl text-xs font-bold uppercase tracking-widest text-rose-400 hover:bg-rose-500/10 transition-colors border border-rose-500/20 mt-2"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => { loginLiff(); setMobileOpen(false); }}
|
||||
className="block w-full py-3 glass text-center rounded-xl text-xs font-bold uppercase tracking-widest text-[#00B900] hover:bg-[#00B900]/10 transition-colors border border-[#00B900]/50 mt-2"
|
||||
>
|
||||
Login with LINE
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -428,7 +356,7 @@ export default function Navbar({ categories = [] }: NavbarProps) {
|
||||
].map(item => (
|
||||
<Link
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
href={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"
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import type { Product } from '../types'
|
||||
import { getCategoryImage, formatPrice } from '../types'
|
||||
import Link from 'next/link'
|
||||
import type { Product } from '@homaroma/types'
|
||||
import { getCategoryImage, formatPrice } from '@homaroma/types'
|
||||
|
||||
interface ProductCardProps {
|
||||
product: Product
|
||||
@@ -11,7 +11,7 @@ export default function ProductCard({ product }: ProductCardProps) {
|
||||
<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">
|
||||
<img
|
||||
src={product.images?.[0]?.url || getCategoryImage(product.category.name)}
|
||||
src={product.images?.[0]?.url || product.category.imageUrl || getCategoryImage(product.category.name)}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-700"
|
||||
alt={product.scent.name}
|
||||
/>
|
||||
@@ -52,7 +52,7 @@ export default function ProductCard({ product }: ProductCardProps) {
|
||||
<div className="flex-1" />
|
||||
|
||||
<Link
|
||||
to={`/products/${product.id}`}
|
||||
href={`/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
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"use client";
|
||||
import { useEffect } from 'react'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import { usePathname } from 'next/navigation'
|
||||
|
||||
/**
|
||||
* Scrolls window to top on every route change.
|
||||
* Place this inside <BrowserRouter> so it has access to location context.
|
||||
* Scrolls window to top on every route change (Next.js version).
|
||||
*/
|
||||
export default function ScrollToTop() {
|
||||
const { pathname } = useLocation()
|
||||
const pathname = usePathname()
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo({ top: 0, behavior: 'instant' })
|
||||
|
||||
Reference in new issue
Block a user