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

253 lines
8.4 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, safeUrl } from '../../utils/escape';
interface HeroProps {
heading?: string;
subtitle?: string;
/** New dynamic CTAs. When set (length > 0), legacy primary/secondary fields are ignored. */
ctas?: CtaButton[];
/** Legacy — kept for backwards compatibility with saved projects. */
buttonText?: string;
buttonHref?: string;
secondaryButtonText?: string;
secondaryButtonHref?: string;
bgType?: 'color' | 'gradient' | 'image' | 'video';
bgColor?: string;
bgGradientFrom?: string;
bgGradientTo?: string;
bgGradientAngle?: number;
bgImage?: string;
bgVideo?: string;
overlayColor?: string;
overlayOpacity?: number;
textColor?: string;
buttonBgColor?: string;
buttonTextColor?: string;
minHeight?: string;
verticalAlign?: 'top' | 'center' | 'bottom';
textAlign?: 'left' | 'center' | 'right';
anchorId?: string;
style?: CSSProperties;
}
// Helper: build the background CSS value
function buildBackground(props: HeroProps): string {
switch (props.bgType) {
case 'gradient':
return `linear-gradient(${props.bgGradientAngle || 135}deg, ${props.bgGradientFrom || '#667eea'}, ${props.bgGradientTo || '#764ba2'})`;
case 'image':
return props.bgImage ? `url('${props.bgImage}') center/cover no-repeat` : '#1e293b';
case 'color':
default:
return props.bgColor || '#1e293b';
}
}
export const HeroSimple: UserComponent<HeroProps> = ({
heading = 'Build Something Amazing',
subtitle = 'Create beautiful websites without writing a single line of code.',
ctas,
buttonText,
buttonHref,
secondaryButtonText,
secondaryButtonHref,
bgType = 'color',
bgColor = '#1e293b',
bgGradientFrom = '#667eea',
bgGradientTo = '#764ba2',
bgGradientAngle = 135,
bgImage = '',
bgVideo = '',
overlayColor = '#000000',
overlayOpacity = 0,
textColor = '#ffffff',
buttonBgColor = '#3b82f6',
buttonTextColor = '#ffffff',
minHeight = '500px',
verticalAlign = 'center',
textAlign = 'center',
anchorId,
style = {},
}) => {
const { connectors: { connect, drag } } = useNode();
const bg = buildBackground({
bgType, bgColor, bgGradientFrom, bgGradientTo, bgGradientAngle, bgImage,
} as HeroProps);
const justifyMap = { top: 'flex-start', center: 'center', bottom: 'flex-end' };
const effectiveCtas = normalizeCtas({ ctas, buttonText, buttonHref, secondaryButtonText, secondaryButtonHref });
const ctaDefaults = {
primaryBg: buttonBgColor,
primaryText: buttonTextColor,
outlineText: textColor,
};
return (
<section
ref={(ref: HTMLElement | null): void => { if (ref) connect(drag(ref)); }}
id={anchorId || undefined}
style={{
...style,
background: bgType !== 'image' ? bg : undefined,
backgroundImage: bgType === 'image' && bgImage ? `url('${bgImage}')` : undefined,
backgroundSize: bgType === 'image' ? 'cover' : undefined,
backgroundPosition: bgType === 'image' ? 'center' : undefined,
minHeight: minHeight === '100vh' ? '100vh' : minHeight,
display: 'flex',
alignItems: justifyMap[verticalAlign] || 'center',
justifyContent: 'center',
position: 'relative',
overflow: 'hidden',
padding: '60px 20px',
}}
>
{/* Video background */}
{bgType === 'video' && bgVideo && (
<video
src={bgVideo}
autoPlay muted loop playsInline
style={{
position: 'absolute', top: 0, left: 0, width: '100%', height: '100%',
objectFit: 'cover', zIndex: 0,
}}
/>
)}
{/* Overlay (renders AFTER video so it sits on top) */}
{overlayOpacity > 0 && (
<div style={{
position: 'absolute', top: 0, left: 0, right: 0, bottom: 0,
backgroundColor: overlayColor,
opacity: overlayOpacity / 100,
zIndex: 1,
}} />
)}
{/* Content */}
<div style={{
maxWidth: '800px',
width: '100%',
position: 'relative',
zIndex: 2,
textAlign: textAlign as any,
}}>
<h1 style={{
fontSize: '48px', fontWeight: '700', color: textColor,
marginBottom: '16px', lineHeight: '1.2',
}}>
{heading}
</h1>
<p style={{
fontSize: '20px', color: textColor,
opacity: 0.85, marginBottom: '32px', lineHeight: '1.6',
whiteSpace: 'pre-line',
}}>
{subtitle}
</p>
<div style={{ display: 'flex', gap: '12px', justifyContent: textAlign === 'center' ? 'center' : textAlign === 'right' ? 'flex-end' : 'flex-start', 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 ---------- */
HeroSimple.craft = {
displayName: 'Hero',
props: {
heading: 'Build Something Amazing',
subtitle: 'Create beautiful websites without writing a single line of code.',
ctas: [
{ text: 'Get Started', href: '#', variant: 'primary' },
] as CtaButton[],
bgType: 'color',
bgColor: '#1e293b',
bgGradientFrom: '#667eea',
bgGradientTo: '#764ba2',
bgGradientAngle: 135,
bgImage: '',
bgVideo: '',
overlayColor: '#000000',
overlayOpacity: 0,
textColor: '#ffffff',
buttonBgColor: '#3b82f6',
buttonTextColor: '#ffffff',
minHeight: '500px',
verticalAlign: 'center',
textAlign: 'center',
anchorId: '',
style: {},
},
rules: {
canDrag: () => true,
canMoveIn: () => false,
canMoveOut: () => true,
},
};
/* ---------- HTML export ---------- */
(HeroSimple as any).toHtml = (props: HeroProps, _childrenHtml: string) => {
const bg = buildBackground(props);
const justifyMap: Record<string, string> = { top: 'flex-start', center: 'center', bottom: 'flex-end' };
const sectionStyle = cssPropsToString({
background: props.bgType !== 'image' ? bg : undefined,
backgroundImage: props.bgType === 'image' && props.bgImage ? `url('${props.bgImage}')` : undefined,
backgroundSize: props.bgType === 'image' ? 'cover' : undefined,
backgroundPosition: props.bgType === 'image' ? 'center' : undefined,
minHeight: props.minHeight || '500px',
display: 'flex',
alignItems: justifyMap[props.verticalAlign || 'center'],
justifyContent: 'center',
position: 'relative',
overflow: 'hidden',
padding: '60px 20px',
...props.style,
});
let overlayHtml = '';
if ((props.overlayOpacity || 0) > 0) {
overlayHtml = `<div style="position:absolute;top:0;left:0;right:0;bottom:0;background-color:${props.overlayColor || '#000'};opacity:${(props.overlayOpacity || 0) / 100};z-index:1"></div>`;
}
let videoHtml = '';
if (props.bgType === 'video' && props.bgVideo) {
videoHtml = `<video src="${escapeAttr(safeUrl(props.bgVideo))}" autoplay muted loop playsinline style="position:absolute;top:0;left:0;width:100%;height:100%;object-fit:cover;z-index:0"></video>`;
}
const textAlign = props.textAlign || 'center';
const justifyBtn = textAlign === 'center' ? 'center' : textAlign === 'right' ? 'flex-end' : 'flex-start';
const ctas = normalizeCtas(props);
const buttonsHtml = ctasToHtml(ctas, {
primaryBg: props.buttonBgColor || '#3b82f6',
primaryText: props.buttonTextColor || '#fff',
outlineText: props.textColor || '#fff',
});
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
return {
html: `<section${idAttr} style="${sectionStyle}">
${videoHtml}${overlayHtml}
<div style="max-width:800px;width:100%;position:relative;z-index:2;text-align:${textAlign}">
<h1 style="font-size:48px;font-weight:700;color:${props.textColor || '#fff'};margin-bottom:16px;line-height:1.2">${escapeHtml(props.heading || '')}</h1>
<p style="font-size:20px;color:${props.textColor || '#fff'};opacity:0.85;margin-bottom:32px;line-height:1.6;white-space:pre-line">${escapeHtml(props.subtitle || '')}</p>
<div style="display:flex;gap:12px;justify-content:${justifyBtn};flex-wrap:wrap">${buttonsHtml}</div>
</div>
</section>`,
};
};