fix update Section Block

This commit is contained in:
Your Name committed 2026-06-17 21:08:24 -04:00
1 parent 996dfdabd0
commit 6f67ee6077
47 files changed
+1792 -197

No files matched your search

@@ -5,6 +5,8 @@ import Link from 'next/link'
import { getCategoryImage, formatPrice, type Product } from '@homaroma/types'
import { useCartStore } from '../store/cartStore'
import { resolveLocalized } from '../lib/localization'
import { useTranslation } from 'react-i18next'
import { useLang } from '../hooks/useLang'
interface Props {
product: Product
@@ -13,11 +15,13 @@ interface Props {
export default function AddToCartSection({ product }: Props) {
const [selectedVariant, setSelectedVariant] = useState(0)
const { addToCart, toggleCart } = useCartStore()
const { t } = useTranslation()
const lang = useLang()
const currentVariant = product.variants[selectedVariant]
const categoryName = resolveLocalized(product.category.name)
const productName = resolveLocalized(product.name) || product.scent.name
const description = resolveLocalized(product.description)
const categoryName = resolveLocalized(product.category.name, lang)
const productName = resolveLocalized(product.name, lang) || product.scent.name
const description = resolveLocalized(product.description, lang)
const handleAddToCart = () => {
if (!currentVariant) return
+133 -29
View File
@@ -1,46 +1,135 @@
"use client";
import Link from 'next/link'
import type { Category } from '@homaroma/types'
import type { Category, MenuItem, StoreSettings } from '@homaroma/types'
import { useTranslation } from 'react-i18next'
import { resolveLocalized } from '../lib/localization'
import { useLang } from '../hooks/useLang'
interface FooterProps {
categories?: Category[]
exploreMenu?: MenuItem[]
supportMenu?: MenuItem[]
settings?: StoreSettings | null
}
export default function Footer({ categories = [] }: FooterProps) {
export default function Footer({
categories = [],
exploreMenu = [],
supportMenu = [],
settings = null,
}: FooterProps) {
const { t } = useTranslation()
const lang = useLang()
// Translate a menu item label using its translations array or i18n key map
const translateMenuLabel = (item: MenuItem): string => {
// Use outer `lang` from closure — hooks cannot be called inside nested functions
if (lang !== 'en' && item.translations && item.translations.length > 0) {
const trans = item.translations.find((tr: any) => tr.langCode === lang)
if (trans?.label) return trans.label
}
const map: Record<string, string> = {
'Home': 'nav.home',
'Shop': 'nav.shop',
'Scent Guide': 'nav.scentGuide',
'About Us': 'nav.about',
'About': 'nav.about',
'FAQ': 'nav.faq',
'Contact': 'nav.contact',
'Contact Us': 'nav.contact',
'Track Order': 'nav.trackOrder',
'Delivery & Returns': 'nav.delivery',
}
return map[item.label] ? t(map[item.label]) : item.label
}
// Social links — use settings if available, fallback to placeholder buttons
const hasSocialLinks = settings && (settings.facebookUrl || settings.instagramUrl || settings.tiktokUrl)
return (
<footer className="border-t border-gray-200 bg-white px-6 py-20 text-pop-black">
<div className="mx-auto grid max-w-7xl grid-cols-1 gap-12 md:grid-cols-4">
{/* Col 1 — Brand */}
<div className="space-y-6">
<div className="text-2xl font-bold tracking-widest">
HomAroma <span className="text-sm font-semibold italic text-pop-pink">Essential</span>
</div>
<p className="text-sm leading-relaxed text-gray-600">
{t('footer.catchphrase')}
{settings?.footerDescription || t('footer.catchphrase')}
</p>
<div className="flex gap-4">
{['IG', 'FB', 'TW', 'YT'].map((s) => (
<a key={s} href="#" className="flex h-9 w-9 items-center justify-center rounded-full border border-gray-200 bg-pop-bg text-[10px] font-bold text-pop-black transition-all duration-300 hover:border-pop-pink hover:bg-pop-pink hover:text-white">
{s}
</a>
))}
{hasSocialLinks ? (
<>
{settings!.facebookUrl && (
<a
href={settings!.facebookUrl}
target="_blank"
rel="noreferrer"
className="flex h-9 w-9 items-center justify-center rounded-full border border-gray-200 bg-pop-bg text-[10px] font-bold text-pop-black transition-all duration-300 hover:border-pop-pink hover:bg-pop-pink hover:text-white"
>
FB
</a>
)}
{settings!.instagramUrl && (
<a
href={settings!.instagramUrl}
target="_blank"
rel="noreferrer"
className="flex h-9 w-9 items-center justify-center rounded-full border border-gray-200 bg-pop-bg text-[10px] font-bold text-pop-black transition-all duration-300 hover:border-pop-pink hover:bg-pop-pink hover:text-white"
>
IG
</a>
)}
{settings!.tiktokUrl && (
<a
href={settings!.tiktokUrl}
target="_blank"
rel="noreferrer"
className="flex h-9 w-9 items-center justify-center rounded-full border border-gray-200 bg-pop-bg text-[10px] font-bold text-pop-black transition-all duration-300 hover:border-pop-pink hover:bg-pop-pink hover:text-white"
>
TT
</a>
)}
</>
) : (
['IG', 'FB', 'TW', 'YT'].map((s) => (
<a
key={s}
href="#"
className="flex h-9 w-9 items-center justify-center rounded-full border border-gray-200 bg-pop-bg text-[10px] font-bold text-pop-black transition-all duration-300 hover:border-pop-pink hover:bg-pop-pink hover:text-white"
>
{s}
</a>
))
)}
</div>
</div>
{/* Col 2 — Explore */}
<div className="space-y-4">
<h4 className="text-xs font-bold uppercase tracking-widest text-pop-black">{t('footer.explore')}</h4>
<div className="flex flex-col space-y-2 text-sm text-gray-600">
<Link href="/" className="transition-colors hover:text-pop-pink">{t('nav.home')}</Link>
<Link href="/shop" className="transition-colors hover:text-pop-pink">{t('nav.shop')}</Link>
<Link href="/scent-guide" className="transition-colors hover:text-pop-pink">{t('nav.scentGuide')}</Link>
<Link href="/about" className="transition-colors hover:text-pop-pink">{t('nav.about')}</Link>
<Link href="/contact" className="transition-colors hover:text-pop-pink">{t('nav.contact')}</Link>
<Link href="/faq" className="transition-colors hover:text-pop-pink">{t('nav.faq')}</Link>
{exploreMenu.length > 0 ? (
exploreMenu.map((item) => (
<Link key={item.id} href={item.url} className="transition-colors hover:text-pop-pink">
{translateMenuLabel(item)}
</Link>
))
) : (
<>
<Link href="/" className="transition-colors hover:text-pop-pink">{t('nav.home')}</Link>
<Link href="/shop" className="transition-colors hover:text-pop-pink">{t('nav.shop')}</Link>
<Link href="/scent-guide" className="transition-colors hover:text-pop-pink">{t('nav.scentGuide')}</Link>
<Link href="/about" className="transition-colors hover:text-pop-pink">{t('nav.about')}</Link>
<Link href="/contact" className="transition-colors hover:text-pop-pink">{t('nav.contact')}</Link>
<Link href="/faq" className="transition-colors hover:text-pop-pink">{t('nav.faq')}</Link>
</>
)}
</div>
</div>
{/* Col 3 — Collections */}
<div className="space-y-4">
<h4 className="text-xs font-bold uppercase tracking-widest text-pop-black">{t('nav.collections')}</h4>
<div className="flex flex-col space-y-2 text-sm text-gray-600">
@@ -52,23 +141,38 @@ export default function Footer({ categories = [] }: FooterProps) {
</div>
</div>
<div className="space-y-6">
<h4 className="text-xs font-bold uppercase tracking-widest text-pop-black">Newsletter</h4>
<p className="text-xs text-gray-500">Subscribe for exclusive releases and sensory wisdom.</p>
<div className="flex rounded-xl border border-gray-200 bg-pop-bg p-1">
<input
type="email"
placeholder="Your email"
className="w-full border-none bg-transparent p-2 text-sm text-pop-black outline-none placeholder:text-gray-400"
/>
<button className="shrink-0 rounded-lg bg-pop-black px-4 py-2 text-xs font-bold uppercase text-white transition-colors hover:bg-pop-pink">
Join
</button>
{/* Col 4 — Support & Legal */}
<div className="space-y-4">
<h4 className="text-xs font-bold uppercase tracking-widest text-pop-black">
{settings?.footerSupportHeading
? (typeof settings.footerSupportHeading === 'string'
? settings.footerSupportHeading
: settings.footerSupportHeading[lang]
|| settings.footerSupportHeading.en
|| 'Support & Legal')
: t('footer.support', 'Support & Legal')}
</h4>
<div className="flex flex-col space-y-2 text-sm text-gray-600">
{supportMenu.map((item) => (
<Link key={item.id} href={item.url} className="transition-colors hover:text-pop-pink">
{translateMenuLabel(item)}
</Link>
))}
</div>
<div className="pt-4">
<p className="text-[10px] uppercase tracking-widest text-gray-500">
{settings?.footerCopyrightText
? (typeof settings.footerCopyrightText === 'string'
? settings.footerCopyrightText
: settings.footerCopyrightText[lang]
|| settings.footerCopyrightText.en
|| `© ${new Date().getFullYear()} HomAroma. ${t('footer.rights')}`)
: `© ${new Date().getFullYear()} HomAroma. ${t('footer.rights')}`}
</p>
</div>
<p className="text-[10px] uppercase tracking-widest text-gray-500">
© {new Date().getFullYear()} HomAroma. {t('footer.rights')}
</p>
</div>
</div>
</footer>
)
+9 -30
View File
@@ -10,13 +10,14 @@ import { useWishlistStore } from '../store/wishlistStore'
import { useAuthStore } from '../store/authStore'
import { useTranslation } from 'react-i18next'
import { resolveLocalized } from '../lib/localization'
import { API_BASE } from '../lib/api'
import { useLang } from '../hooks/useLang'
import { DesktopMegaMenu } from './navigation/DesktopMegaMenu'
import { MobileDrawer } from './navigation/MobileDrawer'
import type { MegaMenuPayload } from './navigation/menuTypes'
interface NavbarProps {
categories?: Category[]
initialMenu?: MegaMenuPayload[]
}
const LANGUAGES = [
@@ -58,14 +59,14 @@ function createFallbackMenu(categories: Category[]): MegaMenuPayload[] {
]
}
export default function Navbar({ categories = [] }: NavbarProps) {
export default function Navbar({ categories = [], initialMenu = [] }: NavbarProps) {
const [searchOpen, setSearchOpen] = useState(false)
const [mobileOpen, setMobileOpen] = useState(false)
const [langSheetOpen, setLangSheetOpen] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const [scrolled, setScrolled] = useState(false)
const [megaMenu, setMegaMenu] = useState<MegaMenuPayload[]>([])
const [menuLoaded, setMenuLoaded] = useState(false)
// Seed from server-fetched initialMenu — no client flash
const [megaMenu] = useState<MegaMenuPayload[]>(initialMenu)
const navRef = useRef<HTMLElement>(null)
const router = useRouter()
const pathname = usePathname()
@@ -76,10 +77,11 @@ export default function Navbar({ categories = [] }: NavbarProps) {
const logout = useAuthStore((state) => state.logout)
const cartCount = getCartCount()
const { t, i18n } = useTranslation()
const lang = i18n.language ? i18n.language.substring(0, 2) : 'th'
const lang = useLang()
const fallbackMenu = useMemo(() => createFallbackMenu(categories), [categories])
const menuItems = menuLoaded && megaMenu.length > 0 ? megaMenu : fallbackMenu
// Use server menu if available, fall back to categories-based menu
const menuItems = megaMenu.length > 0 ? megaMenu : fallbackMenu
const changeLanguage = (code: string) => {
i18n.changeLanguage(code)
@@ -96,30 +98,7 @@ export default function Navbar({ categories = [] }: NavbarProps) {
router.push('/profile')
}
useEffect(() => {
let isMounted = true
async function fetchMegaMenu() {
try {
const response = await fetch(`${API_BASE}/menus/main`)
if (!response.ok) throw new Error(`menus/main returned ${response.status}`)
const data = await response.json() as { payload?: MegaMenuPayload[] }
if (isMounted) {
setMegaMenu(Array.isArray(data.payload) ? data.payload : [])
setMenuLoaded(true)
}
} catch (error) {
console.error('[website-menu] Failed to fetch main menu', error)
if (isMounted) setMenuLoaded(true)
}
}
fetchMegaMenu()
return () => {
isMounted = false
}
}, [])
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 20)
@@ -342,7 +321,7 @@ export default function Navbar({ categories = [] }: NavbarProps) {
<p className="mb-3 text-xs font-semibold uppercase tracking-widest text-pop-black/55">{t('nav.search.quickLinks')}</p>
<div className="flex flex-wrap gap-2">
{[
...categories.map((category) => ({ label: resolveLocalized(category.name), to: `/category/${category.id}` })),
...categories.map((category) => ({ label: resolveLocalized(category.name, lang), to: `/category/${category.id}` })),
{ label: t('nav.scentGuide'), to: '/scent-guide' },
{ label: t('nav.search.allProducts'), to: '/shop' },
].map((item) => (
+8 -2
View File
@@ -1,7 +1,11 @@
"use client"
import Link from 'next/link'
import type { Product } from '@homaroma/types'
import { getCategoryImage, formatPrice } from '@homaroma/types'
import { resolveLocalized } from '../lib/localization'
import { useTranslation } from 'react-i18next'
import { useLang } from '../hooks/useLang'
import WishlistButton from './WishlistButton'
interface ProductCardProps {
@@ -9,8 +13,10 @@ interface ProductCardProps {
}
export default function ProductCard({ product }: ProductCardProps) {
const categoryName = resolveLocalized(product.category.name)
const productName = resolveLocalized(product.name) || product.scent.name
const { i18n } = useTranslation()
const lang = useLang()
const categoryName = resolveLocalized(product.category.name, lang)
const productName = resolveLocalized(product.name, lang) || product.scent.name
const imageUrl = product.images?.[0]?.url || product.category.imageUrl || getCategoryImage(categoryName)
const firstPrice = product.variants[0]?.price ?? product.price
@@ -1,14 +1,17 @@
"use client"
import Link from 'next/link'
import { Clock, Mail, MapPin, MessageCircle } from 'lucide-react'
import type { Category, PageSection, Product } from '@homaroma/types'
import { getCategoryImage } from '@homaroma/types'
import ProductCard from '../ProductCard'
import { localizedFromRecord, resolveLocalized, toRecord } from '../../lib/localization'
import { useLang } from '../../hooks/useLang'
type DynamicPageRendererProps = {
sections: PageSection[]
categories: Category[]
lang?: string
lang?: string // optional override; defaults to i18n.language
}
function categoryName(category: Category, lang: string) {
@@ -540,7 +543,10 @@ function ContactInfoSection({ payload }: { payload: Record<string, unknown> }) {
)
}
export default function DynamicPageRenderer({ sections, categories, lang = 'th' }: DynamicPageRendererProps) {
export default function DynamicPageRenderer({ sections, categories, lang: langProp }: DynamicPageRendererProps) {
const i18nLang = useLang()
// Use i18n.language reactively — switches immediately when user changes language
const lang = langProp ?? i18nLang
return (
<div className="flex w-full flex-col">
{sections.map((section) => {