import React, { CSSProperties } from 'react'; import { useNode, UserComponent } from '@craftjs/core'; import { cssPropsToString } from '../../utils/style-helpers'; import { escapeHtml, escapeAttr } from '../../utils/escape'; interface GalleryImage { src: string; alt: string; caption?: string; } interface GalleryProps { images?: GalleryImage[]; columns?: number; gap?: string; style?: CSSProperties; lightbox?: boolean; } const placeholderSvg = (index: number) => { const colors = ['#3b82f6', '#8b5cf6', '#10b981', '#f59e0b', '#ef4444', '#ec4899']; const color = colors[index % colors.length]; return `data:image/svg+xml,${encodeURIComponent(`${index + 1}`)}`; }; const defaultImages: GalleryImage[] = [ { src: placeholderSvg(0), alt: 'Gallery image 1', caption: 'First image' }, { src: placeholderSvg(1), alt: 'Gallery image 2', caption: 'Second image' }, { src: placeholderSvg(2), alt: 'Gallery image 3', caption: 'Third image' }, { src: placeholderSvg(3), alt: 'Gallery image 4', caption: 'Fourth image' }, { src: placeholderSvg(4), alt: 'Gallery image 5', caption: 'Fifth image' }, { src: placeholderSvg(5), alt: 'Gallery image 6', caption: 'Sixth image' }, ]; export const Gallery: UserComponent = ({ images = defaultImages, columns = 3, gap = '16px', style = {}, lightbox = false, }) => { const { connectors: { connect, drag }, selected, } = useNode((node) => ({ selected: node.events.selected, })); return (
{ if (ref) connect(drag(ref)); }} style={{ padding: '60px 20px', backgroundColor: '#ffffff', outline: selected ? '2px solid #3b82f6' : 'none', ...style, }} >
{images.map((img, i) => (
{img.alt} {img.caption && (
{img.caption}
)}
))}
); }; /* ---------- Settings panel ---------- */ const GallerySettings: React.FC = () => { const { actions: { setProp }, props } = useNode((node) => ({ props: node.data.props as GalleryProps, })); const images = props.images || defaultImages; const inputStyle: CSSProperties = { width: '100%', padding: '3px 6px', background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, fontSize: 11, }; const labelStyle: CSSProperties = { fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }; const updateImage = (index: number, field: keyof GalleryImage, value: string) => { setProp((p: GalleryProps) => { const updated = [...(p.images || defaultImages)]; updated[index] = { ...updated[index], [field]: value }; p.images = updated; }); }; const addImage = () => { setProp((p: GalleryProps) => { const current = p.images || defaultImages; p.images = [...current, { src: placeholderSvg(current.length), alt: `Gallery image ${current.length + 1}`, caption: '' }]; }); }; const removeImage = (index: number) => { setProp((p: GalleryProps) => { const updated = [...(p.images || defaultImages)]; updated.splice(index, 1); p.images = updated; }); }; const columnOptions = [2, 3, 4, 5, 6]; const gapOptions = ['8px', '12px', '16px', '24px', '32px']; return (
{['#ffffff', '#f8fafc', '#f1f5f9', '#18181b', '#0f172a'].map((c) => (
{columnOptions.map((n) => ( ))}
{gapOptions.map((g) => ( ))}
{images.map((img, i) => (
updateImage(i, 'src', e.target.value)} placeholder="Image URL" style={{ ...inputStyle, flex: 1 }} />
updateImage(i, 'alt', e.target.value)} placeholder="Alt text" style={{ ...inputStyle, flex: 1 }} /> updateImage(i, 'caption', e.target.value)} placeholder="Caption" style={{ ...inputStyle, flex: 1 }} />
))}
); }; /* ---------- Craft config ---------- */ Gallery.craft = { displayName: 'Gallery', props: { images: defaultImages, columns: 3, gap: '16px', style: { backgroundColor: '#ffffff' }, lightbox: false, }, rules: { canDrag: () => true, canMoveIn: () => false, canMoveOut: () => true, }, related: { settings: GallerySettings, }, }; /* ---------- HTML export ---------- */ (Gallery as any).toHtml = (props: GalleryProps, _childrenHtml: string) => { const sectionStyle = cssPropsToString({ padding: '60px 20px', ...props.style, }); const images = props.images || defaultImages; const columns = props.columns || 3; const gap = props.gap || '16px'; const lightbox = props.lightbox || false; const galleryId = 'gallery_' + Math.random().toString(36).slice(2, 8); const items = images.map((img) => { const caption = img.caption ? `
${escapeHtml(img.caption)}
` : ''; const clickAttr = lightbox ? ` onclick="${galleryId}_open('${escapeAttr(img.src)}')" style="cursor:pointer;position:relative;overflow:hidden;border-radius:8px"` : ' style="position:relative;overflow:hidden;border-radius:8px"'; return ` ${escapeAttr(img.alt)} ${caption} `; }).join('\n '); let lightboxHtml = ''; if (lightbox) { lightboxHtml = ` `; } return { html: `
${items}
${lightboxHtml} `, }; };