import React, { CSSProperties } from 'react';
import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers';
import { escapeHtml, escapeAttr } from '../../utils/escape';
interface Testimonial {
quote: string;
name: string;
title: string;
rating: number;
}
interface TestimonialsProps {
testimonials?: Testimonial[];
layout?: 'grid' | 'single';
columns?: number;
style?: CSSProperties;
cardBg?: string;
starColor?: string;
anchorId?: string;
}
const defaultTestimonials: Testimonial[] = [
{ quote: 'This product has completely transformed our workflow. Highly recommended for any team.', name: 'Sarah Johnson', title: 'Marketing Director', rating: 5 },
{ quote: 'Outstanding support and an incredibly intuitive interface. We saw results from day one.', name: 'Michael Chen', title: 'CTO, TechStart', rating: 5 },
{ quote: 'The best investment we have made this year. Simple, powerful, and reliable.', name: 'Emily Rodriguez', title: 'Founder, DesignLab', rating: 4 },
];
function renderStars(count: number, color: string): React.ReactNode {
return (
{[1, 2, 3, 4, 5].map((i) => (
))}
);
}
function starsHtml(count: number, color: string): string {
const stars = [1, 2, 3, 4, 5].map((i) =>
``
).join('');
return `${stars}
`;
}
export const Testimonials: UserComponent = ({
testimonials = defaultTestimonials,
layout = 'grid',
columns = 3,
style = {},
cardBg = '#f8fafc',
starColor = '#f59e0b',
anchorId,
}) => {
const {
connectors: { connect, drag },
selected,
} = useNode((node) => ({
selected: node.events.selected,
}));
const cardStyle: CSSProperties = {
backgroundColor: cardBg,
borderRadius: '12px',
padding: '32px 24px',
textAlign: 'center',
border: '1px solid #e2e8f0',
};
const renderCard = (t: Testimonial, i: number) => (
{renderStars(t.rating, starColor)}
“{t.quote}”
{t.name}
{t.title}
);
const items = testimonials.length > 0 ? testimonials : defaultTestimonials;
return (
{ if (ref) connect(drag(ref)); }}
id={anchorId || undefined}
style={{
padding: '80px 20px',
backgroundColor: '#ffffff',
outline: selected ? '2px solid #3b82f6' : 'none',
...style,
}}
>
{layout === 'grid' ? (
{items.map((t, i) => renderCard(t, i))}
) : (
// Static single testimonial (parity with the static toHtml export --
// no carousel controls, since the published site has no JS for this
// component). Always shows the first testimonial.
{renderCard(items[0], 0)}
)}
);
};
/* ---------- Craft config ---------- */
Testimonials.craft = {
displayName: 'Testimonials',
props: {
testimonials: defaultTestimonials,
layout: 'grid',
columns: 3,
style: { backgroundColor: '#ffffff' },
cardBg: '#f8fafc',
starColor: '#f59e0b',
anchorId: '',
},
rules: {
canDrag: () => true,
canMoveIn: () => false,
canMoveOut: () => true,
},
};
/* ---------- HTML export ---------- */
(Testimonials as any).toHtml = (props: TestimonialsProps, _childrenHtml: string) => {
const {
testimonials = defaultTestimonials,
layout = 'grid',
columns = 3,
style = {},
cardBg = '#f8fafc',
starColor = '#f59e0b',
} = props;
const items = testimonials.length > 0 ? testimonials : defaultTestimonials;
const sectionStyle = cssPropsToString({
padding: '80px 20px',
backgroundColor: '#ffffff',
...style,
});
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
const cardCss = `background-color:${cardBg};border-radius:12px;padding:32px 24px;text-align:center;border:1px solid #e2e8f0`;
const cardHtml = (t: Testimonial): string => `
${starsHtml(t.rating, starColor)}
“${escapeHtml(t.quote)}”
${escapeHtml(t.name)}
${escapeHtml(t.title)}
`;
if (layout === 'single') {
// Static parity with the editor's single-layout render: exactly ONE
// testimonial card (the first), no carousel controls -- the published
// export has no JS for this component.
return {
html: ``,
};
}
const cards = items.map(cardHtml).join('\n ');
return {
html: ``,
};
};