Files
site-builder/craft/src/components/sections/CallToAction.tsx
T

199 lines
6.5 KiB
TypeScript
Raw Normal View History

import React, { CSSProperties } from 'react';
import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers';
import { CtaButton, normalizeCtas, ctaInlineStyle, ctasToHtml } from './_cta-helpers';
import { escapeHtml, escapeAttr, cssValue } from '../../utils/escape';
interface CallToActionProps {
heading?: string;
description?: string;
ctas?: CtaButton[];
/** Legacy props kept for backward compat with saved projects. */
buttonText?: string;
buttonHref?: string;
secondaryButtonText?: string;
secondaryButtonHref?: string;
bgType?: 'color' | 'gradient' | 'image';
bgValue?: string;
overlayColor?: string;
overlayOpacity?: number;
textColor?: string;
buttonColor?: string;
anchorId?: string;
style?: CSSProperties;
}
const defaultGradient = 'linear-gradient(135deg, #2563eb 0%, #7c3aed 100%)';
export const CallToAction: UserComponent<CallToActionProps> = ({
heading = 'Ready to Get Started?',
description = 'Join thousands of satisfied users and start building your dream website today.',
ctas,
buttonText,
buttonHref,
secondaryButtonText,
secondaryButtonHref,
bgType = 'gradient',
bgValue = defaultGradient,
overlayColor = '#000000',
overlayOpacity = 0,
textColor = '#ffffff',
buttonColor = '#ffffff',
anchorId,
style = {},
}) => {
const {
connectors: { connect, drag },
selected,
} = useNode((node) => ({
selected: node.events.selected,
}));
const bgStyle: CSSProperties = {};
if (bgType === 'color') {
bgStyle.backgroundColor = bgValue;
} else if (bgType === 'gradient') {
bgStyle.background = bgValue;
} else if (bgType === 'image') {
bgStyle.backgroundImage = `url(${bgValue})`;
bgStyle.backgroundSize = 'cover';
bgStyle.backgroundPosition = 'center';
}
const isButtonDark = buttonColor === '#ffffff' || buttonColor === '#f8fafc';
const buttonTextColor = isButtonDark ? '#18181b' : '#ffffff';
const effectiveCtas = normalizeCtas({ ctas, buttonText, buttonHref, secondaryButtonText, secondaryButtonHref });
const ctaDefaults = { primaryBg: buttonColor, primaryText: buttonTextColor, outlineText: textColor };
return (
<section
ref={(ref: HTMLElement | null): void => { if (ref) connect(drag(ref)); }}
id={anchorId || undefined}
style={{
position: 'relative',
padding: '80px 20px',
textAlign: 'center',
outline: selected ? '2px solid #3b82f6' : 'none',
...bgStyle,
...style,
}}
>
{/* Overlay */}
{bgType === 'image' && overlayOpacity > 0 && (
<div
style={{
position: 'absolute',
inset: 0,
backgroundColor: overlayColor,
opacity: overlayOpacity / 100,
pointerEvents: 'none',
}}
/>
)}
<div style={{ maxWidth: '700px', margin: '0 auto', position: 'relative', zIndex: 1 }}>
<h2 style={{ fontSize: '36px', fontWeight: '700', color: textColor, marginBottom: '12px' }}>
{heading}
</h2>
<p style={{ fontSize: '18px', color: textColor, opacity: 0.85, marginBottom: '28px', lineHeight: '1.6' }}>
{description}
</p>
<div style={{ display: 'flex', gap: '12px', justifyContent: 'center', flexWrap: 'wrap' }}>
{effectiveCtas.map((cta, i) => (
<a key={i} href={cta.href || '#'} onClick={(e) => e.preventDefault()}
style={ctaInlineStyle(cta, ctaDefaults)}>
{cta.text}
</a>
))}
</div>
</div>
</section>
);
};
/* ---------- Craft config ---------- */
CallToAction.craft = {
displayName: 'Call to Action',
props: {
heading: 'Ready to Get Started?',
description: 'Join thousands of satisfied users and start building your dream website today.',
ctas: [
{ text: 'Get Started', href: '#', variant: 'primary' },
{ text: 'Learn More', href: '#', variant: 'outline' },
] as CtaButton[],
anchorId: '',
bgType: 'gradient',
bgValue: defaultGradient,
overlayColor: '#000000',
overlayOpacity: 0,
textColor: '#ffffff',
buttonColor: '#ffffff',
style: {},
},
rules: {
canDrag: () => true,
canMoveIn: () => false,
canMoveOut: () => true,
},
};
/* ---------- HTML export ---------- */
(CallToAction as any).toHtml = (props: CallToActionProps, _childrenHtml: string) => {
const bgType = props.bgType || 'gradient';
const bgValue = props.bgValue || defaultGradient;
// Sanitized -- raw string-interpolation sink in the heading/description
// style attributes below.
const textColor = cssValue(props.textColor) || '#ffffff';
const buttonColor = props.buttonColor || '#ffffff';
const isButtonDark = buttonColor === '#ffffff' || buttonColor === '#f8fafc';
const buttonTextColor = isButtonDark ? '#18181b' : '#ffffff';
const sectionCss: CSSProperties = {
position: 'relative',
padding: '80px 20px',
textAlign: 'center',
...props.style,
};
if (bgType === 'color') {
sectionCss.backgroundColor = bgValue;
} else if (bgType === 'gradient') {
sectionCss.background = bgValue;
} else if (bgType === 'image') {
sectionCss.backgroundImage = `url(${bgValue})`;
sectionCss.backgroundSize = 'cover';
sectionCss.backgroundPosition = 'center';
}
const sectionStyle = cssPropsToString(sectionCss);
let overlayHtml = '';
if (bgType === 'image' && (props.overlayOpacity || 0) > 0) {
const overlayStyle = cssPropsToString({
position: 'absolute',
inset: '0',
backgroundColor: props.overlayColor || '#000000',
opacity: String((props.overlayOpacity || 0) / 100) as any,
pointerEvents: 'none',
});
overlayHtml = `<div${overlayStyle ? ` style="${overlayStyle}"` : ''}></div>`;
}
const ctas = normalizeCtas(props);
const buttonsHtml = ctasToHtml(ctas, { primaryBg: buttonColor, primaryText: buttonTextColor, outlineText: textColor });
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
return {
html: `<section${idAttr}${sectionStyle ? ` style="${sectionStyle}"` : ''}>
${overlayHtml}<div style="max-width:700px;margin:0 auto;position:relative;z-index:1">
<h2 style="font-size:36px;font-weight:700;color:${textColor};margin-bottom:12px">${escapeHtml(props.heading || '')}</h2>
<p style="font-size:18px;color:${textColor};opacity:0.85;margin-bottom:28px;line-height:1.6">${escapeHtml(props.description || '')}</p>
<div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap">${buttonsHtml}</div>
</div>
</section>`,
};
};