This commit is contained in:
Your Name committed 2026-07-12 19:10:19 -04:00
1 parent 305b593698
commit 2a9dd8ddec
13 files changed
+1231 -4

No files matched your search

@@ -0,0 +1,125 @@
import Image from 'next/image'
import Link from '../LocalizedLink'
import type { CustomBlockComponentProps } from '../../config/customRegistry'
import { resolveLocalized } from '../../lib/localization'
type LocalizedValue = string | Record<string, string>
type BakeryCategory = {
title: LocalizedValue
description?: LocalizedValue
href: string
imageUrl?: string
}
type ResolvedBakeryCategory = {
title: string
description: string
href: string
imageUrl?: string
}
const defaultItems: BakeryCategory[] = [
{
title: 'Breads',
description: 'Daily loaves, sourdough, and soft sandwich breads.',
href: '/shop',
},
{
title: 'Pastries',
description: 'Layered butter pastries for breakfast and gifting.',
href: '/shop',
},
{
title: 'Cakes',
description: 'Celebration cakes and slices for quiet afternoons.',
href: '/shop',
},
{
title: 'Cookies',
description: 'Small-batch cookies with crisp edges and soft centers.',
href: '/shop',
},
]
function readString(value: unknown, fallback: string) {
return typeof value === 'string' && value.trim() ? value : fallback
}
function readLocalized(value: unknown, lang: string, fallback: string) {
const text = resolveLocalized(value as LocalizedValue | null | undefined, lang)
return text.trim() ? text : fallback
}
function readItems(value: unknown, lang: string): ResolvedBakeryCategory[] {
const sourceItems = Array.isArray(value) ? value : defaultItems
const items = sourceItems.reduce<ResolvedBakeryCategory[]>((acc, item) => {
if (!item || typeof item !== 'object' || Array.isArray(item)) return acc
const record = item as Record<string, unknown>
const title = readLocalized(record.title, lang, '')
if (!title) return acc
acc.push({
title,
description: readLocalized(record.description, lang, ''),
href: readString(record.href, '/shop'),
imageUrl: typeof record.imageUrl === 'string' ? record.imageUrl : undefined,
})
return acc
}, [])
return items.length > 0 ? items : readItems(defaultItems, lang)
}
export default function BakeryCategories({ props, lang }: CustomBlockComponentProps) {
const title = readLocalized(props?.title, lang, 'Bakery Categories')
const subtitle = readLocalized(props?.subtitle, lang, 'Explore fresh-baked favorites by collection.')
const layout = readString(props?.layout, 'grid')
const items = readItems(props?.items, lang)
const gridClass = layout === 'horizontal'
? 'grid-cols-1 md:grid-cols-4'
: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4'
return (
<section className="ui-section-band bg-surface-page">
<div className="ui-section-shell space-y-12">
<div className="mx-auto max-w-2xl space-y-4 text-center">
<h2 className="ui-section-title">{title}</h2>
<p className="ui-section-subtitle">{subtitle}</p>
</div>
<div className={`grid gap-5 ${gridClass}`}>
{items.map((item) => (
<Link
key={item.title}
href={item.href}
className="group overflow-hidden rounded-2xl border border-line-primary/20 bg-surface-card shadow-card transition hover:-translate-y-1 hover:shadow-card-hover"
>
<div className="relative flex aspect-[4/3] items-center justify-center overflow-hidden bg-surface-muted">
{item.imageUrl ? (
<Image
src={item.imageUrl}
alt={item.title}
fill
unoptimized
className="object-cover transition duration-700 group-hover:scale-105"
/>
) : (
<span className="font-serif text-6xl font-semibold text-text-main/20">
{item.title.slice(0, 1)}
</span>
)}
</div>
<div className="space-y-3 p-6">
<h3 className="text-xl font-semibold text-text-main">{item.title}</h3>
<p className="text-sm leading-relaxed text-muted-text">{item.description}</p>
</div>
</Link>
))}
</div>
</div>
</section>
)
}
@@ -10,6 +10,7 @@ import ProductCard from '../ProductCard'
import { localizedFromRecord, resolveCategoryName, resolveLocalized, toRecord } from '../../lib/localization'
import { useLang } from '../../hooks/useLang'
import { useTranslation } from 'react-i18next'
import { customRegistry } from '../../config/customRegistry'
import { Swiper, SwiperSlide } from 'swiper/react'
import { Autoplay, Pagination, Navigation } from 'swiper/modules'
import 'swiper/css'
@@ -1127,6 +1128,24 @@ export default function DynamicPageRenderer({ sections, categories, lang: langPr
<ContactFormSection payload={payload} lang={lang} />
</ScrollReveal>
)
case 'CUSTOM': {
const componentId = typeof payload.componentId === 'string' ? payload.componentId : ''
const props = toRecord(payload.props)
const CustomComponent = customRegistry[componentId]
if (!CustomComponent) {
if (process.env.NODE_ENV !== 'production') {
console.warn(`[DynamicPageRenderer] Unknown custom component: ${componentId}`)
}
return null
}
return (
<ScrollReveal key={section.id}>
<CustomComponent props={props} lang={lang} />
</ScrollReveal>
)
}
default:
if (process.env.NODE_ENV !== 'production') {
console.warn(`[DynamicPageRenderer] Unsupported section type: ${section.type}`)
@@ -1137,4 +1156,3 @@ export default function DynamicPageRenderer({ sections, categories, lang: langPr
</div>
)
}