Files
site-builder/craft/src/components/sections/Testimonials.tsx
T

222 lines
7.7 KiB
TypeScript
Raw Normal View History

import React, { CSSProperties, useState } 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 (
<div style={{ display: 'flex', gap: '2px', justifyContent: 'center', marginBottom: '12px' }}>
{[1, 2, 3, 4, 5].map((i) => (
<i
key={i}
className={`fa ${i <= count ? 'fa-star' : 'fa-star-o'}`}
style={{ color, fontSize: '14px' }}
/>
))}
</div>
);
}
function starsHtml(count: number, color: string): string {
const stars = [1, 2, 3, 4, 5].map((i) =>
`<i class="fa ${i <= count ? 'fa-star' : 'fa-star-o'}" style="color:${color};font-size:14px"></i>`
).join('');
return `<div style="display:flex;gap:2px;justify-content:center;margin-bottom:12px">${stars}</div>`;
}
export const Testimonials: UserComponent<TestimonialsProps> = ({
testimonials = defaultTestimonials,
layout = 'grid',
columns = 3,
style = {},
cardBg = '#f8fafc',
starColor = '#f59e0b',
anchorId,
}) => {
const {
connectors: { connect, drag },
selected,
} = useNode((node) => ({
selected: node.events.selected,
}));
const [currentIndex, setCurrentIndex] = useState(0);
const cardStyle: CSSProperties = {
backgroundColor: cardBg,
borderRadius: '12px',
padding: '32px 24px',
textAlign: 'center',
border: '1px solid #e2e8f0',
};
const renderCard = (t: Testimonial, i: number) => (
<div key={i} style={cardStyle}>
{renderStars(t.rating, starColor)}
<p style={{ fontSize: '15px', color: '#374151', lineHeight: '1.7', marginBottom: '16px', fontStyle: 'italic', fontFamily: 'Inter, sans-serif' }}>
&ldquo;{t.quote}&rdquo;
</p>
<div style={{ fontWeight: '600', fontSize: '14px', color: '#18181b', fontFamily: 'Inter, sans-serif' }}>{t.name}</div>
<div style={{ fontSize: '13px', color: '#64748b', fontFamily: 'Inter, sans-serif' }}>{t.title}</div>
</div>
);
const items = testimonials.length > 0 ? testimonials : defaultTestimonials;
return (
<section
ref={(ref: HTMLElement | null): void => { if (ref) connect(drag(ref)); }}
id={anchorId || undefined}
style={{
padding: '80px 20px',
backgroundColor: '#ffffff',
outline: selected ? '2px solid #3b82f6' : 'none',
...style,
}}
>
<div style={{ maxWidth: '1100px', margin: '0 auto' }}>
{layout === 'grid' ? (
<div style={{ display: 'grid', gridTemplateColumns: `repeat(${columns}, 1fr)`, gap: '24px' }}>
{items.map((t, i) => renderCard(t, i))}
</div>
) : (
<div style={{ maxWidth: '600px', margin: '0 auto', position: 'relative' }}>
{renderCard(items[currentIndex] || items[0], currentIndex)}
{items.length > 1 && (
<div style={{ display: 'flex', justifyContent: 'center', gap: '12px', marginTop: '20px' }}>
<button
onClick={() => setCurrentIndex((prev) => (prev - 1 + items.length) % items.length)}
style={{
width: 36, height: 36, borderRadius: '50%', border: '1px solid #d1d5db',
background: '#ffffff', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 14, color: '#374151',
}}
>
<i className="fa fa-chevron-left" />
</button>
<div style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
{items.map((_, i) => (
<div
key={i}
onClick={() => setCurrentIndex(i)}
style={{
width: 8, height: 8, borderRadius: '50%', cursor: 'pointer',
backgroundColor: i === currentIndex ? '#3b82f6' : '#d1d5db',
}}
/>
))}
</div>
<button
onClick={() => setCurrentIndex((prev) => (prev + 1) % items.length)}
style={{
width: 36, height: 36, borderRadius: '50%', border: '1px solid #d1d5db',
background: '#ffffff', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 14, color: '#374151',
}}
>
<i className="fa fa-chevron-right" />
</button>
</div>
)}
</div>
)}
</div>
</section>
);
};
/* ---------- 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 cards = items.map((t) => {
return `<div style="${cardCss}">
${starsHtml(t.rating, starColor)}
<p style="font-size:15px;color:#374151;line-height:1.7;margin-bottom:16px;font-style:italic;font-family:Inter,sans-serif">&ldquo;${escapeHtml(t.quote)}&rdquo;</p>
<div style="font-weight:600;font-size:14px;color:#18181b;font-family:Inter,sans-serif">${escapeHtml(t.name)}</div>
<div style="font-size:13px;color:#64748b;font-family:Inter,sans-serif">${escapeHtml(t.title)}</div>
</div>`;
}).join('\n ');
if (layout === 'single') {
// For single layout, export as grid with 1 column (simpler static export)
return {
html: `<section${idAttr}${sectionStyle ? ` style="${sectionStyle}"` : ''}>
<div style="max-width:600px;margin:0 auto;display:grid;grid-template-columns:1fr;gap:24px">
${cards}
</div>
</section>`,
};
}
return {
html: `<section${idAttr}${sectionStyle ? ` style="${sectionStyle}"` : ''}>
<div style="max-width:1100px;margin:0 auto;display:grid;grid-template-columns:repeat(${columns},1fr);gap:24px">
${cards}
</div>
</section>`,
};
};