a0cdbd1d82
FeatureItem.mediaType was never read anywhere -- the icon-vs-image choice is (correctly) driven by feat.image truthiness, set via the AssetPicker in FeaturesEditor. Neither the default items nor FeaturesEditor's "add item" object ever set it. Remove the dead field; image-precedence render/export behavior is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
139 lines
5.3 KiB
TypeScript
139 lines
5.3 KiB
TypeScript
import React, { CSSProperties } from 'react';
|
|
import { useNode, UserComponent } from '@craftjs/core';
|
|
import { cssPropsToString } from '../../utils/style-helpers';
|
|
import { escapeHtml, escapeAttr, safeUrl } from '../../utils/escape';
|
|
|
|
interface FeatureItem {
|
|
title: string;
|
|
description: string;
|
|
icon: string;
|
|
image?: string;
|
|
imageAlt?: string;
|
|
buttonText?: string;
|
|
buttonUrl?: string;
|
|
}
|
|
|
|
interface FeaturesGridProps {
|
|
features?: FeatureItem[];
|
|
style?: CSSProperties;
|
|
anchorId?: string;
|
|
}
|
|
|
|
// Keys image/imageAlt/buttonText/buttonUrl are present (blank) on the defaults so
|
|
// the guided panel's generic array editor (which derives fields from the first
|
|
// item's keys) exposes inputs for them. An image renders whenever `image` is set.
|
|
const defaultFeatures: FeatureItem[] = [
|
|
{ title: 'Fast & Reliable', description: 'Built for performance with optimized loading and rock-solid uptime.', icon: '⚡', image: '', imageAlt: '', buttonText: '', buttonUrl: '' },
|
|
{ title: 'Easy to Use', description: 'Intuitive drag-and-drop interface that anyone can master in minutes.', icon: '✨', image: '', imageAlt: '', buttonText: '', buttonUrl: '' },
|
|
{ title: 'Fully Responsive', description: 'Looks great on every device, from phones to ultrawide monitors.', icon: '📱', image: '', imageAlt: '', buttonText: '', buttonUrl: '' },
|
|
];
|
|
|
|
export const FeaturesGrid: UserComponent<FeaturesGridProps> = ({
|
|
features = defaultFeatures,
|
|
style = {},
|
|
anchorId,
|
|
}) => {
|
|
const {
|
|
connectors: { connect, drag },
|
|
selected,
|
|
} = useNode((node) => ({
|
|
selected: node.events.selected,
|
|
}));
|
|
|
|
return (
|
|
<section
|
|
ref={(ref: HTMLElement | null): void => { if (ref) connect(drag(ref)); }}
|
|
id={anchorId || undefined}
|
|
style={{
|
|
padding: '80px 20px',
|
|
backgroundColor: '#ffffff',
|
|
outline: selected ? '2px solid #3b82f6' : 'none',
|
|
...style,
|
|
}}
|
|
>
|
|
<div style={{ maxWidth: '1100px', margin: '0 auto', display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '32px' }}>
|
|
{(Array.isArray(features) ? features : []).map((feat, i) => (
|
|
<div
|
|
key={i}
|
|
style={{
|
|
textAlign: 'center',
|
|
padding: '32px 24px',
|
|
borderRadius: '12px',
|
|
backgroundColor: '#f8fafc',
|
|
border: '1px solid #e2e8f0',
|
|
}}
|
|
>
|
|
{feat.image ? (
|
|
<img
|
|
src={feat.image}
|
|
alt={feat.imageAlt || feat.title || ''}
|
|
style={{ maxWidth: '100%', height: 'auto', marginBottom: '16px', borderRadius: '8px' }}
|
|
/>
|
|
) : (
|
|
<div style={{ fontSize: '36px', marginBottom: '16px' }}>{feat.icon}</div>
|
|
)}
|
|
<h3 style={{ fontSize: '20px', fontWeight: '600', color: '#18181b', marginBottom: '8px' }}>{feat.title}</h3>
|
|
<p style={{ fontSize: '14px', color: '#64748b', lineHeight: '1.6' }}>{feat.description}</p>
|
|
{feat.buttonText ? (
|
|
<a
|
|
href={feat.buttonUrl || '#'}
|
|
onClick={(e) => e.preventDefault()}
|
|
style={{ display: 'inline-block', marginTop: '16px', padding: '10px 24px', background: '#3b82f6', color: '#fff', borderRadius: '8px', textDecoration: 'none', fontSize: '14px', fontWeight: 600 }}
|
|
>
|
|
{feat.buttonText}
|
|
</a>
|
|
) : null}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
/* ---------- Craft config ---------- */
|
|
|
|
FeaturesGrid.craft = {
|
|
displayName: 'Features Grid',
|
|
props: {
|
|
features: defaultFeatures,
|
|
style: { backgroundColor: '#ffffff' },
|
|
anchorId: '',
|
|
},
|
|
rules: {
|
|
canDrag: () => true,
|
|
canMoveIn: () => false,
|
|
canMoveOut: () => true,
|
|
},
|
|
};
|
|
|
|
/* ---------- HTML export ---------- */
|
|
|
|
(FeaturesGrid as any).toHtml = (props: FeaturesGridProps, _childrenHtml: string) => {
|
|
const sectionStyle = cssPropsToString({
|
|
padding: '80px 20px',
|
|
...props.style,
|
|
});
|
|
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
|
|
const cards = (props.features || defaultFeatures).map((feat) => {
|
|
const media = feat.image
|
|
? `<img src="${escapeAttr(safeUrl(feat.image))}" alt="${escapeAttr(feat.imageAlt || feat.title || '')}" style="max-width:100%;height:auto;margin-bottom:16px;border-radius:8px">`
|
|
: `<div style="font-size:36px;margin-bottom:16px">${escapeHtml(feat.icon)}</div>`;
|
|
const button = feat.buttonText
|
|
? `\n <a href="${escapeAttr(safeUrl(feat.buttonUrl || '#'))}" style="display:inline-block;margin-top:16px;padding:10px 24px;background:#3b82f6;color:#fff;border-radius:8px;text-decoration:none;font-size:14px;font-weight:600">${escapeHtml(feat.buttonText)}</a>`
|
|
: '';
|
|
return `<div style="text-align:center;padding:32px 24px;border-radius:12px;background-color:#f8fafc;border:1px solid #e2e8f0">
|
|
${media}
|
|
<h3 style="font-size:20px;font-weight:600;color:#18181b;margin-bottom:8px">${escapeHtml(feat.title)}</h3>
|
|
<p style="font-size:14px;color:#64748b;line-height:1.6">${escapeHtml(feat.description)}</p>${button}
|
|
</div>`;
|
|
}).join('\n ');
|
|
|
|
return {
|
|
html: `<section${idAttr}${sectionStyle ? ` style="${sectionStyle}"` : ''}>
|
|
<div style="max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(3,1fr);gap:32px">
|
|
${cards}
|
|
</div>
|
|
</section>`,
|
|
};
|
|
};
|