Files
CityCards-Website/src/components/LandingTrustSection.tsx

432 lines
20 KiB
TypeScript

import { useState, useRef, useEffect } from 'react';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import { motion, AnimatePresence, useMotionValue, useTransform, PanInfo } from 'motion/react';
const testimonials = [
{
id: 1,
name: 'Sarah Mitchell',
role: 'Travel Blogger',
company: 'Wanderlust Adventures',
avatar: 'https://images.unsplash.com/photo-1494790108755-2616b612b1ac?q=80&w=150&auto=format&fit=crop&ixlib=rb-4.0.3',
quote: 'CityCards completely transformed our Australian city-hopping adventure. The curated attraction passes saved us 60% on costs and the skip-the-line access was invaluable. Every city felt like a personalized experience tailored just for us.',
signature: 'Sarah M.'
},
{
id: 2,
name: 'Michael Chen',
role: 'Travel Photographer',
company: 'Urban Lens Studio',
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?q=80&w=150&auto=format&fit=crop&ixlib=rb-4.0.3',
quote: 'As someone who captures cities worldwide, CityCards gave me access to unique perspectives and hidden gems across Australia. The mobile city guide made discovering photo spots seamless, from Sydney\'s harbor to Melbourne\'s laneways.',
signature: 'Michael C.'
},
{
id: 3,
name: 'Emma Rodriguez',
role: 'Adventure Seeker',
company: 'Solo Travel Co.',
avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?q=80&w=150&auto=format&fit=crop&ixlib=rb-4.0.3',
quote: 'Solo traveling became effortless with CityCards. From instant bookings to local recommendations, I felt confident exploring Australian cities. The comprehensive city guides unlocked experiences I never knew existed.',
signature: 'Emma R.'
},
{
id: 4,
name: 'David Park',
role: 'Family Traveler',
company: 'Adventure Families',
avatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?q=80&w=150&auto=format&fit=crop&ixlib=rb-4.0.3',
quote: 'Planning family trips used to be overwhelming, but CityCards simplified everything. The kids loved the interactive city experiences and we saved hours with skip-the-line access at every single attraction we visited.',
signature: 'David P.'
},
{
id: 5,
name: 'Lisa Thompson',
role: 'Business Traveler',
company: 'Global Solutions Inc.',
avatar: 'https://images.unsplash.com/photo-1544005313-94ddf0286df2?q=80&w=150&auto=format&fit=crop&ixlib=rb-4.0.3',
quote: 'Between business meetings, CityCards helped me maximize my limited free time in every city. Quick access to top attractions without the hassle of traditional booking made every business trip memorable and productive.',
signature: 'Lisa T.'
},
{
id: 6,
name: 'James Wilson',
role: 'Cultural Explorer',
company: 'Heritage Travels',
avatar: 'https://images.unsplash.com/photo-1519244703995-f4e0f30006d5?q=80&w=150&auto=format&fit=crop&ixlib=rb-4.0.3',
quote: 'CityCards opened doors to authentic cultural experiences I would have missed otherwise. The curated recommendations led to discoveries that became the absolute highlights of our entire cultural journey through Australia\'s diverse cities.',
signature: 'James W.'
}
];
// Custom SVG quotation marks
const QuoteStart = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 8C10 9.3 9.3 10 8 10C6.7 10 6 9.3 6 8C6 6.7 6.7 6 8 6C9.3 6 10 6.7 10 8ZM18 8C18 9.3 17.3 10 16 10C14.7 10 14 9.3 14 8C14 6.7 14.7 6 16 6C17.3 6 18 6.7 18 8ZM8 12L6 18H10L8 12ZM16 12L14 18H18L16 12Z" fill="currentColor"/>
</svg>
);
const QuoteEnd = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 16C14 14.7 14.7 14 16 14C17.3 14 18 14.7 18 16C18 17.3 17.3 18 16 18C14.7 18 14 17.3 14 16ZM6 16C6 14.7 6.7 14 8 14C9.3 14 10 14.7 10 16C10 17.3 9.3 18 8 18C6.7 18 6 17.3 6 16ZM16 12L18 6H14L16 12ZM8 12L10 6H6L8 12Z" fill="currentColor"/>
</svg>
);
export function LandingTrustSection() {
const [currentIndex, setCurrentIndex] = useState(0);
const [hoveredCard, setHoveredCard] = useState<number | null>(null);
const [dragConstraints, setDragConstraints] = useState({ left: 0, right: 0 });
const [showNameOnProgress, setShowNameOnProgress] = useState(false);
const carouselRef = useRef<HTMLDivElement>(null);
const x = useMotionValue(0);
// Calculate how many cards to show based on screen size
const [cardsPerView, setCardsPerView] = useState(1);
useEffect(() => {
const updateCardsPerView = () => {
if (window.innerWidth >= 1200) {
setCardsPerView(2);
} else {
setCardsPerView(1);
}
};
updateCardsPerView();
window.addEventListener('resize', updateCardsPerView);
return () => window.removeEventListener('resize', updateCardsPerView);
}, []);
const totalSlides = Math.ceil(testimonials.length / cardsPerView);
const maxIndex = totalSlides - 1;
useEffect(() => {
if (carouselRef.current) {
const cardWidth = carouselRef.current.offsetWidth;
const maxDrag = -(cardWidth * maxIndex);
setDragConstraints({ left: maxDrag, right: 0 });
}
}, [maxIndex, cardsPerView]);
const handlePrevious = () => {
setCurrentIndex(prev => Math.max(0, prev - 1));
};
const handleNext = () => {
setCurrentIndex(prev => Math.min(maxIndex, prev + 1));
};
const handleDragEnd = (event: any, info: PanInfo) => {
const offset = info.offset.x;
const velocity = info.velocity.x;
if (Math.abs(offset) > 100 || Math.abs(velocity) > 500) {
if (offset > 0 && currentIndex > 0) {
setCurrentIndex(prev => prev - 1);
} else if (offset < 0 && currentIndex < maxIndex) {
setCurrentIndex(prev => prev + 1);
}
}
};
const progress = totalSlides > 1 ? (currentIndex / maxIndex) * 100 : 0;
const getCurrentTestimonialNames = () => {
const startIndex = currentIndex * cardsPerView;
const endIndex = Math.min(startIndex + cardsPerView, testimonials.length);
return testimonials.slice(startIndex, endIndex).map(t => t.name).join(', ');
};
return (
<section className="py-16 md:py-24 bg-background relative overflow-hidden">
{/* Subtle background texture */}
<div
className="absolute inset-0 opacity-[0.02]"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23000' fill-opacity='0.03'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3z'/%3E%3C/g%3E%3C/svg%3E")`
}}
/>
<div className="container mx-auto px-4" style={{ overflow: 'visible' }}>
{/* Header */}
<div className="text-center mb-16">
<h2 className="font-poppins text-2xl md:text-3xl lg:text-4xl leading-tight mb-6 text-foreground">
<span className="font-light">What Our</span>{' '}
<span className="font-bold italic text-primary">
Travelers
</span>{' '}
<span className="font-light">Say</span>
</h2>
<p className="font-poppins text-xl leading-relaxed font-normal text-gray-700 max-w-2xl mx-auto">
Real stories from real travelers who've discovered amazing cities with CityCards
</p>
</div>
{/* Carousel Container */}
<div className="relative max-w-7xl mx-auto" style={{ overflow: 'visible' }}>
{/* Carousel */}
<div className="mx-8 py-6">
<motion.div
ref={carouselRef}
className="flex"
animate={{ x: `${-currentIndex * 100}%` }}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
drag="x"
dragConstraints={dragConstraints}
onDragEnd={handleDragEnd}
style={{ cursor: 'grab', overflow: 'visible' }}
whileDrag={{ cursor: 'grabbing' }}
>
{testimonials.map((testimonial, index) => {
const cardRotation = (index % 3 - 1) * 1.5 + (Math.random() - 0.5) * 1;
const cardOffset = (index % 2) * 10;
return (
<motion.div
key={testimonial.id}
className="flex-shrink-0 px-8 py-4"
style={{
width: cardsPerView === 2 ? '50%' : '100%',
}}
whileHover={{
scale: 1.02,
y: -5,
transition: { duration: 0.2 }
}}
onHoverStart={() => setHoveredCard(testimonial.id)}
onHoverEnd={() => setHoveredCard(null)}
>
{/* Paper Card with enhanced realism */}
<div
className="relative bg-white rounded-lg p-8"
style={{
transform: `rotate(${cardRotation}deg) translateY(${cardOffset}px)`,
transformOrigin: 'center center',
minHeight: '360px',
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="font-poppins text-base leading-relaxed font-normal text-foreground relative z-10"
>
{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-poppins text-lg leading-snug font-semibold text-foreground">
{testimonial.name}
</div>
<div className="font-poppins text-sm leading-relaxed font-normal text-gray-600">
{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 }}
animate={{
pathLength: 1,
opacity: 1,
transition: {
pathLength: { duration: 2, delay: index * 0.1 },
opacity: { duration: 0.5, delay: index * 0.1 }
}
}}
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>
);
})}
</motion.div>
</div>
{/* Enhanced Progress Bar with hover names */}
<div className="mt-10 max-w-md mx-auto">
{/* Enhanced slide indicators */}
<div className="flex justify-center space-x-3">
{Array.from({ length: totalSlides }).map((_, index) => (
<button
key={index}
onClick={() => setCurrentIndex(index)}
className={`w-4 h-4 rounded-full transition-all duration-300 transform ${
index === currentIndex
? 'bg-warm-coral scale-110 shadow-lg'
: 'bg-gray-300 hover:bg-gray-400 hover:scale-105'
}`}
style={{
boxShadow: index === currentIndex
? '0 4px 8px rgba(249, 95, 98, 0.3), 0 2px 4px rgba(249, 95, 98, 0.2)'
: '0 2px 4px rgba(0,0,0,0.1)'
}}
/>
))}
</div>
</div>
</div>
</div>
</section>
);
}