x
This commit is contained in:
1 parent
7b98d41555
commit
996dfdabd0
10 files changed
+959
-252
No files matched your search
@@ -0,0 +1,102 @@
|
||||
"use client"
|
||||
|
||||
import Link from 'next/link'
|
||||
import { useState, useRef } from 'react'
|
||||
import type { MegaMenuPayload } from './menuTypes'
|
||||
import { getLocalizedMenuText } from './menuTypes'
|
||||
|
||||
interface DesktopMegaMenuProps {
|
||||
items: MegaMenuPayload[]
|
||||
lang: string
|
||||
currentPath: string
|
||||
}
|
||||
|
||||
export function DesktopMegaMenu({ items, lang, currentPath }: DesktopMegaMenuProps) {
|
||||
const [openId, setOpenId] = useState<string | null>(null)
|
||||
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
const handleOpen = (id: string) => {
|
||||
if (closeTimer.current) clearTimeout(closeTimer.current)
|
||||
setOpenId(id)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
closeTimer.current = setTimeout(() => setOpenId(null), 150)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="hidden space-x-7 text-sm font-medium uppercase tracking-wide md:flex">
|
||||
{items.map((item, index) => {
|
||||
const uniqueId = item.nodeId || item.id || `menu-${item.url || index}`
|
||||
const isOpen = openId === uniqueId
|
||||
const hasColumns = !!item.columns?.length
|
||||
const isActive = item.url ? currentPath === item.url : false
|
||||
|
||||
return (
|
||||
<div
|
||||
key={uniqueId}
|
||||
className="relative"
|
||||
onMouseEnter={() => handleOpen(uniqueId)}
|
||||
onMouseLeave={handleClose}
|
||||
>
|
||||
<Link
|
||||
href={item.url || '#'}
|
||||
className={`flex items-center gap-1 border-b py-2 transition-colors ${
|
||||
isOpen || isActive
|
||||
? 'border-pop-pink text-pop-black'
|
||||
: 'border-transparent text-pop-black/65 hover:border-pop-pink hover:text-pop-black'
|
||||
}`}
|
||||
>
|
||||
{getLocalizedMenuText(item.label, lang)}
|
||||
{hasColumns && (
|
||||
<svg
|
||||
className={`h-3 w-3 opacity-50 transition-transform duration-300 ${isOpen ? '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>
|
||||
)}
|
||||
</Link>
|
||||
|
||||
{hasColumns && (
|
||||
<div
|
||||
className={`fixed left-0 right-0 z-50 transition-opacity duration-200 ${
|
||||
isOpen ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0'
|
||||
}`}
|
||||
style={{ top: 'var(--navbar-bottom, 64px)' }}
|
||||
onMouseEnter={() => handleOpen(uniqueId)}
|
||||
onMouseLeave={handleClose}
|
||||
>
|
||||
<div className="w-full cursor-default border-b border-black/5 bg-white/95 shadow-brutal backdrop-blur-xl">
|
||||
<div className="mx-auto flex max-w-7xl justify-center gap-16 px-6 py-10">
|
||||
{item.columns!.map((column, columnIndex) => (
|
||||
<div key={columnIndex} className="flex min-w-[140px] flex-col text-left">
|
||||
<h4 className="mb-4 text-[10px] font-bold uppercase tracking-widest text-pop-black/50">
|
||||
{getLocalizedMenuText(column.title, lang)}
|
||||
</h4>
|
||||
<ul className="space-y-3">
|
||||
{column.links.map((link, linkIndex) => (
|
||||
<li key={linkIndex}>
|
||||
<Link
|
||||
href={link.url || '#'}
|
||||
className="block text-sm font-normal normal-case tracking-normal text-pop-black/65 transition-colors hover:text-pop-pink"
|
||||
>
|
||||
{getLocalizedMenuText(link.label, lang)}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { ChevronLeft, ChevronRight, Globe, X } from 'lucide-react'
|
||||
import type { MegaMenuPayload, MenuColumn, MenuLink } from './menuTypes'
|
||||
import { getLocalizedMenuText } from './menuTypes'
|
||||
|
||||
type MenuNode =
|
||||
| { type: 'root'; data: MegaMenuPayload[]; title: string | Record<string, string> }
|
||||
| { type: 'mega'; data: MenuColumn[]; title: string | Record<string, string> }
|
||||
| { type: 'links'; data: MenuLink[]; title: string | Record<string, string> }
|
||||
|
||||
interface MobileDrawerProps {
|
||||
isOpen: boolean
|
||||
items: MegaMenuPayload[]
|
||||
lang: string
|
||||
customerName?: string | null
|
||||
onClose: () => void
|
||||
onOpenLang: () => void
|
||||
onLogin: () => void
|
||||
onLogout: () => void
|
||||
}
|
||||
|
||||
export function MobileDrawer({
|
||||
isOpen,
|
||||
items,
|
||||
lang,
|
||||
customerName,
|
||||
onClose,
|
||||
onOpenLang,
|
||||
onLogin,
|
||||
onLogout,
|
||||
}: MobileDrawerProps) {
|
||||
const [stack, setStack] = useState<MenuNode[]>([{ type: 'root', data: items, title: 'Menu' }])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
const timer = setTimeout(() => {
|
||||
setStack([{ type: 'root', data: items, title: 'Menu' }])
|
||||
}, 300)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setStack((current) => {
|
||||
if (current.length === 1 && current[0].data !== items) {
|
||||
return [{ type: 'root', data: items, title: 'Menu' }]
|
||||
}
|
||||
return current
|
||||
})
|
||||
}, 0)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [isOpen, items])
|
||||
|
||||
const pushView = (node: MenuNode) => setStack((prev) => [...prev, node])
|
||||
const popView = () => setStack((prev) => (prev.length > 1 ? prev.slice(0, -1) : prev))
|
||||
|
||||
const linkClass = 'block w-full py-4 text-left text-sm font-medium uppercase tracking-wide text-pop-black/65 transition-colors hover:text-pop-pink'
|
||||
const nestedLinkClass = 'block w-full py-2 text-left text-xs uppercase tracking-widest text-pop-black/65 transition-colors hover:text-pop-pink'
|
||||
|
||||
const renderView = (view: MenuNode) => {
|
||||
if (view.type === 'root') {
|
||||
return (
|
||||
<div className="space-y-1 px-6 py-2">
|
||||
{view.data.map((item, index) => (
|
||||
<div key={item.id || item.nodeId || `menu-root-${index}`} className="border-b border-pop-black/10 last:border-0">
|
||||
{item.columns?.length ? (
|
||||
<button
|
||||
onClick={() => pushView({ type: 'mega', data: item.columns!, title: item.label })}
|
||||
className="group flex w-full items-center justify-between py-4 text-sm font-medium uppercase tracking-wide text-pop-black/65 transition-colors hover:text-pop-pink"
|
||||
>
|
||||
<span>{getLocalizedMenuText(item.label, lang)}</span>
|
||||
<ChevronRight className="h-4 w-4 text-pop-black/35 transition-colors group-hover:text-pop-pink" />
|
||||
</button>
|
||||
) : (
|
||||
<Link href={item.url || '#'} onClick={onClose} className={linkClass}>
|
||||
{getLocalizedMenuText(item.label, lang)}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{customerName && (
|
||||
<div className="border-b border-pop-black/10">
|
||||
<Link href="/profile" onClick={onClose} className={linkClass}>
|
||||
My Profile
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (view.type === 'mega') {
|
||||
return (
|
||||
<div className="px-6 py-4">
|
||||
{view.data.map((column, columnIndex) => (
|
||||
<div key={columnIndex} className="mb-8 last:mb-0">
|
||||
<h4 className="mb-4 text-[10px] font-bold uppercase tracking-widest text-pop-black/45">
|
||||
{getLocalizedMenuText(column.title, lang)}
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{column.links.map((link, linkIndex) => (
|
||||
<div key={linkIndex}>
|
||||
{link.links?.length ? (
|
||||
<button
|
||||
onClick={() => pushView({ type: 'links', data: link.links!, title: link.label })}
|
||||
className="group flex w-full items-center justify-between py-2 text-xs uppercase tracking-widest text-pop-black/65 transition-colors hover:text-pop-pink"
|
||||
>
|
||||
<span>{getLocalizedMenuText(link.label, lang)}</span>
|
||||
<ChevronRight className="h-4 w-4 text-pop-black/35 transition-colors group-hover:text-pop-pink" />
|
||||
</button>
|
||||
) : (
|
||||
<Link href={link.url || '#'} onClick={onClose} className={nestedLinkClass}>
|
||||
{getLocalizedMenuText(link.label, lang)}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2 px-6 py-4">
|
||||
{view.data.map((link, linkIndex) => (
|
||||
<div key={linkIndex}>
|
||||
{link.links?.length ? (
|
||||
<button
|
||||
onClick={() => pushView({ type: 'links', data: link.links!, title: link.label })}
|
||||
className="group flex w-full items-center justify-between py-2 text-xs uppercase tracking-widest text-pop-black/65 transition-colors hover:text-pop-pink"
|
||||
>
|
||||
<span>{getLocalizedMenuText(link.label, lang)}</span>
|
||||
<ChevronRight className="h-4 w-4 text-pop-black/35 transition-colors group-hover:text-pop-pink" />
|
||||
</button>
|
||||
) : (
|
||||
<Link href={link.url || '#'} onClick={onClose} className={nestedLinkClass}>
|
||||
{getLocalizedMenuText(link.label, lang)}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`fixed inset-0 z-[60] transition-all duration-300 md:hidden ${isOpen ? 'visible' : 'invisible'}`}>
|
||||
<div
|
||||
className={`absolute inset-0 bg-pop-black/40 backdrop-blur-sm transition-opacity duration-300 ${isOpen ? 'opacity-100' : 'opacity-0'}`}
|
||||
onClick={onClose}
|
||||
/>
|
||||
<div
|
||||
className={`absolute right-0 top-0 flex h-full w-80 max-w-[85vw] flex-col bg-white shadow-2xl transition-transform duration-300 ${
|
||||
isOpen ? 'translate-x-0' : 'translate-x-full'
|
||||
}`}
|
||||
>
|
||||
<div className="z-10 flex shrink-0 items-center justify-between border-b border-pop-black/10 bg-white px-6 py-5">
|
||||
<span className="text-sm font-bold uppercase tracking-widest text-pop-black">Menu</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={onOpenLang}
|
||||
className="flex shrink-0 items-center gap-1 rounded-md border border-pop-pink/30 bg-pop-bg px-2.5 py-1 text-[10px] font-bold uppercase text-pop-pink"
|
||||
>
|
||||
<Globe className="h-3 w-3" />
|
||||
{lang.toUpperCase()}
|
||||
</button>
|
||||
<button onClick={onClose} className="rounded-full p-1.5 text-pop-black/65 transition hover:bg-pop-bg hover:text-pop-pink">
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative flex-1 overflow-hidden bg-white">
|
||||
{stack.map((view, index) => {
|
||||
const isCurrent = index === stack.length - 1
|
||||
const isPast = index < stack.length - 1
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={`absolute inset-0 flex flex-col bg-white transition-transform duration-300 ease-in-out ${
|
||||
isCurrent ? 'translate-x-0' : isPast ? '-translate-x-full' : 'translate-x-full'
|
||||
}`}
|
||||
>
|
||||
{index > 0 && (
|
||||
<button
|
||||
onClick={popView}
|
||||
className="flex shrink-0 items-center border-b border-pop-black/5 px-6 py-4 text-xs font-bold uppercase tracking-widest text-pop-pink transition hover:bg-pop-bg"
|
||||
>
|
||||
<ChevronLeft className="mr-2 h-4 w-4" />
|
||||
Back to {getLocalizedMenuText(stack[index - 1].title, lang)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{index > 0 && (
|
||||
<div className="shrink-0 px-6 py-5">
|
||||
<h3 className="text-lg font-bold uppercase tracking-widest text-pop-black">{getLocalizedMenuText(view.title, lang)}</h3>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1 overflow-y-auto pb-6">{renderView(view)}</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="z-10 shrink-0 space-y-3 border-t border-pop-black/10 bg-white px-6 pb-8 pt-6">
|
||||
<Link
|
||||
href="/shop"
|
||||
onClick={onClose}
|
||||
className="block w-full rounded-xl bg-pop-black py-3.5 text-center text-xs font-bold uppercase tracking-widest text-white shadow-sm transition-colors hover:bg-pop-pink"
|
||||
>
|
||||
Shop Now
|
||||
</Link>
|
||||
{customerName ? (
|
||||
<button
|
||||
onClick={() => {
|
||||
onLogout()
|
||||
onClose()
|
||||
}}
|
||||
className="block w-full rounded-xl border border-rose-500/20 py-3.5 text-center text-xs font-bold uppercase tracking-widest text-rose-500 transition-colors hover:bg-rose-50"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => {
|
||||
onLogin()
|
||||
onClose()
|
||||
}}
|
||||
className="block w-full rounded-xl border border-[#00B900]/50 py-3.5 text-center text-xs font-bold uppercase tracking-widest text-[#00B900] transition-colors hover:bg-[#00B900]/10"
|
||||
>
|
||||
Login with LINE
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export interface MenuLink {
|
||||
label: Record<string, string> | string
|
||||
url?: string
|
||||
links?: MenuLink[]
|
||||
}
|
||||
|
||||
export interface MenuColumn {
|
||||
title: Record<string, string> | string
|
||||
links: MenuLink[]
|
||||
}
|
||||
|
||||
export interface MegaMenuPayload {
|
||||
id?: string
|
||||
nodeId?: string
|
||||
label: Record<string, string> | string
|
||||
url?: string
|
||||
columns?: MenuColumn[]
|
||||
}
|
||||
|
||||
export function getLocalizedMenuText(text: Record<string, string> | string | undefined, lang: string) {
|
||||
if (typeof text === 'string') return text
|
||||
if (!text) return ''
|
||||
return text[lang] || text.en || text.th || Object.values(text)[0] || ''
|
||||
}
|
||||
Reference in new issue
Block a user