From cb51c0faebf7d0af0a86f7237503a84dd80fe908 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 19 Jun 2026 19:49:51 -0400 Subject: [PATCH] x --- apps/spa/src/components/Navbar.tsx | 28 +++++++++++++-- apps/website/package.json | 11 +++--- apps/website/src/app/[slug]/page.tsx | 15 +++++++- apps/website/src/app/globals.css | 9 +++++ apps/website/src/app/layout.tsx | 2 +- apps/website/src/app/page.tsx | 14 +++++++- apps/website/src/components/Footer.tsx | 2 +- apps/website/src/components/Navbar.tsx | 35 +++++++++++++++---- .../dynamic/DynamicPageRenderer.tsx | 32 +++++++++++++---- .../components/navigation/DesktopMegaMenu.tsx | 2 +- bun.lock | 7 ++-- 11 files changed, 127 insertions(+), 30 deletions(-) diff --git a/apps/spa/src/components/Navbar.tsx b/apps/spa/src/components/Navbar.tsx index 6d43d0f..ab1e9fe 100644 --- a/apps/spa/src/components/Navbar.tsx +++ b/apps/spa/src/components/Navbar.tsx @@ -86,20 +86,42 @@ export default function Navbar({ topOffsetClassName = 'top-0' }: NavbarProps) { // Keep --navbar-bottom in sync so the fixed mega-menu dropdown sits flush below the navbar useEffect(() => { + let animationFrameId: number; + const navElement = navRef.current; + const updateNavBottom = () => { - if (navRef.current) { - const bottom = navRef.current.getBoundingClientRect().bottom + if (navElement) { + const bottom = navElement.getBoundingClientRect().bottom document.documentElement.style.setProperty('--navbar-bottom', `${bottom}px`) } } + updateNavBottom() + window.addEventListener('scroll', updateNavBottom, { passive: true }) window.addEventListener('resize', updateNavBottom) + + const observer = new ResizeObserver(() => { + cancelAnimationFrame(animationFrameId); + animationFrameId = requestAnimationFrame(updateNavBottom); + }); + + if (navElement) { + observer.observe(navElement); + navElement.addEventListener('transitionend', updateNavBottom); + } + observer.observe(document.body); + return () => { window.removeEventListener('scroll', updateNavBottom) window.removeEventListener('resize', updateNavBottom) + observer.disconnect() + if (navElement) { + navElement.removeEventListener('transitionend', updateNavBottom); + } + cancelAnimationFrame(animationFrameId); } - }, []) + }, [topOffsetClassName]) useEffect(() => { setMobileOpen(false) }, [navigate]) useEffect(() => { diff --git a/apps/website/package.json b/apps/website/package.json index 7f86303..b198777 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -9,15 +9,16 @@ "lint": "eslint" }, "dependencies": { + "@homaroma/types": "workspace:*", + "axios": "^1.7.0", + "i18next": "^24.0.0", + "lucide-react": "^0.475.0", "next": "16.2.4", "react": "19.2.4", "react-dom": "19.2.4", - "@homaroma/types": "workspace:*", - "lucide-react": "^0.475.0", - "zustand": "^5.0.0", - "i18next": "^24.0.0", "react-i18next": "^15.0.0", - "axios": "^1.7.0" + "swiper": "^12.2.0", + "zustand": "^5.0.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/apps/website/src/app/[slug]/page.tsx b/apps/website/src/app/[slug]/page.tsx index 142bf16..e5a06a1 100644 --- a/apps/website/src/app/[slug]/page.tsx +++ b/apps/website/src/app/[slug]/page.tsx @@ -3,6 +3,7 @@ import type { Metadata } from 'next' import DynamicPageRenderer from '../../components/dynamic/DynamicPageRenderer' import { getCategoriesResult, getCmsPageResult } from '../../lib/cms' +import { splitCmsLayoutSections } from '../../lib/cms-sections' const lang = 'all' @@ -78,10 +79,22 @@ export default async function CmsCustomPage({ params }: { params: Promise<{ slug console.error(`[website-cms] categories unavailable for CMS page "${slug}": ${categoriesResult.error}`) } + const { announcementSections, heroSections, bottomSections } = splitCmsLayoutSections(sections) + return (
{sections.length > 0 ? ( - + <> + {announcementSections.length > 0 && ( + + )} + {heroSections.length > 0 && ( + + )} + {bottomSections.length > 0 && ( + + )} + ) : (

HomAroma

diff --git a/apps/website/src/app/globals.css b/apps/website/src/app/globals.css index 8f629b2..9b7d500 100644 --- a/apps/website/src/app/globals.css +++ b/apps/website/src/app/globals.css @@ -85,3 +85,12 @@ html { border-radius: 16px; box-shadow: var(--shadow-brutal); } + +/* Announcement Bar Layout Shifting via CSS :has() */ +body:has(.announcement-bar) .global-navbar[data-scrolled="false"] { + top: 44px; /* Adjusts Navbar down when AnnouncementBar is present at the top */ +} + +body:has(.announcement-bar) .global-main { + padding-top: 116px; /* 72px original pt + 44px AnnouncementBar */ +} diff --git a/apps/website/src/app/layout.tsx b/apps/website/src/app/layout.tsx index e1f5762..cdadb5d 100644 --- a/apps/website/src/app/layout.tsx +++ b/apps/website/src/app/layout.tsx @@ -89,7 +89,7 @@ export default async function RootLayout({ -
+
{children}
{sections.length > 0 ? ( - + <> + {announcementSections.length > 0 && ( + + )} + {heroSections.length > 0 && ( + + )} + {bottomSections.length > 0 && ( + + )} + ) : (

diff --git a/apps/website/src/components/Footer.tsx b/apps/website/src/components/Footer.tsx index 55f3175..7f789a3 100644 --- a/apps/website/src/components/Footer.tsx +++ b/apps/website/src/components/Footer.tsx @@ -61,7 +61,7 @@ export default function Footer({ {/* Col 1 — Brand */}

- {brandName} Essential + {brandName}

{resolveLocalized(settings?.footerDescription, effectiveLang) || t('footer.catchphrase')} diff --git a/apps/website/src/components/Navbar.tsx b/apps/website/src/components/Navbar.tsx index aa03243..dc7f281 100644 --- a/apps/website/src/components/Navbar.tsx +++ b/apps/website/src/components/Navbar.tsx @@ -111,22 +111,42 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n }, []) useEffect(() => { + let animationFrameId: number; + const navElement = navRef.current; + const updateNavBottom = () => { - if (navRef.current) { - const bottom = navRef.current.getBoundingClientRect().bottom + if (navElement) { + const bottom = navElement.getBoundingClientRect().bottom document.documentElement.style.setProperty('--navbar-bottom', `${bottom}px`) } } updateNavBottom() + window.addEventListener('scroll', updateNavBottom, { passive: true }) window.addEventListener('resize', updateNavBottom) + const observer = new ResizeObserver(() => { + cancelAnimationFrame(animationFrameId); + animationFrameId = requestAnimationFrame(updateNavBottom); + }); + + if (navElement) { + observer.observe(navElement); + navElement.addEventListener('transitionend', updateNavBottom); + } + observer.observe(document.body); + return () => { window.removeEventListener('scroll', updateNavBottom) window.removeEventListener('resize', updateNavBottom) + observer.disconnect() + if (navElement) { + navElement.removeEventListener('transitionend', updateNavBottom); + } + cancelAnimationFrame(animationFrameId); } - }, []) + }, [scrolled]) useEffect(() => { document.body.style.overflow = (searchOpen || mobileOpen || langSheetOpen) ? 'hidden' : '' @@ -143,7 +163,7 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n } } - 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 iconButtonClass = 'group rounded-full p-2 text-pop-black transition-all hover:bg-pop-pink/10 hover:text-pop-pink md:p-2.5' const activeLanguage = LANGUAGES.find((language) => lang.startsWith(language.code) || language.code.startsWith(lang)) ?? LANGUAGES[0] const brandName = settings?.brandName || 'HomAroma' @@ -151,7 +171,8 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n <>

@@ -262,7 +283,7 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n 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 + {t('nav.login', 'เข้าสู่ระบบด้วย LINE')} )}
diff --git a/apps/website/src/components/dynamic/DynamicPageRenderer.tsx b/apps/website/src/components/dynamic/DynamicPageRenderer.tsx index 38ac3e1..e51d358 100644 --- a/apps/website/src/components/dynamic/DynamicPageRenderer.tsx +++ b/apps/website/src/components/dynamic/DynamicPageRenderer.tsx @@ -7,6 +7,9 @@ import { getCategoryImage } from '@homaroma/types' import ProductCard from '../ProductCard' import { localizedFromRecord, resolveCategoryName, resolveLocalized, toRecord } from '../../lib/localization' import { useLang } from '../../hooks/useLang' +import { Swiper, SwiperSlide } from 'swiper/react' +import { Autoplay } from 'swiper/modules' +import 'swiper/css' type DynamicPageRendererProps = { sections: PageSection[] @@ -68,16 +71,31 @@ function AnnouncementBar({ payload, lang }: { payload: Record, const i18nLang = useLang() const resolvedLang = lang === 'all' ? i18nLang : lang const slides = Array.isArray(payload.slides) ? payload.slides : [] + const backgroundColor = typeof payload.backgroundColor === 'string' ? payload.backgroundColor : undefined + const textColor = typeof payload.textColor === 'string' ? payload.textColor : undefined + const speed = typeof payload.speed === 'number' ? payload.speed : 3000 + if (!slides.length) return null return ( -
- {slides.map((slide, index) => ( - - {index > 0 && /} - {localizedFromRecord({ slide }, 'slide', resolvedLang)} - - ))} +
+ 1} + className="w-full select-none" + > + {slides.map((slide, index) => ( + + + {localizedFromRecord({ slide }, 'slide', resolvedLang)} + + + ))} +
) } diff --git a/apps/website/src/components/navigation/DesktopMegaMenu.tsx b/apps/website/src/components/navigation/DesktopMegaMenu.tsx index dfb7ae7..5a2e59d 100644 --- a/apps/website/src/components/navigation/DesktopMegaMenu.tsx +++ b/apps/website/src/components/navigation/DesktopMegaMenu.tsx @@ -44,7 +44,7 @@ export function DesktopMegaMenu({ items, lang, currentPath }: DesktopMegaMenuPro 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' + : 'border-transparent text-pop-black hover:border-pop-pink hover:text-pop-black' }`} > {getLocalizedMenuText(item.label, lang)} diff --git a/bun.lock b/bun.lock index b5cc45c..cf5b93d 100644 --- a/bun.lock +++ b/bun.lock @@ -127,6 +127,7 @@ "react": "19.2.4", "react-dom": "19.2.4", "react-i18next": "^15.0.0", + "swiper": "^12.2.0", "zustand": "^5.0.0", }, "devDependencies": { @@ -737,7 +738,7 @@ "axe-core": ["axe-core@4.12.0", "", {}, "sha512-FTavr/7Ba0IptwGOPxnQvdyW2tAsdLBMTBXz7rKH6xJ2skpyxpBxyHkDdBs4lf69yRqYpkqCdfhnwS8YULGOmg=="], - "axios": ["axios@1.16.1", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A=="], + "axios": ["axios@1.17.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw=="], "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], @@ -1747,8 +1748,6 @@ "is-bun-module/semver": ["semver@7.8.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg=="], - "mcp-server/axios": ["axios@1.17.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw=="], - "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], @@ -1769,6 +1768,8 @@ "type-is/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + "@liff/send-messages/@line/bot-sdk/axios": ["axios@1.16.1", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A=="], + "@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "@prisma/streams-local/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],