2026-04-05 18:31:16 -07:00
|
|
|
import React, { CSSProperties, useState, useEffect, useRef, useCallback } from 'react';
|
|
|
|
|
import { useNode, UserComponent } from '@craftjs/core';
|
|
|
|
|
import { cssPropsToString } from '../../utils/style-helpers';
|
2026-07-12 12:05:57 -07:00
|
|
|
import { escapeHtml, escapeAttr, safeUrl } from '../../utils/escape';
|
2026-04-05 18:31:16 -07:00
|
|
|
|
|
|
|
|
interface Slide {
|
|
|
|
|
type: 'image' | 'content';
|
|
|
|
|
imageSrc?: string;
|
|
|
|
|
heading?: string;
|
|
|
|
|
text?: string;
|
|
|
|
|
buttonText?: string;
|
|
|
|
|
buttonHref?: string;
|
|
|
|
|
bgColor?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface ContentSliderProps {
|
|
|
|
|
slides?: Slide[];
|
|
|
|
|
autoplay?: boolean;
|
|
|
|
|
interval?: number;
|
|
|
|
|
showDots?: boolean;
|
|
|
|
|
showArrows?: boolean;
|
|
|
|
|
height?: string;
|
|
|
|
|
style?: CSSProperties;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const defaultSlides: Slide[] = [
|
|
|
|
|
{
|
|
|
|
|
type: 'image',
|
|
|
|
|
imageSrc: '',
|
|
|
|
|
heading: 'First Slide',
|
|
|
|
|
text: 'Welcome to our showcase',
|
|
|
|
|
buttonText: 'Learn More',
|
|
|
|
|
buttonHref: '#',
|
|
|
|
|
bgColor: 'linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%)',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: 'image',
|
|
|
|
|
imageSrc: '',
|
|
|
|
|
heading: 'Second Slide',
|
|
|
|
|
text: 'Discover something amazing',
|
|
|
|
|
buttonText: 'Get Started',
|
|
|
|
|
buttonHref: '#',
|
|
|
|
|
bgColor: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
type: 'image',
|
|
|
|
|
imageSrc: '',
|
|
|
|
|
heading: 'Third Slide',
|
|
|
|
|
text: 'Build your future today',
|
|
|
|
|
buttonText: 'Contact Us',
|
|
|
|
|
buttonHref: '#',
|
|
|
|
|
bgColor: 'linear-gradient(135deg, #f59e0b 0%, #ef4444 100%)',
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
export const ContentSlider: UserComponent<ContentSliderProps> = ({
|
|
|
|
|
slides = defaultSlides,
|
|
|
|
|
autoplay = true,
|
|
|
|
|
interval = 5000,
|
|
|
|
|
showDots = true,
|
|
|
|
|
showArrows = true,
|
|
|
|
|
height = '400px',
|
|
|
|
|
style = {},
|
|
|
|
|
}) => {
|
|
|
|
|
const {
|
|
|
|
|
connectors: { connect, drag },
|
|
|
|
|
selected,
|
|
|
|
|
} = useNode((node) => ({
|
|
|
|
|
selected: node.events.selected,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
|
|
|
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
|
|
|
const items = slides.length > 0 ? slides : defaultSlides;
|
|
|
|
|
|
|
|
|
|
const goTo = useCallback((index: number) => {
|
|
|
|
|
setActiveIndex(((index % items.length) + items.length) % items.length);
|
|
|
|
|
}, [items.length]);
|
|
|
|
|
|
|
|
|
|
const goNext = useCallback(() => goTo(activeIndex + 1), [activeIndex, goTo]);
|
|
|
|
|
const goPrev = useCallback(() => goTo(activeIndex - 1), [activeIndex, goTo]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (autoplay && items.length > 1) {
|
|
|
|
|
timerRef.current = setInterval(goNext, interval);
|
|
|
|
|
return () => { if (timerRef.current) clearInterval(timerRef.current); };
|
|
|
|
|
}
|
|
|
|
|
}, [autoplay, interval, goNext, items.length]);
|
|
|
|
|
|
|
|
|
|
const arrowStyle: CSSProperties = {
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
top: '50%',
|
|
|
|
|
transform: 'translateY(-50%)',
|
|
|
|
|
width: '40px',
|
|
|
|
|
height: '40px',
|
|
|
|
|
borderRadius: '50%',
|
|
|
|
|
border: 'none',
|
|
|
|
|
background: 'rgba(255,255,255,0.9)',
|
|
|
|
|
color: '#18181b',
|
|
|
|
|
fontSize: '16px',
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
zIndex: 2,
|
|
|
|
|
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const renderSlide = (slide: Slide, i: number) => {
|
|
|
|
|
const bg = slide.imageSrc
|
|
|
|
|
? { backgroundImage: `url(${slide.imageSrc})`, backgroundSize: 'cover', backgroundPosition: 'center' }
|
|
|
|
|
: slide.bgColor?.startsWith('linear-gradient')
|
|
|
|
|
? { backgroundImage: slide.bgColor }
|
|
|
|
|
: { backgroundColor: slide.bgColor || '#3b82f6' };
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={i}
|
|
|
|
|
style={{
|
|
|
|
|
position: 'absolute',
|
|
|
|
|
top: 0,
|
|
|
|
|
left: 0,
|
|
|
|
|
width: '100%',
|
|
|
|
|
height: '100%',
|
|
|
|
|
opacity: i === activeIndex ? 1 : 0,
|
|
|
|
|
transition: 'opacity 0.5s ease-in-out',
|
|
|
|
|
display: 'flex',
|
|
|
|
|
flexDirection: 'column',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
...bg,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{(slide.heading || slide.text || slide.buttonText) && (
|
|
|
|
|
<div style={{ textAlign: 'center', padding: '20px', zIndex: 1 }}>
|
|
|
|
|
{slide.heading && (
|
|
|
|
|
<h2 style={{ fontSize: '36px', fontWeight: '700', color: '#ffffff', marginBottom: '12px', fontFamily: 'Inter, sans-serif', textShadow: '0 2px 8px rgba(0,0,0,0.3)' }}>
|
|
|
|
|
{slide.heading}
|
|
|
|
|
</h2>
|
|
|
|
|
)}
|
|
|
|
|
{slide.text && (
|
|
|
|
|
<p style={{ fontSize: '18px', color: 'rgba(255,255,255,0.9)', marginBottom: '20px', fontFamily: 'Inter, sans-serif', textShadow: '0 1px 4px rgba(0,0,0,0.3)' }}>
|
|
|
|
|
{slide.text}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
{slide.buttonText && (
|
|
|
|
|
<a
|
|
|
|
|
href={slide.buttonHref || '#'}
|
|
|
|
|
onClick={(e) => e.preventDefault()}
|
|
|
|
|
style={{
|
|
|
|
|
display: 'inline-block',
|
|
|
|
|
padding: '12px 28px',
|
|
|
|
|
background: '#ffffff',
|
|
|
|
|
color: '#18181b',
|
|
|
|
|
textDecoration: 'none',
|
|
|
|
|
borderRadius: '8px',
|
|
|
|
|
fontWeight: '600',
|
|
|
|
|
fontSize: '15px',
|
|
|
|
|
fontFamily: 'Inter, sans-serif',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{slide.buttonText}
|
|
|
|
|
</a>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<section
|
|
|
|
|
ref={(ref: HTMLElement | null): void => { if (ref) connect(drag(ref)); }}
|
|
|
|
|
style={{
|
|
|
|
|
position: 'relative',
|
|
|
|
|
width: '100%',
|
|
|
|
|
height,
|
|
|
|
|
overflow: 'hidden',
|
|
|
|
|
outline: selected ? '2px solid #3b82f6' : 'none',
|
|
|
|
|
...style,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{items.map((slide, i) => renderSlide(slide, i))}
|
|
|
|
|
|
|
|
|
|
{showArrows && items.length > 1 && (
|
|
|
|
|
<>
|
|
|
|
|
<button onClick={goPrev} style={{ ...arrowStyle, left: '16px' }}>
|
|
|
|
|
<i className="fa fa-chevron-left" />
|
|
|
|
|
</button>
|
|
|
|
|
<button onClick={goNext} style={{ ...arrowStyle, right: '16px' }}>
|
|
|
|
|
<i className="fa fa-chevron-right" />
|
|
|
|
|
</button>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{showDots && items.length > 1 && (
|
|
|
|
|
<div style={{ position: 'absolute', bottom: '16px', left: '50%', transform: 'translateX(-50%)', display: 'flex', gap: '8px', zIndex: 2 }}>
|
|
|
|
|
{items.map((_, i) => (
|
|
|
|
|
<button
|
|
|
|
|
key={i}
|
|
|
|
|
onClick={() => goTo(i)}
|
|
|
|
|
style={{
|
|
|
|
|
width: '10px',
|
|
|
|
|
height: '10px',
|
|
|
|
|
borderRadius: '50%',
|
|
|
|
|
border: 'none',
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
backgroundColor: i === activeIndex ? '#ffffff' : 'rgba(255,255,255,0.5)',
|
|
|
|
|
transition: 'background-color 0.3s',
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</section>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/* ---------- Craft config ---------- */
|
|
|
|
|
|
|
|
|
|
ContentSlider.craft = {
|
|
|
|
|
displayName: 'Content Slider',
|
|
|
|
|
props: {
|
|
|
|
|
slides: defaultSlides,
|
|
|
|
|
autoplay: true,
|
|
|
|
|
interval: 5000,
|
|
|
|
|
showDots: true,
|
|
|
|
|
showArrows: true,
|
|
|
|
|
height: '400px',
|
|
|
|
|
style: {},
|
|
|
|
|
},
|
|
|
|
|
rules: {
|
|
|
|
|
canDrag: () => true,
|
|
|
|
|
canMoveIn: () => false,
|
|
|
|
|
canMoveOut: () => true,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/* ---------- HTML export ---------- */
|
|
|
|
|
|
|
|
|
|
(ContentSlider as any).toHtml = (props: ContentSliderProps, _childrenHtml: string) => {
|
|
|
|
|
const {
|
|
|
|
|
slides = defaultSlides,
|
|
|
|
|
autoplay = true,
|
|
|
|
|
interval = 5000,
|
|
|
|
|
showDots = true,
|
|
|
|
|
showArrows = true,
|
|
|
|
|
height = '400px',
|
|
|
|
|
style = {},
|
|
|
|
|
} = props;
|
|
|
|
|
|
|
|
|
|
const items = slides.length > 0 ? slides : defaultSlides;
|
|
|
|
|
const uid = 'cs_' + Math.random().toString(36).slice(2, 8);
|
|
|
|
|
|
|
|
|
|
const sectionStyle = cssPropsToString({
|
|
|
|
|
position: 'relative',
|
|
|
|
|
width: '100%',
|
|
|
|
|
height,
|
|
|
|
|
overflow: 'hidden',
|
|
|
|
|
...style,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const slidesHtml = items.map((slide, i) => {
|
|
|
|
|
const hasBgImage = slide.imageSrc;
|
|
|
|
|
const bgStyle = hasBgImage
|
2026-07-12 12:05:57 -07:00
|
|
|
? `background-image:url('${escapeAttr(safeUrl(slide.imageSrc!))}');background-size:cover;background-position:center`
|
2026-04-05 18:31:16 -07:00
|
|
|
: slide.bgColor?.startsWith('linear-gradient')
|
|
|
|
|
? `background-image:${slide.bgColor}`
|
|
|
|
|
: `background-color:${slide.bgColor || '#3b82f6'}`;
|
|
|
|
|
|
|
|
|
|
const contentParts: string[] = [];
|
|
|
|
|
if (slide.heading) {
|
2026-07-12 11:49:10 -07:00
|
|
|
contentParts.push(`<h2 style="font-size:36px;font-weight:700;color:#ffffff;margin-bottom:12px;font-family:Inter,sans-serif;text-shadow:0 2px 8px rgba(0,0,0,0.3)">${escapeHtml(slide.heading)}</h2>`);
|
2026-04-05 18:31:16 -07:00
|
|
|
}
|
|
|
|
|
if (slide.text) {
|
2026-07-12 11:49:10 -07:00
|
|
|
contentParts.push(`<p style="font-size:18px;color:rgba(255,255,255,0.9);margin-bottom:20px;font-family:Inter,sans-serif;text-shadow:0 1px 4px rgba(0,0,0,0.3)">${escapeHtml(slide.text)}</p>`);
|
2026-04-05 18:31:16 -07:00
|
|
|
}
|
|
|
|
|
if (slide.buttonText) {
|
2026-07-12 12:05:57 -07:00
|
|
|
contentParts.push(`<a href="${escapeAttr(safeUrl(slide.buttonHref || '#'))}" style="display:inline-block;padding:12px 28px;background:#ffffff;color:#18181b;text-decoration:none;border-radius:8px;font-weight:600;font-size:15px;font-family:Inter,sans-serif">${escapeHtml(slide.buttonText)}</a>`);
|
2026-04-05 18:31:16 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const innerHtml = contentParts.length > 0
|
|
|
|
|
? `<div style="text-align:center;padding:20px;z-index:1">${contentParts.join('\n ')}</div>`
|
|
|
|
|
: '';
|
|
|
|
|
|
|
|
|
|
return `<div id="${uid}_s${i}" style="position:absolute;top:0;left:0;width:100%;height:100%;opacity:${i === 0 ? 1 : 0};transition:opacity 0.5s ease-in-out;display:flex;flex-direction:column;align-items:center;justify-content:center;${bgStyle}">
|
|
|
|
|
${innerHtml}
|
|
|
|
|
</div>`;
|
|
|
|
|
}).join('\n ');
|
|
|
|
|
|
|
|
|
|
const arrowsHtml = showArrows && items.length > 1
|
|
|
|
|
? `<button onclick="${uid}_prev()" style="position:absolute;top:50%;left:16px;transform:translateY(-50%);width:40px;height:40px;border-radius:50%;border:none;background:rgba(255,255,255,0.9);color:#18181b;font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:2;box-shadow:0 2px 8px rgba(0,0,0,0.15)"><i class="fa fa-chevron-left"></i></button>
|
|
|
|
|
<button onclick="${uid}_next()" style="position:absolute;top:50%;right:16px;transform:translateY(-50%);width:40px;height:40px;border-radius:50%;border:none;background:rgba(255,255,255,0.9);color:#18181b;font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:2;box-shadow:0 2px 8px rgba(0,0,0,0.15)"><i class="fa fa-chevron-right"></i></button>`
|
|
|
|
|
: '';
|
|
|
|
|
|
|
|
|
|
const dotsHtml = showDots && items.length > 1
|
|
|
|
|
? `<div style="position:absolute;bottom:16px;left:50%;transform:translateX(-50%);display:flex;gap:8px;z-index:2">
|
|
|
|
|
${items.map((_, i) => `<button onclick="${uid}_go(${i})" id="${uid}_d${i}" style="width:10px;height:10px;border-radius:50%;border:none;cursor:pointer;background-color:${i === 0 ? '#ffffff' : 'rgba(255,255,255,0.5)'};transition:background-color 0.3s"></button>`).join('\n ')}
|
|
|
|
|
</div>`
|
|
|
|
|
: '';
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
html: `<section${sectionStyle ? ` style="${sectionStyle}"` : ''}>
|
|
|
|
|
${slidesHtml}
|
|
|
|
|
${arrowsHtml}
|
|
|
|
|
${dotsHtml}
|
|
|
|
|
<script>
|
|
|
|
|
(function(){
|
|
|
|
|
var current=0, total=${items.length}, uid="${uid}";
|
|
|
|
|
function show(idx){
|
|
|
|
|
document.getElementById(uid+"_s"+current).style.opacity="0";
|
|
|
|
|
${showDots ? `document.getElementById(uid+"_d"+current).style.backgroundColor="rgba(255,255,255,0.5)";` : ''}
|
|
|
|
|
current=((idx%total)+total)%total;
|
|
|
|
|
document.getElementById(uid+"_s"+current).style.opacity="1";
|
|
|
|
|
${showDots ? `document.getElementById(uid+"_d"+current).style.backgroundColor="#ffffff";` : ''}
|
|
|
|
|
}
|
|
|
|
|
window["${uid}_go"]=show;
|
|
|
|
|
window["${uid}_next"]=function(){show(current+1);};
|
|
|
|
|
window["${uid}_prev"]=function(){show(current-1);};
|
|
|
|
|
${autoplay && items.length > 1 ? `setInterval(function(){show(current+1);},${interval});` : ''}
|
|
|
|
|
})();
|
|
|
|
|
</script>
|
|
|
|
|
</section>`,
|
|
|
|
|
};
|
|
|
|
|
};
|