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 HeroProps {
heading? : string ;
subtitle? : string ;
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' ;
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.' ,
buttonText = 'Get Started' ,
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' ,
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' };
return (
< section
ref = {( ref : HTMLElement | null ) : void => { if ( ref ) connect ( drag ( ref )); }}
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' }}>
{ buttonText && (
< a href = { buttonHref } onClick = {( e ) => e . preventDefault ()} style = {{
display : 'inline-block' , padding : '14px 36px' , backgroundColor : buttonBgColor ,
color : buttonTextColor , textDecoration : 'none' , borderRadius : '8px' ,
fontWeight : '600' , fontSize : '16px' ,
}}>
{ buttonText }
</ a >
)}
{ secondaryButtonText && (
< a href = { secondaryButtonHref } onClick = {( e ) => e . preventDefault ()} style = {{
display : 'inline-block' , padding : '14px 36px' ,
backgroundColor : 'transparent' , color : textColor ,
textDecoration : 'none' , borderRadius : '8px' , fontWeight : '600' ,
fontSize : '16px' , border : `2px solid ${ textColor } ` ,
}}>
{ secondaryButtonText }
</ a >
)}
</ div >
</ div >
</ section >
);
};
/* ---------- Settings panel ---------- */
const inputStyle : React.CSSProperties = {
width : '100%' , padding : '6px 8px' , background : '#27272a' ,
color : '#e4e4e7' , border : '1px solid #3f3f46' , borderRadius : 4 , fontSize : 12 ,
};
const labelStyle : React.CSSProperties = {
fontSize : 11 , color : '#a1a1aa' , display : 'block' , marginBottom : 4 ,
};
const btnStyle = ( active : boolean ) : React . CSSProperties => ({
flex : 1 , padding : '6px 4px' , fontSize : 11 , borderRadius : 4 , cursor : 'pointer' ,
border : '1px solid #3f3f46' ,
background : active ? '#3b82f6' : '#27272a' ,
color : active ? '#fff' : '#a1a1aa' ,
fontWeight : active ? 600 : 400 ,
});
const HeroSettings : React.FC = () => {
const { actions : { setProp }, props } = useNode (( node ) => ({
props : node.data.props as HeroProps ,
}));
return (
< div style = {{ padding : 12 , display : 'flex' , flexDirection : 'column' , gap : 12 }}>
{ /* Content */ }
< div >
< label style = { labelStyle }> Heading </ label >
< input type = "text" value = { props . heading || '' } onChange = {( e ) => setProp (( p : HeroProps ) => { p . heading = e . target . value ; })} style = { inputStyle } />
</ div >
< div >
< label style = { labelStyle }> Subtitle </ label >
< textarea value = { props . subtitle || '' } onChange = {( e ) => setProp (( p : HeroProps ) => { p . subtitle = e . target . value ; })} rows = { 3 } style = {{ ... inputStyle , resize : 'vertical' as const }} />
</ div >
< div >
< label style = { labelStyle }> Button Text </ label >
< input type = "text" value = { props . buttonText || '' } onChange = {( e ) => setProp (( p : HeroProps ) => { p . buttonText = e . target . value ; })} style = { inputStyle } />
</ div >
< div >
< label style = { labelStyle }> Button URL </ label >
< input type = "text" value = { props . buttonHref || '' } onChange = {( e ) => setProp (( p : HeroProps ) => { p . buttonHref = e . target . value ; })} placeholder = "#" style = { inputStyle } />
</ div >
< div >
< label style = { labelStyle }> Secondary Button Text </ label >
< input type = "text" value = { props . secondaryButtonText || '' } onChange = {( e ) => setProp (( p : HeroProps ) => { p . secondaryButtonText = e . target . value ; })} placeholder = "Leave blank to hide" style = { inputStyle } />
</ div >
{ /* Background Type */ }
< div >
< label style = { labelStyle }> Background Type </ label >
< div style = {{ display : 'flex' , gap : 4 }}>
{([ 'color' , 'gradient' , 'image' , 'video' ] as const ). map (( t ) => (
< button key = { t } onClick = {() => setProp (( p : HeroProps ) => { p . bgType = t ; })}
style = { btnStyle ( props . bgType === t )}>
{ t === 'color' ? 'Color' : t === 'gradient' ? 'Gradient' : t === 'image' ? 'Image' : 'Video' }
</ button >
))}
</ div >
</ div >
{ /* Background controls based on type */ }
{ props . bgType === 'color' && (
< div >
< label style = { labelStyle }> Background Color </ label >
< div style = {{ display : 'flex' , gap : 6 , alignItems : 'center' }}>
< input type = "color" value = { props . bgColor || '#1e293b' }
onChange = {( e ) => setProp (( p : HeroProps ) => { p . bgColor = e . target . value ; })}
style = {{ width : 36 , height : 30 , border : 'none' , cursor : 'pointer' , background : 'none' }} />
< input type = "text" value = { props . bgColor || '#1e293b' }
onChange = {( e ) => setProp (( p : HeroProps ) => { p . bgColor = e . target . value ; })}
style = {{ ... inputStyle , flex : 1 }} />
</ div >
</ div >
)}
{ props . bgType === 'gradient' && (
<>
< div style = {{ display : 'flex' , gap : 8 }}>
< div style = {{ flex : 1 }}>
< label style = { labelStyle }> From </ label >
< div style = {{ display : 'flex' , gap : 4 , alignItems : 'center' }}>
< input type = "color" value = { props . bgGradientFrom || '#667eea' }
onChange = {( e ) => setProp (( p : HeroProps ) => { p . bgGradientFrom = e . target . value ; })}
style = {{ width : 30 , height : 26 , border : 'none' , cursor : 'pointer' , background : 'none' }} />
< input type = "text" value = { props . bgGradientFrom || '#667eea' }
onChange = {( e ) => setProp (( p : HeroProps ) => { p . bgGradientFrom = e . target . value ; })}
style = {{ ... inputStyle , fontSize : 10 }} />
</ div >
</ div >
< div style = {{ flex : 1 }}>
< label style = { labelStyle }> To </ label >
< div style = {{ display : 'flex' , gap : 4 , alignItems : 'center' }}>
< input type = "color" value = { props . bgGradientTo || '#764ba2' }
onChange = {( e ) => setProp (( p : HeroProps ) => { p . bgGradientTo = e . target . value ; })}
style = {{ width : 30 , height : 26 , border : 'none' , cursor : 'pointer' , background : 'none' }} />
< input type = "text" value = { props . bgGradientTo || '#764ba2' }
onChange = {( e ) => setProp (( p : HeroProps ) => { p . bgGradientTo = e . target . value ; })}
style = {{ ... inputStyle , fontSize : 10 }} />
</ div >
</ div >
</ div >
< div >
< label style = { labelStyle }> Angle : { props . bgGradientAngle || 135 } ° </ label >
< input type = "range" min = { 0 } max = { 360 } value = { props . bgGradientAngle || 135 }
onChange = {( e ) => setProp (( p : HeroProps ) => { p . bgGradientAngle = parseInt ( e . target . value ); })}
style = {{ width : '100%' }} />
</ div >
</>
)}
{ props . bgType === 'image' && (
< div >
< label style = { labelStyle }> Background Image URL </ label >
< input type = "text" value = { props . bgImage || '' } placeholder = "https://..."
onChange = {( e ) => setProp (( p : HeroProps ) => { p . bgImage = e . target . value ; })} style = { inputStyle } />
</ div >
)}
{ props . bgType === 'video' && (
< div >
< label style = { labelStyle }> Background Video URL </ label >
< input type = "text" value = { props . bgVideo || '' } placeholder = "https://...mp4"
onChange = {( e ) => setProp (( p : HeroProps ) => { p . bgVideo = e . target . value ; })} style = { inputStyle } />
</ div >
)}
{ /* Overlay */ }
{( props . bgType === 'image' || props . bgType === 'video' ) && (
< div >
< label style = { labelStyle }> Overlay ({ props . overlayOpacity || 0 } % )</ label >
< div style = {{ display : 'flex' , gap : 6 , alignItems : 'center' }}>
< input type = "color" value = { props . overlayColor || '#000000' }
onChange = {( e ) => setProp (( p : HeroProps ) => { p . overlayColor = e . target . value ; })}
style = {{ width : 30 , height : 26 , border : 'none' , cursor : 'pointer' , background : 'none' }} />
< input type = "range" min = { 0 } max = { 100 } value = { props . overlayOpacity || 0 }
onChange = {( e ) => setProp (( p : HeroProps ) => { p . overlayOpacity = parseInt ( e . target . value ); })}
style = {{ flex : 1 }} />
</ div >
</ div >
)}
{ /* Text & Button Colors */ }
< div >
< label style = { labelStyle }> Text Color </ label >
< div style = {{ display : 'flex' , gap : 6 , alignItems : 'center' }}>
< input type = "color" value = { props . textColor || '#ffffff' }
onChange = {( e ) => setProp (( p : HeroProps ) => { p . textColor = e . target . value ; })}
style = {{ width : 36 , height : 30 , border : 'none' , cursor : 'pointer' , background : 'none' }} />
< input type = "text" value = { props . textColor || '#ffffff' }
onChange = {( e ) => setProp (( p : HeroProps ) => { p . textColor = e . target . value ; })}
style = {{ ... inputStyle , flex : 1 }} />
</ div >
</ div >
< div style = {{ display : 'flex' , gap : 8 }}>
< div style = {{ flex : 1 }}>
< label style = { labelStyle }> Button BG </ label >
< input type = "color" value = { props . buttonBgColor || '#3b82f6' }
onChange = {( e ) => setProp (( p : HeroProps ) => { p . buttonBgColor = e . target . value ; })}
style = {{ width : '100%' , height : 30 , border : '1px solid #3f3f46' , borderRadius : 4 , cursor : 'pointer' }} />
</ div >
< div style = {{ flex : 1 }}>
< label style = { labelStyle }> Button Text </ label >
< input type = "color" value = { props . buttonTextColor || '#ffffff' }
onChange = {( e ) => setProp (( p : HeroProps ) => { p . buttonTextColor = e . target . value ; })}
style = {{ width : '100%' , height : 30 , border : '1px solid #3f3f46' , borderRadius : 4 , cursor : 'pointer' }} />
</ div >
</ div >
{ /* Layout */ }
< div >
< label style = { labelStyle }> Min Height </ label >
< div style = {{ display : 'flex' , gap : 4 }}>
{[ '300px' , '400px' , '500px' , '600px' , '100vh' ]. map (( h ) => (
< button key = { h } onClick = {() => setProp (( p : HeroProps ) => { p . minHeight = h ; })}
style = { btnStyle ( props . minHeight === h )}>{ h === '100vh' ? 'Full' : h }</ button >
))}
</ div >
</ div >
< div >
< label style = { labelStyle }> Vertical Align </ label >
< div style = {{ display : 'flex' , gap : 4 }}>
{([ 'top' , 'center' , 'bottom' ] as const ). map (( v ) => (
< button key = { v } onClick = {() => setProp (( p : HeroProps ) => { p . verticalAlign = v ; })}
style = { btnStyle ( props . verticalAlign === v )}>{ v }</ button >
))}
</ div >
</ div >
< div >
< label style = { labelStyle }> Text Align </ label >
< div style = {{ display : 'flex' , gap : 4 }}>
{([ 'left' , 'center' , 'right' ] as const ). map (( a ) => (
< button key = { a } onClick = {() => setProp (( p : HeroProps ) => { p . textAlign = a ; })}
style = { btnStyle ( props . textAlign === a )}>
< i className = { `fa fa-align- ${ a } ` } />
</ button >
))}
</ div >
</ div >
</ div >
);
};
/* ---------- Craft config ---------- */
HeroSimple . craft = {
displayName : 'Hero' ,
props : {
heading : 'Build Something Amazing' ,
subtitle : 'Create beautiful websites without writing a single line of code.' ,
buttonText : 'Get Started' ,
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' ,
style : {},
},
rules : {
canDrag : () => true ,
canMoveIn : () => false ,
canMoveOut : () => true ,
},
related : {
settings : HeroSettings ,
},
};
/* ---------- HTML export ---------- */
( HeroSimple as any ). toHtml = ( props : HeroProps , _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 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=" ${ 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' ;
let buttonsHtml = '' ;
if ( props . buttonText ) {
buttonsHtml += `<a href=" ${ props . buttonHref || '#' } " style="display:inline-block;padding:14px 36px;background-color: ${ props . buttonBgColor || '#3b82f6' } ;color: ${ props . buttonTextColor || '#fff' } ;text-decoration:none;border-radius:8px;font-weight:600;font-size:16px"> ${ esc ( props . buttonText ) } </a>` ;
}
if ( props . secondaryButtonText ) {
buttonsHtml += `<a href=" ${ props . secondaryButtonHref || '#' } " style="display:inline-block;padding:14px 36px;background:transparent;color: ${ props . textColor || '#fff' } ;text-decoration:none;border-radius:8px;font-weight:600;font-size:16px;border:2px solid ${ props . textColor || '#fff' } "> ${ esc ( props . secondaryButtonText ) } </a>` ;
}
return {
html : `<section 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"> ${ esc ( 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"> ${ esc ( props . subtitle || '' ) } </p>
<div style="display:flex;gap:12px;justify-content: ${ justifyBtn } ;flex-wrap:wrap"> ${ buttonsHtml } </div>
</div>
</section>` ,
};
};