Files
WooNooW/customer-spa/src/pages/DynamicPage/sections/FeatureGridSection.tsx
2026-06-04 15:10:37 +07:00

239 lines
12 KiB
TypeScript

import { cn } from '@/lib/utils';
import * as LucideIcons from 'lucide-react';
import { getSectionBackground } from '@/lib/sectionStyles';
import { SectionBackgroundRenderer } from '@/components/SectionBackgroundRenderer';
interface FeatureItem {
title?: string;
description?: string;
icon?: string;
// Post-card fields (from related_posts dynamic source)
url?: string;
featured_image?: string;
excerpt?: string;
date?: string;
}
interface FeatureGridSectionProps {
id: string;
layout?: string;
colorScheme?: string;
heading?: string;
items?: FeatureItem[];
elementStyles?: Record<string, any>;
}
export function FeatureGridSection({
id,
layout = 'grid-3',
colorScheme = 'default',
heading,
items = [],
features = [],
elementStyles,
styles,
}: FeatureGridSectionProps & { features?: FeatureItem[], styles?: Record<string, any> }) {
const heightMap: Record<string, string> = {
'default': 'py-12 md:py-20',
'small': 'py-8 md:py-12',
'medium': 'py-16 md:py-24',
'large': 'py-24 md:py-36',
'fullscreen': 'min-h-screen flex flex-col justify-center',
};
const customPadding = styles?.paddingTop || styles?.paddingBottom;
const heightClasses = customPadding ? '' : (heightMap[styles?.heightPreset || 'default'] || 'py-12 md:py-20');
const safeItems = Array.isArray(items) ? items : [];
const safeFeatures = Array.isArray(features) ? features : [];
const listItems = safeItems.length > 0 ? safeItems : safeFeatures;
const gridCols = {
'grid-2': 'md:grid-cols-2',
'grid-3': 'md:grid-cols-3',
'grid-4': 'md:grid-cols-2 lg:grid-cols-4',
}[layout] || 'md:grid-cols-3';
// Detect if these are post-cards (from related_posts) — they have a url field
const isPostCards = listItems.some(item => !!item.url);
const getTextStyles = (elementName: string) => {
const styles = elementStyles?.[elementName] || {};
return {
classNames: cn(
styles.fontSize,
styles.fontWeight,
{
'font-sans': styles.fontFamily === 'secondary',
'font-serif': styles.fontFamily === 'primary',
}
),
style: {
color: styles.color,
textAlign: styles.textAlign,
backgroundColor: styles.backgroundColor,
borderColor: styles.borderColor,
borderWidth: styles.borderWidth,
borderRadius: styles.borderRadius,
}
};
};
const headingStyle = getTextStyles('heading');
const featureItemStyle = getTextStyles('feature_item');
const linkStyle = getTextStyles('link');
const sectionBg = getSectionBackground(styles);
return (
<section
id={id}
className={cn(
'wn-section wn-feature-grid relative overflow-hidden w-full',
`wn-feature-grid--${layout}`,
`wn-scheme--${colorScheme}`,
heightClasses
)}
style={sectionBg.style}
>
<SectionBackgroundRenderer bg={sectionBg} />
<div className="mx-auto px-4 relative z-10 w-full">
{heading && (
<h2
className={cn(
"wn-features__heading text-center mb-10",
!elementStyles?.heading?.fontSize && "text-2xl md:text-3xl lg:text-4xl",
!elementStyles?.heading?.fontWeight && "font-bold",
headingStyle.classNames
)}
style={headingStyle.style}
>
{heading}
</h2>
)}
<div className={cn('grid gap-6', gridCols)}>
{listItems.map((item, index) => {
// ── Post Card (from related_posts) ──────────────────────────
if (isPostCards) {
return (
<a
key={index}
href={item.url || '#'}
className={cn(
'wn-post-card group block rounded-xl overflow-hidden transition-all duration-200',
'bg-white shadow-md hover:shadow-xl hover:-translate-y-1',
featureItemStyle.classNames
)}
style={featureItemStyle.style}
>
{/* Thumbnail */}
{item.featured_image ? (
<div className="aspect-[16/9] overflow-hidden bg-gray-100">
<img
src={item.featured_image}
alt={item.title || ''}
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-105"
/>
</div>
) : (
<div className="aspect-[16/9] bg-gradient-to-br from-gray-100 to-gray-200 flex items-center justify-center">
<svg className="w-10 h-10 text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
)}
{/* Card Body */}
<div className="p-5">
{item.date && (
<p className="text-xs text-gray-400 mb-2 uppercase tracking-wider">{item.date}</p>
)}
{item.title && (
<h3 className={cn("font-semibold text-gray-900 leading-snug mb-2 group-hover:text-primary transition-colors line-clamp-2 w-full",
!elementStyles?.feature_item?.fontSize && "text-base",
featureItemStyle.classNames
)} style={featureItemStyle.style}>
{item.title}
</h3>
)}
{(item.excerpt || item.description) && (
<p className={cn("text-gray-500 line-clamp-3 mb-4 w-full",
!elementStyles?.feature_item?.fontSize && "text-sm",
featureItemStyle.classNames
)} style={featureItemStyle.style}>
{item.excerpt || item.description}
</p>
)}
<span className={cn("inline-flex items-center gap-1 text-sm font-medium text-primary", linkStyle.classNames)} style={linkStyle.style}>
Read more
<svg className="w-4 h-4 transition-transform group-hover:translate-x-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</span>
</div>
</a>
);
}
// ── Feature Card (icon + title + desc) ─────────────────────
return (
<div
key={index}
className={cn(
'wn-feature-grid__item',
'p-6 rounded-xl',
!featureItemStyle.style?.backgroundColor && {
'bg-white shadow-lg': colorScheme !== 'primary',
'bg-white/10': colorScheme === 'primary',
},
featureItemStyle.classNames
)}
style={featureItemStyle.style}
>
{item.icon && (() => {
const IconComponent = (LucideIcons as any)[item.icon];
if (!IconComponent) return null;
return (
<div className="wn-feature-grid__icon mb-4 inline-block p-3 rounded-full bg-primary/10 text-primary">
<IconComponent className="w-8 h-8" />
</div>
);
})()}
{item.title && (
<h3
className={cn(
"wn-feature-grid__item-title mb-3",
!featureItemStyle.classNames && "text-xl font-semibold"
)}
style={{ color: featureItemStyle.style?.color }}
>
{item.title}
</h3>
)}
{item.description && (
<p
className={cn(
'wn-feature-grid__item-desc',
!featureItemStyle.style?.color && {
'text-gray-600': colorScheme !== 'primary',
'text-white/80': colorScheme === 'primary',
}
)}
style={{ color: featureItemStyle.style?.color }}
>
{item.description}
</p>
)}
</div>
);
})}
</div>
{/* Empty state for related posts */}
{isPostCards && listItems.length === 0 && (
<p className="text-center text-gray-400 text-sm py-8">No related articles found.</p>
)}
</div>
</section>
);
}