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 > ` ,
} ;
} ;