This commit is contained in:
Your Name committed 2026-09-25 07:28:27 +07:00
1 parent 3ac0cb4f53
commit 5e724fae20
17 files changed
+2404 -278

No files matched your search

@@ -0,0 +1,157 @@
import { Swiper, SwiperSlide } from 'swiper/react';
import { Autoplay, Pagination, Navigation } from 'swiper/modules';
import { Link } from 'react-router-dom';
import type { HeroCarouselSectionPayload } from '@homaroma/types';
import { useTranslation } from 'react-i18next';
import { getLocalizedField } from '../utils/localization';
// Import Swiper styles
import 'swiper/css';
import 'swiper/css/pagination';
import 'swiper/css/navigation';
interface DynamicHeroCarouselProps {
payload: HeroCarouselSectionPayload;
}
export default function DynamicHeroCarousel({ payload }: DynamicHeroCarouselProps) {
const { i18n } = useTranslation();
const lang = i18n.language;
const slides = (payload.slides || []).map((slide) => ({
...slide,
title: getLocalizedField(slide.title, lang),
subtitle: getLocalizedField(slide.subtitle, lang),
buttonText: getLocalizedField(slide.buttonText, lang),
alt: getLocalizedField(slide.alt, lang),
}));
if (slides.length === 0) return null;
const autoplay = payload.autoplay !== false;
const interval = payload.interval || 4500;
const showArrows = payload.showArrows !== false;
const showDots = payload.showDots !== false;
const aspectRatio = payload.aspectRatio || '21/7';
const mobileAspectRatio = payload.mobileAspectRatio || '16/9';
const renderTitle = (text: string) => {
if (!text) return null;
return text.split('[').map((part, i) => {
if (part.includes(']')) {
const [accent, rest] = part.split(']');
return (
<span key={i}>
<span className="text-pop-pink font-medium italic">
{accent}
</span>
{rest}
</span>
);
}
return <span key={i}>{part}</span>;
});
};
const getAspectClass = () => {
switch (aspectRatio) {
case '21/6': return 'md:aspect-[21/6]';
case '21/7': return 'md:aspect-[21/7]';
case '21/9': return 'md:aspect-[21/9]';
case '16/5': return 'md:aspect-[16/5]';
case '16/6': return 'md:aspect-[16/6]';
case '4/1': return 'md:aspect-[4/1]';
case '5/1': return 'md:aspect-[5/1]';
default: return 'md:aspect-[21/7]';
}
};
const getMobileAspectClass = () => {
switch (mobileAspectRatio) {
case '16/9': return 'aspect-[16/9]';
case '4/3': return 'aspect-[4/3]';
case '1/1': return 'aspect-[1/1]';
case '21/9': return 'aspect-[21/9]';
default: return 'aspect-[16/9]';
}
};
return (
<section className="relative w-full overflow-hidden bg-pop-bg">
<Swiper
modules={[Autoplay, Pagination, Navigation]}
slidesPerView={1}
loop={slides.length > 1}
autoplay={autoplay ? { delay: interval, disableOnInteraction: false } : false}
pagination={showDots ? { clickable: true, bulletActiveClass: 'swiper-pagination-bullet-active !bg-pop-pink' } : false}
navigation={showArrows}
grabCursor={true}
className="w-full select-none [&_.swiper-button-next]:text-white [&_.swiper-button-prev]:text-white [&_.swiper-button-next]:after:text-sm md:[&_.swiper-button-next]:after:text-base [&_.swiper-button-prev]:after:text-sm md:[&_.swiper-button-prev]:after:text-base [&_.swiper-button-next]:w-8 md:[&_.swiper-button-next]:w-10 [&_.swiper-button-next]:h-8 md:[&_.swiper-button-next]:h-10 [&_.swiper-button-prev]:w-8 md:[&_.swiper-button-prev]:w-10 [&_.swiper-button-prev]:h-8 md:[&_.swiper-button-prev]:h-10 [&_.swiper-button-next]:bg-black/35 [&_.swiper-button-prev]:bg-black/35 [&_.swiper-button-next]:backdrop-blur-sm [&_.swiper-button-prev]:backdrop-blur-sm [&_.swiper-button-next]:rounded-full [&_.swiper-button-prev]:rounded-full [&_.swiper-button-next]:hover:bg-black/60 [&_.swiper-button-prev]:hover:bg-black/60 [&_.swiper-pagination-bullet]:bg-white/60 [&_.swiper-pagination-bullet-active]:!bg-pop-pink [&_.swiper-pagination]:!bottom-2.5 md:[&_.swiper-pagination]:!bottom-4"
>
{slides.map((slide, index) => {
const imageUrl = slide.imageUrl || '';
const mobileImageUrl = slide.mobileImageUrl || imageUrl;
const linkUrl = slide.linkUrl || '';
const altText = slide.alt || slide.title || `Slide ${index + 1}`;
const focalPoint = typeof slide.focalPoint === 'object' && slide.focalPoint ? slide.focalPoint : undefined;
if (!imageUrl) return null;
const slideContent = (
<div className={`relative w-full ${getMobileAspectClass()} ${getAspectClass()} overflow-hidden`}>
{/* Desktop Image */}
<img
src={imageUrl}
alt={altText}
className="hidden md:block absolute inset-0 w-full h-full object-cover"
style={{ objectPosition: focalPoint ? `${focalPoint.x} ${focalPoint.y}` : undefined }}
/>
{/* Mobile Image */}
<img
src={mobileImageUrl}
alt={altText}
className="block md:hidden absolute inset-0 w-full h-full object-cover"
style={{ objectPosition: focalPoint ? `${focalPoint.x} ${focalPoint.y}` : undefined }}
/>
{/* Text Overlay */}
{(slide.title || slide.subtitle || slide.buttonText) && (
<div className="absolute inset-0 z-10 bg-gradient-to-t from-black/70 via-black/20 to-transparent flex flex-col justify-end p-6 md:p-10">
<div className="max-w-xl space-y-1.5 md:space-y-2">
{slide.subtitle && (
<p className="text-[10px] md:text-xs uppercase font-bold tracking-widest text-pop-yellow drop-shadow-md">
{slide.subtitle}
</p>
)}
{slide.title && (
<h3 className="text-lg md:text-3xl font-bold text-white drop-shadow-md leading-tight">
{renderTitle(slide.title)}
</h3>
)}
{slide.buttonText && (
<span className="inline-block mt-2 px-4 py-1.5 bg-pop-pink text-white text-xs font-bold rounded-full shadow-lg hover:bg-pop-red transition-all">
{slide.buttonText}
</span>
)}
</div>
</div>
)}
</div>
);
return (
<SwiperSlide key={index}>
{linkUrl ? (
<Link to={linkUrl} className="block w-full h-full group cursor-pointer">
{slideContent}
</Link>
) : (
slideContent
)}
</SwiperSlide>
);
})}
</Swiper>
</section>
);
}
+46 -22
View File
@@ -2,6 +2,7 @@ import type { PageSection, Category, AccordionSectionPayload, ContactInfoSection
import DynamicHero from './DynamicHero';
import DynamicHeroV2 from './DynamicHeroV2';
import DynamicHeroVideo from './DynamicHeroVideo';
import DynamicHeroCarousel from './DynamicHeroCarousel';
import DynamicUspBar from './DynamicUspBar';
import DynamicCategories from './DynamicCategories';
import DynamicProducts from './DynamicProducts';
@@ -36,24 +37,30 @@ export default function DynamicPageRenderer({
categories,
categoriesLoading
}: DynamicPageRendererProps) {
useEffect(() => {
const HERO_TYPES = new Set([
'HERO',
'HERO_V2',
'HERO_VIDEO',
'HERO_ARTICLE',
'HERO_MINIMAL',
'HERO_SPLIT_MINIMAL',
'HERO_VIDEO_MINIMAL',
]);
const firstActiveSection = sections.find(s => s.isActive && s.type.toUpperCase() !== 'ANNOUNCEMENT_BAR');
if (!firstActiveSection && sections.length > 0 && sections.every(s => !s.isActive || s.type.toUpperCase() === 'ANNOUNCEMENT_BAR')) {
return;
}
const isHero = Boolean(firstActiveSection && HERO_TYPES.has(firstActiveSection.type.toUpperCase()));
const HERO_TYPES = new Set([
'HERO',
'HERO_V2',
'HERO_VIDEO',
'HERO_CAROUSEL',
'HERO_ARTICLE',
'HERO_MINIMAL',
'HERO_SPLIT_MINIMAL',
'HERO_VIDEO_MINIMAL',
]);
const firstActiveSection = sections.find(s => s.isActive && s.type.toUpperCase() !== 'ANNOUNCEMENT_BAR');
const firstPayload = (firstActiveSection?.payload || {}) as Record<string, unknown>;
const defaultSpacing = firstActiveSection?.type.toUpperCase() === 'HERO_CAROUSEL' ? 'below_header' : 'under_header';
const firstHeaderSpacing = typeof firstPayload.headerSpacing === 'string' ? firstPayload.headerSpacing : defaultSpacing;
const isHeroFirst = Boolean(
firstActiveSection &&
HERO_TYPES.has(firstActiveSection.type.toUpperCase()) &&
firstHeaderSpacing === 'under_header'
);
useEffect(() => {
if (typeof document !== 'undefined') {
document.documentElement.dataset.heroFirst = isHero ? 'true' : 'false';
document.documentElement.dataset.heroFirst = isHeroFirst ? 'true' : 'false';
}
return () => {
@@ -61,23 +68,29 @@ export default function DynamicPageRenderer({
document.documentElement.dataset.heroFirst = 'false';
}
};
}, [sections]);
}, [sections, isHeroFirst]);
return (
<div className="flex flex-col w-full">
{sections.map((section) => {
if (!section.isActive) return null;
switch (section.type.toUpperCase()) {
// ไม่ animate — แสดงตั้งแต่ต้น
case 'ANNOUNCEMENT_BAR':
return <DynamicAnnouncementBar key={section.id} payload={section.payload} />;
const isFirstContent = section.id === firstActiveSection?.id;
const needsTopSpacing = isFirstContent && !isHeroFirst;
const rendered = (() => {
switch (section.type.toUpperCase()) {
// ไม่ animate — แสดงตั้งแต่ต้น
case 'ANNOUNCEMENT_BAR':
return <DynamicAnnouncementBar key={section.id} payload={section.payload} />;
case 'HERO':
return <DynamicHero key={section.id} payload={section.payload} />;
case 'HERO_V2':
return <DynamicHeroV2 key={section.id} payload={section.payload} />;
case 'HERO_VIDEO':
return <DynamicHeroVideo key={section.id} payload={section.payload} />;
case 'HERO_CAROUSEL':
return <DynamicHeroCarousel key={section.id} payload={section.payload} />;
case 'USP_BAR':
return <DynamicUspBar key={section.id} payload={section.payload} />;
@@ -148,7 +161,18 @@ export default function DynamicPageRenderer({
console.warn(`[DynamicPageRenderer] Unknown block type: ${section.type}`);
return null;
}
})}
})();
if (needsTopSpacing && rendered) {
return (
<div key={section.id} style={{ paddingTop: 'var(--navbar-bottom, 72px)' }} className="w-full">
{rendered}
</div>
);
}
return rendered;
})}
</div>
);
}
+324 -129
View File
@@ -39,6 +39,8 @@ export default function Navbar({ topOffsetClassName = 'top-0', isTransparent = f
const { megaMenu, fetchMegaMenu } = useMenuStore()
const navRef = useRef<HTMLElement>(null)
const previousScrollY = useRef(0)
const [scrollingDown, setScrollingDown] = useState(false)
const LANGUAGES = [
{ code: 'th', label: 'ไทย', short: 'TH' },
@@ -66,8 +68,19 @@ export default function Navbar({ topOffsetClassName = 'top-0', isTransparent = f
checkAnnouncement()
const timer = setTimeout(checkAnnouncement, 300)
previousScrollY.current = window.scrollY
const onScroll = () => {
setScrolled(window.scrollY > 20)
const currentScrollY = window.scrollY
const delta = currentScrollY - previousScrollY.current
setScrolled(currentScrollY > 20)
if (currentScrollY <= 20) {
setScrollingDown(false)
} else if (delta > 4) {
setScrollingDown(true)
} else if (delta < -4) {
setScrollingDown(false)
}
previousScrollY.current = currentScrollY
checkAnnouncement()
}
window.addEventListener('scroll', onScroll, { passive: true })
@@ -163,155 +176,337 @@ export default function Navbar({ topOffsetClassName = 'top-0', isTransparent = f
return () => observer.disconnect();
}, [location.pathname]);
const shouldBeTransparent = (isTransparent || isHeroFirst) && !scrolled && !mobileOpen && !searchOpen && !langSheetOpen
const headerBehavior =
(settings?.themeTokens as any)?.headerBehavior ||
settings?.headerBehavior ||
'hide_on_scroll'
const mobileHeaderBehavior =
(settings?.themeTokens as any)?.mobileHeaderBehavior ||
settings?.mobileHeaderBehavior ||
headerBehavior
const mobileHeaderStyle =
(settings?.themeTokens as any)?.mobileHeaderStyle ||
settings?.mobileHeaderStyle ||
'logo_center'
const activeHeaderBehavior = isDesktop ? headerBehavior : mobileHeaderBehavior
const isHideOnScroll = activeHeaderBehavior === 'hide_on_scroll'
const isCheckout = location.pathname.startsWith('/checkout')
const hideHeader =
!isCheckout && isHideOnScroll && scrollingDown && !mobileOpen && !searchOpen && !langSheetOpen
const shouldBeTransparent = (isHeroFirst || Boolean(isTransparent && isHeroFirst)) && !scrolled && !mobileOpen && !searchOpen && !langSheetOpen
const isTwoTier = settings?.headerStyle === 'two_tier'
const renderActionButtons = () => (
<div className="flex items-center gap-1.5 md:gap-2 shrink-0">
{/* Search */}
<button
onClick={() => { setSearchOpen(true); setMobileOpen(false); }}
className="group rounded-full p-1.5 text-pop-black/80 transition-all hover:bg-pop-black/5 md:p-2"
aria-label="Search"
>
<svg className="w-4 h-4 group-hover:text-pop-black 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 7 7 0 0114 0z" />
</svg>
</button>
{/* Wishlist */}
<Link
to="/wishlist"
onClick={() => setMobileOpen(false)}
className="group relative rounded-full p-1.5 text-pop-black/80 transition-all hover:bg-pop-black/5 md:p-2"
aria-label="Wishlist"
>
<Heart className="w-4 h-4 group-hover:text-rose-500 transition-colors" />
{wishlistCount > 0 && (
<span className="absolute -top-1 -right-1 bg-rose-500 text-white text-[9px] font-bold w-4 h-4 rounded-full flex items-center justify-center">
{wishlistCount}
</span>
)}
</Link>
{/* Cart */}
<button
onClick={() => { toggleCart(true); setMobileOpen(false); }}
className="group relative rounded-full p-1.5 text-pop-black/80 transition-all hover:bg-pop-black/5 md:p-2"
aria-label="Cart"
>
<ShoppingBag className="w-4 h-4 group-hover:text-pop-black transition-colors" />
{cartCount > 0 && (
<span className="absolute -top-1 -right-1 bg-rose-500 text-white text-[9px] font-bold w-4 h-4 rounded-full flex items-center justify-center">
{cartCount}
</span>
)}
</button>
{/* Language switcher (desktop) */}
{isClientReady && isDesktop && (
<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">
<Globe className="w-3.5 h-3.5" strokeWidth={1.5} />
{LANGUAGES.find(l => i18n.language.startsWith(l.code))?.short || 'TH'}
</button>
<div className="absolute top-full right-0 pt-2 w-36 opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-all duration-300">
<div className="bg-pop-bg/95 backdrop-blur-xl border border-pop-black/10 rounded-xl overflow-hidden shadow-2xl p-1.5 space-y-0.5">
{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-3 py-2.5 text-xs rounded-lg transition-colors font-medium ${isActive ? 'text-pop-black bg-pop-black/10 font-bold' : 'text-pop-black/70 hover:bg-pop-black/5 hover:text-pop-black'}`}
>
<span>{lang.label}</span>
{isActive && <Check className="w-3.5 h-3.5" />}
</button>
)
})}
</div>
</div>
</div>
)}
{/* Auth / Profile (desktop) */}
{isClientReady && isDesktop && (
customer ? (
<div className="relative group flex items-center ml-1">
<Link to="/profile">
<img
src={customer.pictureUrl || `https://ui-avatars.com/api/?name=${customer.displayName}`}
alt="Profile"
className="w-9 h-9 rounded-full border-2 border-pop-black/20 cursor-pointer object-cover animate-fade-in"
/>
</Link>
<div className="absolute top-full right-0 pt-3 w-36 opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-all duration-300">
<div className="bg-pop-bg/95 backdrop-blur-xl border border-pop-black/10 rounded-xl overflow-hidden shadow-xl p-1">
<div className="px-3 py-2 border-b border-pop-black/10 mb-1">
<p className="text-[10px] text-pop-black/60 uppercase tracking-widest leading-tight truncate">{t('nav.signedInAs', 'Signed in as')}</p>
<p className="text-xs text-pop-black font-bold truncate">{customer.displayName}</p>
</div>
<Link
to="/profile"
className="block px-3 py-2 text-xs uppercase tracking-widest text-pop-black/70 hover:text-pop-black hover:bg-pop-black/5 rounded-lg transition-colors font-bold"
>
My Profile
</Link>
<button
onClick={logoutLiff}
className="w-full text-left px-3 py-2 text-xs uppercase tracking-widest text-rose-500 hover:text-rose-600 hover:bg-rose-50 rounded-lg transition-colors font-bold"
>
{t('nav.logout', 'Logout')}
</button>
</div>
</div>
</div>
) : (
<button
onClick={loginLiff}
className="ml-2 px-4 py-2 rounded-full text-xs font-bold uppercase tracking-widest bg-[#00B900] text-white hover:bg-[#009900] transition-all duration-300 shadow-sm"
>
{t('nav.login', 'LINE Login')}
</button>
)
)}
</div>
)
const renderMobileActionButtons = (includeSearch = true) => (
<div className="flex items-center gap-1.5 shrink-0">
{includeSearch && (
<button
onClick={() => { setSearchOpen(true); setMobileOpen(false); }}
className="group rounded-full p-1.5 text-pop-black/80 transition-all hover:bg-pop-black/5"
aria-label="Search"
>
<svg className="w-4 h-4 group-hover:text-pop-black 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 7 7 0 0114 0z" />
</svg>
</button>
)}
<Link
to="/wishlist"
onClick={() => setMobileOpen(false)}
className="group relative rounded-full p-1.5 text-pop-black/80 transition-all hover:bg-pop-black/5"
aria-label="Wishlist"
>
<Heart className="w-4 h-4 group-hover:text-rose-500 transition-colors" />
{wishlistCount > 0 && (
<span className="absolute -top-1 -right-1 bg-rose-500 text-white text-[9px] font-bold w-4 h-4 rounded-full flex items-center justify-center">
{wishlistCount}
</span>
)}
</Link>
<button
onClick={() => { toggleCart(true); setMobileOpen(false); }}
className="group relative rounded-full p-1.5 text-pop-black/80 transition-all hover:bg-pop-black/5"
aria-label="Cart"
>
<ShoppingBag className="w-4 h-4 group-hover:text-pop-black transition-colors" />
{cartCount > 0 && (
<span className="absolute -top-1 -right-1 bg-rose-500 text-white text-[9px] font-bold w-4 h-4 rounded-full flex items-center justify-center">
{cartCount}
</span>
)}
</button>
</div>
)
const renderMobileHamburger = () => (
<button
onClick={() => setMobileOpen(prev => !prev)}
className="p-1.5 rounded-full text-pop-black/80 hover:bg-pop-black/5 transition-all"
aria-label={mobileOpen ? 'Close menu' : 'Open menu'}
>
{mobileOpen ? <X className="w-5 h-5" /> : <MenuIcon className="w-5 h-5" />}
</button>
)
const renderMobileNavbar = () => {
if (mobileHeaderStyle === 'logo_left') {
return (
<div className="flex w-full items-center justify-between gap-3 md:hidden">
<div className="flex shrink-0 items-center">
<Link to="/" onClick={() => setMobileOpen(false)} className="text-xl font-bold italic tracking-widest text-pop-black uppercase shrink-0">
{logoUrl ? <img src={logoUrl} alt={brandName} className="h-8 object-contain" /> : brandName}
</Link>
</div>
<div className="flex items-center gap-1.5 shrink-0">
{renderMobileActionButtons(true)}
{renderMobileHamburger()}
</div>
</div>
)
}
if (mobileHeaderStyle === 'search_bar_below') {
return (
<div className="flex w-full flex-col gap-2 md:hidden">
<div className="flex w-full items-center justify-between gap-3">
<div className="flex shrink-0 items-center">
<Link to="/" onClick={() => setMobileOpen(false)} className="text-lg font-bold italic tracking-widest text-pop-black uppercase shrink-0">
{logoUrl ? <img src={logoUrl} alt={brandName} className="h-7 object-contain" /> : brandName}
</Link>
</div>
<div className="flex items-center gap-1.5 shrink-0">
{renderMobileActionButtons(false)}
{renderMobileHamburger()}
</div>
</div>
<form onSubmit={handleSearch} className="relative w-full">
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="ค้นหากลิ่น, หมวดหมู่..."
className="w-full rounded-full border border-pop-black/15 bg-pop-black/[0.04] py-1.5 pl-8 pr-3 text-xs text-pop-black placeholder:text-pop-black/40 focus:outline-none focus:ring-1 focus:ring-pop-black"
/>
<svg className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-pop-black/40" 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>
</form>
</div>
)
}
// Default: 'logo_center'
return (
<div className="flex w-full items-center justify-between md:hidden">
<div className="flex items-center">
{renderMobileHamburger()}
</div>
<div className="flex items-center justify-center text-center">
<Link to="/" onClick={() => setMobileOpen(false)} className="text-xl font-bold italic tracking-widest text-pop-black uppercase shrink-0">
{logoUrl ? <img src={logoUrl} alt={brandName} className="h-8 object-contain mx-auto" /> : brandName}
</Link>
</div>
<div className="flex items-center justify-end">
{renderMobileActionButtons(true)}
</div>
</div>
)
}
return (
<>
<nav
ref={navRef}
className={`fixed ${finalTopClassName} z-50 w-full transition-all duration-300 ${
hideHeader ? 'pointer-events-none -translate-y-full' : 'translate-y-0'
} ${
scrolled
? 'border-b border-pop-black/10 shadow-lg shadow-pop-black/10 backdrop-blur-xl bg-pop-bg/90'
: shouldBeTransparent
? 'border-transparent shadow-none bg-transparent backdrop-blur-none'
: 'border-b border-pop-black/10 shadow-none bg-pop-bg'
} px-4 py-3.5 md:px-6 md:py-4`}
} ${isTwoTier ? 'px-4 py-2.5 md:px-8 md:py-3' : 'px-4 py-3.5 md:px-6 md:py-4'}`}
>
{/* Single-row layout: Logo (left) | Menu (center, desktop) | Actions (right) */}
<div className="w-full flex items-center justify-between gap-4">
{/* Mobile Navigation Bar (< md) */}
{renderMobileNavbar()}
{/* Left: Logo */}
<div className="flex items-center shrink-0">
<Link to="/" onClick={() => setMobileOpen(false)} className="text-xl font-bold italic tracking-widest text-pop-black uppercase shrink-0 md:text-2xl">
{logoUrl ? <img src={logoUrl} alt={brandName} className="h-8 md:h-10 object-contain" /> : brandName}
</Link>
</div>
{/* Desktop Navigation Bar (>= md) */}
{isTwoTier ? (
<div className="hidden md:flex w-full flex-col">
{/* Top Tier: Logo Centered + Actions Right (Desktop) */}
<div className={`flex w-full items-center justify-between transition-all duration-300 md:grid md:grid-cols-3 ${
scrolled ? 'max-h-0 opacity-0 overflow-hidden py-0' : 'max-h-24 py-1.5 opacity-100'
}`}>
<div className="flex items-center" />
{/* Center: Desktop mega menu */}
{isClientReady && isDesktop && (
<div className="flex-1 flex justify-center">
<DesktopMegaMenu items={megaMenu} />
<div className="flex items-center justify-center text-center">
<Link to="/" onClick={() => setMobileOpen(false)} className="text-xl font-bold italic tracking-widest text-pop-black uppercase shrink-0 md:text-2xl">
{logoUrl ? <img src={logoUrl} alt={brandName} className="h-8 md:h-10 object-contain mx-auto" /> : brandName}
</Link>
</div>
<div className="flex items-center justify-end">
{renderActionButtons()}
</div>
</div>
)}
{/* Right: Actions */}
<div className="flex items-center gap-1.5 md:gap-2 shrink-0">
{/* Search — desktop & mobile */}
<button
onClick={() => { setSearchOpen(true); setMobileOpen(false); }}
className="group rounded-full p-1.5 text-pop-black/80 transition-all hover:bg-pop-black/5 md:p-2"
aria-label="Search"
>
<svg className="w-4 h-4 group-hover:text-pop-black 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 7 7 0 0114 0z" />
</svg>
</button>
{/* Bottom Tier: Desktop centered mega menu */}
<div className={`hidden md:flex w-full items-center justify-between transition-all duration-300 ${
scrolled ? 'py-0.5' : 'border-t border-pop-black/5 pt-2 mt-1'
}`}>
<div className={`transition-all duration-300 ${scrolled ? 'w-auto opacity-100' : 'w-0 opacity-0 overflow-hidden'}`}>
<Link to="/" className="text-sm font-bold tracking-widest text-pop-black uppercase">
{logoUrl ? <img src={logoUrl} alt={brandName} className="h-6 w-auto object-contain" /> : brandName}
</Link>
</div>
<Link
to="/wishlist"
onClick={() => setMobileOpen(false)}
className="group relative rounded-full p-1.5 text-pop-black/80 transition-all hover:bg-pop-black/5 md:p-2"
aria-label="Wishlist"
>
<Heart className="w-4 h-4 group-hover:text-rose-500 transition-colors" />
{wishlistCount > 0 && (
<span className="absolute -top-1 -right-1 bg-rose-500 text-white text-[9px] font-bold w-4 h-4 rounded-full flex items-center justify-center">
{wishlistCount}
</span>
)}
</Link>
<button
onClick={() => { toggleCart(true); setMobileOpen(false); }}
className="group relative rounded-full p-1.5 text-pop-black/80 transition-all hover:bg-pop-black/5 md:p-2"
aria-label="Cart"
>
<ShoppingBag className="w-4 h-4 group-hover:text-pop-black transition-colors" />
{cartCount > 0 && (
<span className="absolute -top-1 -right-1 bg-rose-500 text-white text-[9px] font-bold w-4 h-4 rounded-full flex items-center justify-center">
{cartCount}
</span>
)}
</button>
{isClientReady && isDesktop && (
<>
<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">
<Globe className="w-3.5 h-3.5" strokeWidth={1.5} />
{LANGUAGES.find(l => i18n.language.startsWith(l.code))?.short || 'TH'}
</button>
<div className="absolute top-full right-0 pt-2 w-36 opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-all duration-300">
<div className="bg-pop-bg/95 backdrop-blur-xl border border-pop-black/10 rounded-xl overflow-hidden shadow-2xl p-1.5 space-y-0.5">
{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-3 py-2.5 text-xs rounded-lg transition-colors font-medium ${isActive ? 'text-pop-black bg-pop-black/10 font-bold' : 'text-pop-black/70 hover:bg-pop-black/5 hover:text-pop-black'}`}
>
<span>{lang.label}</span>
{isActive && <Check className="w-3.5 h-3.5" />}
</button>
)
})}
</div>
</div>
</div>
{customer ? (
<div className="relative group flex items-center ml-1">
<Link to="/profile">
<img
src={customer.pictureUrl || `https://ui-avatars.com/api/?name=${customer.displayName}`}
alt="Profile"
className="w-9 h-9 rounded-full border-2 border-pop-black/20 cursor-pointer object-cover animate-fade-in"
/>
</Link>
<div className="absolute top-full right-0 pt-3 w-36 opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-all duration-300">
<div className="bg-pop-bg/95 backdrop-blur-xl border border-pop-black/10 rounded-xl overflow-hidden shadow-xl p-1">
<div className="px-3 py-2 border-b border-pop-black/10 mb-1">
<p className="text-[10px] text-pop-black/60 uppercase tracking-widest leading-tight truncate">{t('nav.signedInAs', 'Signed in as')}</p>
<p className="text-xs text-pop-black font-bold truncate">{customer.displayName}</p>
</div>
<Link
to="/profile"
className="block px-3 py-2 text-xs uppercase tracking-widest text-pop-black/70 hover:text-pop-black hover:bg-pop-black/5 rounded-lg transition-colors font-bold"
>
My Profile
</Link>
<button
onClick={logoutLiff}
className="w-full text-left px-3 py-2 text-xs uppercase tracking-widest text-rose-500 hover:text-rose-600 hover:bg-rose-50 rounded-lg transition-colors font-bold"
>
{t('nav.logout', 'Logout')}
</button>
</div>
</div>
</div>
) : (
<button
onClick={loginLiff}
className="ml-2 px-4 py-2 rounded-full text-xs font-bold uppercase tracking-widest bg-[#00B900] text-white hover:bg-[#009900] transition-all duration-300 shadow-sm"
>
{t('nav.login', 'LINE Login')}
</button>
<div className="flex-1 flex justify-center">
{isClientReady && isDesktop && (
<DesktopMegaMenu items={megaMenu} />
)}
</>
</div>
<div className={`transition-all duration-300 ${scrolled ? 'w-8' : 'w-0'}`} />
</div>
</div>
) : (
/* Single-row layout: Logo (left) | Menu (center, desktop) | Actions (right) */
<div className="hidden md:flex w-full items-center justify-between gap-4">
{/* Left: Logo */}
<div className="flex items-center shrink-0">
<Link to="/" onClick={() => setMobileOpen(false)} className="text-xl font-bold italic tracking-widest text-pop-black uppercase shrink-0 md:text-2xl">
{logoUrl ? <img src={logoUrl} alt={brandName} className="h-8 md:h-10 object-contain" /> : brandName}
</Link>
</div>
{/* Center: Desktop mega menu */}
{isClientReady && isDesktop && (
<div className="flex-1 flex justify-center">
<DesktopMegaMenu items={megaMenu} />
</div>
)}
{isClientReady && !isDesktop && (
<button
onClick={() => setMobileOpen(prev => !prev)}
className="p-1.5 rounded-full text-pop-black/80 hover:bg-pop-black/5 transition-all"
aria-label={mobileOpen ? 'Close menu' : 'Open menu'}
>
{mobileOpen ? <X className="w-5 h-5" /> : <MenuIcon className="w-5 h-5" />}
</button>
)}
{/* Right: Actions */}
{renderActionButtons()}
</div>
</div>
)}
</nav>
{isClientReady && !isDesktop && mobileOpen && (
+1 -1
View File
@@ -45,7 +45,7 @@ export default function HomePage() {
return (
<div className="min-h-screen bg-pop-bg">
<Navbar categories={categories} isTransparent={true} />
<Navbar categories={categories} />
{loading ? (
<div className="min-h-[60vh] flex items-center justify-center">