x
This commit is contained in:
1 parent
173c583787
commit
b415fb2eb4
2 files changed
+169
-147
No files matched your search
@@ -27,6 +27,7 @@ export default function Navbar({ topOffsetClassName = 'top-0', isTransparent = f
|
|||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
const [scrolled, setScrolled] = useState(false)
|
const [scrolled, setScrolled] = useState(false)
|
||||||
const [hasAnnouncement, setHasAnnouncement] = useState(false)
|
const [hasAnnouncement, setHasAnnouncement] = useState(false)
|
||||||
|
const [isClientReady, setIsClientReady] = useState(false)
|
||||||
const [settings, setSettings] = useState<StoreSettings | null>(null)
|
const [settings, setSettings] = useState<StoreSettings | null>(null)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
@@ -55,6 +56,7 @@ export default function Navbar({ topOffsetClassName = 'top-0', isTransparent = f
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setIsClientReady(true)
|
||||||
fetchMegaMenu()
|
fetchMegaMenu()
|
||||||
|
|
||||||
const checkAnnouncement = () => {
|
const checkAnnouncement = () => {
|
||||||
@@ -173,7 +175,7 @@ export default function Navbar({ topOffsetClassName = 'top-0', isTransparent = f
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Center: Desktop mega menu */}
|
{/* Center: Desktop mega menu */}
|
||||||
{isDesktop && (
|
{isClientReady && isDesktop && (
|
||||||
<div className="flex-1 flex justify-center">
|
<div className="flex-1 flex justify-center">
|
||||||
<DesktopMegaMenu items={megaMenu} />
|
<DesktopMegaMenu items={megaMenu} />
|
||||||
</div>
|
</div>
|
||||||
@@ -219,7 +221,7 @@ export default function Navbar({ topOffsetClassName = 'top-0', isTransparent = f
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{isDesktop && (
|
{isClientReady && isDesktop && (
|
||||||
<>
|
<>
|
||||||
<div className="relative group flex items-center">
|
<div className="relative group flex items-center">
|
||||||
<button className="px-3 py-2 rounded-full text-[10px] font-bold transition-all text-pop-black border border-pop-black/20 hover:bg-pop-black/5 bg-pop-bg flex items-center gap-1.5 uppercase tracking-wider">
|
<button className="px-3 py-2 rounded-full text-[10px] font-bold transition-all text-pop-black border border-pop-black/20 hover:bg-pop-black/5 bg-pop-bg flex items-center gap-1.5 uppercase tracking-wider">
|
||||||
@@ -286,7 +288,7 @@ export default function Navbar({ topOffsetClassName = 'top-0', isTransparent = f
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isDesktop && (
|
{isClientReady && !isDesktop && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setMobileOpen(prev => !prev)}
|
onClick={() => setMobileOpen(prev => !prev)}
|
||||||
className="p-1.5 rounded-full text-pop-black/80 hover:bg-pop-black/5 transition-all"
|
className="p-1.5 rounded-full text-pop-black/80 hover:bg-pop-black/5 transition-all"
|
||||||
@@ -299,7 +301,7 @@ export default function Navbar({ topOffsetClassName = 'top-0', isTransparent = f
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{!isDesktop && (
|
{isClientReady && !isDesktop && mobileOpen && (
|
||||||
<MobileDrawer
|
<MobileDrawer
|
||||||
isOpen={mobileOpen}
|
isOpen={mobileOpen}
|
||||||
onClose={() => setMobileOpen(false)}
|
onClose={() => setMobileOpen(false)}
|
||||||
@@ -345,47 +347,45 @@ export default function Navbar({ topOffsetClassName = 'top-0', isTransparent = f
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
{langSheetOpen && (
|
||||||
className={`fixed inset-0 z-[110] md:hidden transition-all duration-300 ${langSheetOpen ? 'visible' : 'invisible'}`}
|
<div className="fixed inset-0 z-[110] md:hidden transition-all duration-300">
|
||||||
>
|
<div
|
||||||
<div
|
className="absolute inset-0 bg-pop-black/40 backdrop-blur-sm transition-opacity duration-300"
|
||||||
className={`absolute inset-0 bg-pop-black/40 backdrop-blur-sm transition-opacity duration-300 ${langSheetOpen ? 'opacity-100' : 'opacity-0'}`}
|
onClick={() => setLangSheetOpen(false)}
|
||||||
onClick={() => setLangSheetOpen(false)}
|
/>
|
||||||
/>
|
<div className="absolute bottom-0 left-0 right-0 bg-pop-bg rounded-t-3xl transition-transform duration-300 shadow-2xl pb-safe">
|
||||||
<div
|
<div className="flex justify-center pt-3 pb-1">
|
||||||
className={`absolute bottom-0 left-0 right-0 bg-pop-bg rounded-t-3xl transition-transform duration-300 shadow-2xl pb-safe ${langSheetOpen ? 'translate-y-0' : 'translate-y-full'}`}
|
<div className="w-12 h-1.5 bg-pop-black/20 rounded-full" />
|
||||||
>
|
|
||||||
<div className="flex justify-center pt-3 pb-1">
|
|
||||||
<div className="w-12 h-1.5 bg-pop-black/20 rounded-full" />
|
|
||||||
</div>
|
|
||||||
<div className="px-6 pb-6 pt-2">
|
|
||||||
<div className="flex justify-between items-center mb-4">
|
|
||||||
<h3 className="text-sm font-bold uppercase tracking-widest text-pop-black flex items-center gap-2">
|
|
||||||
<Globe className="w-4 h-4" />
|
|
||||||
Select Language
|
|
||||||
</h3>
|
|
||||||
<button onClick={() => setLangSheetOpen(false)} className="p-2 rounded-full hover:bg-pop-black/5 text-pop-black/80">
|
|
||||||
<X className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="px-6 pb-6 pt-2">
|
||||||
{LANGUAGES.map(lang => {
|
<div className="flex justify-between items-center mb-4">
|
||||||
const isActive = i18n.language.startsWith(lang.code)
|
<h3 className="text-sm font-bold uppercase tracking-widest text-pop-black flex items-center gap-2">
|
||||||
return (
|
<Globe className="w-4 h-4" />
|
||||||
<button
|
Select Language
|
||||||
key={lang.code}
|
</h3>
|
||||||
onClick={() => changeLanguage(lang.code)}
|
<button onClick={() => setLangSheetOpen(false)} className="p-2 rounded-full hover:bg-pop-black/5 text-pop-black/80">
|
||||||
className={`w-full flex items-center justify-between px-4 py-4 rounded-xl transition-colors font-medium border border-transparent ${isActive ? 'bg-pop-black/10 border-pop-black/20 text-pop-black font-bold' : 'bg-pop-bg/50 text-pop-black/80 hover:bg-pop-black/5'}`}
|
<X className="w-4 h-4" />
|
||||||
>
|
</button>
|
||||||
<span className="text-base">{lang.label}</span>
|
</div>
|
||||||
{isActive && <Check className="w-5 h-5" />}
|
<div className="space-y-1">
|
||||||
</button>
|
{LANGUAGES.map(lang => {
|
||||||
)
|
const isActive = i18n.language.startsWith(lang.code)
|
||||||
})}
|
return (
|
||||||
|
<button
|
||||||
|
key={lang.code}
|
||||||
|
onClick={() => changeLanguage(lang.code)}
|
||||||
|
className={`w-full flex items-center justify-between px-4 py-4 rounded-xl transition-colors font-medium border border-transparent ${isActive ? 'bg-pop-black/10 border-pop-black/20 text-pop-black font-bold' : 'bg-pop-bg/50 text-pop-black/80 hover:bg-pop-black/5'}`}
|
||||||
|
>
|
||||||
|
<span className="text-base">{lang.label}</span>
|
||||||
|
{isActive && <Check className="w-5 h-5" />}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -47,6 +47,8 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
|
|||||||
const [langSheetOpen, setLangSheetOpen] = useState(false)
|
const [langSheetOpen, setLangSheetOpen] = useState(false)
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
const [scrolled, setScrolled] = useState(false)
|
const [scrolled, setScrolled] = useState(false)
|
||||||
|
const [isClientReady, setIsClientReady] = useState(false)
|
||||||
|
const [canRenderDesktopMenu, setCanRenderDesktopMenu] = useState(false)
|
||||||
// Seed from server-fetched initialMenu — no client flash
|
// Seed from server-fetched initialMenu — no client flash
|
||||||
const [megaMenu] = useState<MegaMenuPayload[]>(initialMenu)
|
const [megaMenu] = useState<MegaMenuPayload[]>(initialMenu)
|
||||||
const navRef = useRef<HTMLElement>(null)
|
const navRef = useRef<HTMLElement>(null)
|
||||||
@@ -153,6 +155,18 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
|
|||||||
const isHome = getPathWithoutLocale(pathname) === '/'
|
const isHome = getPathWithoutLocale(pathname) === '/'
|
||||||
const isTransparent = isHome && !scrolled && !mobileOpen && !searchOpen && !langSheetOpen
|
const isTransparent = isHome && !scrolled && !mobileOpen && !searchOpen && !langSheetOpen
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsClientReady(true)
|
||||||
|
|
||||||
|
const mediaQuery = window.matchMedia('(min-width: 768px)')
|
||||||
|
const updateDesktopMenu = () => setCanRenderDesktopMenu(mediaQuery.matches)
|
||||||
|
|
||||||
|
updateDesktopMenu()
|
||||||
|
mediaQuery.addEventListener('change', updateDesktopMenu)
|
||||||
|
|
||||||
|
return () => mediaQuery.removeEventListener('change', updateDesktopMenu)
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<nav
|
<nav
|
||||||
@@ -182,7 +196,9 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-1 justify-center">
|
<div className="flex flex-1 justify-center">
|
||||||
<DesktopMegaMenu items={menuItems} lang={lang} currentPath={pathname} />
|
{canRenderDesktopMenu && (
|
||||||
|
<DesktopMegaMenu items={menuItems} lang={lang} currentPath={pathname} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex shrink-0 items-center gap-1.5 md:gap-2">
|
<div className="flex shrink-0 items-center gap-1.5 md:gap-2">
|
||||||
@@ -227,81 +243,89 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="group relative hidden items-center md:flex">
|
{canRenderDesktopMenu && (
|
||||||
<button className="ui-nav-chip">
|
<>
|
||||||
<Globe className="h-3.5 w-3.5" strokeWidth={1.5} />
|
<div className="group relative flex items-center">
|
||||||
{activeLanguage.short}
|
<button className="ui-nav-chip">
|
||||||
</button>
|
<Globe className="h-3.5 w-3.5" strokeWidth={1.5} />
|
||||||
<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">
|
{activeLanguage.short}
|
||||||
<div className="ui-nav-panel space-y-0.5 p-1.5">
|
</button>
|
||||||
{LANGUAGES.map((language) => {
|
<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">
|
||||||
const isActive = lang.startsWith(language.code) || language.code.startsWith(lang)
|
<div className="ui-nav-panel space-y-0.5 p-1.5">
|
||||||
return (
|
{LANGUAGES.map((language) => {
|
||||||
<button
|
const isActive = lang.startsWith(language.code) || language.code.startsWith(lang)
|
||||||
key={language.code}
|
return (
|
||||||
onClick={() => changeLanguage(language.code)}
|
<button
|
||||||
className={`flex w-full items-center justify-between rounded-lg px-3 py-2.5 text-xs font-medium transition-colors ${
|
key={language.code}
|
||||||
isActive ? 'bg-surface-page font-bold text-text-main' : 'text-text-main/65 hover:bg-surface-page hover:text-brand-primary'
|
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-surface-page font-bold text-text-main' : 'text-text-main/65 hover:bg-surface-page hover:text-brand-primary'
|
||||||
<span>{language.label}</span>
|
}`}
|
||||||
{isActive && <Check className="h-3.5 w-3.5" />}
|
>
|
||||||
</button>
|
<span>{language.label}</span>
|
||||||
)
|
{isActive && <Check className="h-3.5 w-3.5" />}
|
||||||
})}
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative hidden items-center md:flex">
|
<div className="relative flex items-center">
|
||||||
{customer ? (
|
{customer ? (
|
||||||
<Link href="/profile" className="ml-1">
|
<Link href="/profile" className="ml-1">
|
||||||
{customer.pictureUrl ? (
|
{customer.pictureUrl ? (
|
||||||
<Image
|
<Image
|
||||||
src={customer.pictureUrl}
|
src={customer.pictureUrl}
|
||||||
alt={customer.displayName || 'Profile'}
|
alt={customer.displayName || 'Profile'}
|
||||||
width={36}
|
width={36}
|
||||||
height={36}
|
height={36}
|
||||||
unoptimized
|
unoptimized
|
||||||
className="h-9 w-9 rounded-full border-2 border-brand-primary/30 object-cover"
|
className="h-9 w-9 rounded-full border-2 border-brand-primary/30 object-cover"
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="flex h-9 w-9 items-center justify-center rounded-full border-2 border-brand-primary/30 text-text-main/65">
|
||||||
|
<User className="h-4 w-4" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<span className="flex h-9 w-9 items-center justify-center rounded-full border-2 border-brand-primary/30 text-text-main/65">
|
<button
|
||||||
<User className="h-4 w-4" />
|
onClick={loginWithLine}
|
||||||
</span>
|
className="ui-button-line ml-2"
|
||||||
|
>
|
||||||
|
{t('nav.login', 'เข้าสู่ระบบด้วย LINE')}
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</div>
|
||||||
) : (
|
</>
|
||||||
<button
|
)}
|
||||||
onClick={loginWithLine}
|
|
||||||
className="ui-button-line ml-2"
|
|
||||||
>
|
|
||||||
{t('nav.login', 'เข้าสู่ระบบด้วย LINE')}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
{isClientReady && !canRenderDesktopMenu && (
|
||||||
onClick={() => setMobileOpen((prev) => !prev)}
|
<button
|
||||||
className={`${iconButtonClass} md:hidden`}
|
onClick={() => setMobileOpen((prev) => !prev)}
|
||||||
aria-label={mobileOpen ? 'Close menu' : 'Open menu'}
|
className={iconButtonClass}
|
||||||
>
|
aria-label={mobileOpen ? 'Close menu' : 'Open menu'}
|
||||||
{mobileOpen ? <X className="h-5 w-5" /> : <MenuIcon className="h-5 w-5" />}
|
>
|
||||||
</button>
|
{mobileOpen ? <X className="h-5 w-5" /> : <MenuIcon className="h-5 w-5" />}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<MobileDrawer
|
{isClientReady && mobileOpen && (
|
||||||
isOpen={mobileOpen}
|
<MobileDrawer
|
||||||
items={menuItems}
|
isOpen={mobileOpen}
|
||||||
lang={lang}
|
items={menuItems}
|
||||||
customerName={customer?.displayName}
|
lang={lang}
|
||||||
onClose={() => setMobileOpen(false)}
|
customerName={customer?.displayName}
|
||||||
onOpenLang={() => setLangSheetOpen(true)}
|
onClose={() => setMobileOpen(false)}
|
||||||
onLogin={loginWithLine}
|
onOpenLang={() => setLangSheetOpen(true)}
|
||||||
onLogout={logout}
|
onLogin={loginWithLine}
|
||||||
/>
|
onLogout={logout}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{searchOpen && (
|
{searchOpen && (
|
||||||
<div
|
<div
|
||||||
@@ -356,49 +380,47 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={`fixed inset-0 z-[110] transition-all duration-300 md:hidden ${langSheetOpen ? 'visible' : 'invisible'}`}>
|
{langSheetOpen && (
|
||||||
<div
|
<div className="fixed inset-0 z-[110] transition-all duration-300 md:hidden">
|
||||||
className={`absolute inset-0 bg-text-main/40 backdrop-blur-sm transition-opacity duration-300 ${langSheetOpen ? 'opacity-100' : 'opacity-0'}`}
|
<div
|
||||||
onClick={() => setLangSheetOpen(false)}
|
className="absolute inset-0 bg-text-main/40 backdrop-blur-sm transition-opacity duration-300"
|
||||||
/>
|
onClick={() => setLangSheetOpen(false)}
|
||||||
<div
|
/>
|
||||||
className={`absolute bottom-0 left-0 right-0 rounded-t-3xl bg-surface-card shadow-2xl transition-transform duration-300 ${
|
<div className="absolute bottom-0 left-0 right-0 rounded-t-3xl bg-surface-card 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-text-main/15" />
|
||||||
>
|
|
||||||
<div className="flex justify-center pb-1 pt-3">
|
|
||||||
<div className="h-1.5 w-12 rounded-full bg-text-main/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-text-main">
|
|
||||||
<Globe className="h-4 w-4" />
|
|
||||||
Select Language
|
|
||||||
</h3>
|
|
||||||
<button onClick={() => setLangSheetOpen(false)} className="rounded-full p-2 text-text-main/65 hover:bg-surface-page">
|
|
||||||
<X className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="px-6 pb-6 pt-2">
|
||||||
{LANGUAGES.map((language) => {
|
<div className="mb-4 flex items-center justify-between">
|
||||||
const isActive = lang.startsWith(language.code) || language.code.startsWith(lang)
|
<h3 className="flex items-center gap-2 text-sm font-bold uppercase tracking-widest text-text-main">
|
||||||
return (
|
<Globe className="h-4 w-4" />
|
||||||
<button
|
Select Language
|
||||||
key={language.code}
|
</h3>
|
||||||
onClick={() => changeLanguage(language.code)}
|
<button onClick={() => setLangSheetOpen(false)} className="rounded-full p-2 text-text-main/65 hover:bg-surface-page">
|
||||||
className={`flex w-full items-center justify-between rounded-xl border border-transparent px-4 py-4 font-medium transition-colors ${
|
<X className="h-4 w-4" />
|
||||||
isActive ? 'border-brand-primary/20 bg-surface-page font-bold text-text-main' : 'bg-surface-page/40 text-text-main/65 hover:bg-surface-page'
|
</button>
|
||||||
}`}
|
</div>
|
||||||
>
|
<div className="space-y-1">
|
||||||
<span className="text-base">{language.label}</span>
|
{LANGUAGES.map((language) => {
|
||||||
{isActive && <Check className="h-5 w-5" />}
|
const isActive = lang.startsWith(language.code) || language.code.startsWith(lang)
|
||||||
</button>
|
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-brand-primary/20 bg-surface-page font-bold text-text-main' : 'bg-surface-page/40 text-text-main/65 hover:bg-surface-page'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="text-base">{language.label}</span>
|
||||||
|
{isActive && <Check className="h-5 w-5" />}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user