2026-07-12 13:16:53 -07:00
|
|
|
import { CSSProperties } from 'react';
|
2026-07-12 15:56:25 -07:00
|
|
|
import { escapeHtml, escapeAttr, safeUrl, cssValue } from '../../utils/escape';
|
2026-05-25 12:43:28 -07:00
|
|
|
|
|
|
|
|
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';
|
2026-07-12 15:56:25 -07:00
|
|
|
// Sanitized -- these are raw string-interpolation sinks into style="...".
|
|
|
|
|
// Callers pass user-controlled design-token colors (e.g. HeroSimple's
|
|
|
|
|
// buttonBgColor/buttonTextColor/textColor) through CtaStyleDefaults, so
|
|
|
|
|
// sanitize once here rather than at every call site.
|
|
|
|
|
const outlineText = cssValue(defaults.outlineText) || '#000000';
|
|
|
|
|
const primaryBg = cssValue(defaults.primaryBg) || '#000000';
|
|
|
|
|
const primaryText = cssValue(defaults.primaryText) || '#ffffff';
|
2026-05-25 12:43:28 -07:00
|
|
|
switch (variant) {
|
|
|
|
|
case 'outline':
|
2026-07-12 15:56:25 -07:00
|
|
|
return `display:inline-block;padding:14px 36px;background-color:transparent;color:${outlineText};text-decoration:none;border-radius:8px;font-weight:600;font-size:16px;border:2px solid ${outlineText}`;
|
2026-05-25 12:43:28 -07:00
|
|
|
case 'ghost':
|
2026-07-12 15:56:25 -07:00
|
|
|
return `display:inline-block;padding:14px 24px;background-color:transparent;color:${outlineText};text-decoration:underline;border-radius:8px;font-weight:600;font-size:16px`;
|
2026-05-25 12:43:28 -07:00
|
|
|
case 'primary':
|
|
|
|
|
default:
|
2026-07-12 15:56:25 -07:00
|
|
|
return `display:inline-block;padding:14px 36px;background-color:${primaryBg};color:${primaryText};text-decoration:none;border-radius:8px;font-weight:600;font-size:16px`;
|
2026-05-25 12:43:28 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function ctasToHtml(ctas: CtaButton[], defaults: CtaStyleDefaults): string {
|
|
|
|
|
return ctas.map((c) => {
|
|
|
|
|
const target = c.target === '_blank' ? ' target="_blank" rel="noopener noreferrer"' : '';
|
2026-07-12 12:05:57 -07:00
|
|
|
return `<a href="${escapeAttr(safeUrl(c.href || '#'))}"${target} style="${ctaCssString(c, defaults)}">${escapeHtml(c.text || '')}</a>`;
|
2026-05-25 12:43:28 -07:00
|
|
|
}).join('');
|
|
|
|
|
}
|
|
|
|
|
|