import React, { CSSProperties } from 'react'; import { useNode, UserComponent } from '@craftjs/core'; import { cssPropsToString } from '../../utils/style-helpers'; import { escapeHtml, escapeAttr, scopeId } from '../../utils/escape'; interface Counter { number: number; suffix: string; label: string; } interface NumberCounterProps { counters?: Counter[]; columns?: number; numberColor?: string; labelColor?: string; numberSize?: string; style?: CSSProperties; } const defaultCounters: Counter[] = [ { number: 150, suffix: '+', label: 'Projects' }, { number: 50, suffix: '+', label: 'Clients' }, { number: 10, suffix: '', label: 'Years' }, { number: 99, suffix: '%', label: 'Satisfaction' }, ]; export const NumberCounter: UserComponent = ({ counters = defaultCounters, columns = 4, numberColor = '#3b82f6', labelColor = '#6b7280', numberSize = '48px', style = {}, }) => { const { connectors: { connect, drag }, selected, } = useNode((node) => ({ selected: node.events.selected, })); const items = counters.length > 0 ? counters : defaultCounters; return (
{ if (ref) connect(drag(ref)); }} style={{ padding: '60px 20px', backgroundColor: '#ffffff', outline: selected ? '2px solid #3b82f6' : 'none', ...style, }} >
{items.map((counter, i) => (
{counter.number}{counter.suffix} {counter.label}
))}
); }; /* ---------- Craft config ---------- */ NumberCounter.craft = { displayName: 'Number Counter', props: { counters: defaultCounters, columns: 4, numberColor: '#3b82f6', labelColor: '#6b7280', numberSize: '48px', style: { backgroundColor: '#ffffff' }, }, rules: { canDrag: () => true, canMoveIn: () => false, canMoveOut: () => true, }, }; /* ---------- HTML export ---------- */ (NumberCounter as any).toHtml = (props: NumberCounterProps, _childrenHtml: string, nodeId?: string) => { const { counters = defaultCounters, columns = 4, numberColor = '#3b82f6', labelColor = '#6b7280', numberSize = '48px', style = {}, } = props; const items = counters.length > 0 ? counters : defaultCounters; // Deterministic AND unique id for this counter instance's wrapper/span // ids and getElementById() calls inside its inline script -- scoped on // the Craft node id so two NumberCounter instances (e.g. both left at // default props) don't collide and end up animating each other's digits. const seed = items.map((c) => `${c.number}${c.suffix}::${c.label}`).join('|'); const uid = scopeId(nodeId, seed, 'nc'); const sectionStyle = cssPropsToString({ padding: '60px 20px', backgroundColor: '#ffffff', ...style, }); const countersHtml = items.map((counter, i) => { return `
0${escapeHtml(counter.suffix)} ${escapeHtml(counter.label)}
`; }).join('\n '); return { html: `
${countersHtml}
`, }; };