x
This commit is contained in:
1 parent
7b98d41555
commit
996dfdabd0
10 files changed
+959
-252
No files matched your search
@@ -1,59 +1,155 @@
|
||||
"use client";
|
||||
"use client"
|
||||
|
||||
import Link from 'next/link'
|
||||
import { useRouter, usePathname } from 'next/navigation'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useMemo, useRef } from 'react'
|
||||
import type { Category } from '@homaroma/types'
|
||||
import { Heart, ShoppingBag, MapPin, User } from 'lucide-react'
|
||||
import { Check, Globe, Heart, Menu as MenuIcon, ShoppingBag, User, X } from 'lucide-react'
|
||||
import { useCartStore } from '../store/cartStore'
|
||||
import { useWishlistStore } from '../store/wishlistStore'
|
||||
import { useAuthStore } from '../store/authStore'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { resolveLocalized } from '../lib/localization'
|
||||
import { API_BASE } from '../lib/api'
|
||||
import { DesktopMegaMenu } from './navigation/DesktopMegaMenu'
|
||||
import { MobileDrawer } from './navigation/MobileDrawer'
|
||||
import type { MegaMenuPayload } from './navigation/menuTypes'
|
||||
|
||||
interface NavbarProps {
|
||||
categories?: Category[]
|
||||
}
|
||||
|
||||
const LANGUAGES = [
|
||||
{ code: 'th', label: 'ไทย', short: 'TH' },
|
||||
{ code: 'en', label: 'English', short: 'EN' },
|
||||
{ code: 'ja', label: '日本語', short: 'JP' },
|
||||
{ code: 'zh', label: '简体中文', short: 'ZH' },
|
||||
{ code: 'vi', label: 'Tiếng Việt', short: 'VN' },
|
||||
{ code: 'ko', label: '한국어', short: 'KO' },
|
||||
]
|
||||
|
||||
function createFallbackMenu(categories: Category[]): MegaMenuPayload[] {
|
||||
const categoryLinks = categories.map((category) => ({
|
||||
label: {
|
||||
th: resolveLocalized(category.name, 'th') || resolveLocalized(category.name),
|
||||
en: resolveLocalized(category.name, 'en') || resolveLocalized(category.name),
|
||||
},
|
||||
url: `/category/${category.id}`,
|
||||
}))
|
||||
|
||||
return [
|
||||
{ id: 'home', label: { th: 'หน้าหลัก', en: 'Home' }, url: '/' },
|
||||
{
|
||||
id: 'shop',
|
||||
label: { th: 'ร้านค้า', en: 'Shop' },
|
||||
url: '/shop',
|
||||
columns: categoryLinks.length
|
||||
? [
|
||||
{
|
||||
title: { th: 'คอลเลกชัน', en: 'Collections' },
|
||||
links: categoryLinks,
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
},
|
||||
{ id: 'promotions', label: { th: 'โปรโมชัน', en: 'Promotions' }, url: '/coupons' },
|
||||
{ id: 'scent-guide', label: { th: 'คลาสแนะนำกลิ่น', en: 'Scent Guide' }, url: '/scent-guide' },
|
||||
{ id: 'about', label: { th: 'เกี่ยวกับเรา', en: 'About' }, url: '/about' },
|
||||
]
|
||||
}
|
||||
|
||||
export default function Navbar({ categories = [] }: NavbarProps) {
|
||||
const [searchOpen, setSearchOpen] = useState(false)
|
||||
const [mobileOpen, setMobileOpen] = useState(false)
|
||||
const [langSheetOpen, setLangSheetOpen] = useState(false)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [scrolled, setScrolled] = useState(false)
|
||||
const [localOrdersCount, setLocalOrdersCount] = useState(0)
|
||||
const [megaMenu, setMegaMenu] = useState<MegaMenuPayload[]>([])
|
||||
const [menuLoaded, setMenuLoaded] = useState(false)
|
||||
const navRef = useRef<HTMLElement>(null)
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
|
||||
const { toggleCart, getCartCount } = useCartStore()
|
||||
const wishlistCount = useWishlistStore((state) => state.items.length)
|
||||
const customer = useAuthStore((state) => state.customer)
|
||||
const logout = useAuthStore((state) => state.logout)
|
||||
const cartCount = getCartCount()
|
||||
const { t, i18n } = useTranslation()
|
||||
const lang = i18n.language ? i18n.language.substring(0, 2) : 'th'
|
||||
|
||||
const toggleLanguage = () => {
|
||||
const newLang = i18n.language.startsWith('en') ? 'th' : 'en'
|
||||
i18n.changeLanguage(newLang)
|
||||
localStorage.setItem('homaroma_lang', newLang)
|
||||
const fallbackMenu = useMemo(() => createFallbackMenu(categories), [categories])
|
||||
const menuItems = menuLoaded && megaMenu.length > 0 ? megaMenu : fallbackMenu
|
||||
|
||||
const changeLanguage = (code: string) => {
|
||||
i18n.changeLanguage(code)
|
||||
localStorage.setItem('homaroma_lang', code)
|
||||
setLangSheetOpen(false)
|
||||
}
|
||||
|
||||
const loginWithLine = () => {
|
||||
const maybeWindow = window as Window & { liff?: { login: () => void } }
|
||||
if (maybeWindow.liff?.login) {
|
||||
maybeWindow.liff.login()
|
||||
return
|
||||
}
|
||||
router.push('/profile')
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true
|
||||
|
||||
async function fetchMegaMenu() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/menus/main`)
|
||||
if (!response.ok) throw new Error(`menus/main returned ${response.status}`)
|
||||
const data = await response.json() as { payload?: MegaMenuPayload[] }
|
||||
if (isMounted) {
|
||||
setMegaMenu(Array.isArray(data.payload) ? data.payload : [])
|
||||
setMenuLoaded(true)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[website-menu] Failed to fetch main menu', error)
|
||||
if (isMounted) setMenuLoaded(true)
|
||||
}
|
||||
}
|
||||
|
||||
fetchMegaMenu()
|
||||
|
||||
return () => {
|
||||
isMounted = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => setScrolled(window.scrollY > 20)
|
||||
window.addEventListener('scroll', onScroll, { passive: true })
|
||||
|
||||
const stored = localStorage.getItem('homaroma_local_orders')
|
||||
if (stored) {
|
||||
try {
|
||||
const parsed = JSON.parse(stored) as unknown[]
|
||||
queueMicrotask(() => setLocalOrdersCount(parsed.length))
|
||||
} catch {}
|
||||
}
|
||||
|
||||
onScroll()
|
||||
return () => window.removeEventListener('scroll', onScroll)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = (searchOpen || mobileOpen) ? 'hidden' : ''
|
||||
const updateNavBottom = () => {
|
||||
if (navRef.current) {
|
||||
const bottom = navRef.current.getBoundingClientRect().bottom
|
||||
document.documentElement.style.setProperty('--navbar-bottom', `${bottom}px`)
|
||||
}
|
||||
}
|
||||
|
||||
updateNavBottom()
|
||||
window.addEventListener('scroll', updateNavBottom, { passive: true })
|
||||
window.addEventListener('resize', updateNavBottom)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', updateNavBottom)
|
||||
window.removeEventListener('resize', updateNavBottom)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = (searchOpen || mobileOpen || langSheetOpen) ? 'hidden' : ''
|
||||
return () => { document.body.style.overflow = '' }
|
||||
}, [searchOpen, mobileOpen])
|
||||
}, [searchOpen, mobileOpen, langSheetOpen])
|
||||
|
||||
const handleSearch = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
@@ -65,230 +161,150 @@ export default function Navbar({ categories = [] }: NavbarProps) {
|
||||
}
|
||||
}
|
||||
|
||||
const navLinkClass = (path: string) =>
|
||||
pathname === path
|
||||
? 'text-pop-pink'
|
||||
: 'text-pop-black/75 transition-colors hover:text-pop-pink'
|
||||
|
||||
const iconButtonClass = 'rounded-full border border-gray-200 bg-white p-2.5 text-pop-black shadow-sm transition hover:border-pop-pink hover:text-pop-pink hover:shadow-brutal'
|
||||
const iconButtonClass = 'rounded-full p-1.5 text-pop-black/65 transition-all hover:bg-pop-bg hover:text-pop-pink md:p-2'
|
||||
const activeLanguage = LANGUAGES.find((language) => i18n.language.startsWith(language.code)) ?? LANGUAGES[0]
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav
|
||||
className={`fixed top-0 z-50 flex w-full items-center justify-between px-4 py-3.5 transition-all duration-300 md:px-6 md:py-4 ${
|
||||
ref={navRef}
|
||||
className={`fixed top-0 z-50 w-full border-b border-black/5 px-4 py-3.5 transition-all duration-300 md:px-6 md:py-4 ${
|
||||
scrolled
|
||||
? 'border-b border-gray-200 bg-white/95 shadow-brutal backdrop-blur-xl'
|
||||
: 'border-b border-gray-100 bg-white/85 backdrop-blur-xl'
|
||||
? 'bg-white/95 shadow-lg shadow-black/5 backdrop-blur-xl'
|
||||
: 'bg-white/80 backdrop-blur-xl md:bg-[#F9F8F5] md:backdrop-blur-none'
|
||||
}`}
|
||||
>
|
||||
<Link href="/" onClick={() => setMobileOpen(false)} className="shrink-0 text-xl font-bold uppercase italic tracking-widest text-pop-black md:text-2xl">
|
||||
HomAroma <span className="text-xs font-semibold not-italic text-pop-pink md:text-sm">Essential</span>
|
||||
</Link>
|
||||
<div className="flex w-full items-center justify-between gap-4">
|
||||
<div className="flex shrink-0 items-center">
|
||||
<Link
|
||||
href="/"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className="shrink-0 text-xl font-bold uppercase italic tracking-widest text-pop-black md:text-2xl"
|
||||
>
|
||||
HomAroma <span className="hidden text-xs font-light not-italic text-pop-black/55 md:inline md:text-sm">Essential</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="hidden space-x-7 text-sm font-semibold uppercase tracking-wide md:flex">
|
||||
<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>
|
||||
<div className="flex flex-1 justify-center">
|
||||
<DesktopMegaMenu items={menuItems} lang={lang} currentPath={pathname} />
|
||||
</div>
|
||||
|
||||
{categories.length > 0 && (
|
||||
<div className="group relative">
|
||||
<button className="flex items-center gap-1 text-sm font-semibold uppercase tracking-wide text-pop-black/75 transition-colors hover:text-pop-pink">
|
||||
{t('nav.collections')}
|
||||
<svg className="h-3 w-3 opacity-60 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>
|
||||
<div className="flex shrink-0 items-center gap-1.5 md:gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setSearchOpen(true)
|
||||
setMobileOpen(false)
|
||||
}}
|
||||
className={iconButtonClass}
|
||||
aria-label="Search"
|
||||
>
|
||||
<svg className="h-4 w-4 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 0114 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<Link
|
||||
href="/wishlist"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className={`relative ${iconButtonClass}`}
|
||||
aria-label="Wishlist"
|
||||
>
|
||||
<Heart className="h-4 w-4 transition-colors" />
|
||||
{wishlistCount > 0 && (
|
||||
<span className="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full bg-rose-500 text-[9px] font-bold text-white">
|
||||
{wishlistCount}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
toggleCart(true)
|
||||
setMobileOpen(false)
|
||||
}}
|
||||
className={`relative ${iconButtonClass}`}
|
||||
aria-label="Cart"
|
||||
>
|
||||
<ShoppingBag className="h-4 w-4 transition-colors" />
|
||||
{cartCount > 0 && (
|
||||
<span className="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full bg-rose-500 text-[9px] font-bold text-white">
|
||||
{cartCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="group relative hidden items-center md:flex">
|
||||
<button className="flex items-center gap-1.5 rounded-full border border-pop-pink/30 bg-white px-3 py-2 text-[10px] font-bold uppercase tracking-wider text-pop-black transition-all hover:bg-pop-bg hover:text-pop-pink">
|
||||
<Globe className="h-3.5 w-3.5" strokeWidth={1.5} />
|
||||
{activeLanguage.short}
|
||||
</button>
|
||||
<div className="pointer-events-none absolute left-1/2 top-full mt-4 w-56 -translate-x-1/2 overflow-hidden rounded-2xl border border-gray-200 bg-white opacity-0 shadow-brutal transition-all duration-300 group-hover:pointer-events-auto group-hover:opacity-100">
|
||||
<div className="space-y-0.5 p-2">
|
||||
{categories.map((cat) => (
|
||||
<Link
|
||||
key={cat.id}
|
||||
href={`/category/${cat.id}`}
|
||||
className="flex items-center justify-between rounded-xl px-4 py-2.5 text-xs uppercase tracking-widest text-gray-600 transition-colors hover:bg-pop-bg hover:text-pop-pink"
|
||||
>
|
||||
{resolveLocalized(cat.name)}
|
||||
<svg className="h-3 w-3 opacity-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>
|
||||
))}
|
||||
<div className="pointer-events-none absolute right-0 top-full w-36 pt-2 opacity-0 transition-all duration-300 group-hover:pointer-events-auto group-hover:opacity-100">
|
||||
<div className="space-y-0.5 overflow-hidden rounded-xl border border-pop-black/10 bg-white/95 p-1.5 shadow-2xl backdrop-blur-xl">
|
||||
{LANGUAGES.map((language) => {
|
||||
const isActive = i18n.language.startsWith(language.code)
|
||||
return (
|
||||
<button
|
||||
key={language.code}
|
||||
onClick={() => changeLanguage(language.code)}
|
||||
className={`flex w-full items-center justify-between rounded-lg px-3 py-2.5 text-xs font-medium transition-colors ${
|
||||
isActive ? 'bg-pop-bg font-bold text-pop-black' : 'text-pop-black/65 hover:bg-pop-bg hover:text-pop-pink'
|
||||
}`}
|
||||
>
|
||||
<span>{language.label}</span>
|
||||
{isActive && <Check className="h-3.5 w-3.5" />}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Link href="/scent-guide" className={navLinkClass('/scent-guide')}>{t('nav.scentGuide')}</Link>
|
||||
<Link href="/about" className={navLinkClass('/about')}>{t('nav.about')}</Link>
|
||||
</div>
|
||||
|
||||
<div className="hidden items-center gap-3 md:flex">
|
||||
<Link href="/track" className={`relative ${iconButtonClass}`} aria-label="Track Orders" title="Track Orders">
|
||||
<MapPin className="h-4 w-4" />
|
||||
{localOrdersCount > 0 && <span className="absolute right-0 top-0 h-2 w-2 rounded-full bg-rose-500" />}
|
||||
</Link>
|
||||
|
||||
<Link href="/wishlist" className={`relative ${iconButtonClass}`} aria-label="Wishlist" title="Wishlist">
|
||||
<Heart className="h-4 w-4" />
|
||||
{wishlistCount > 0 && (
|
||||
<span className="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full bg-rose-500 text-[9px] font-bold text-white">
|
||||
{wishlistCount}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
<Link href="/profile" className={iconButtonClass} aria-label="Profile" title={customer?.displayName || 'Profile'}>
|
||||
{customer?.pictureUrl ? (
|
||||
<img src={customer.pictureUrl} alt={customer.displayName || 'Profile'} className="h-4 w-4 rounded-full object-cover" />
|
||||
) : (
|
||||
<User className="h-4 w-4" />
|
||||
)}
|
||||
</Link>
|
||||
|
||||
<button onClick={() => setSearchOpen(true)} className={iconButtonClass} aria-label="Search">
|
||||
<svg className="h-4 w-4" 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 ${iconButtonClass}`} aria-label="Cart">
|
||||
<ShoppingBag className="h-4 w-4" />
|
||||
{cartCount > 0 && (
|
||||
<span className="absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full bg-pop-pink text-[9px] font-bold text-white">
|
||||
{cartCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<Link href="/shop" className="rounded-full bg-pop-black px-4 py-2 text-xs font-bold uppercase tracking-widest text-white transition hover:bg-pop-pink">
|
||||
{t('nav.shopNow')}
|
||||
</Link>
|
||||
|
||||
<button onClick={toggleLanguage} className="rounded-full border border-pop-pink/30 bg-white px-3 py-2 text-xs font-bold uppercase text-pop-pink transition hover:bg-pop-bg">
|
||||
{i18n.language.startsWith('en') ? 'EN' : 'TH'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 md:hidden">
|
||||
<Link href="/track" onClick={() => setMobileOpen(false)} className={`relative ${iconButtonClass}`} aria-label="Track Orders">
|
||||
<MapPin className="h-4 w-4" />
|
||||
{localOrdersCount > 0 && <span className="absolute right-1 top-1 h-2 w-2 rounded-full bg-rose-500" />}
|
||||
</Link>
|
||||
<Link href="/wishlist" onClick={() => setMobileOpen(false)} className={`relative ${iconButtonClass}`} aria-label="Wishlist">
|
||||
<Heart className="h-4 w-4" />
|
||||
{wishlistCount > 0 && (
|
||||
<span className="absolute right-0 top-0 flex h-4 w-4 translate-x-1/4 -translate-y-1/4 items-center justify-center rounded-full bg-rose-500 text-[9px] font-bold text-white">
|
||||
{wishlistCount}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
<Link href="/profile" onClick={() => setMobileOpen(false)} className={iconButtonClass} aria-label="Profile">
|
||||
{customer?.pictureUrl ? (
|
||||
<img src={customer.pictureUrl} alt={customer.displayName || 'Profile'} className="h-4 w-4 rounded-full object-cover" />
|
||||
) : (
|
||||
<User className="h-4 w-4" />
|
||||
)}
|
||||
</Link>
|
||||
<button onClick={() => { setSearchOpen(true); setMobileOpen(false) }} className={iconButtonClass} aria-label="Search">
|
||||
<svg className="h-5 w-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 ${iconButtonClass}`} aria-label="Cart">
|
||||
<ShoppingBag className="h-5 w-5" />
|
||||
{cartCount > 0 && (
|
||||
<span className="absolute right-0 top-0 flex h-4 w-4 translate-x-1/4 -translate-y-1/4 items-center justify-center rounded-full bg-pop-pink text-[9px] font-bold text-white">
|
||||
{cartCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button onClick={() => setMobileOpen(prev => !prev)} className={iconButtonClass} aria-label={mobileOpen ? 'Close menu' : 'Open menu'}>
|
||||
<div className="relative flex h-5 w-5 flex-col justify-center gap-[5px]">
|
||||
<span className={`block h-0.5 w-5 rounded-full bg-pop-black transition-all duration-300 ${mobileOpen ? 'absolute rotate-45' : ''}`} />
|
||||
<span className={`block h-0.5 rounded-full bg-pop-black transition-all duration-300 ${mobileOpen ? 'w-0 opacity-0' : 'w-4'}`} />
|
||||
<span className={`block h-0.5 rounded-full bg-pop-black transition-all duration-300 ${mobileOpen ? 'absolute w-5 -rotate-45' : 'w-5'}`} />
|
||||
<div className="relative hidden items-center md:flex">
|
||||
{customer ? (
|
||||
<Link href="/profile" className="ml-1">
|
||||
{customer.pictureUrl ? (
|
||||
<img
|
||||
src={customer.pictureUrl}
|
||||
alt={customer.displayName || 'Profile'}
|
||||
className="h-9 w-9 rounded-full border-2 border-pop-pink/30 object-cover"
|
||||
/>
|
||||
) : (
|
||||
<span className="flex h-9 w-9 items-center justify-center rounded-full border-2 border-pop-pink/30 text-pop-black/65">
|
||||
<User className="h-4 w-4" />
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
) : (
|
||||
<button
|
||||
onClick={loginWithLine}
|
||||
className="ml-2 rounded-full bg-[#00B900] px-4 py-2 text-xs font-bold uppercase tracking-widest text-white shadow-sm transition-all duration-300 hover:bg-[#009900]"
|
||||
>
|
||||
LINE Login
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setMobileOpen((prev) => !prev)}
|
||||
className={`${iconButtonClass} md:hidden`}
|
||||
aria-label={mobileOpen ? 'Close menu' : 'Open menu'}
|
||||
>
|
||||
{mobileOpen ? <X className="h-5 w-5" /> : <MenuIcon className="h-5 w-5" />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className={`fixed inset-0 z-40 transition-all duration-300 md:hidden ${mobileOpen ? 'visible' : 'invisible'}`}>
|
||||
<div
|
||||
className={`absolute inset-0 bg-pop-black/45 backdrop-blur-sm transition-opacity duration-300 ${mobileOpen ? 'opacity-100' : 'opacity-0'}`}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
/>
|
||||
<div
|
||||
className={`absolute right-0 top-0 flex h-full w-72 flex-col border-l border-gray-200 bg-white shadow-2xl transition-transform duration-300 ${mobileOpen ? 'translate-x-0' : 'translate-x-full'}`}
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-gray-100 px-6 py-5">
|
||||
<span className="text-sm font-bold uppercase tracking-widest text-pop-black">{t('nav.menu')}</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<button onClick={toggleLanguage} className="rounded-md border border-pop-pink/30 px-2.5 py-1 text-[10px] font-bold uppercase text-pop-pink">
|
||||
{i18n.language.startsWith('en') ? 'EN' : 'TH'}
|
||||
</button>
|
||||
<button onClick={() => setMobileOpen(false)} className="rounded-full border border-gray-200 p-1.5 text-pop-black transition hover:text-pop-pink">
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 space-y-1 overflow-y-auto px-4 py-6">
|
||||
{[
|
||||
{ to: '/', label: t('nav.home') },
|
||||
{ to: '/shop', label: t('nav.shop') },
|
||||
{ to: '/coupons', label: `🔥 ${t('nav.promotions') || 'Promotions'}` },
|
||||
{ to: '/wishlist', label: '♡ Wishlist' },
|
||||
{ to: '/profile', label: customer ? `👤 ${customer.displayName || 'Profile'}` : '👤 Profile' },
|
||||
{ to: '/scent-guide', label: `✨ ${t('nav.scentGuide')}` },
|
||||
{ to: '/about', label: t('nav.about') },
|
||||
{ to: '/faq', label: t('nav.faq') },
|
||||
{ to: '/contact', label: t('nav.contact') },
|
||||
].map(item => (
|
||||
<Link
|
||||
key={item.to}
|
||||
href={item.to}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className={`block rounded-xl px-4 py-3 text-sm font-semibold tracking-wide transition-all duration-200 ${
|
||||
pathname === item.to ? 'border border-pop-pink/20 bg-pop-bg text-pop-pink' : 'text-pop-black/75 hover:bg-pop-bg hover:text-pop-pink'
|
||||
}`}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
{categories.length > 0 && (
|
||||
<div className="pt-4">
|
||||
<p className="px-4 pb-2 text-[10px] uppercase tracking-widest text-gray-500">{t('nav.collections')}</p>
|
||||
{categories.map(cat => (
|
||||
<Link
|
||||
key={cat.id}
|
||||
href={`/category/${cat.id}`}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className="flex items-center justify-between rounded-xl px-4 py-2.5 text-sm text-gray-600 transition-all hover:bg-pop-bg hover:text-pop-pink"
|
||||
>
|
||||
{resolveLocalized(cat.name)}
|
||||
<svg className="h-3 w-3 opacity-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>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 border-t border-gray-100 px-4 pb-8 pt-4">
|
||||
<Link href="/shop" onClick={() => setMobileOpen(false)} className="block w-full rounded-xl bg-pop-black py-3 text-center text-xs font-bold uppercase tracking-widest text-white transition hover:bg-pop-pink">
|
||||
{t('nav.shopNow')}
|
||||
</Link>
|
||||
<Link href="/scent-guide" onClick={() => setMobileOpen(false)} className="block w-full rounded-xl border border-pop-pink/30 bg-white py-3 text-center text-xs font-bold uppercase tracking-widest text-pop-pink transition hover:bg-pop-bg">
|
||||
{t('nav.findMyScent')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<MobileDrawer
|
||||
isOpen={mobileOpen}
|
||||
items={menuItems}
|
||||
lang={lang}
|
||||
customerName={customer?.displayName}
|
||||
onClose={() => setMobileOpen(false)}
|
||||
onOpenLang={() => setLangSheetOpen(true)}
|
||||
onLogin={loginWithLine}
|
||||
onLogout={logout}
|
||||
/>
|
||||
|
||||
{searchOpen && (
|
||||
<div
|
||||
@@ -297,25 +313,23 @@ export default function Navbar({ categories = [] }: NavbarProps) {
|
||||
>
|
||||
<div className="w-full max-w-2xl space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-xs uppercase tracking-widest text-gray-500">{t('nav.search.title')}</p>
|
||||
<button onClick={() => setSearchOpen(false)} className="rounded-full border border-gray-200 bg-white p-2 text-pop-black transition hover:text-pop-pink">
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<p className="text-xs font-semibold uppercase tracking-widest text-pop-black/55">{t('nav.search.title')}</p>
|
||||
<button onClick={() => setSearchOpen(false)} className="rounded-full bg-white p-2 text-pop-black/65 transition hover:text-pop-pink">
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSearch}>
|
||||
<div className="flex overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-brutal transition-colors focus-within:border-pop-pink">
|
||||
<svg className="m-4 h-5 w-5 shrink-0 text-gray-400" 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" />
|
||||
<div className="flex overflow-hidden rounded-2xl border-2 border-pop-black/10 bg-white shadow-sm transition-colors focus-within:border-pop-pink">
|
||||
<svg className="m-4 h-5 w-5 shrink-0 text-pop-black/35" 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 0114 0z" />
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search scents, categories..."
|
||||
className="w-full border-none bg-transparent p-4 pl-0 text-lg text-pop-black outline-none placeholder:text-gray-400 md:text-xl"
|
||||
placeholder={t('nav.search.placeholder')}
|
||||
className="w-full border-none bg-transparent p-4 pl-0 text-lg text-pop-black outline-none placeholder:text-pop-black/35 md:text-xl"
|
||||
autoFocus
|
||||
/>
|
||||
<button type="submit" className="shrink-0 bg-pop-black px-4 text-xs font-bold uppercase tracking-widest text-white transition-colors hover:bg-pop-pink md:px-6">
|
||||
@@ -325,18 +339,18 @@ export default function Navbar({ categories = [] }: NavbarProps) {
|
||||
</form>
|
||||
|
||||
<div>
|
||||
<p className="mb-3 text-xs uppercase tracking-widest text-gray-500">Quick Links</p>
|
||||
<p className="mb-3 text-xs font-semibold uppercase tracking-widest text-pop-black/55">{t('nav.search.quickLinks')}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{[
|
||||
...categories.map(c => ({ label: resolveLocalized(c.name), to: `/category/${c.id}` })),
|
||||
{ label: '✨ Scent Guide', to: '/scent-guide' },
|
||||
{ label: 'All Products', to: '/shop' },
|
||||
].map(item => (
|
||||
...categories.map((category) => ({ label: resolveLocalized(category.name), to: `/category/${category.id}` })),
|
||||
{ label: t('nav.scentGuide'), to: '/scent-guide' },
|
||||
{ label: t('nav.search.allProducts'), to: '/shop' },
|
||||
].map((item) => (
|
||||
<Link
|
||||
key={item.to}
|
||||
href={item.to}
|
||||
onClick={() => setSearchOpen(false)}
|
||||
className="rounded-full border border-gray-200 bg-white px-4 py-1.5 text-xs font-semibold text-gray-600 transition-all hover:border-pop-pink hover:text-pop-pink"
|
||||
className="rounded-full border border-pop-black/10 bg-white px-4 py-1.5 text-xs font-semibold text-pop-black/65 transition-all hover:border-pop-pink hover:text-pop-pink"
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
@@ -346,6 +360,50 @@ export default function Navbar({ categories = [] }: NavbarProps) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`fixed inset-0 z-[110] transition-all duration-300 md:hidden ${langSheetOpen ? 'visible' : 'invisible'}`}>
|
||||
<div
|
||||
className={`absolute inset-0 bg-pop-black/40 backdrop-blur-sm transition-opacity duration-300 ${langSheetOpen ? 'opacity-100' : 'opacity-0'}`}
|
||||
onClick={() => setLangSheetOpen(false)}
|
||||
/>
|
||||
<div
|
||||
className={`absolute bottom-0 left-0 right-0 rounded-t-3xl bg-white shadow-2xl transition-transform duration-300 ${
|
||||
langSheetOpen ? 'translate-y-0' : 'translate-y-full'
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-center pb-1 pt-3">
|
||||
<div className="h-1.5 w-12 rounded-full bg-pop-black/15" />
|
||||
</div>
|
||||
<div className="px-6 pb-6 pt-2">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h3 className="flex items-center gap-2 text-sm font-bold uppercase tracking-widest text-pop-black">
|
||||
<Globe className="h-4 w-4" />
|
||||
Select Language
|
||||
</h3>
|
||||
<button onClick={() => setLangSheetOpen(false)} className="rounded-full p-2 text-pop-black/65 hover:bg-pop-bg">
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{LANGUAGES.map((language) => {
|
||||
const isActive = i18n.language.startsWith(language.code)
|
||||
return (
|
||||
<button
|
||||
key={language.code}
|
||||
onClick={() => changeLanguage(language.code)}
|
||||
className={`flex w-full items-center justify-between rounded-xl border border-transparent px-4 py-4 font-medium transition-colors ${
|
||||
isActive ? 'border-pop-pink/20 bg-pop-bg font-bold text-pop-black' : 'bg-pop-bg/40 text-pop-black/65 hover:bg-pop-bg'
|
||||
}`}
|
||||
>
|
||||
<span className="text-base">{language.label}</span>
|
||||
{isActive && <Check className="h-5 w-5" />}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user