Rename ssr to website, website to spa, add spa nginx config

This commit is contained in:
Your Name committed 2026-06-03 09:38:15 +07:00
1 parent fc6bf0218e
commit 8514342c2f
103 files changed
+801 -768

No files matched your search

+44 -116
View File
@@ -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"
>