import React, { CSSProperties } from 'react'; import { useNode, UserComponent } from '@craftjs/core'; import { cssPropsToString } from '../../utils/style-helpers'; import { escapeAttr, safeUrl, cssValue } from '../../utils/escape'; interface SocialLink { platform: string; url: string; } interface SocialLinksProps { links?: SocialLink[]; iconSize?: string; iconColor?: string; iconBgColor?: string; iconShape?: 'none' | 'circle' | 'square' | 'rounded'; gap?: string; alignment?: 'left' | 'center' | 'right'; style?: CSSProperties; } const platformIcons: Record = { facebook: 'fa-facebook', twitter: 'fa-twitter', instagram: 'fa-instagram', linkedin: 'fa-linkedin', youtube: 'fa-youtube', github: 'fa-github', tiktok: 'fa-music', pinterest: 'fa-pinterest', snapchat: 'fa-snapchat', whatsapp: 'fa-whatsapp', spotify: 'fa-spotify', twitch: 'fa-twitch', }; const platformLabels: Record = { facebook: 'Facebook', twitter: 'Twitter / X', instagram: 'Instagram', linkedin: 'LinkedIn', youtube: 'YouTube', github: 'GitHub', tiktok: 'TikTok', pinterest: 'Pinterest', snapchat: 'Snapchat', whatsapp: 'WhatsApp', spotify: 'Spotify', twitch: 'Twitch', }; const defaultLinks: SocialLink[] = [ { platform: 'facebook', url: '#' }, { platform: 'twitter', url: '#' }, { platform: 'instagram', url: '#' }, { platform: 'linkedin', url: '#' }, ]; const getShapeStyle = (shape: string, size: string): CSSProperties => { if (shape === 'none') return {}; const numSize = parseInt(size) || 24; const boxSize = `${numSize + 16}px`; const base: CSSProperties = { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: boxSize, height: boxSize, }; if (shape === 'circle') return { ...base, borderRadius: '50%' }; if (shape === 'square') return { ...base, borderRadius: '0' }; if (shape === 'rounded') return { ...base, borderRadius: '6px' }; return base; }; const alignMap: Record = { left: 'flex-start', center: 'center', right: 'flex-end', }; export const SocialLinks: UserComponent = ({ links = defaultLinks, iconSize = '20px', iconColor = '#ffffff', iconBgColor = '#374151', iconShape = 'circle', gap = '10px', alignment = 'center', style = {}, }) => { const { connectors: { connect, drag }, selected, } = useNode((node) => ({ selected: node.events.selected, })); return (
{ if (ref) connect(drag(ref)); }} style={{ display: 'flex', flexWrap: 'wrap', gap, justifyContent: alignMap[alignment] || 'center', alignItems: 'center', outline: selected ? '2px solid #3b82f6' : 'none', ...style, }} > {links.map((link, i) => { const iconClass = platformIcons[link.platform] || 'fa-link'; const shapeStyle = getShapeStyle(iconShape, iconSize); const hasBg = iconShape !== 'none'; return ( e.preventDefault()} title={platformLabels[link.platform] || link.platform} style={{ textDecoration: 'none', color: iconColor, backgroundColor: hasBg ? iconBgColor : 'transparent', transition: 'opacity 0.2s', ...shapeStyle, }} > ); })}
); }; /* ---------- Craft config ---------- */ SocialLinks.craft = { displayName: 'Social Links', props: { links: defaultLinks, iconSize: '20px', iconColor: '#ffffff', iconBgColor: '#374151', iconShape: 'circle', gap: '10px', alignment: 'center', style: {}, }, rules: { canDrag: () => true, canMoveIn: () => false, canMoveOut: () => true, }, }; /* ---------- HTML export ---------- */ (SocialLinks as any).toHtml = (props: SocialLinksProps, _childrenHtml: string) => { const links = props.links || defaultLinks; // Sanitized -- raw string-interpolation sinks in aStyle/getShapeStr below. const iconSize = cssValue(props.iconSize) || '20px'; const iconColor = cssValue(props.iconColor) || '#ffffff'; const iconBgColor = cssValue(props.iconBgColor) || '#374151'; const iconShape = props.iconShape || 'circle'; const gap = props.gap || '10px'; const alignment = props.alignment || 'center'; const hasBg = iconShape !== 'none'; const wrapperStyle = cssPropsToString({ display: 'flex', flexWrap: 'wrap', gap, justifyContent: alignMap[alignment] || 'center', alignItems: 'center', ...props.style, }); const numSize = parseInt(iconSize) || 20; const boxSize = `${numSize + 16}px`; const getShapeStr = (): string => { const parts: string[] = [ `display:inline-flex`, `align-items:center`, `justify-content:center`, `width:${boxSize}`, `height:${boxSize}`, ]; if (iconShape === 'circle') parts.push('border-radius:50%'); else if (iconShape === 'square') parts.push('border-radius:0'); else if (iconShape === 'rounded') parts.push('border-radius:6px'); return parts.join(';'); }; const linksHtml = links.map((link) => { const iconClass = platformIcons[link.platform] || 'fa-link'; const title = platformLabels[link.platform] || link.platform; let aStyle = `text-decoration:none;color:${iconColor};background-color:${hasBg ? iconBgColor : 'transparent'}`; if (hasBg) { aStyle += `;${getShapeStr()}`; } // The link's only content is the icon glyph, so the glyph itself is // aria-hidden and the accessible name lives on the link (aria-label, // mirroring the existing `title` tooltip since title support in // screen readers is inconsistent). return ``; }).join('\n '); return { html: ` ${linksHtml} `, }; };