This commit is contained in:
Your Name committed 2026-06-28 21:34:58 -04:00
1 parent 3965f4f4da
commit 70877882b0
82 files changed
+4353 -186

No files matched your search

@@ -1,6 +1,7 @@
"use client"
import Link from '../LocalizedLink'
import Image from 'next/image'
import { useState, useRef } from 'react'
import type { MegaMenuPayload } from './menuTypes'
import { getLocalizedMenuText } from './menuTypes'
@@ -11,17 +12,56 @@ interface DesktopMegaMenuProps {
currentPath: string
}
// Render a 1:1 image using next/image for optimization (website uses CDN images)
function MenuImage({
src,
alt,
placeholder,
className = '',
}: {
src?: string | null
alt: string
placeholder?: string
className?: string
}) {
const [loaded, setLoaded] = useState(false)
if (!src) {
if (!placeholder) return null
return (
<span className="text-text-main/30 text-xs text-center px-3 leading-relaxed">{placeholder}</span>
)
}
return (
<Image
src={src}
alt={alt}
fill
sizes="(max-width: 1024px) 0px, 224px"
className={`object-cover transition-opacity duration-300 ${loaded ? 'opacity-100' : 'opacity-0'} ${className}`}
onLoad={() => setLoaded(true)}
loading="lazy"
/>
)
}
export function DesktopMegaMenu({ items, lang, currentPath }: DesktopMegaMenuProps) {
const [openId, setOpenId] = useState<string | null>(null)
const [hoveredLinkImage, setHoveredLinkImage] = useState<string | null>(null)
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
const handleOpen = (id: string) => {
if (closeTimer.current) clearTimeout(closeTimer.current)
if (openId !== id) setHoveredLinkImage(null)
setOpenId(id)
}
const handleClose = () => {
closeTimer.current = setTimeout(() => setOpenId(null), 150)
closeTimer.current = setTimeout(() => {
setOpenId(null)
setHoveredLinkImage(null)
}, 150)
}
return (
@@ -32,6 +72,10 @@ export function DesktopMegaMenu({ items, lang, currentPath }: DesktopMegaMenuPro
const hasColumns = !!item.columns?.length
const isActive = item.url ? currentPath === item.url : false
const hasAnyColumnIcon = item.columns?.some(col => !!col.iconUrl)
const hasAnyLinkImage = item.columns?.some(col => col.links.some(l => !!l.imageUrl))
const showRightPanel = item.rootImageUrl || hasAnyLinkImage
return (
<div
key={uniqueId}
@@ -70,26 +114,68 @@ export function DesktopMegaMenu({ items, lang, currentPath }: DesktopMegaMenuPro
onMouseLeave={handleClose}
>
<div className="w-full cursor-default border-b border-black/5 bg-surface-card/95 shadow-card 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-text-main/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-text-main/65 transition-colors hover:text-brand-primary"
>
{getLocalizedMenuText(link.label, lang)}
</Link>
</li>
))}
</ul>
<div className="mx-auto flex max-w-7xl items-start gap-10 px-6 py-10">
{/* LEFT: Root menu image (1:1) */}
{item.rootImageUrl && (
<div className="relative hidden h-56 w-56 shrink-0 overflow-hidden rounded-xl border border-black/8 lg:block">
<MenuImage
src={item.rootImageUrl}
alt={getLocalizedMenuText(item.label, lang)}
/>
</div>
))}
)}
{/* CENTER: Columns */}
<div className="flex flex-1 gap-12">
{item.columns!.map((column, columnIndex) => (
<div key={columnIndex} className="flex min-w-[160px] gap-4 text-left items-start">
{/* Column icon (1:1) */}
{hasAnyColumnIcon && (
<div className="relative h-14 w-14 shrink-0 mt-0.5 overflow-hidden rounded-lg border border-black/8 bg-surface-muted">
<MenuImage
src={column.iconUrl}
alt={getLocalizedMenuText(column.title, lang)}
/>
</div>
)}
{/* Title and links */}
<div className="flex flex-col">
<h4 className="mb-3 text-[10px] font-bold uppercase tracking-widest text-text-main/50">
{getLocalizedMenuText(column.title, lang)}
</h4>
<ul className="space-y-2.5">
{column.links.map((link, linkIndex) => (
<li key={linkIndex}>
<Link
href={link.url || '#'}
className="block text-sm font-normal normal-case tracking-normal text-text-main/65 transition-colors duration-150 hover:text-brand-primary"
onMouseEnter={() => setHoveredLinkImage(link.imageUrl || null)}
onMouseLeave={() => setHoveredLinkImage(null)}
>
{getLocalizedMenuText(link.label, lang)}
</Link>
</li>
))}
</ul>
</div>
</div>
))}
</div>
{/* RIGHT: Link hover preview / Root fallback (1:1) */}
{showRightPanel && (
<div className="relative hidden h-56 w-56 shrink-0 overflow-hidden rounded-xl border border-black/8 bg-surface-muted lg:flex items-center justify-center">
<MenuImage
src={hoveredLinkImage ?? item.rootImageUrl}
alt="Preview"
placeholder="Hover a link to preview"
/>
</div>
)}
</div>
</div>
</div>
@@ -2,11 +2,13 @@ export interface MenuLink {
label: Record<string, string> | string
url?: string
links?: MenuLink[]
imageUrl?: string | null // รูปที่แสดงเมื่อ Hover ลิงก์
}
export interface MenuColumn {
title: Record<string, string> | string
links: MenuLink[]
iconUrl?: string | null // รูปประจำคอลัมน์ (1:1)
}
export interface MegaMenuPayload {
@@ -15,6 +17,7 @@ export interface MegaMenuPayload {
label: Record<string, string> | string
url?: string
columns?: MenuColumn[]
rootImageUrl?: string | null // รูปหลักฝั่งซ้ายสุด (1:1)
}
export function getLocalizedMenuText(text: Record<string, string> | string | undefined, lang: string) {