x
This commit is contained in:
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { ComponentType } from 'react';
|
||||
import type { CustomSectionPayload } from '@homaroma/types';
|
||||
import BakeryCategories from '../components/custom/BakeryCategories';
|
||||
|
||||
export type CustomBlockComponentProps = {
|
||||
props: CustomSectionPayload['props'];
|
||||
};
|
||||
|
||||
export type CustomBlockComponent = ComponentType<CustomBlockComponentProps>;
|
||||
|
||||
export const customRegistry: Record<string, CustomBlockComponent> = {
|
||||
BakeryCategories,
|
||||
};
|
||||
Reference in new issue
Block a user