migration wip
This commit is contained in:
224
components/cards/TestimonialCard.tsx
Normal file
224
components/cards/TestimonialCard.tsx
Normal file
@@ -0,0 +1,224 @@
|
||||
'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;
|
||||
@@ -34,13 +34,14 @@ export {
|
||||
} from './CategoryCard';
|
||||
|
||||
// Card Grid Components
|
||||
export {
|
||||
CardGrid,
|
||||
CardGrid2,
|
||||
CardGrid3,
|
||||
CardGrid4,
|
||||
export {
|
||||
CardGrid,
|
||||
CardGrid2,
|
||||
CardGrid3,
|
||||
CardGrid4,
|
||||
CardGridAuto,
|
||||
type CardGridProps,
|
||||
type GridColumns,
|
||||
type GridGap
|
||||
} from './CardGrid';
|
||||
type GridGap
|
||||
} from './CardGrid';
|
||||
export { TestimonialCard, TestimonialGrid, parseWpTestimonial, type TestimonialCardProps } from './TestimonialCard';
|
||||
Reference in New Issue
Block a user