194 lines
6.4 KiB
TypeScript
194 lines
6.4 KiB
TypeScript
import Image from 'next/image'
|
|
import { ArrowRight, CakeSlice, CheckCircle, Heart, Leaf, Sparkles, type LucideIcon } from 'lucide-react'
|
|
import Link from '../LocalizedLink'
|
|
import type { CustomBlockComponentProps } from '../../config/customRegistry'
|
|
import { resolveLocalized } from '../../lib/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 = resolveLocalized(value as LocalizedValue | null | undefined, 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-surface-muted shadow-card ${className}`}>
|
|
{image.imageUrl ? (
|
|
<Image
|
|
src={image.imageUrl}
|
|
alt={image.alt}
|
|
fill
|
|
unoptimized
|
|
className="object-cover transition duration-700 hover:scale-105"
|
|
/>
|
|
) : (
|
|
<div className="flex h-full w-full items-center justify-center text-sm font-semibold text-muted-text">
|
|
{image.alt}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default function StoryFeatureMosaic({ props, lang }: CustomBlockComponentProps) {
|
|
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-surface-page">
|
|
<div className="ui-section-shell">
|
|
<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-brand-primary md:text-base">
|
|
{subtitle}
|
|
</p>
|
|
)}
|
|
<h2 className="text-3xl font-semibold leading-tight text-text-main md:text-4xl">
|
|
{title}
|
|
</h2>
|
|
</div>
|
|
{description && (
|
|
<p className="whitespace-pre-line text-sm leading-7 text-muted-text md:text-base">
|
|
{description}
|
|
</p>
|
|
)}
|
|
{buttonText && buttonHref && (
|
|
<Link
|
|
href={buttonHref}
|
|
className="inline-flex items-center gap-3 rounded-full border border-brand-primary px-6 py-3 text-sm font-semibold text-text-main transition hover:bg-brand-primary hover:text-surface-card"
|
|
>
|
|
{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-brand-primary/30 bg-surface-card text-brand-primary shadow-card">
|
|
<Icon className="h-7 w-7" strokeWidth={1.7} />
|
|
</div>
|
|
<div className="min-w-0">
|
|
<h3 className="text-sm font-semibold leading-snug text-text-main">
|
|
{feature.title}
|
|
</h3>
|
|
{feature.subtitle && (
|
|
<p className="mt-1 text-xs leading-snug text-muted-text">
|
|
{feature.subtitle}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|