This commit is contained in:
Your Name committed 2026-07-13 20:04:23 -04:00
1 parent de09293051
commit d8cca4fcfe
7 files changed
+876 -1

No files matched your search

@@ -0,0 +1,193 @@
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { ArrowRight, CakeSlice, CheckCircle, Heart, Leaf, Sparkles, type LucideIcon } from 'lucide-react';
import type { CustomBlockComponentProps } from '../../config/customRegistry';
import { getLocalizedField } from '../../utils/localization';
type LocalizedValue = string | Record<string, string>;
type MosaicImage = {
imageUrl: string;
alt?: LocalizedValue;
};
type FeatureItem = {
icon?: string;
title: LocalizedValue;
subtitle?: LocalizedValue;
};
type ResolvedImage = {
imageUrl: string;
alt: string;
};
type ResolvedFeature = {
icon: string;
title: string;
subtitle: string;
};
const iconMap: Record<string, LucideIcon> = {
leaf: Leaf,
cake: CakeSlice,
fresh: CakeSlice,
heart: Heart,
care: Heart,
sparkles: Sparkles,
premium: Sparkles,
check: CheckCircle,
};
const defaultImages: MosaicImage[] = [
{ imageUrl: '', alt: 'Dessert preparation' },
{ imageUrl: '', alt: 'Fresh bakery' },
{ imageUrl: '', alt: 'Our team' },
];
const defaultFeatures: FeatureItem[] = [
{ icon: 'leaf', title: 'Quality ingredients' },
{ icon: 'cake', title: 'Fresh daily' },
{ icon: 'heart', title: 'Made with care' },
];
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 readImages(value: unknown, lang: string): ResolvedImage[] {
const sourceImages = Array.isArray(value) ? value : defaultImages;
return sourceImages.reduce<ResolvedImage[]>((acc, item, index) => {
if (!item || typeof item !== 'object' || Array.isArray(item)) return acc;
const record = item as Record<string, unknown>;
const imageUrl = readString(record.imageUrl, '');
const alt = readLocalized(record.alt, lang, `Story image ${index + 1}`);
acc.push({ imageUrl, alt });
return acc;
}, []).slice(0, 3);
}
function readFeatures(value: unknown, lang: string): ResolvedFeature[] {
const sourceFeatures = Array.isArray(value) ? value : defaultFeatures;
return sourceFeatures.reduce<ResolvedFeature[]>((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({
icon: readString(record.icon, 'check'),
title,
subtitle: readLocalized(record.subtitle, lang, ''),
});
return acc;
}, []).slice(0, 4);
}
function StoryImage({ image, index }: { image: ResolvedImage; index: number }) {
const className = index === 0
? 'lg:col-span-3 lg:aspect-[4/3]'
: 'lg:col-span-2 lg:aspect-[4/3]';
return (
<div className={`relative aspect-[4/3] overflow-hidden rounded-2xl bg-pop-black/5 shadow-sm ${className}`}>
{image.imageUrl ? (
<img
src={image.imageUrl}
alt={image.alt}
className="h-full w-full object-cover transition duration-700 hover:scale-105"
/>
) : (
<div className="flex h-full w-full items-center justify-center text-sm font-semibold text-pop-black/60">
{image.alt}
</div>
)}
</div>
);
}
export default function StoryFeatureMosaic({ props }: CustomBlockComponentProps) {
const { i18n } = useTranslation();
const lang = i18n.language;
const title = readLocalized(props?.title, lang, 'Our Story');
const subtitle = readLocalized(props?.subtitle, lang, 'From craft to happiness in every bite');
const description = readLocalized(props?.description, lang, '');
const buttonText = readLocalized(props?.buttonText, lang, '');
const buttonHref = readString(props?.buttonHref, '');
const images = readImages(props?.images, lang);
const features = readFeatures(props?.features, lang);
return (
<section className="ui-section-band bg-pop-bg">
<div className="ui-section-inner">
<div className="grid gap-8 lg:grid-cols-12 lg:items-center">
<div className="space-y-4 lg:col-span-3">
<div className="space-y-2">
{subtitle && (
<p className="text-sm font-semibold text-pop-pink md:text-base">
{subtitle}
</p>
)}
<h2 className="text-3xl font-semibold leading-tight text-pop-black md:text-4xl">
{title}
</h2>
</div>
{description && (
<p className="whitespace-pre-line text-sm leading-7 text-pop-black/70 md:text-base">
{description}
</p>
)}
{buttonText && buttonHref && (
<Link
to={buttonHref}
className="inline-flex items-center gap-3 rounded-full border border-pop-pink px-6 py-3 text-sm font-semibold text-pop-black transition hover:bg-pop-pink hover:text-pop-bg"
>
{buttonText}
<ArrowRight className="h-4 w-4" />
</Link>
)}
</div>
<div className="grid gap-4 sm:grid-cols-3 lg:col-span-7 lg:grid-cols-7">
{images.map((image, index) => (
<StoryImage key={`${image.imageUrl}-${index}`} image={image} index={index} />
))}
</div>
<div className="space-y-5 lg:col-span-2">
{features.map((feature, index) => {
const Icon = iconMap[feature.icon] ?? CheckCircle;
return (
<div key={`${feature.title}-${index}`} className="flex items-center gap-4">
<div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-full border border-pop-pink/30 bg-pop-bg text-pop-pink shadow-sm">
<Icon className="h-7 w-7" strokeWidth={1.7} />
</div>
<div className="min-w-0">
<h3 className="text-sm font-semibold leading-snug text-pop-black">
{feature.title}
</h3>
{feature.subtitle && (
<p className="mt-1 text-xs leading-snug text-pop-black/65">
{feature.subtitle}
</p>
)}
</div>
</div>
);
})}
</div>
</div>
</div>
</section>
);
}