import React, { CSSProperties } from 'react'; import { useNode, UserComponent } from '@craftjs/core'; import { cssPropsToString } from '../../utils/style-helpers'; import { escapeHtml, escapeAttr, safeUrl, cssValue } from '../../utils/escape'; interface PricingPlan { name: string; price: string; period: string; features: string[]; buttonText: string; buttonHref: string; isFeatured: boolean; } interface PricingTableProps { plans?: PricingPlan[]; style?: CSSProperties; featuredBg?: string; bulletType?: string; anchorId?: string; /* ---- Regular (non-featured) card colors ---- All optional; each falls back to the exact literal the card was previously hard-coded to (or, for the button, to featuredBg -- the button's original derivation) when left unset, so existing saved projects render pixel-identical until a color is explicitly picked. */ cardBg?: string; textColor?: string; subColor?: string; featColor?: string; checkColor?: string; btnBg?: string; btnColor?: string; animation?: string; animationDelay?: string; hideOnDesktop?: boolean; hideOnTablet?: boolean; hideOnMobile?: boolean; } const bulletChars: Record = { check: '✓', dot: '●', arrow: '→', star: '★', dash: '—', none: '', }; const defaultPlans: PricingPlan[] = [ { name: 'Basic', price: '$9', period: '/month', features: ['1 Website', '10 GB Storage', 'Free SSL Certificate', 'Email Support'], buttonText: 'Get Started', buttonHref: '#', isFeatured: false, }, { name: 'Pro', price: '$29', period: '/month', features: ['10 Websites', '100 GB Storage', 'Free SSL Certificate', 'Priority Support', 'Custom Domain', 'Analytics Dashboard'], buttonText: 'Get Started', buttonHref: '#', isFeatured: true, }, { name: 'Enterprise', price: '$99', period: '/month', features: ['Unlimited Websites', '1 TB Storage', 'Free SSL Certificate', '24/7 Phone Support', 'Custom Domain', 'Advanced Analytics', 'API Access', 'Team Collaboration'], buttonText: 'Contact Sales', buttonHref: '#', isFeatured: false, }, ]; export const PricingTable: UserComponent = ({ plans = defaultPlans, style = {}, featuredBg = '#3b82f6', bulletType = 'check', anchorId, cardBg, textColor, subColor, featColor, checkColor, btnBg, btnColor, }) => { const { connectors: { connect, drag }, selected, } = useNode((node) => ({ selected: node.events.selected, })); const regCardBg = cardBg || '#ffffff'; const regTextColor = textColor || '#18181b'; const regSubColor = subColor || '#64748b'; const regFeatColor = featColor || '#4b5563'; const regCheckColor = checkColor || '#10b981'; const regBtnBg = btnBg || featuredBg; const regBtnColor = btnColor || '#ffffff'; return (
{ if (ref) connect(drag(ref)); }} id={anchorId || undefined} style={{ padding: '80px 20px', backgroundColor: '#ffffff', outline: selected ? '2px solid #3b82f6' : 'none', ...style, }} >
{plans.map((plan, i) => (
{plan.isFeatured && (
Most Popular
)}

{plan.name}

{plan.price} {plan.period}
    {(Array.isArray(plan.features) ? plan.features : []).map((feature, fi) => (
  • {bulletChars[bulletType] || '✓'} {feature}
  • ))}
e.preventDefault()} style={{ marginTop: 'auto', display: 'inline-block', padding: '14px 32px', backgroundColor: plan.isFeatured ? '#ffffff' : regBtnBg, color: plan.isFeatured ? featuredBg : regBtnColor, textDecoration: 'none', borderRadius: '8px', fontWeight: '600', fontSize: '14px', width: '100%', textAlign: 'center', }} > {plan.buttonText}
))}
); }; /* ---------- Craft config ---------- */ PricingTable.craft = { displayName: 'Pricing Table', props: { plans: defaultPlans, style: { backgroundColor: '#ffffff' }, featuredBg: '#3b82f6', bulletType: 'check', anchorId: '', cardBg: '', textColor: '', subColor: '', featColor: '', checkColor: '', btnBg: '', btnColor: '', animation: '', animationDelay: '', hideOnDesktop: false, hideOnTablet: false, hideOnMobile: false, }, rules: { canDrag: () => true, canMoveIn: () => false, canMoveOut: () => true, }, }; /* ---------- HTML export ---------- */ (PricingTable as any).toHtml = (props: PricingTableProps, _childrenHtml: string) => { const bulletType = props.bulletType || 'check'; const sectionStyle = cssPropsToString({ padding: '80px 20px', ...props.style, }); const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : ''; const plans = props.plans || defaultPlans; // Sanitized -- featuredBg is a raw string-interpolation sink below (drives // cardBg/btnBg/btnColor, all raw-interpolated into style="..."). const featuredBg = cssValue(props.featuredBg) || '#3b82f6'; // Sanitized -- regular (non-featured) card color overrides, all raw // string-interpolation sinks into style="..." below. Each falls back to // the exact literal the card was previously hard-coded to (or, for the // button, to featuredBg) when unset, so unmodified pricing tables render // identically to before these props existed. const regCardBg = cssValue(props.cardBg) || '#ffffff'; const regTextColor = cssValue(props.textColor) || '#18181b'; const regSubColor = cssValue(props.subColor) || '#64748b'; const regFeatColor = cssValue(props.featColor) || '#4b5563'; const regCheckColor = cssValue(props.checkColor) || '#10b981'; const regBtnBg = cssValue(props.btnBg) || featuredBg; const regBtnColor = cssValue(props.btnColor) || '#ffffff'; const cards = plans.map((plan) => { const cardBg = plan.isFeatured ? featuredBg : regCardBg; const cardBorder = plan.isFeatured ? 'border:none;' : 'border:1px solid #e2e8f0;'; const textColor = plan.isFeatured ? '#ffffff' : regTextColor; const subColor = plan.isFeatured ? 'rgba(255,255,255,0.8)' : regSubColor; const featColor = plan.isFeatured ? 'rgba(255,255,255,0.9)' : regFeatColor; const checkColor = plan.isFeatured ? '#bbf7d0' : regCheckColor; const btnBg = plan.isFeatured ? '#ffffff' : regBtnBg; const btnColor = plan.isFeatured ? featuredBg : regBtnColor; const scale = plan.isFeatured ? 'transform:scale(1.05);' : ''; const shadow = plan.isFeatured ? 'box-shadow:0 20px 60px rgba(59,130,246,0.3);' : 'box-shadow:0 1px 3px rgba(0,0,0,0.06);'; const featuresHtml = (Array.isArray(plan.features) ? plan.features : []).map((f) => `
  • ${bulletChars[bulletType] || '✓'}${escapeHtml(f)}
  • ` ).join('\n '); const badge = plan.isFeatured ? `
    Most Popular
    ` : ''; return `
    ${badge}

    ${escapeHtml(plan.name)}

    ${escapeHtml(plan.price)} ${escapeHtml(plan.period)}
      ${featuresHtml}
    ${escapeHtml(plan.buttonText)}
    `; }).join('\n '); return { html: `
    ${cards}
    `, }; };