Files
homaroma/apps/spa/src/components/DynamicFeaturedTiles.tsx
T
2026-10-06 15:35:04 +07:00

184 lines
7.2 KiB
TypeScript

import { Link } from 'react-router-dom';
import type { FeaturedTilesSectionPayload } from '@homaroma/types';
import { useTranslation } from 'react-i18next';
import { getLocalizedField } from '../utils/localization';
interface DynamicFeaturedTilesProps {
payload: FeaturedTilesSectionPayload;
}
export default function DynamicFeaturedTiles({ payload }: DynamicFeaturedTilesProps) {
const { i18n } = useTranslation();
const lang = i18n.language;
const tiles = Array.isArray(payload.tiles) ? payload.tiles : [];
const columns = payload.columns || 2;
const layoutPreset = payload.layoutPreset;
const textAlign = payload.textAlign || (layoutPreset === 'editorial' ? 'left' : 'center');
const ctaStyle = payload.ctaStyle || (layoutPreset === 'editorial' ? 'arrow_link' : 'pill_button');
const sectionAspect = payload.imageAspectRatio || (layoutPreset === 'editorial' ? 'landscape' : undefined);
const imageRounded = payload.imageRounded || (layoutPreset === 'editorial' ? 'md' : '3xl');
const sectionTitle = getLocalizedField(payload.title, lang);
const sectionSubtitle = getLocalizedField(payload.subtitle, lang);
const getGridColsClass = (cols: number) => {
switch (cols) {
case 3:
return 'grid-cols-1 md:grid-cols-3';
case 4:
return 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4';
case 2:
default:
return 'grid-cols-1 md:grid-cols-2';
}
};
const getAspectClass = (ratio?: string) => {
const r = ratio || sectionAspect || 'square';
switch (r) {
case 'landscape':
return 'aspect-[16/9]';
case 'wide':
return 'aspect-[4/3]';
case 'portrait':
return 'aspect-[3/4]';
case 'tall':
return 'aspect-[2/3]';
case 'auto':
return 'aspect-auto';
case 'square':
default:
return 'aspect-square';
}
};
const getRoundedClass = (rounded?: string) => {
switch (rounded) {
case 'none':
return 'rounded-none';
case 'sm':
return 'rounded-md';
case 'md':
return 'rounded-xl';
case '2xl':
return 'rounded-2xl';
case 'full':
return 'rounded-full';
case '3xl':
default:
return 'rounded-3xl';
}
};
const isLeftAlign = textAlign === 'left';
const isRightAlign = textAlign === 'right';
const alignContainerClass = isLeftAlign
? 'items-start text-left'
: isRightAlign
? 'items-end text-right'
: 'items-center text-center';
return (
<section
className="ui-section-band w-full overflow-hidden"
style={{ backgroundColor: payload.backgroundColor, color: payload.textColor }}
>
<div className="ui-section-inner space-y-12">
{/* Optional Section Header */}
{(sectionTitle || sectionSubtitle) && (
<div className="text-center space-y-3 max-w-2xl mx-auto">
{sectionTitle && (
<h2 className="font-serif text-3xl md:text-4xl font-light tracking-wide" style={{ color: payload.textColor }}>
{sectionTitle}
</h2>
)}
{sectionSubtitle && (
<p className="text-sm md:text-base opacity-80 font-light" style={{ color: payload.textColor }}>
{sectionSubtitle}
</p>
)}
<div className="w-12 h-[1px] bg-pop-pink mx-auto"></div>
</div>
)}
{/* Campaign Tiles Grid */}
<div className={`grid gap-6 md:gap-8 lg:gap-10 ${getGridColsClass(columns)}`}>
{tiles.map((tile, idx) => {
const tileTitle = getLocalizedField(tile.title, lang);
const tileDescription = getLocalizedField(tile.description, lang);
const buttonText = getLocalizedField(tile.buttonText, lang);
const imageAspectRatio = tile.imageAspectRatio || sectionAspect || 'square';
const cardContent = (
<div className={`group flex flex-col ${alignContainerClass} space-y-4 w-full`}>
{/* Image Container */}
<div className={`relative ${getAspectClass(imageAspectRatio)} w-full overflow-hidden ${getRoundedClass(imageRounded)} bg-neutral-100 shadow-md`}>
{tile.imageUrl ? (
<img
src={tile.imageUrl}
alt={tileTitle}
className="w-full h-full object-cover transition-transform duration-700 ease-out group-hover:scale-105"
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
}}
/>
) : (
<div className="w-full h-full bg-neutral-200 flex items-center justify-center text-neutral-400 text-xs">
No Image
</div>
)}
</div>
{/* Content Details */}
<div className={`space-y-2.5 w-full flex flex-col ${alignContainerClass}`}>
{tileTitle && (
<h3 className="font-serif text-lg md:text-xl font-normal tracking-wide text-neutral-800 group-hover:text-pop-pink transition-colors" style={{ color: payload.textColor }}>
{tileTitle}
</h3>
)}
{tileDescription && (
<p className="text-xs md:text-sm font-light leading-relaxed opacity-80" style={{ color: payload.textColor }}>
{tileDescription}
</p>
)}
{/* CTA Button */}
{buttonText && ctaStyle !== 'none' && (
<div className={isLeftAlign ? 'pt-0.5' : 'pt-2'}>
{ctaStyle === 'arrow_link' ? (
<span className="inline-flex items-center gap-1.5 text-xs md:text-sm font-medium tracking-wide text-neutral-800 group-hover:text-pop-pink transition-colors" style={{ color: payload.textColor }}>
<span>{buttonText}</span>
<span className="transition-transform duration-200 group-hover:translate-x-1 font-sans">→</span>
</span>
) : ctaStyle === 'solid_button' ? (
<span className="inline-flex rounded-xl bg-neutral-900 text-white px-7 py-2.5 text-xs font-bold uppercase tracking-[0.2em] transition hover:bg-neutral-800 shadow-sm">
{buttonText}
</span>
) : (
<span className="inline-flex rounded-full border border-current px-7 py-2.5 text-xs font-bold uppercase tracking-[0.2em] transition hover:bg-neutral-900 hover:text-white">
{buttonText}
</span>
)}
</div>
)}
</div>
</div>
);
return tile.buttonLink ? (
<Link key={idx} to={tile.buttonLink} className="block w-full focus:outline-none">
{cardContent}
</Link>
) : (
<div key={idx} className="w-full">
{cardContent}
</div>
);
})}
</div>
</div>
</section>
);
}