323 lines
15 KiB
TypeScript
323 lines
15 KiB
TypeScript
import { useState } from 'react';
|
|
import { motion } from 'motion/react';
|
|
|
|
const QuoteStart = ({ className }: { className?: string }) => (
|
|
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M6.5 10c-.223 0-.437.034-.65.065.069-.232.14-.468.254-.68.114-.308.292-.575.469-.844.148-.291.409-.488.601-.737.201-.242.475-.403.692-.604.213-.21.492-.315.714-.463.232-.133.434-.28.65-.35l.539-.222.474-.197-.485-1.938-.597.144c-.191.048-.424.104-.689.171-.271.05-.56.187-.882.312-.318.142-.686.238-1.028.466-.344.218-.738.411-1.091.746-.363.334-.738.632-1.062 1.004-.324.363-.61.747-.822 1.131-.424.199-.924.471-1.397.991C.598 7.391.047 8.26.01 9.444c-.037 1.218.5 2.407 1.351 3.202.851.794 2.024 1.203 3.197 1.116 1.173-.087 2.317-.68 3.035-1.603.718-.924.968-2.19.68-3.364-.287-1.174-1.239-2.206-2.474-2.583C5.799 5.787 4.74 6.27 4.027 7.104c-.712.834-.91 1.97-.537 2.972z"/>
|
|
</svg>
|
|
);
|
|
|
|
const QuoteEnd = ({ className }: { className?: string }) => (
|
|
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M17.5 14c.223 0 .437-.034.65-.065-.069.232-.14.468-.254.68-.114.308-.292.575-.469.844-.148.291-.409.488-.601.737-.201.242-.475.403-.692.604-.213.21-.492.315-.714.463-.232.133-.434.28-.65.35l-.539.222-.474.197.485 1.938.597-.144c.191-.048.424-.104.689-.171.271-.05.56-.187.882-.312.318-.142.686-.238 1.028-.466.344-.218.738-.411 1.091-.746.363-.334.738-.632 1.062-1.004.324-.363.61-.747.822-1.131.424-.199.924-.471 1.397-.991 1.019.388 1.57-1.481 1.607-2.665.037-1.218-.5-2.407-1.351-3.202-.851-.794-2.024-1.203-3.197-1.116-1.173.087-2.317.68-3.035 1.603-.718.924-.968 2.19-.68 3.364.287 1.174 1.239 2.206 2.474 2.583z"/>
|
|
</svg>
|
|
);
|
|
|
|
interface Testimonial {
|
|
id: number;
|
|
quote: string;
|
|
name: string;
|
|
company: string;
|
|
signature: string;
|
|
}
|
|
|
|
const testimonials: Testimonial[] = [
|
|
{
|
|
id: 1,
|
|
quote: "CityCards transformed our Melbourne trip into an unforgettable adventure. The seamless access to attractions and insider recommendations made every moment magical.",
|
|
name: "Sarah Chen",
|
|
company: "Travel Blogger",
|
|
signature: "Sarah"
|
|
},
|
|
{
|
|
id: 2,
|
|
quote: "As a frequent business traveler, CityCards saves me time and money. The convenience of having everything in one place is incredible - no more queuing or hassle!",
|
|
name: "Michael Torres",
|
|
company: "Business Executive",
|
|
signature: "Michael"
|
|
},
|
|
{
|
|
id: 3,
|
|
quote: "The family pass was perfect for our vacation. Our kids loved the instant access to attractions, and we loved the savings. Highly recommended for family trips!",
|
|
name: "Emma Wilson",
|
|
company: "Marketing Manager",
|
|
signature: "Emma"
|
|
},
|
|
{
|
|
id: 4,
|
|
quote: "CityCards made exploring Melbourne so easy and affordable. The local insights and recommendations helped us discover hidden gems we never would have found otherwise.",
|
|
name: "James Rodriguez",
|
|
company: "Photographer",
|
|
signature: "James"
|
|
},
|
|
{
|
|
id: 5,
|
|
quote: "Incredible value and convenience! The mobile app worked flawlessly, and having all attraction entries pre-paid meant we could focus on enjoying our honeymoon.",
|
|
name: "Lisa Thompson",
|
|
company: "Teacher",
|
|
signature: "Lisa"
|
|
}
|
|
];
|
|
|
|
export function EnhancedTestimonials() {
|
|
const [hoveredCard, setHoveredCard] = useState<number | null>(null);
|
|
|
|
// Calculate dynamic card rotation and offset
|
|
const getCardStyle = (index: number) => {
|
|
const baseRotation = (index % 3 - 1) * 1.5; // -1.5, 0, 1.5 degrees
|
|
const hoverRotation = hoveredCard === testimonials[index].id ? 0 : baseRotation;
|
|
const cardRotation = hoverRotation + (Math.sin(index * 0.5) * 0.8);
|
|
const cardOffset = Math.cos(index * 0.7) * 2;
|
|
|
|
return { cardRotation, cardOffset };
|
|
};
|
|
|
|
return (
|
|
<section className="py-20 lg:py-32 bg-gradient-to-b from-muted/20 to-background relative overflow-hidden">
|
|
{/* Background decorative elements */}
|
|
<div className="absolute inset-0 opacity-30">
|
|
<div className="absolute top-20 left-20 w-32 h-32 bg-primary/5 rounded-full blur-3xl" />
|
|
<div className="absolute bottom-20 right-20 w-40 h-40 bg-secondary/5 rounded-full blur-3xl" />
|
|
</div>
|
|
|
|
<div className="container mx-auto px-4 relative z-10">
|
|
{/* Section Header */}
|
|
<motion.div
|
|
className="text-center mb-16 lg:mb-20"
|
|
initial={{ opacity: 0, y: 30 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.6, ease: "easeOut" }}
|
|
>
|
|
<h2 className="text-4xl lg:text-5xl xl:text-6xl mb-6">
|
|
<span className="font-light">What our</span>{' '}
|
|
<span className="bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent font-bold italic pr-1">
|
|
travelers
|
|
</span>{' '}
|
|
<span className="font-normal">say</span>
|
|
</h2>
|
|
<p className="text-lg text-muted-foreground max-w-2xl mx-auto leading-relaxed">
|
|
Real stories from travelers who've discovered the magic of seamless city exploration with CityCards
|
|
</p>
|
|
</motion.div>
|
|
|
|
{/* Testimonials Grid */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 lg:gap-12 max-w-4xl mx-auto">
|
|
{testimonials.slice(0, 2).map((testimonial, index) => {
|
|
const { cardRotation, cardOffset } = getCardStyle(index);
|
|
|
|
return (
|
|
<motion.div
|
|
key={testimonial.id}
|
|
className="flex justify-center"
|
|
initial={{ opacity: 0, y: 40 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true }}
|
|
transition={{
|
|
duration: 0.7,
|
|
delay: index * 0.1,
|
|
ease: "easeOut"
|
|
}}
|
|
onMouseEnter={() => setHoveredCard(testimonial.id)}
|
|
onMouseLeave={() => setHoveredCard(null)}
|
|
>
|
|
<div
|
|
className="relative bg-white rounded-lg p-8"
|
|
style={{
|
|
transform: `rotate(${cardRotation}deg) translateY(${cardOffset}px)`,
|
|
transformOrigin: 'center center',
|
|
minHeight: '480px',
|
|
background: `
|
|
radial-gradient(circle at 20% 80%, rgba(255, 248, 235, 0.8) 0%, transparent 50%),
|
|
radial-gradient(circle at 80% 20%, rgba(250, 245, 230, 0.6) 0%, transparent 50%),
|
|
linear-gradient(145deg, #ffffff 0%, #fefefe 25%, #fdfdfd 50%, #fcfcfc 75%, #fbfbfb 100%)
|
|
`,
|
|
boxShadow: `
|
|
0 8px 32px rgba(0, 0, 0, 0.12),
|
|
0 4px 16px rgba(0, 0, 0, 0.08),
|
|
0 2px 8px rgba(0, 0, 0, 0.06),
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.8),
|
|
inset 0 -1px 0 rgba(0, 0, 0, 0.02)
|
|
`,
|
|
border: '1px solid rgba(0, 0, 0, 0.04)',
|
|
filter: hoveredCard === testimonial.id ? 'brightness(1.02)' : 'brightness(1)',
|
|
transition: 'all 0.3s ease'
|
|
}}
|
|
>
|
|
{/* Enhanced paper texture */}
|
|
<div
|
|
className="absolute inset-0 rounded-lg opacity-40 pointer-events-none"
|
|
style={{
|
|
backgroundImage: `
|
|
url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23f5f0e8' fill-opacity='0.4'%3E%3Cpath d='M10 10h1v1h-1zM20 15h1v1h-1zM30 25h1v1h-1zM40 30h1v1h-1zM50 40h1v1h-1zM15 50h1v1h-1z'/%3E%3C/g%3E%3C/svg%3E"),
|
|
radial-gradient(circle at 25% 75%, rgba(245, 240, 232, 0.3) 0%, transparent 40%),
|
|
radial-gradient(circle at 75% 25%, rgba(250, 245, 235, 0.2) 0%, transparent 35%)
|
|
`,
|
|
mixBlendMode: 'multiply'
|
|
}}
|
|
/>
|
|
|
|
{/* Paper creases and folds */}
|
|
<div
|
|
className="absolute inset-0 rounded-lg opacity-20 pointer-events-none"
|
|
style={{
|
|
background: `
|
|
linear-gradient(135deg, transparent 40%, rgba(0,0,0,0.02) 45%, rgba(0,0,0,0.01) 55%, transparent 60%),
|
|
linear-gradient(45deg, transparent 30%, rgba(0,0,0,0.015) 35%, transparent 40%)
|
|
`
|
|
}}
|
|
/>
|
|
|
|
{/* Corner fold effect */}
|
|
<div
|
|
className="absolute top-0 right-0 w-12 h-12 opacity-15 pointer-events-none"
|
|
style={{
|
|
background: `
|
|
linear-gradient(-45deg,
|
|
transparent 40%,
|
|
rgba(0,0,0,0.08) 45%,
|
|
rgba(0,0,0,0.12) 50%,
|
|
rgba(0,0,0,0.08) 55%,
|
|
transparent 60%
|
|
)
|
|
`,
|
|
borderTopRightRadius: '8px',
|
|
clipPath: 'polygon(60% 0%, 100% 0%, 100% 60%)'
|
|
}}
|
|
/>
|
|
|
|
{/* Paperclip decoration */}
|
|
<div
|
|
className="absolute -top-2 -right-2 w-8 h-12 opacity-60 pointer-events-none z-10"
|
|
style={{
|
|
background: `
|
|
linear-gradient(145deg, #e0e0e0 0%, #d0d0d0 50%, #c8c8c8 100%)
|
|
`,
|
|
borderRadius: '2px 2px 4px 4px',
|
|
boxShadow: `
|
|
0 2px 4px rgba(0,0,0,0.1),
|
|
inset 0 1px 0 rgba(255,255,255,0.5),
|
|
inset 0 -1px 0 rgba(0,0,0,0.1)
|
|
`,
|
|
transform: 'rotate(8deg)'
|
|
}}
|
|
>
|
|
<div
|
|
className="absolute inset-1 border border-gray-400 rounded-sm"
|
|
style={{
|
|
background: 'transparent',
|
|
borderStyle: 'solid',
|
|
borderWidth: '1px'
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* Tape effect on left edge */}
|
|
<div
|
|
className="absolute -left-1 top-16 w-4 h-16 opacity-30 pointer-events-none"
|
|
style={{
|
|
background: `
|
|
linear-gradient(90deg,
|
|
rgba(255,255,220,0.8) 0%,
|
|
rgba(255,255,220,0.6) 50%,
|
|
rgba(255,255,220,0.4) 100%
|
|
)
|
|
`,
|
|
borderRadius: '2px',
|
|
boxShadow: 'inset 0 1px 2px rgba(0,0,0,0.1)',
|
|
transform: 'rotate(-2deg)'
|
|
}}
|
|
/>
|
|
|
|
{/* Enhanced quotation marks */}
|
|
<div className="mb-6">
|
|
<QuoteStart className="w-8 h-8 text-amber-700/30 mb-4" />
|
|
<p
|
|
className="text-gray-700 leading-relaxed text-base relative z-10 font-poppins"
|
|
>
|
|
{testimonial.quote}
|
|
</p>
|
|
<div className="flex justify-end mt-2">
|
|
<QuoteEnd className="w-6 h-6 text-amber-700/30" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Enhanced Profile Section with sticker effect */}
|
|
<div className="mb-8 relative z-10">
|
|
<div className="font-semibold text-gray-900 text-lg font-poppins">
|
|
{testimonial.name}
|
|
</div>
|
|
<div className="text-gray-600 text-sm font-poppins">
|
|
{testimonial.company}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Enhanced signature with writing animation */}
|
|
<div className="flex justify-end relative z-10">
|
|
<motion.div
|
|
className="text-right transform -rotate-1"
|
|
initial={{ pathLength: 0, opacity: 0 }}
|
|
whileInView={{
|
|
pathLength: 1,
|
|
opacity: 1,
|
|
transition: {
|
|
pathLength: { duration: 2, delay: index * 0.1 },
|
|
opacity: { duration: 0.5, delay: index * 0.1 }
|
|
}
|
|
}}
|
|
viewport={{ once: true }}
|
|
style={{
|
|
fontFamily: "'Dancing Script', 'Brush Script MT', cursive",
|
|
fontSize: '32px',
|
|
color: 'rgba(101, 84, 63, 0.8)',
|
|
textShadow: `
|
|
1px 1px 2px rgba(0, 0, 0, 0.1),
|
|
0 0 4px rgba(101, 84, 63, 0.2)
|
|
`,
|
|
filter: 'drop-shadow(0 1px 2px rgba(0,0,0,0.1))'
|
|
}}
|
|
>
|
|
{testimonial.signature}
|
|
</motion.div>
|
|
</div>
|
|
|
|
{/* Subtle aging spots */}
|
|
<div
|
|
className="absolute w-3 h-3 rounded-full opacity-8 pointer-events-none"
|
|
style={{
|
|
background: 'radial-gradient(circle, rgba(160, 120, 80, 0.15) 0%, transparent 70%)',
|
|
top: '15%',
|
|
right: '20%'
|
|
}}
|
|
/>
|
|
<div
|
|
className="absolute w-2 h-2 rounded-full opacity-8 pointer-events-none"
|
|
style={{
|
|
background: 'radial-gradient(circle, rgba(140, 110, 70, 0.12) 0%, transparent 70%)',
|
|
bottom: '25%',
|
|
left: '15%'
|
|
}}
|
|
/>
|
|
|
|
{/* Pin shadow effect */}
|
|
{index % 3 === 0 && (
|
|
<div
|
|
className="absolute w-2 h-2 rounded-full opacity-20 pointer-events-none"
|
|
style={{
|
|
background: 'radial-gradient(circle, rgba(0,0,0,0.3) 0%, transparent 70%)',
|
|
top: '8px',
|
|
left: '50%',
|
|
transform: 'translateX(-50%)',
|
|
filter: 'blur(1px)'
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|