import React, { CSSProperties } from 'react'; import { escapeHtml, escapeAttr, safeUrl } from '../../utils/escape'; export type CtaVariant = 'primary' | 'outline' | 'ghost'; export interface CtaButton { text: string; href: string; variant?: CtaVariant; target?: '_blank'; } export interface CtaStyleDefaults { primaryBg: string; primaryText: string; outlineText: string; } /** * Read the effective list of CTAs for a section, falling back to legacy * primary/secondary props when ctas[] is absent. New sections write ctas[] * directly; old sections keep rendering until the user touches the settings. */ export function normalizeCtas(props: { ctas?: CtaButton[]; buttonText?: string; buttonHref?: string; secondaryButtonText?: string; secondaryButtonHref?: string; }): CtaButton[] { if (Array.isArray(props.ctas) && props.ctas.length > 0) { return props.ctas.filter((c) => c && (c.text || c.href)); } const legacy: CtaButton[] = []; if (props.buttonText) legacy.push({ text: props.buttonText, href: props.buttonHref || '#', variant: 'primary' }); if (props.secondaryButtonText) legacy.push({ text: props.secondaryButtonText, href: props.secondaryButtonHref || '#', variant: 'outline' }); return legacy; } export function ctaInlineStyle(cta: CtaButton, defaults: CtaStyleDefaults): CSSProperties { const variant = cta.variant || 'primary'; switch (variant) { case 'outline': return { display: 'inline-block', padding: '14px 36px', backgroundColor: 'transparent', color: defaults.outlineText, textDecoration: 'none', borderRadius: '8px', fontWeight: 600, fontSize: '16px', border: `2px solid ${defaults.outlineText}`, }; case 'ghost': return { display: 'inline-block', padding: '14px 24px', backgroundColor: 'transparent', color: defaults.outlineText, textDecoration: 'underline', borderRadius: '8px', fontWeight: 600, fontSize: '16px', }; case 'primary': default: return { display: 'inline-block', padding: '14px 36px', backgroundColor: defaults.primaryBg, color: defaults.primaryText, textDecoration: 'none', borderRadius: '8px', fontWeight: 600, fontSize: '16px', }; } } export function ctaCssString(cta: CtaButton, defaults: CtaStyleDefaults): string { const variant = cta.variant || 'primary'; switch (variant) { case 'outline': return `display:inline-block;padding:14px 36px;background-color:transparent;color:${defaults.outlineText};text-decoration:none;border-radius:8px;font-weight:600;font-size:16px;border:2px solid ${defaults.outlineText}`; case 'ghost': return `display:inline-block;padding:14px 24px;background-color:transparent;color:${defaults.outlineText};text-decoration:underline;border-radius:8px;font-weight:600;font-size:16px`; case 'primary': default: return `display:inline-block;padding:14px 36px;background-color:${defaults.primaryBg};color:${defaults.primaryText};text-decoration:none;border-radius:8px;font-weight:600;font-size:16px`; } } export function ctasToHtml(ctas: CtaButton[], defaults: CtaStyleDefaults): string { return ctas.map((c) => { const target = c.target === '_blank' ? ' target="_blank" rel="noopener noreferrer"' : ''; return `${escapeHtml(c.text || '')}`; }).join(''); } /* ---------- CTAs editor (settings UI) ---------- */ interface CtasEditorProps { ctas: CtaButton[]; /** Called whenever the user mutates the array. Sections wire this via setProp. */ onChange: (next: CtaButton[]) => void; /** Max items the user can add. Default 4. */ max?: number; } const inputStyle: CSSProperties = { width: '100%', padding: '6px 8px', background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, fontSize: 12, }; const labelStyle: CSSProperties = { fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 4, }; export const CtasEditor: React.FC = ({ ctas, onChange, max = 4 }) => { const update = (i: number, patch: Partial) => { const next = ctas.slice(); next[i] = { ...next[i], ...patch }; onChange(next); }; const remove = (i: number) => onChange(ctas.filter((_, j) => j !== i)); const add = () => { if (ctas.length >= max) return; onChange([...ctas, { text: 'New button', href: '#', variant: ctas.length === 0 ? 'primary' : 'outline' }]); }; const move = (i: number, dir: -1 | 1) => { const j = i + dir; if (j < 0 || j >= ctas.length) return; const next = ctas.slice(); [next[i], next[j]] = [next[j], next[i]]; onChange(next); }; return ( Buttons ({ctas.length}) {ctas.length === 0 && ( No buttons. Click "Add button" to insert one. )} {ctas.map((cta, i) => ( Button {i + 1} move(i, -1)} disabled={i === 0} title="Move up" style={iconBtn(i === 0)}>↑ move(i, 1)} disabled={i === ctas.length - 1} title="Move down" style={iconBtn(i === ctas.length - 1)}>↓ remove(i)} title="Remove" style={{ ...iconBtn(false), color: '#fca5a5' }}>✕ Text update(i, { text: e.target.value })} style={inputStyle} /> URL update(i, { href: e.target.value })} placeholder="https://… or #anchor" style={inputStyle} /> Style update(i, { variant: e.target.value as CtaVariant })} style={{ ...inputStyle, padding: '5px 6px' }}> Primary (filled) Outline Ghost (text) update(i, { target: e.target.checked ? '_blank' : undefined })} /> New tab ))} {ctas.length < max && ( + Add button{ctas.length === 0 ? '' : ` (${max - ctas.length} more)`} )} ); }; function iconBtn(disabled: boolean): CSSProperties { return { width: 22, height: 22, fontSize: 11, background: '#27272a', color: disabled ? '#52525b' : '#a1a1aa', border: '1px solid #3f3f46', borderRadius: 4, cursor: disabled ? 'not-allowed' : 'pointer', }; }