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';
|
|
|
|
|
|
|
|
|
|
interface CTASectionProps {
|
|
|
|
|
heading?: string;
|
|
|
|
|
description?: string;
|
|
|
|
|
buttonText?: string;
|
|
|
|
|
buttonHref?: string;
|
|
|
|
|
gradient?: string;
|
|
|
|
|
style?: CSSProperties;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const defaultGradient = 'linear-gradient(135deg, #2563eb 0%, #7c3aed 100%)';
|
|
|
|
|
|
|
|
|
|
export const CTASection: UserComponent<CTASectionProps> = ({
|
|
|
|
|
heading = 'Ready to Get Started?',
|
|
|
|
|
description = 'Join thousands of satisfied users and start building your dream website today.',
|
|
|
|
|
buttonText = 'Start Free Trial',
|
|
|
|
|
buttonHref = '#',
|
|
|
|
|
gradient = defaultGradient,
|
|
|
|
|
style = {},
|
|
|
|
|
}) => {
|
|
|
|
|
const {
|
|
|
|
|
connectors: { connect, drag },
|
|
|
|
|
selected,
|
|
|
|
|
} = useNode((node) => ({
|
|
|
|
|
selected: node.events.selected,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<section
|
|
|
|
|
ref={(ref: HTMLElement | null): void => { if (ref) connect(drag(ref)); }}
|
|
|
|
|
style={{
|
|
|
|
|
background: gradient,
|
|
|
|
|
padding: '80px 20px',
|
|
|
|
|
textAlign: 'center',
|
|
|
|
|
outline: selected ? '2px solid #3b82f6' : 'none',
|
|
|
|
|
...style,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div style={{ maxWidth: '700px', margin: '0 auto' }}>
|
|
|
|
|
<h2 style={{ fontSize: '36px', fontWeight: '700', color: '#ffffff', marginBottom: '12px' }}>
|
|
|
|
|
{heading}
|
|
|
|
|
</h2>
|
|
|
|
|
<p style={{ fontSize: '18px', color: 'rgba(255,255,255,0.85)', marginBottom: '28px', lineHeight: '1.6' }}>
|
|
|
|
|
{description}
|
|
|
|
|
</p>
|
|
|
|
|
<a
|
|
|
|
|
href={buttonHref}
|
|
|
|
|
onClick={(e) => e.preventDefault()}
|
|
|
|
|
style={{
|
|
|
|
|
display: 'inline-block',
|
|
|
|
|
padding: '14px 36px',
|
|
|
|
|
backgroundColor: '#ffffff',
|
|
|
|
|
color: '#18181b',
|
|
|
|
|
textDecoration: 'none',
|
|
|
|
|
borderRadius: '8px',
|
|
|
|
|
fontWeight: '600',
|
|
|
|
|
fontSize: '16px',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{buttonText}
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/* ---------- Settings panel ---------- */
|
|
|
|
|
|
|
|
|
|
const CTASectionSettings: React.FC = () => {
|
|
|
|
|
const { actions: { setProp }, props } = useNode((node) => ({
|
|
|
|
|
props: node.data.props as CTASectionProps,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const gradientPresets = [
|
|
|
|
|
{ label: 'Blue-Purple', value: 'linear-gradient(135deg, #2563eb 0%, #7c3aed 100%)' },
|
|
|
|
|
{ label: 'Purple', value: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' },
|
|
|
|
|
{ label: 'Teal', value: 'linear-gradient(135deg, #0d9488 0%, #0f766e 100%)' },
|
|
|
|
|
{ label: 'Sunset', value: 'linear-gradient(135deg, #f97316 0%, #ec4899 100%)' },
|
|
|
|
|
{ label: 'Dark', value: 'linear-gradient(135deg, #1e293b 0%, #0f172a 100%)' },
|
|
|
|
|
{ label: 'Ocean', value: 'linear-gradient(135deg, #0ea5e9 0%, #6366f1 100%)' },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div style={{ padding: '12px', display: 'flex', flexDirection: 'column', gap: '14px' }}>
|
|
|
|
|
<div>
|
|
|
|
|
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Heading</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={props.heading || ''}
|
|
|
|
|
onChange={(e) => setProp((p: CTASectionProps) => { p.heading = e.target.value; })}
|
|
|
|
|
style={{ width: '100%', padding: '4px 8px', background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, fontSize: 12 }}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Description</label>
|
|
|
|
|
<textarea
|
|
|
|
|
value={props.description || ''}
|
|
|
|
|
onChange={(e) => setProp((p: CTASectionProps) => { p.description = e.target.value; })}
|
|
|
|
|
rows={2}
|
|
|
|
|
style={{ width: '100%', padding: '4px 8px', background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, fontSize: 12, resize: 'vertical' }}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Button Text</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={props.buttonText || ''}
|
|
|
|
|
onChange={(e) => setProp((p: CTASectionProps) => { p.buttonText = e.target.value; })}
|
|
|
|
|
style={{ width: '100%', padding: '4px 8px', background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, fontSize: 12 }}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Button URL</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={props.buttonHref || ''}
|
|
|
|
|
onChange={(e) => setProp((p: CTASectionProps) => { p.buttonHref = e.target.value; })}
|
|
|
|
|
placeholder="https://..."
|
|
|
|
|
style={{ width: '100%', padding: '4px 8px', background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, fontSize: 12 }}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Gradient</label>
|
|
|
|
|
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
|
|
|
|
{gradientPresets.map((g) => (
|
|
|
|
|
<button
|
|
|
|
|
key={g.label}
|
|
|
|
|
onClick={() => setProp((p: CTASectionProps) => { p.gradient = g.value; })}
|
|
|
|
|
title={g.label}
|
|
|
|
|
style={{
|
|
|
|
|
width: 32, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
|
|
|
|
background: g.value, cursor: 'pointer',
|
|
|
|
|
outline: props.gradient === g.value ? '2px solid #3b82f6' : 'none',
|
|
|
|
|
outlineOffset: 1,
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/* ---------- Craft config ---------- */
|
|
|
|
|
|
|
|
|
|
CTASection.craft = {
|
|
|
|
|
displayName: 'CTA Section',
|
|
|
|
|
props: {
|
|
|
|
|
heading: 'Ready to Get Started?',
|
|
|
|
|
description: 'Join thousands of satisfied users and start building your dream website today.',
|
|
|
|
|
buttonText: 'Start Free Trial',
|
|
|
|
|
buttonHref: '#',
|
|
|
|
|
gradient: defaultGradient,
|
|
|
|
|
style: {},
|
|
|
|
|
},
|
|
|
|
|
rules: {
|
|
|
|
|
canDrag: () => true,
|
|
|
|
|
canMoveIn: () => false,
|
|
|
|
|
canMoveOut: () => true,
|
|
|
|
|
},
|
|
|
|
|
related: {
|
|
|
|
|
settings: CTASectionSettings,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/* ---------- HTML export ---------- */
|
|
|
|
|
|
|
|
|
|
(CTASection as any).toHtml = (props: CTASectionProps, _childrenHtml: string) => {
|
2026-05-24 15:54:48 -07:00
|
|
|
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>');
|
2026-04-05 18:31:16 -07:00
|
|
|
const sectionStyle = cssPropsToString({
|
|
|
|
|
background: props.gradient || defaultGradient,
|
|
|
|
|
padding: '80px 20px',
|
|
|
|
|
textAlign: 'center',
|
|
|
|
|
...props.style,
|
|
|
|
|
});
|
|
|
|
|
return {
|
|
|
|
|
html: `<section${sectionStyle ? ` style="${sectionStyle}"` : ''}>
|
|
|
|
|
<div style="max-width:700px;margin:0 auto">
|
|
|
|
|
<h2 style="font-size:36px;font-weight:700;color:#ffffff;margin-bottom:12px">${esc(props.heading || '')}</h2>
|
|
|
|
|
<p style="font-size:18px;color:rgba(255,255,255,0.85);margin-bottom:28px;line-height:1.6">${esc(props.description || '')}</p>
|
|
|
|
|
<a href="${props.buttonHref || '#'}" style="display:inline-block;padding:14px 36px;background-color:#ffffff;color:#18181b;text-decoration:none;border-radius:8px;font-weight:600;font-size:16px">${esc(props.buttonText || '')}</a>
|
|
|
|
|
</div>
|
|
|
|
|
</section>`,
|
|
|
|
|
};
|
|
|
|
|
};
|