This commit is contained in:
Your Name committed 2026-10-01 06:59:38 +07:00
1 parent 0ceee3dbff
commit 2d39edbd57
18 files changed
+804 -153

No files matched your search

@@ -4,7 +4,7 @@ import { notFound } from 'next/navigation'
import type { Metadata } from 'next'
import { generateSeoMetadata } from '@/lib/seo'
import type { Category, Product } from '@homaroma/types'
import { getCategoryImage } from '@homaroma/types'
import { getCategoryImage, getCategoryMedia, resolveCategoryMediaUrl } from '@homaroma/types'
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
import ProductCard from '@/components/ProductCard'
import { type ApiResult, fetchJsonResult, getCategoriesResult, getCmsPageResult, getStoreSettings } from '@/lib/cms'
@@ -37,18 +37,21 @@ export async function generateMetadata({ params }: { params: Promise<{ id: strin
: (brandName ? `Shop ${brandName} category products.` : 'Shop category products.')
const category = result.ok ? result.data.category : undefined
const categoryImage = category?.imageUrl || (categoryName ? getCategoryImage(categoryName) : undefined) || settings?.defaultOgImage
const categoryImage = category
? resolveCategoryMediaUrl(category, 'social_share', categoryName ? getCategoryImage(categoryName) : '')
: undefined
const socialImage = categoryImage || settings?.defaultOgImage
return generateSeoMetadata({
title: categoryName ? `${categoryName}` : 'Category',
description,
path: `/category/${id}`,
lang,
openGraph: categoryImage
openGraph: socialImage
? {
title: categoryName || undefined,
description,
images: [categoryImage],
images: [socialImage],
}
: undefined,
})
@@ -93,7 +96,13 @@ export default async function CategoryPage({ params }: { params: Promise<{ id: s
const category = categoryProductsResult.data.category
const products = categoryProductsResult.data.data ?? []
const categoryName = resolveLocalized(category.name, lang)
const categoryImageUrl = category.imageUrl || getCategoryImage(categoryName)
const fallbackImageUrl = getCategoryImage(categoryName)
const categoryThumbnailUrl = resolveCategoryMediaUrl(category, 'thumbnail', fallbackImageUrl)
const heroDesktopUrl = resolveCategoryMediaUrl(category, 'hero_desktop', fallbackImageUrl)
const heroMobileUrl = resolveCategoryMediaUrl(category, 'hero_mobile', heroDesktopUrl)
const heroDesktopMedia = getCategoryMedia(category, 'hero_desktop')
const heroMobileMedia = getCategoryMedia(category, 'hero_mobile')
const hasArtDirectedHero = Boolean(category.media?.some((item) => item.role === 'hero_desktop' || item.role === 'hero_mobile'))
const layoutType = category.layoutType || 'split_showcase'
// Determine Product Grid Class based on layout archetype
@@ -114,19 +123,30 @@ export default async function CategoryPage({ params }: { params: Promise<{ id: s
{announcementSections.length > 0 && (
<DynamicPageRenderer sections={announcementSections} categories={categories} lang={lang} />
)}
{heroSections.length > 0 ? (
{heroSections.length > 0 && !(layoutType === 'editorial_hero' && hasArtDirectedHero) ? (
<DynamicPageRenderer sections={heroSections} categories={categories} lang={lang} />
) : layoutType === 'editorial_hero' ? (
/* ── Archetype 2: Editorial Hero Layout ── */
<section className="relative overflow-hidden min-h-[380px] md:min-h-[460px] flex items-center justify-center border-b border-line-primary/10 bg-black">
{categoryImageUrl && (
{heroDesktopUrl && (
<div className="absolute inset-0 z-0">
<Image
src={categoryImageUrl}
src={heroDesktopUrl}
alt={categoryName}
fill
unoptimized
className="object-cover object-center filter brightness-[0.45] saturate-125 scale-105"
sizes="(min-width: 768px) 100vw, 1px"
className="hidden object-cover filter brightness-[0.45] saturate-125 scale-105 md:block"
style={{ objectPosition: `${(heroDesktopMedia?.focalX ?? 0.5) * 100}% ${(heroDesktopMedia?.focalY ?? 0.5) * 100}%` }}
/>
<Image
src={heroMobileUrl}
alt={categoryName}
fill
unoptimized
sizes="(max-width: 767px) 100vw, 1px"
className="object-cover filter brightness-[0.45] saturate-125 scale-105 md:hidden"
style={{ objectPosition: `${(heroMobileMedia?.focalX ?? 0.5) * 100}% ${(heroMobileMedia?.focalY ?? 0.5) * 100}%` }}
/>
<div className="absolute inset-0 bg-gradient-to-t from-black via-black/70 to-black/40" />
</div>
@@ -201,7 +221,7 @@ export default async function CategoryPage({ params }: { params: Promise<{ id: s
</div>
<div className="relative w-48 h-48 md:w-72 md:h-72 shrink-0 rounded-3xl overflow-hidden shadow-lg border border-line-primary/5">
<Image
src={categoryImageUrl}
src={categoryThumbnailUrl}
alt={categoryName}
fill
unoptimized