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

@@ -1,4 +1,4 @@
import type { PageSection, Category, AccordionSectionPayload, ContactInfoSectionPayload, ContactFormSectionPayload } from '@homaroma/types';
import type { PageSection, Category, AccordionSectionPayload, ContactInfoSectionPayload, ContactFormSectionPayload, CustomSectionPayload } from '@homaroma/types';
import DynamicHero from './DynamicHero';
import DynamicHeroV2 from './DynamicHeroV2';
import DynamicUspBar from './DynamicUspBar';
@@ -19,6 +19,7 @@ import AccordionSection from './sections/AccordionSection';
import ContactInfoSection from './sections/ContactInfoSection';
import ContactFormSection from './sections/ContactFormSection';
import ScrollReveal from './ScrollReveal';
import { customRegistry } from '../config/customRegistry';
interface DynamicPageRendererProps {
sections: PageSection[];
@@ -93,6 +94,21 @@ export default function DynamicPageRenderer({
return <ScrollReveal key={section.id}><ContactInfoSection payload={section.payload as ContactInfoSectionPayload & { title?: string }} /></ScrollReveal>;
case 'CONTACT_FORM':
return <ScrollReveal key={section.id}><ContactFormSection payload={section.payload as ContactFormSectionPayload} /></ScrollReveal>;
case 'CUSTOM': {
const payload = section.payload as CustomSectionPayload;
const CustomComponent = customRegistry[payload.componentId];
if (!CustomComponent) {
console.warn(`[DynamicPageRenderer] Unknown custom component: ${payload.componentId}`);
return null;
}
return (
<ScrollReveal key={section.id}>
<CustomComponent props={payload.props ?? {}} />
</ScrollReveal>
);
}
default:
console.warn(`[DynamicPageRenderer] Unknown block type: ${section.type}`);
return null;
@@ -0,0 +1,127 @@
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import type { CustomBlockComponentProps } from '../../config/customRegistry';
import { getLocalizedField } from '../../utils/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 = getLocalizedField(value, 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 }: CustomBlockComponentProps) {
const { i18n } = useTranslation();
const lang = i18n.language;
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-pop-bg">
<div className="ui-section-inner space-y-12">
<div className="mx-auto max-w-2xl space-y-4 text-center">
<h2 className="text-4xl font-bold tracking-tight text-pop-black md:text-5xl">
{title}
</h2>
<p className="ui-section-subtitle">{subtitle}</p>
</div>
<div className={`grid gap-5 ${gridClass}`}>
{items.map((item) => (
<Link
key={item.title}
to={item.href}
className="ui-shell-card ui-shell-card-hover group overflow-hidden"
>
<div className="relative flex aspect-[4/3] items-center justify-center overflow-hidden bg-pop-black/5">
{item.imageUrl ? (
<img
src={item.imageUrl}
alt={item.title}
className="h-full w-full object-cover transition duration-700 group-hover:scale-105"
/>
) : (
<span className="font-serif text-6xl font-semibold text-pop-black/20">
{item.title.slice(0, 1)}
</span>
)}
</div>
<div className="space-y-3 p-6">
<h3 className="text-xl font-semibold text-pop-black">{item.title}</h3>
<p className="text-sm leading-relaxed text-pop-black/70">{item.description}</p>
</div>
</Link>
))}
</div>
</div>
</section>
);
}