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

196 lines
7.0 KiB
TypeScript
Raw Normal View History

import React, { CSSProperties } from 'react';
import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers';
import { escapeHtml, escapeAttr, safeUrl } 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(`<svg xmlns="http://www.w3.org/2000/svg" width="400" height="300" viewBox="0 0 400 300"><rect fill="${color}" width="400" height="300" opacity="0.15"/><rect fill="${color}" x="150" y="100" width="100" height="100" rx="12" opacity="0.3"/><text x="200" y="160" text-anchor="middle" font-family="sans-serif" font-size="24" fill="${color}" opacity="0.6">${index + 1}</text></svg>`)}`;
};
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<GalleryProps> = ({
images = defaultImages,
columns = 3,
gap = '16px',
style = {},
lightbox = false,
}) => {
const {
connectors: { connect, drag },
selected,
} = useNode((node) => ({
selected: node.events.selected,
}));
return (
<section
ref={(ref: HTMLElement | null): void => { if (ref) connect(drag(ref)); }}
style={{
padding: '60px 20px',
backgroundColor: '#ffffff',
outline: selected ? '2px solid #3b82f6' : 'none',
...style,
}}
>
<div
style={{
maxWidth: '1100px',
margin: '0 auto',
display: 'grid',
gridTemplateColumns: `repeat(${columns}, 1fr)`,
gap: gap,
}}
>
{images.map((img, i) => (
<div key={i} style={{ position: 'relative', overflow: 'hidden', borderRadius: '8px' }}>
<img
src={img.src}
alt={img.alt}
style={{
width: '100%',
height: '200px',
objectFit: 'cover',
display: 'block',
borderRadius: '8px',
backgroundColor: '#f1f5f9',
}}
/>
{img.caption && (
<div
style={{
position: 'absolute',
bottom: '0',
left: '0',
right: '0',
padding: '8px 12px',
background: 'linear-gradient(transparent, rgba(0,0,0,0.7))',
color: '#ffffff',
fontSize: '12px',
borderBottomLeftRadius: '8px',
borderBottomRightRadius: '8px',
}}
>
{img.caption}
</div>
)}
</div>
))}
</div>
</section>
);
};
/* ---------- 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,
},
};
/* ---------- 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
? `<div style="position:absolute;bottom:0;left:0;right:0;padding:8px 12px;background:linear-gradient(transparent,rgba(0,0,0,0.7));color:#ffffff;font-size:12px;border-bottom-left-radius:8px;border-bottom-right-radius:8px">${escapeHtml(img.caption)}</div>`
: '';
// Lightbox items carry the image URL as a data attribute rather than an
// inline onclick with an interpolated src -- a single delegated click
// listener below reads it, so a src containing a quote can't break out
// of a per-item event-handler string.
const lbAttr = lightbox ? ` data-lb-src="${escapeAttr(safeUrl(img.src || ''))}" role="button" tabindex="0"` : '';
const itemStyle = lightbox ? 'cursor:pointer;position:relative;overflow:hidden;border-radius:8px' : 'position:relative;overflow:hidden;border-radius:8px';
return `<div${lbAttr} style="${itemStyle}">
<img src="${escapeAttr(safeUrl(img.src || ''))}" alt="${escapeAttr(img.alt)}" style="width:100%;height:200px;object-fit:cover;display:block;border-radius:8px;background-color:#f1f5f9" />
${caption}
</div>`;
}).join('\n ');
let lightboxHtml = '';
let gridIdAttr = '';
if (lightbox) {
gridIdAttr = ` id="${galleryId}_grid"`;
lightboxHtml = `
<div id="${galleryId}_overlay" role="dialog" aria-modal="true" aria-label="Image preview" onclick="${galleryId}_close()" style="display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);z-index:9999;justify-content:center;align-items:center;cursor:pointer">
<img id="${galleryId}_img" src="" alt="" style="max-width:90%;max-height:90%;object-fit:contain;border-radius:8px" />
</div>
<script>
function ${galleryId}_close(){document.getElementById('${galleryId}_overlay').style.display='none';}
function ${galleryId}_open(src){
var o = document.getElementById('${galleryId}_overlay');
document.getElementById('${galleryId}_img').src = src;
o.style.display = 'flex';
}
document.getElementById('${galleryId}_grid').addEventListener('click', function(e){
var t = e.target.closest('[data-lb-src]');
if(!t) return;
${galleryId}_open(t.getAttribute('data-lb-src'));
});
document.getElementById('${galleryId}_grid').addEventListener('keydown', function(e){
if(e.key!=='Enter' && e.key!==' ') return;
var t = e.target.closest('[data-lb-src]');
if(!t) return;
e.preventDefault();
${galleryId}_open(t.getAttribute('data-lb-src'));
});
document.addEventListener('keydown', function(e){
if(e.key==='Escape'){ ${galleryId}_close(); }
});
</script>`;
}
return {
html: `<section${sectionStyle ? ` style="${sectionStyle}"` : ''}>
<div${gridIdAttr} style="max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(${columns},1fr);gap:${gap}">
${items}
</div>${lightboxHtml}
</section>`,
};
};