x
This commit is contained in:
1 parent
de09293051
commit
d8cca4fcfe
7 files changed
+876
-1
No files matched your search
@@ -0,0 +1,193 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ComponentType } from 'react'
|
||||
import type { CustomSectionPayload } from '@homaroma/types'
|
||||
import BakeryCategories from '../components/custom/BakeryCategories'
|
||||
import StoryFeatureMosaic from '../components/custom/StoryFeatureMosaic'
|
||||
|
||||
export type CustomBlockComponentProps = {
|
||||
props: CustomSectionPayload['props']
|
||||
@@ -11,4 +12,5 @@ export type CustomBlockComponent = ComponentType<CustomBlockComponentProps>
|
||||
|
||||
export const customRegistry: Record<string, CustomBlockComponent> = {
|
||||
BakeryCategories,
|
||||
StoryFeatureMosaic,
|
||||
}
|
||||
Reference in new issue
Block a user