Files
CityCards-Website/src/components/HowItWorksPage.tsx
2025-11-11 15:02:35 +05:30

1076 lines
45 KiB
TypeScript

import { useState, useEffect, useRef, useCallback } from 'react';
import { motion, useInView, AnimatePresence } from 'motion/react';
import { ArrowRight, ChevronLeft, ChevronRight } from 'lucide-react';
import { Button } from './ui/button';
import Navbar from './Navbar';
import { Footer } from './Footer';
import { ImageWithFallback } from './figma/ImageWithFallback';
import { AttractionHassleFreeSection } from './AttractionHassleFreeSection';
import { MobileAppSection } from './MobileAppSection';
import { WhyChooseCityCards } from './WhyChooseCityCards';
import { EnhancedTestimonials } from './EnhancedTestimonials';
import { ReviewsSection } from './ReviewsSection';
import { Layout } from '../Layout';
// Hero Carousel Component - Full Screen Pages
function HeroCarousel({ onCheckoutClick, onPassesClick }: { onCheckoutClick: () => void; onPassesClick: () => void }) {
const [currentSlide, setCurrentSlide] = useState(0);
const [direction, setDirection] = useState(0);
const slides = [
{
id: 0,
type: 'overview',
title: (
<>
<span className="font-light text-gray-900">Your</span>{' '}
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
Smart, Seamless
</span>
<br />
<span className="font-semibold text-gray-900">All-in-One City Pass</span>
</>
),
description: "CityCards is your all-in-one solution for exploring cities worldwide. Get instant access to top attractions, skip long queues, and save up to 40% on your adventures—all from your phone.",
showButtons: true,
showStats: true,
gradient: "from-primary/5 via-white to-secondary/5",
image: "https://images.unsplash.com/photo-1517144447511-aebb25bbc5fa?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxjaXR5JTIwc2t5bGluZSUyMHRyYXZlbHxlbnwxfHx8fDE3NjIxNzAxNTF8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral"
},
{
id: 1,
type: 'feature',
title: "Instant Access",
subtitle: "QR Code Entry",
description: "Skip the ticket lines with instant QR code access. Just scan your CityCard at 50+ top attractions in every city and walk right in. No printing, no waiting, no hassle.",
icon: (
<svg className="w-12 h-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
),
gradient: "from-blue-500 to-cyan-500",
bgGradient: "from-blue-50 via-white to-cyan-50",
image: "https://images.unsplash.com/photo-1713685714770-384c5654f6be?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx0b3VyaXN0JTIwYXR0cmFjdGlvbnMlMjBjaXR5fGVufDF8fHx8MTc2MjI1MjMyNnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
showButtons: true,
},
{
id: 2,
type: 'feature',
title: "Save Big",
subtitle: "Up to 40% Off",
description: "Why pay full price? CityCards bundles the best attractions at a fraction of the cost. Save up to 40% compared to buying individual tickets—the more you explore, the more you save.",
icon: (
<svg className="w-12 h-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
),
gradient: "from-green-500 to-emerald-500",
bgGradient: "from-green-50 via-white to-emerald-50",
image: "https://images.unsplash.com/photo-1650821414031-cf7291ce938c?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx0cmF2ZWwlMjB2YWNhdGlvbiUyMHNhdmluZ3MlMjBidWRnZXR8ZW58MXx8fHwxNzYyMjUyNTIxfDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
showButtons: true,
},
{
id: 3,
type: 'feature',
title: "Magic Itinerary",
subtitle: "AI-Powered Planning",
description: "Let AI be your personal travel assistant. Our Magic Itinerary creates customized daily plans based on your interests, optimizes your route, and adapts in real-time. Smart travel made simple.",
icon: (
<svg className="w-12 h-12" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
</svg>
),
gradient: "from-purple-500 to-pink-500",
bgGradient: "from-purple-50 via-white to-pink-50",
image: "https://images.unsplash.com/photo-1741721816773-ff31d089c227?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzbWFydHBob25lJTIwbW9iaWxlJTIwYXBwJTIwdHJhdmVsfGVufDF8fHx8MTc2MjI1MjMyNnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
showButtons: true,
}
];
// Auto-advance carousel
useEffect(() => {
const timer = setInterval(() => {
setDirection(1);
setCurrentSlide((prev) => (prev + 1) % slides.length);
}, 6000); // Change slide every 6 seconds
return () => clearInterval(timer);
}, [slides.length]);
const nextSlide = () => {
setDirection(1);
setCurrentSlide((prev) => (prev + 1) % slides.length);
};
const prevSlide = () => {
setDirection(-1);
setCurrentSlide((prev) => (prev - 1 + slides.length) % slides.length);
};
const slideVariants = {
enter: (direction: number) => ({
x: direction > 0 ? '100%' : '-100%',
opacity: 0
}),
center: {
zIndex: 1,
x: 0,
opacity: 1
},
exit: (direction: number) => ({
zIndex: 0,
x: direction < 0 ? '100%' : '-100%',
opacity: 0
})
};
const currentSlideData = slides[currentSlide];
return (
<section className="relative min-h-screen overflow-hidden">
{/* Main Content Container - Split Layout */}
<div className="relative min-h-screen flex w-full">
<AnimatePresence initial={false} custom={direction} mode="wait">
<motion.div
key={currentSlide}
className="absolute inset-0 w-full h-full flex"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.5 }}
>
{/* Left Side - Content Panel (60% width) */}
<motion.div
initial={{ opacity: 0, x: -30 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="relative w-full md:w-[60%] h-full flex items-center"
style={{ backgroundColor: '#FFF5F5' }}
>
<div className="px-6 sm:px-8 md:px-10 lg:px-16 xl:px-20 py-8 w-full">
{currentSlideData.type === 'overview' ? (
// Overview Slide (First Slide)
<>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
className="mb-10"
>
{/* Title with Badge Style */}
<h1 className="font-poppins text-4xl sm:text-5xl md:text-6xl lg:text-7xl leading-tight mb-6">
<span className="font-normal text-gray-900">Your </span>
<span className="inline-block text-white px-6 py-2 rounded-2xl font-semibold" style={{ backgroundColor: '#F95F62' }}>
Smart
</span>
<br />
<span className="font-bold text-gray-900">All-in-One City Pass</span>
</h1>
{/* Description */}
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.5 }}
className="font-poppins font-normal text-base md:text-lg text-gray-700 leading-relaxed max-w-2xl"
>
{currentSlideData.description}
</motion.p>
</motion.div>
{/* CTA Buttons */}
{currentSlideData.showButtons && (
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.6 }}
className="flex flex-col sm:flex-row gap-4"
>
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
<Button
onClick={onCheckoutClick}
withShine={true}
className="font-poppins font-semibold h-14 px-10 rounded-full text-white bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 transition-all duration-300 shadow-lg hover:shadow-xl"
>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.5rem', whiteSpace: 'nowrap' }}>
Get Started <ArrowRight className="w-5 h-5" style={{ display: 'inline-block', flexShrink: 0 }} />
</span>
</Button>
</motion.div>
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
<Button
onClick={onPassesClick}
variant="outline"
className="font-poppins font-semibold h-14 px-10 rounded-full border-2 border-primary !text-primary bg-white hover:!bg-primary hover:!text-white transition-all duration-300"
>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.5rem', whiteSpace: 'nowrap' }}>
Learn More
</span>
</Button>
</motion.div>
</motion.div>
)}
</>
) : (
// Feature Slides
<>
{/* Subtitle Badge */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="mb-6"
>
<span className="inline-block text-white px-5 py-2 rounded-xl font-poppins font-semibold text-sm" style={{ backgroundColor: '#F95F62' }}>
{currentSlideData.subtitle}
</span>
</motion.div>
{/* Title */}
<motion.h1
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="font-poppins text-4xl sm:text-5xl md:text-6xl lg:text-7xl leading-tight font-bold text-gray-900 mb-6"
>
{currentSlideData.title}
</motion.h1>
{/* Description */}
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="font-poppins font-normal text-base md:text-lg text-gray-700 leading-relaxed max-w-2xl mb-8"
>
{currentSlideData.description}
</motion.p>
{/* CTA Buttons */}
{currentSlideData.showButtons && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6 }}
className="flex flex-col sm:flex-row gap-4"
>
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
<Button
onClick={onCheckoutClick}
withShine={true}
className="font-poppins font-semibold h-14 px-10 rounded-full text-white bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 transition-all duration-300 shadow-lg hover:shadow-xl"
>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.5rem', whiteSpace: 'nowrap' }}>
Get Started <ArrowRight className="w-5 h-5" style={{ display: 'inline-block', flexShrink: 0 }} />
</span>
</Button>
</motion.div>
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
<Button
onClick={onPassesClick}
variant="outline"
className="font-poppins font-semibold h-14 px-10 rounded-full border-2 border-primary !text-primary bg-white hover:!bg-primary hover:!text-white transition-all duration-300"
>
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '0.5rem', whiteSpace: 'nowrap' }}>
Learn More
</span>
</Button>
</motion.div>
</motion.div>
)}
</>
)}
</div>
</motion.div>
{/* Right Side - Full Height Image (40% width) */}
<motion.div
initial={{ opacity: 0, x: 30 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="relative w-full md:w-[40%] h-full overflow-hidden hidden md:block"
>
<ImageWithFallback
src={currentSlideData.image}
alt={currentSlideData.title || "CityCards"}
className="w-full h-full object-cover"
/>
{/* Subtle overlay for depth */}
<div className="absolute inset-0 bg-gradient-to-l from-transparent to-black/5" />
</motion.div>
</motion.div>
</AnimatePresence>
</div>
{/* Navigation Controls */}
<div className="absolute bottom-12 left-1/2 -translate-x-1/2 z-20 flex items-center gap-6">
{/* Arrow Navigation */}
<button
onClick={prevSlide}
className="w-12 h-12 rounded-full bg-white/90 backdrop-blur-sm shadow-lg flex items-center justify-center text-gray-900 hover:bg-white transition-all duration-200 hover:scale-110"
aria-label="Previous slide"
>
<ChevronLeft className="w-6 h-6" />
</button>
{/* Progress Dots */}
<div className="flex gap-3">
{slides.map((_, index) => (
<button
key={index}
onClick={() => {
setDirection(index > currentSlide ? 1 : -1);
setCurrentSlide(index);
}}
className="group relative focus:outline-none"
aria-label={`Go to slide ${index + 1}`}
>
<div
className={`h-2 rounded-full transition-all duration-300 ${index === currentSlide
? 'w-12 bg-primary shadow-lg shadow-primary/30'
: 'w-2 bg-white/60 group-hover:bg-white group-hover:w-4'
}`}
/>
</button>
))}
</div>
{/* Arrow Navigation */}
<button
onClick={nextSlide}
className="w-12 h-12 rounded-full bg-white/90 backdrop-blur-sm shadow-lg flex items-center justify-center text-gray-900 hover:bg-white transition-all duration-200 hover:scale-110"
aria-label="Next slide"
>
<ChevronRight className="w-6 h-6" />
</button>
</div>
{/* Slide Counter */}
<div className="absolute top-24 right-8 z-20 bg-white/90 backdrop-blur-sm rounded-full px-4 py-2 shadow-lg">
<span className="font-poppins font-medium text-sm text-gray-900">
{currentSlide + 1} / {slides.length}
</span>
</div>
{/* Scroll Indicator (only on first slide) */}
{currentSlide === 0 && (
<motion.div
className="absolute bottom-8 left-1/2 -translate-x-1/2 z-10"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 1, duration: 0.6 }}
>
<motion.div
animate={{ y: [0, 8, 0] }}
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
className="flex flex-col items-center gap-2"
>
<span className="font-poppins text-xs text-gray-600 font-medium">Scroll to explore</span>
<ChevronRight className="w-5 h-5 text-gray-600 rotate-90" />
</motion.div>
</motion.div>
)}
</section>
);
}
interface User {
email: string;
name: string;
}
interface HowItWorksPageProps {
onBackClick: () => void;
onHomeClick: () => void;
onMelbourneClick: () => void;
onPassesClick: () => void;
onCheckoutClick: () => void;
onSignInClick: () => void;
onSignOutClick: () => void;
onAttractionsClick: () => void;
onBlogsClick: () => void;
onHowItWorksClick: () => void;
onFAQClick: () => void;
onPrivacyPolicyClick: () => void;
onAboutUsClick: () => void;
onProfileClick: () => void;
onCityCardsClick: () => void;
onMagicItineraryClick: () => void;
onPostCardsClick: () => void;
onOffersClick: () => void;
onContactUsClick?: () => void;
onEsimsClick?: () => void;
onHotelDiscountsClick?: () => void;
onSuperSavingsClick?: () => void;
currentPage: string;
user: User | null;
}
interface Step {
number: string;
title: string;
description: string;
images: string[];
}
const steps: Step[] = [
{
number: '1',
title: 'Choose City',
description: 'Browse our collection of amazing destinations and select the city you want to explore. From vibrant Melbourne to other exciting locations, find your perfect urban adventure.',
images: [
'https://images.unsplash.com/photo-1494522855154-9297ac14b55f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxjaXR5JTIwc2t5bGluZXxlbnwxfHx8fDE3NjExMTY4Mzd8MA&ixlib=rb-4.1.0&q=80&w=1080',
'https://images.unsplash.com/photo-1513635269975-59663e0ac1ad?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxjaXR5JTIwdHJhdmVsfGVufDF8fHx8MTc2MTExNjgzNnww&ixlib=rb-4.1.0&q=80&w=1080',
'https://images.unsplash.com/photo-1477959858617-67f85cf4f1df?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxjaXR5JTIwbGFuZG1hcmt8ZW58MXx8fHwxNzYxMTE2ODM4fDA&ixlib=rb-4.1.0&q=80&w=1080'
]
},
{
number: '2',
title: 'Get Your Pass',
description: 'Select from our Unlimited or Selective CityCard options. Get instant digital delivery with access to 50+ top attractions. Skip the lines and unlock exclusive benefits right away.',
images: [
'https://images.unsplash.com/photo-1567748534085-467f8a8a475d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx0cmF2ZWwlMjB0aWNrZXRzJTIwcGFzc3xlbnwxfHx8fDE3NjExMTY4Mzd8MA&ixlib=rb-4.1.0&q=80&w=1080',
'https://images.unsplash.com/photo-1743017784681-ba0cb69e2ead?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtb2JpbGUlMjBwaG9uZSUyMHRyYXZlbCUyMGJvb2tpbmd8ZW58MXx8fHwxNzYxMTE2ODM2fDA&ixlib=rb-4.1.0&q=80&w=1080'
]
},
{
number: '3',
title: 'Explore with Magic Itinerary',
description: 'Use our AI-powered Magic Itinerary planner to create your perfect experience. Get personalized recommendations, optimized routes, and real-time updates. Download the app to access offline maps, audio guides, and digital tickets all in one place.',
images: [
'https://images.unsplash.com/photo-1759802524049-2421ddaee0fe?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxjaXR5JTIwbWFwJTIwcGxhbm5pbmd8ZW58MXx8fHwxNzYxMTE2ODM3fDA&ixlib=rb-4.1.0&q=80&w=1080'
]
}
];
// Step Component
function StepSection({
step,
index,
onInView
}: {
step: Step;
index: number;
onInView: (index: number) => void;
}) {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, {
margin: '-35% 0px -35% 0px',
amount: 0.5
});
// Notify parent when this step comes into view
useEffect(() => {
if (isInView) {
onInView(index);
}
}, [isInView, index, onInView]);
// Rotation angles for diagonal effect
const rotations = [-3, 3, -2];
const rotation = rotations[index];
// For alternating layout:
// Step 0 (index 0): Content on left, Image on right
// Step 1 (index 1): Content on right, Image on left
// Step 2 (index 2): Content on left, Image on right
const isContentLeft = index % 2 === 0; // Even indices: content left, image right
return (
<div ref={ref} className={`mx-[0px] ${index === 0 ? 'mt-0 mb-[60px]' : 'my-[60px]'}`}>
<motion.div
className="flex items-center"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.8 }}
animate={{
opacity: isInView ? 1 : 0.35
}}
style={{
transform: 'translateZ(0)',
backfaceVisibility: 'hidden',
WebkitFontSmoothing: 'antialiased',
willChange: 'opacity'
}}
>
<div className={`grid grid-cols-1 lg:grid-cols-2 gap-6 lg:gap-12 items-center w-full`}>
{/* Text Content - Left side for even indices, right side for odd indices */}
<motion.div
className={`space-y-4 ${isContentLeft ? 'lg:order-1' : 'lg:order-2'}`}
initial={{ opacity: 0, x: isContentLeft ? -50 : 50 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.8 }}
style={{
transform: 'translateZ(0)',
backfaceVisibility: 'hidden',
WebkitFontSmoothing: 'antialiased'
}}
>
<div className="space-y-3 relative">
{/* Pin next to heading - shows when step is active */}
{isInView && (
<motion.div
className={`absolute top-0 ${isContentLeft ? '-left-8' : '-left-8 lg:left-auto lg:-right-8'}`}
initial={{ opacity: 0, scale: 0, y: -10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
transition={{ duration: 0.5, type: "spring", bounce: 0.5 }}
>
<div className="relative">
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-4 h-1 bg-black/20 rounded-full blur-sm" />
<div className="w-6 h-6 rounded-full bg-gradient-to-br from-[#F95F62] to-[#E53E41] shadow-lg relative border-2 border-white">
<div className="absolute top-0.5 left-1 w-1.5 h-1.5 bg-white/50 rounded-full" />
<div className="absolute inset-0 rounded-full bg-[#F95F62] opacity-30 animate-ping" />
</div>
</div>
</motion.div>
)}
<motion.h2
className="font-poppins text-3xl md:text-4xl lg:text-5xl"
animate={{
color: isInView ? '#111827' : '#9ca3af'
}}
transition={{ duration: 0.4 }}
style={{
transform: 'translateZ(0)',
backfaceVisibility: 'hidden',
WebkitFontSmoothing: 'antialiased'
}}
>
<span className="font-light block mb-1">{step.number}.</span>
<span className="font-semibold">{step.title}</span>
</motion.h2>
<motion.p
className="font-poppins text-sm md:text-base leading-relaxed font-normal max-w-xl"
animate={{
color: isInView ? '#4b5563' : '#d1d5db'
}}
transition={{ duration: 0.4 }}
style={{
transform: 'translateZ(0)',
backfaceVisibility: 'hidden',
WebkitFontSmoothing: 'antialiased'
}}
>
{step.description}
</motion.p>
</div>
</motion.div>
{/* Image Grid with Diagonal Rotation - Right side for even indices, left side for odd indices */}
<motion.div
className={`relative ${isContentLeft ? 'lg:order-2' : 'lg:order-1'}`}
initial={{ opacity: 0, x: isContentLeft ? 50 : -50 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.8, delay: 0.2 }}
style={{
transformStyle: 'preserve-3d',
transform: 'translateZ(0)',
backfaceVisibility: 'hidden'
}}
>
{/* Pin on top */}
<div className="absolute -top-6 left-1/2 -translate-x-1/2 z-30">
<div className="relative">
<div className="absolute top-2 left-1/2 -translate-x-1/2 w-6 h-1.5 bg-black/20 rounded-full blur-md" />
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-orange-400 to-orange-600 shadow-2xl relative border-2 border-white">
<div className="absolute top-1 left-1.5 w-2 h-2 bg-white/50 rounded-full" />
<div className="absolute top-full left-1/2 -translate-x-1/2 w-1 h-4 bg-gradient-to-b from-gray-600 to-gray-400" />
<div className="absolute top-full left-1/2 -translate-x-1/2 w-1.5 h-1.5 bg-gray-500 rounded-full" />
</div>
</div>
</div>
<motion.div
initial={{ rotate: 0 }}
animate={{
rotate: isInView ? rotation : 0
}}
transition={{
duration: 0.6,
ease: [0.34, 1.56, 0.64, 1]
}}
style={{
transformOrigin: 'center center',
transform: 'translateZ(0)',
backfaceVisibility: 'hidden',
willChange: 'transform'
}}
whileHover={{
rotate: 0,
scale: 1.02,
transition: { duration: 0.3 }
}}
>
<motion.div
className={`relative rounded-xl overflow-hidden transition-all duration-500 ${isInView
? 'ring-4 ring-primary'
: 'ring-2 ring-gray-300'
}`}
animate={{
boxShadow: isInView
? '0 15px 30px -8px rgba(249, 95, 98, 0.5), 0 6px 20px -3px rgba(249, 95, 98, 0.3)'
: '0 0 0 rgba(0, 0, 0, 0)'
}}
>
{index === 0 && (
<div className="p-3 bg-white">
<div className="relative h-[300px] overflow-hidden rounded-lg">
<ImageWithFallback
src={step.images[0]}
alt="Choose your pass"
className="w-full h-full object-cover"
/>
</div>
</div>
)}
{index === 1 && (
<div className="p-3 bg-white">
<div className="relative h-[300px] overflow-hidden rounded-lg">
<ImageWithFallback
src={step.images[1]}
alt="Plan your journey"
className="w-full h-full object-cover"
/>
</div>
</div>
)}
{index === 2 && (
<div className="p-3 bg-white">
<div className="relative h-[300px] overflow-hidden rounded-lg">
<ImageWithFallback
src={step.images[0]}
alt="Explore Melbourne"
className="w-full h-full object-cover"
/>
</div>
</div>
)}
{/* Active Glow */}
{isInView && (
<motion.div
className="absolute inset-0 pointer-events-none"
animate={{
boxShadow: [
'inset 0 0 30px rgba(249, 95, 98, 0)',
'inset 0 0 40px rgba(249, 95, 98, 0.15)',
'inset 0 0 30px rgba(249, 95, 98, 0)'
]
}}
transition={{
duration: 2,
repeat: Infinity,
ease: 'easeInOut'
}}
/>
)}
</motion.div>
</motion.div>
</motion.div>
</div>
</motion.div>
</div>
);
}
export function HowItWorksPage({
onHomeClick,
onMelbourneClick,
onPassesClick,
onCheckoutClick,
onSignInClick,
onSignOutClick,
onAttractionsClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onProfileClick,
onCityCardsClick,
onMagicItineraryClick,
onPostCardsClick,
onOffersClick,
onContactUsClick,
onEsimsClick,
onHotelDiscountsClick,
onSuperSavingsClick,
currentPage,
user
}: HowItWorksPageProps) {
const [scrollProgress, setScrollProgress] = useState(0);
const [activeStep, setActiveStep] = useState(0);
useEffect(() => {
const handleScroll = () => {
const totalHeight = document.documentElement.scrollHeight - window.innerHeight;
const progress = (window.scrollY / totalHeight) * 100;
setScrollProgress(progress);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
// Callback to update active step
const handleStepInView = (index: number) => {
setActiveStep(index);
};
// Calculate path length based on active step
const getPathLength = () => {
if (activeStep === 0) return 0.33;
if (activeStep === 1) return 0.66;
return 1.0;
};
return (
<Layout
activeCity="shared"
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
user={user}
>
<div className="min-h-screen bg-white">
{/* Hero Section - Full Carousel */}
<HeroCarousel
onCheckoutClick={onCheckoutClick}
onPassesClick={onPassesClick}
/>
{/* Unified How It Works Section with Flight Path */}
<section className="relative bg-gradient-to-br from-white via-gray-50/30 to-white overflow-hidden pt-[0px] pr-[0px] pb-12 pl-[0px]">
{/* Curved Flight Path SVG - Desktop Only */}
<div className="hidden lg:block absolute inset-0 pointer-events-none z-0">
<svg
className="w-full h-full"
viewBox="0 0 1400 1200"
fill="none"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMin slice"
>
{/* Define gradients */}
<defs>
<linearGradient id="pinGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="#FF6B6E" />
<stop offset="50%" stopColor="#F95F62" />
<stop offset="100%" stopColor="#E53E41" />
</linearGradient>
</defs>
{/* Main curved flight path - Animated with scroll */}
<motion.path
d="M 300 180 Q 500 220, 650 320 Q 750 420, 400 540 Q 600 660, 900 780 Q 700 900, 450 1000"
stroke="#F95F62"
strokeWidth="3"
strokeDasharray="12 8"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
initial={{ pathLength: 0, opacity: 0 }}
animate={{
pathLength: getPathLength(),
opacity: 0.35
}}
transition={{ duration: 1, ease: "easeInOut" }}
/>
{/* Shadow path for depth - follows main path */}
<motion.path
d="M 303 183 Q 503 223, 653 323 Q 753 423, 403 543 Q 603 663, 903 783 Q 703 903, 453 1003"
stroke="#000000"
strokeWidth="3"
strokeDasharray="12 8"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
initial={{ pathLength: 0, opacity: 0 }}
animate={{
pathLength: getPathLength(),
opacity: 0.02
}}
transition={{ duration: 1, ease: "easeInOut", delay: 0.1 }}
/>
{/* Destination pin at end of path - appears when fully drawn */}
{activeStep === 2 && (
<motion.g
initial={{ opacity: 0, scale: 0 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: 0.8, type: "spring", bounce: 0.5 }}
>
{/* Pin shadow */}
<ellipse
cx="450"
cy="1008"
rx="6"
ry="1.5"
fill="#000000"
opacity="0.2"
/>
{/* Pin circle */}
<circle
cx="450"
cy="1000"
r="8"
fill="url(#pinGradient)"
stroke="white"
strokeWidth="2"
/>
{/* Pin highlight */}
<circle
cx="447"
cy="997"
r="2"
fill="white"
opacity="0.5"
/>
</motion.g>
)}
</svg>
</div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 w-full relative z-10 pt-12">
{/* Header */}
<motion.div
className="text-center mb-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<h1 className="font-poppins text-2xl md:text-3xl lg:text-4xl leading-tight">
<span className="font-light">How it</span>{' '}
<span className="font-semibold">works</span>
</h1>
</motion.div>
{/* Steps Container */}
<div className="space-y-8 lg:space-y-10">
{steps.map((step, index) => (
<StepSection
key={step.number}
step={step}
index={index}
onInView={handleStepInView}
/>
))}
</div>
</div>
</section>
{/* Additional Sections */}
<div className="container mx-auto px-4">
{/* Why Choose CityCards Section */}
<div className="my-20">
<WhyChooseCityCards />
</div>
</div>
{/* Pass Options Overview Section */}
<section className="py-20 bg-white">
<div className="container mx-auto px-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="text-center mb-16"
>
<h2 className="font-poppins text-3xl md:text-4xl lg:text-5xl leading-tight mb-4">
<span className="font-light text-foreground">Choose Your </span>
<span className="font-bold text-primary">Perfect Pass</span>
</h2>
<p className="font-poppins text-lg text-gray-600 max-w-2xl mx-auto">
Flexible options designed to match your travel style
</p>
</motion.div>
<div className="grid md:grid-cols-2 gap-8 max-w-4xl mx-auto">
{/* Flexi Card */}
<motion.div
initial={{ opacity: 0, x: -30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="bg-white rounded-3xl p-8 border-2 border-gray-200 hover:border-gray-300 transition-all duration-300 hover:shadow-xl"
>
<div className="mb-6">
<h3 className="font-poppins font-bold text-2xl text-foreground mb-3">
FLEXI CARD
</h3>
<p className="font-poppins text-gray-600 leading-relaxed">
Perfect for travelers who want to explore selected attractions at their own pace with essential features.
</p>
</div>
<ul className="space-y-3 mb-6 min-h-[210px]">
{[
'Access to selected attractions',
'Limited number of attractions per pass',
'Flexible validity period',
'Priority entry where available',
'Mobile ticket delivery'
].map((feature) => (
<li key={feature} className="flex items-start gap-3">
<div className="w-5 h-5 rounded-full bg-green-500 flex items-center justify-center flex-shrink-0 mt-0.5">
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
</div>
<span className="font-poppins text-gray-700">{feature}</span>
</li>
))}
</ul>
<div className="mb-4">
<p className="font-poppins text-xs text-gray-500 text-center">
Free cancellation up to 24 hours Instant delivery
</p>
</div>
<Button
onClick={onPassesClick}
className="w-full py-6 rounded-full font-poppins font-semibold text-lg bg-foreground hover:bg-foreground/90 text-white transition-all duration-300"
>
VIEW FLEXI OPTIONS
</Button>
</motion.div>
{/* Unlimited Card */}
<motion.div
initial={{ opacity: 0, x: 30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="bg-white rounded-3xl p-8 border-2 border-primary hover:border-primary/80 transition-all duration-300 hover:shadow-xl relative overflow-hidden"
>
{/* Popular Badge */}
<div className="absolute top-4 right-4">
<div className="bg-gradient-to-r from-yellow-400 to-orange-500 text-white px-4 py-1 rounded-full font-poppins font-semibold text-sm">
Most Popular
</div>
</div>
<div className="mb-6">
<h3 className="font-poppins font-bold text-2xl text-foreground mb-3">
UNLIMITED CARD
</h3>
<p className="font-poppins text-gray-600 leading-relaxed">
The ultimate experience for adventure seekers who want unlimited access to all attractions with premium features.
</p>
</div>
<ul className="space-y-3 mb-6 min-h-[210px]">
{[
'Unlimited access to all attractions',
'Time-limited validity (7 days)',
'Skip-the-line access',
'Expert guide inclusion',
'Mobile app access',
'Premium customer support'
].map((feature) => (
<li key={feature} className="flex items-start gap-3">
<div className="w-5 h-5 rounded-full bg-green-500 flex items-center justify-center flex-shrink-0 mt-0.5">
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
</div>
<span className="font-poppins text-gray-700">{feature}</span>
</li>
))}
</ul>
<div className="mb-4">
<p className="font-poppins text-xs text-gray-500 text-center">
Free cancellation up to 24 hours Instant delivery
</p>
</div>
<Button
onClick={onPassesClick}
className="w-full py-6 rounded-full font-poppins font-semibold text-lg bg-primary hover:bg-primary/90 text-white transition-all duration-300"
>
VIEW UNLIMITED OPTIONS
</Button>
</motion.div>
</div>
</div>
</section>
<div className="container mx-auto px-4">
{/* Magic Itinerary Teaser Section */}
<AttractionHassleFreeSection onMagicItineraryClick={onMagicItineraryClick} />
{/* Enhanced Testimonials Section */}
<EnhancedTestimonials />
{/* Mobile App Section */}
<MobileAppSection />
</div>
{/* CTA Section */}
<section className="mt-20 py-32 md:py-40 bg-gradient-to-br from-primary to-secondary relative overflow-hidden">
<div className="absolute inset-0 opacity-10">
<div className="absolute top-0 left-0 w-96 h-96 bg-white rounded-full blur-3xl" />
<div className="absolute bottom-0 right-0 w-96 h-96 bg-white rounded-full blur-3xl" />
</div>
<div className="container mx-auto px-4 relative z-10">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="text-center max-w-3xl mx-auto"
>
<h2 className="font-poppins text-3xl md:text-4xl lg:text-5xl font-semibold text-white mb-6">
Ready to Start Your Adventure?
</h2>
<p className="font-poppins text-lg md:text-xl text-white/90 mb-8 font-normal leading-relaxed">
Join millions of travelers who've discovered the smarter way to explore cities
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button
onClick={onCheckoutClick}
className="px-10 py-6 rounded-full font-poppins font-semibold text-lg bg-white text-primary hover:bg-gray-100 transition-all duration-300 hover:scale-105 shadow-xl"
>
Select Your City
<ArrowRight className="w-5 h-5 ml-2" />
</Button>
<Button
onClick={onPassesClick}
variant="outline"
className="px-10 py-6 rounded-full font-poppins font-semibold text-lg border-2 border-white !text-white bg-transparent hover:!bg-white hover:!text-primary transition-all duration-300"
>
View All Passes
</Button>
</div>
</motion.div>
</div>
</section>
<div className="container mx-auto px-4">
{/* Reviews Section */}
<ReviewsSection />
</div>
</div>
</Layout>
);
}
export default HowItWorksPage;