import React, { CSSProperties } from 'react'; import { useNode, UserComponent } from '@craftjs/core'; import { cssPropsToString } from '../../utils/style-helpers'; 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', }; const platformLabels: Record = { facebook: 'Facebook', twitter: 'Twitter / X', instagram: 'Instagram', linkedin: 'LinkedIn', youtube: 'YouTube', github: 'GitHub', tiktok: 'TikTok', pinterest: 'Pinterest', snapchat: 'Snapchat', whatsapp: 'WhatsApp', }; const allPlatforms = Object.keys(platformIcons); 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, }} > ); })}
); }; /* ---------- Settings panel ---------- */ const SocialLinksSettings: React.FC = () => { const { actions: { setProp }, props } = useNode((node) => ({ props: node.data.props as SocialLinksProps, })); const links = props.links || defaultLinks; const inputStyle: CSSProperties = { width: '100%', padding: '3px 6px', background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, fontSize: 11, }; const updateLink = (index: number, field: keyof SocialLink, value: string) => { setProp((p: SocialLinksProps) => { const updated = [...(p.links || defaultLinks)]; updated[index] = { ...updated[index], [field]: value }; p.links = updated; }); }; const addLink = (platform: string) => { setProp((p: SocialLinksProps) => { p.links = [...(p.links || defaultLinks), { platform, url: '#' }]; }); }; const removeLink = (index: number) => { setProp((p: SocialLinksProps) => { const updated = [...(p.links || defaultLinks)]; updated.splice(index, 1); p.links = updated; }); }; const usedPlatforms = new Set(links.map((l) => l.platform)); const availablePlatforms = allPlatforms.filter((p) => !usedPlatforms.has(p)); const sizePresets = ['14px', '18px', '20px', '24px', '28px', '32px']; const gapPresets = ['4px', '8px', '10px', '14px', '20px']; const iconColorPresets = ['#ffffff', '#18181b', '#3b82f6', '#10b981', '#ef4444', '#8b5cf6', '#f59e0b', '#a1a1aa']; const bgColorPresets = ['#374151', '#18181b', '#3b82f6', '#10b981', '#ef4444', '#8b5cf6', '#0ea5e9', 'transparent']; return (
{/* Links Editor */}
{links.map((link, i) => (
{platformLabels[link.platform] || link.platform}
updateLink(i, 'url', e.target.value)} placeholder="https://..." style={inputStyle} />
))}
{availablePlatforms.length > 0 && (
)}
{/* Alignment */}
{(['left', 'center', 'right'] as const).map((a) => ( ))}
{/* Icon Shape */}
{(['none', 'circle', 'square', 'rounded'] as const).map((s) => ( ))}
{/* Icon Size */}
{sizePresets.map((s) => ( ))}
{/* Icon Color */}
{iconColorPresets.map((c) => (
{/* Icon Background Color */} {props.iconShape !== 'none' && (
{bgColorPresets.map((c) => (
)} {/* Gap */}
{gapPresets.map((g) => ( ))}
); }; /* ---------- 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, }, related: { settings: SocialLinksSettings, }, }; /* ---------- HTML export ---------- */ (SocialLinks as any).toHtml = (props: SocialLinksProps, _childrenHtml: string) => { const links = props.links || defaultLinks; const iconSize = props.iconSize || '20px'; const iconColor = props.iconColor || '#ffffff'; const iconBgColor = 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()}`; } return ``; }).join('\n '); return { html: ` ${linksHtml} `, }; };