2026-04-05 18:31:16 -07:00
import React , { CSSProperties } from 'react' ;
import { useNode , UserComponent } from '@craftjs/core' ;
import { cssPropsToString } from '../../utils/style-helpers' ;
2026-07-12 12:05:57 -07:00
import { escapeHtml , escapeAttr , safeUrl } from '../../utils/escape' ;
2026-04-05 18:31:16 -07:00
interface FeatureItem {
title : string ;
description : string ;
icon : string ;
2026-07-06 17:53:22 -07:00
image? : string ;
imageAlt? : string ;
buttonText? : string ;
buttonUrl? : string ;
}
2026-04-05 18:31:16 -07:00
interface FeaturesGridProps {
features? : FeatureItem [];
style? : CSSProperties ;
2026-05-25 12:43:28 -07:00
anchorId? : string ;
2026-04-05 18:31:16 -07:00
}
2026-07-07 14:16:14 -07:00
// 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.
2026-04-05 18:31:16 -07:00
const defaultFeatures : FeatureItem [] = [
2026-07-07 14:16:14 -07:00
{ 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 : '' },
2026-04-05 18:31:16 -07:00
];
export const FeaturesGrid : UserComponent < FeaturesGridProps > = ({
features = defaultFeatures ,
style = {},
2026-05-25 12:43:28 -07:00
anchorId ,
2026-04-05 18:31:16 -07:00
}) => {
const {
connectors : { connect , drag },
selected ,
} = useNode (( node ) => ({
selected : node.events.selected ,
}));
return (
< section
ref = {( ref : HTMLElement | null ) : void => { if ( ref ) connect ( drag ( ref )); }}
2026-05-25 12:43:28 -07:00
id = { anchorId || undefined }
2026-04-05 18:31:16 -07:00
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' ,
}}
>
2026-07-07 14:16:14 -07:00
{ feat . image ? (
2026-07-06 17:53:22 -07:00
< 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 >
)}
2026-04-05 18:31:16 -07:00
< 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 >
2026-07-06 17:53:22 -07:00
{ 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 }
2026-04-05 18:31:16 -07:00
</ div >
))}
</ div >
</ section >
);
};
/* ---------- Craft config ---------- */
FeaturesGrid . craft = {
displayName : 'Features Grid' ,
props : {
features : defaultFeatures ,
style : { backgroundColor : '#ffffff' },
2026-05-25 12:43:28 -07:00
anchorId : '' ,
2026-04-05 18:31:16 -07:00
},
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 ,
});
2026-07-12 11:49:10 -07:00
const idAttr = props . anchorId ? ` id=" ${ escapeAttr ( props . anchorId ) } "` : '' ;
2026-04-05 18:31:16 -07:00
const cards = ( props . features || defaultFeatures ). map (( feat ) => {
2026-07-07 14:16:14 -07:00
const media = feat . image
2026-07-12 12:05:57 -07:00
? `<img src=" ${ escapeAttr ( safeUrl ( feat . image )) } " alt=" ${ escapeAttr ( feat . imageAlt || feat . title || '' ) } " style="max-width:100%;height:auto;margin-bottom:16px;border-radius:8px">`
2026-07-12 11:49:10 -07:00
: `<div style="font-size:36px;margin-bottom:16px"> ${ escapeHtml ( feat . icon ) } </div>` ;
2026-07-06 17:53:22 -07:00
const button = feat . buttonText
2026-07-12 12:05:57 -07:00
? ` \ 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>`
2026-07-06 17:53:22 -07:00
: '' ;
2026-04-05 18:31:16 -07:00
return `<div style="text-align:center;padding:32px 24px;border-radius:12px;background-color:#f8fafc;border:1px solid #e2e8f0">
2026-07-06 17:53:22 -07:00
${ media }
2026-07-12 11:49:10 -07:00
<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 }
2026-04-05 18:31:16 -07:00
</div>` ;
}). join ( '\n ' );
return {
2026-05-25 12:43:28 -07:00
html : `<section ${ idAttr }${ sectionStyle ? ` style=" ${ sectionStyle } "` : '' } >
2026-04-05 18:31:16 -07:00
<div style="max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(3,1fr);gap:32px">
${ cards }
</div>
</section>` ,
};
};