Files
klz-cables.com/components/cards/TestimonialCard.tsx
2025-12-30 12:10:13 +01:00

224 lines
6.0 KiB
TypeScript

'use client';
import React from 'react';
import { cn } from '../../lib/utils';
import { Star, Quote } from 'lucide-react';
export interface TestimonialCardProps {
quote: string;
author?: string;
role?: string;
company?: string;
rating?: number;
avatar?: string;
variant?: 'default' | 'highlight' | 'compact';
className?: string;
}
/**
* TestimonialCard Component
* Displays customer testimonials with optional ratings and author info
* Maps to WordPress testimonial patterns and quote blocks
*/
export const TestimonialCard: React.FC<TestimonialCardProps> = ({
quote,
author,
role,
company,
rating = 0,
avatar,
variant = 'default',
className = ''
}) => {
// Generate star rating
const renderStars = () => {
if (!rating || rating === 0) return null;
const stars = [];
const fullStars = Math.floor(rating);
const hasHalfStar = rating % 1 >= 0.5;
for (let i = 0; i < fullStars; i++) {
stars.push(
<Star key={`full-${i}`} className="w-4 h-4 fill-yellow-400 text-yellow-400" />
);
}
if (hasHalfStar) {
stars.push(
<Star key="half" className="w-4 h-4 fill-yellow-400 text-yellow-400 opacity-50" />
);
}
const emptyStars = 5 - stars.length;
for (let i = 0; i < emptyStars; i++) {
stars.push(
<Star key={`empty-${i}`} className="w-4 h-4 text-gray-300" />
);
}
return <div className="flex gap-1">{stars}</div>;
};
// Variant-specific styles
const variantStyles = {
default: 'bg-white border border-gray-200 shadow-sm',
highlight: 'bg-gradient-to-br from-primary/5 to-secondary/5 border-primary/20 shadow-lg',
compact: 'bg-gray-50 border border-gray-100 shadow-sm'
};
const paddingStyles = {
default: 'p-6 md:p-8',
highlight: 'p-6 md:p-8',
compact: 'p-4 md:p-6'
};
const quoteIconStyles = {
default: 'w-8 h-8 text-primary/30',
highlight: 'w-10 h-10 text-primary/50',
compact: 'w-6 h-6 text-primary/30'
};
return (
<div className={cn(
'relative rounded-xl',
variantStyles[variant],
paddingStyles[variant],
'transition-all duration-200',
'hover:shadow-md hover:-translate-y-1',
className
)}>
{/* Quote Icon */}
<div className={cn(
'absolute top-4 left-4 md:top-6 md:left-6',
'opacity-90',
quoteIconStyles[variant]
)}>
<Quote />
</div>
{/* Main Content */}
<div className={cn(
'space-y-4 md:space-y-6',
'pl-6 md:pl-8' // Space for quote icon
)}>
{/* Quote Text */}
<blockquote className={cn(
'text-gray-700 leading-relaxed',
variant === 'highlight' && 'text-gray-800 font-medium',
variant === 'compact' && 'text-sm md:text-base'
)}>
"{quote}"
</blockquote>
{/* Rating */}
{rating > 0 && (
<div className="flex items-center gap-2">
{renderStars()}
<span className={cn(
'text-sm font-medium',
variant === 'highlight' ? 'text-primary' : 'text-gray-600'
)}>
{rating.toFixed(1)}
</span>
</div>
)}
{/* Author Info */}
{(author || role || company || avatar) && (
<div className={cn(
'flex items-start gap-3 md:gap-4',
variant === 'compact' && 'gap-2'
)}>
{/* Avatar */}
{avatar && (
<div className={cn(
'flex-shrink-0 rounded-full overflow-hidden',
'w-10 h-10 md:w-12 md:h-12',
variant === 'compact' && 'w-8 h-8'
)}>
<img
src={avatar}
alt={author || 'Avatar'}
className="w-full h-full object-cover"
/>
</div>
)}
{/* Author Details */}
<div className="flex-1 min-w-0">
{author && (
<div className={cn(
'font-semibold text-gray-900',
variant === 'highlight' && 'text-lg',
variant === 'compact' && 'text-base'
)}>
{author}
</div>
)}
{(role || company) && (
<div className={cn(
'text-sm',
'text-gray-600',
variant === 'compact' && 'text-xs'
)}>
{[role, company].filter(Boolean).join(' • ')}
</div>
)}
</div>
</div>
)}
</div>
{/* Decorative corner accent for highlight variant */}
{variant === 'highlight' && (
<div className="absolute top-0 right-0 w-20 h-20 bg-gradient-to-bl from-primary/20 to-transparent rounded-tr-xl" />
)}
</div>
);
};
// Helper function to parse WordPress testimonial content
export function parseWpTestimonial(content: string): Partial<TestimonialCardProps> {
// This would parse WordPress testimonial patterns
// For now, returns basic structure
return {
quote: content.replace(/<[^>]*>/g, '').trim().substring(0, 300) // Strip HTML, limit length
};
}
// Grid wrapper for multiple testimonials
export const TestimonialGrid: React.FC<{
testimonials: TestimonialCardProps[];
columns?: 1 | 2 | 3;
gap?: 'sm' | 'md' | 'lg';
className?: string;
}> = ({ testimonials, columns = 2, gap = 'md', className = '' }) => {
const gapStyles = {
sm: 'gap-4',
md: 'gap-6',
lg: 'gap-8'
};
const columnStyles = {
1: 'grid-cols-1',
2: 'grid-cols-1 md:grid-cols-2',
3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3'
};
return (
<div className={cn(
'grid',
columnStyles[columns],
gapStyles[gap],
className
)}>
{testimonials.map((testimonial, index) => (
<TestimonialCard key={index} {...testimonial} />
))}
</div>
);
};
export default TestimonialCard;