revamp how it works

This commit is contained in:
priyanshuvish
2025-10-28 19:35:56 +05:30
parent 0a39b39934
commit 90f0cb953f
4 changed files with 755 additions and 352 deletions

View File

@@ -1,17 +1,14 @@
import { useState, useEffect, useRef } from 'react';
import { motion } from 'motion/react';
import { Check, ChevronRight, MapPin, Calendar, Smartphone, CreditCard } from 'lucide-react';
import { motion, useInView } from 'motion/react';
import { Button } from './ui/button';
import { Card, CardContent } from './ui/card';
import { Badge } from './ui/badge';
import Navbar from './Navbar';
// import { CitySubmenu } from './CitySubmenu';
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 { Footer } from './Footer';
interface User {
email: string;
@@ -40,48 +37,229 @@ interface HowItWorksPageProps {
onContactUsClick?: () => void;
onEsimsClick?: () => void;
onHotelDiscountsClick?: () => void;
onSuperSavingsClick?: () => void;
currentPage: string;
user: User | null;
}
interface StepItem {
id: string;
interface Step {
number: string;
title: string;
description: string;
icon: React.ReactNode;
isMainStep: boolean;
parentStep?: string;
image?: string;
features?: string[];
images: string[];
}
const steps: StepItem[] = [
const steps: Step[] = [
{
id: 'pick-pass',
title: 'Pick Your Pass',
description: 'Choose between our Selective or Unlimited pass options. Each designed to match your travel style and give you the freedom to explore Melbourne your way.',
icon: <CreditCard className="w-6 h-6" />,
isMainStep: true,
features: ['Selective or Unlimited options', 'Instant digital delivery', 'Skip-the-line access', 'Mobile-friendly tickets']
number: '1',
title: 'Choose Your Pass',
description: 'Select from our Unlimited or Selective CityCard options. Get instant digital delivery with access to 50+ top Melbourne attractions. Skip the lines and start exploring 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',
'https://images.unsplash.com/photo-1677200922658-d0df5b2ac91e?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBhdHRyYWN0aW9uc3xlbnwxfHx8fDE3NjExMTY4Mzh8MA&ixlib=rb-4.1.0&q=80&w=1080'
]
},
{
id: 'plan-itinerary',
number: '2',
title: 'Plan Your Journey',
description: 'Use our smart planning tools to create your perfect Melbourne itinerary. Get personalized recommendations based on your interests and available time.',
icon: <Calendar className="w-6 h-6" />,
isMainStep: true,
features: ['AI-powered suggestions', 'Real-time crowd updates', 'Weather-based planning', 'Flexible scheduling']
description: 'Use our AI-powered Magic Itinerary planner to create your perfect Melbourne experience. Get personalized recommendations, optimized routes, and real-time updates tailored to your interests.',
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',
'https://images.unsplash.com/photo-1741721816773-ff31d089c227?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzbWFydHBob25lJTIwYXBwJTIwdHJhdmVsfGVufDF8fHx8MTc2MTExNjgzOHww&ixlib=rb-4.1.0&q=80&w=1080'
]
},
{
id: 'explore-enjoy',
title: 'Explore & Enjoy',
description: 'Experience Melbourne like never before with our mobile app, offline maps, and 24/7 support. Every moment is designed to be effortless and memorable.',
icon: <MapPin className="w-6 h-6" />,
isMainStep: true,
features: ['Mobile app included', 'Offline map access', '24/7 customer support', 'Audio guide content']
number: '3',
title: 'Explore Melbourne',
description: 'Download the mobile app and start your adventure. Access offline maps, audio guides, and your digital tickets all in one place. Experience Melbourne like never before with 24/7 support at your fingertips.',
images: [
'https://images.unsplash.com/photo-1552333709-c465f89a4dfd?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx0b3VyaXN0JTIwZXhwbG9yaW5nJTIwY2l0eXxlbnwxfHx8fDE3NjExMTY4Mzd8MA&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: '-25% 0px -25% 0px',
amount: 0.3
});
// 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];
return (
<div ref={ref} className={`mx-[0px] ${index === 0 ? 'mt-0 mb-[168px]' : 'my-[168px]'}`}>
<motion.div
className="min-h-screen flex items-center"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.8 }}
animate={{
opacity: isInView ? 1 : 1,
}}
>
<div className={`grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-20 items-center w-full ${
index === 1 ? 'lg:flex lg:flex-row-reverse' : ''
}`}>
{/* Text Content */}
<motion.div
className={`space-y-8 ${index === 1 ? 'order-1 lg:order-2' : ''}`}
initial={{ opacity: 0, x: index === 1 ? 50 : -50 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.8 }}
>
<div className="space-y-6">
<motion.h2
className="font-poppins text-6xl md:text-7xl lg:text-8xl"
animate={{
color: isInView ? '#111827' : '#9ca3af'
}}
transition={{ duration: 0.4 }}
>
<span className="font-light block mb-2">{step.number}.</span>
<span className="font-semibold">{step.title}</span>
</motion.h2>
<motion.p
className="font-poppins text-lg md:text-xl leading-relaxed font-normal max-w-xl"
animate={{
color: isInView ? '#4b5563' : '#d1d5db'
}}
transition={{ duration: 0.4 }}
>
{step.description}
</motion.p>
</div>
</motion.div>
{/* Image Grid with Diagonal Rotation */}
<motion.div
className={`relative ${index === 1 ? 'order-2 lg:order-1' : ''}`}
initial={{ opacity: 0, x: index === 1 ? -50 : 50 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.8, delay: 0.2 }}
style={{
transformStyle: 'preserve-3d'
}}
>
{/* Pin on top */}
<div className="absolute -top-8 left-1/2 -translate-x-1/2 z-30">
<div className="relative">
<div className="absolute top-3 left-1/2 -translate-x-1/2 w-8 h-2 bg-black/20 rounded-full blur-md" />
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-orange-400 to-orange-600 shadow-2xl relative border-3 border-white">
<div className="absolute top-1.5 left-2 w-3 h-3 bg-white/50 rounded-full" />
<div className="absolute top-full left-1/2 -translate-x-1/2 w-1.5 h-5 bg-gradient-to-b from-gray-600 to-gray-400" />
<div className="absolute top-full left-1/2 -translate-x-1/2 w-2 h-2 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'
}}
whileHover={{
rotate: 0,
scale: 1.02,
transition: { duration: 0.3 }
}}
>
<motion.div
className={`relative rounded-2xl overflow-hidden transition-all duration-500 ${
isInView
? 'ring-[6px] ring-primary'
: 'ring-2 ring-gray-300'
}`}
animate={{
boxShadow: isInView
? '0 25px 50px -12px rgba(249, 95, 98, 0.5), 0 10px 30px -5px rgba(249, 95, 98, 0.3)'
: '0 0 0 rgba(0, 0, 0, 0)'
}}
>
{index === 0 && (
<div className="p-4 bg-white">
<div className="relative h-[500px] 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-4 bg-white">
<div className="relative h-[500px] 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-4 bg-white">
<div className="relative h-[500px] 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,
@@ -103,67 +281,38 @@ export function HowItWorksPage({
onContactUsClick,
onEsimsClick,
onHotelDiscountsClick,
onSuperSavingsClick,
currentPage,
user
}: HowItWorksPageProps) {
const [activeStep, setActiveStep] = useState<string>('pick-pass');
const [scrollProgress, setScrollProgress] = useState(0);
const mainSteps = steps; // All steps are now main steps
const stepRefs = useRef<(HTMLDivElement | null)[]>([]);
const containerRef = useRef<HTMLDivElement>(null);
const [activeStep, setActiveStep] = useState(0);
// Scroll-based step activation
useEffect(() => {
const handleScroll = () => {
if (!containerRef.current) return;
const scrollY = window.scrollY;
const windowHeight = window.innerHeight;
const containerTop = containerRef.current.offsetTop;
const containerHeight = containerRef.current.offsetHeight;
// Calculate trigger points for each step (when step indicator is in center of viewport)
const stepElements = stepRefs.current.filter(Boolean);
let currentStepIndex = 0;
let minDistance = Infinity;
stepElements.forEach((stepEl, index) => {
if (stepEl) {
const stepTop = stepEl.offsetTop + containerTop;
const stepCenter = stepTop + stepEl.offsetHeight / 2;
const viewportCenter = scrollY + windowHeight / 2;
const distance = Math.abs(stepCenter - viewportCenter);
if (distance < minDistance) {
minDistance = distance;
currentStepIndex = index;
}
}
});
// Update active step
const newActiveStep = mainSteps[currentStepIndex]?.id || 'pick-pass';
if (newActiveStep !== activeStep) {
setActiveStep(newActiveStep);
}
// Calculate smooth progress through entire stepper
const sectionStart = containerTop - windowHeight / 2;
const sectionEnd = containerTop + containerHeight - windowHeight / 2;
const rawProgress = (scrollY - sectionStart) / (sectionEnd - sectionStart);
const progress = Math.max(0, Math.min(1, rawProgress));
const totalHeight = document.documentElement.scrollHeight - window.innerHeight;
const progress = (window.scrollY / totalHeight) * 100;
setScrollProgress(progress);
};
window.addEventListener('scroll', handleScroll, { passive: true });
handleScroll(); // Initial call
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, [activeStep, mainSteps]);
}, []);
// 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 (
<div className="min-h-screen bg-background">
<div className="min-h-screen bg-white">
{/* Navigation */}
<Navbar
activeCity="Melbourne"
@@ -184,190 +333,178 @@ export function HowItWorksPage({
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
onSuperSavingsClick={onSuperSavingsClick}
currentPage={currentPage}
isUserSignedIn={!!user}
user={user}
/>
{/* <CitySubmenu
currentPage={currentPage}
onClose={() => {}}
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onAttractionsClick={onAttractionsClick}
onPassesClick={onPassesClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
/> */}
<div className="container mx-auto px-4 pt-52 pb-12 relative z-10">
{/* Page Header */}
<motion.div
className="text-center mb-16"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<h1 className="font-poppins font-light text-4xl md:text-5xl lg:text-6xl mb-4">
How{' '}
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
It Works
</span>
</h1>
<p className="font-poppins text-xl leading-relaxed font-normal text-gray-600 max-w-3xl mx-auto">
Discover the simple process that transforms your city exploration from ordinary to extraordinary
</p>
</motion.div>
{/* Vertical Stepper with Alternating Layout */}
<div ref={containerRef} className="relative max-w-6xl mx-auto mb-20">
{/* Central Progress Line */}
<div className="absolute left-1/2 transform -translate-x-0.5 top-0 bottom-0 w-1 bg-gray-200 rounded-full">
{/* Progress Fill */}
<motion.div
className="w-full bg-gradient-to-b from-primary to-secondary rounded-full relative"
initial={{ height: "0%" }}
{/* 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-[70px] 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 2800"
fill="none"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMin slice"
>
{/* Main curved flight path - Animated with scroll */}
<motion.path
d="M 200 300 Q 600 450, 800 650 Q 900 850, 350 1100 Q 700 1350, 1100 1600 Q 800 1850, 400 2100"
stroke="#F95F62"
strokeWidth="4"
strokeDasharray="15 10"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
initial={{ pathLength: 0, opacity: 0 }}
animate={{
height: `${Math.min(100, scrollProgress * 100)}%`
pathLength: getPathLength(),
opacity: 0.4
}}
transition={{ duration: 0.3, ease: [0.25, 0.1, 0.25, 1] }}
>
{/* Ellipse at the end when progress reaches 100% */}
{scrollProgress >= 1 && (
<motion.div
className="absolute -bottom-2 left-1/2 transform -translate-x-1/2 w-6 h-3 bg-gradient-to-r from-primary to-secondary rounded-full shadow-lg"
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.4, ease: [0.25, 0.1, 0.25, 1] }}
transition={{ duration: 0.8, ease: "easeInOut" }}
/>
{/* Shadow path for depth - follows main path */}
<motion.path
d="M 203 305 Q 603 455, 803 655 Q 903 855, 353 1105 Q 703 1355, 1103 1605 Q 803 1855, 403 2105"
stroke="#000000"
strokeWidth="4"
strokeDasharray="15 10"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
initial={{ pathLength: 0, opacity: 0 }}
animate={{
pathLength: getPathLength(),
opacity: 0.02
}}
transition={{ duration: 0.8, ease: "easeInOut", delay: 0.1 }}
/>
{/* 3D Pin marker at the end - only shows when all steps complete */}
{activeStep === 2 && (
<motion.g
initial={{ opacity: 0, scale: 0, y: -20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.4, type: "spring", bounce: 0.5 }}
>
{/* Pin shadow */}
<ellipse
cx="400"
cy="2145"
rx="18"
ry="4"
fill="black"
opacity="0.25"
/>
)}
</motion.div>
</div>
{/* Steps Container */}
<div className="space-y-40">
{mainSteps.map((step, index) => {
const isActive = activeStep === step.id;
const isLeft = index % 2 === 0; // Step 1 & 3 on left, Step 2 on right
const stepNumber = `0${index + 1}`;
{/* Pin body/stick - gradient effect */}
<path
d="M 400 2070 L 398 2130 L 402 2130 Z"
fill="url(#pinGradient)"
opacity="0.8"
/>
return (
<motion.div
key={step.id}
ref={(el) => stepRefs.current[index] = el}
className="relative"
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.2 }}
>
{/* Step Indicator Circle - Always in center */}
<motion.div
className={`
absolute left-1/2 transform -translate-x-1/2 w-16 h-16 rounded-full
flex items-center justify-center z-10 border-4 transition-all duration-500
${isActive
? 'bg-primary border-white text-white shadow-xl scale-110'
: 'bg-white border-gray-300 text-gray-600'
}
`}
animate={{ scale: isActive ? 1.1 : 1 }}
transition={{ duration: 0.3 }}
>
<div className="w-6 h-6">
{step.icon}
</div>
</motion.div>
{/* Pin head - larger and more prominent */}
<circle
cx="400"
cy="2070"
r="22"
fill="url(#pinHeadGradient)"
/>
{/* Content Container - Zigzag Layout */}
<div className="grid grid-cols-12 gap-8 items-center">
{/* Content Card */}
<motion.div
className={`${isLeft ? 'col-span-5' : 'col-span-5 col-start-8'}`}
initial={{ opacity: 0, x: isLeft ? -50 : 50 }}
animate={{
opacity: isActive ? 1 : 0.6,
x: 0,
scale: isActive ? 1 : 0.95
}}
transition={{ duration: 0.5, delay: 0.2 }}
>
<Card className={`p-8 border-2 transition-all duration-500 rounded-3xl ${
isActive
? 'border-primary shadow-xl bg-gradient-to-br from-primary/5 to-secondary/5'
: 'border-gray-200 bg-white'
}`}>
<CardContent className="p-0 text-center">
{/* Step Number */}
<motion.div
className={`text-6xl font-poppins font-light mb-4 transition-colors duration-300 ${
isActive ? 'text-primary' : 'text-gray-300'
}`}
animate={{ scale: isActive ? 1.1 : 1 }}
transition={{ duration: 0.3 }}
>
{stepNumber}
</motion.div>
{/* Pin head border */}
<circle
cx="400"
cy="2070"
r="22"
fill="none"
stroke="white"
strokeWidth="3"
/>
{/* Step Title */}
<h3 className="font-poppins font-normal text-2xl text-gray-900 mb-4">
{step.title}
</h3>
{/* Shine effect on pin head */}
<circle
cx="393"
cy="2063"
r="7"
fill="white"
opacity="0.6"
/>
{/* Step Description */}
<p className="text-gray-600 leading-relaxed mb-6 text-center font-light">
{step.description}
</p>
{/* Inner glow */}
<circle
cx="400"
cy="2070"
r="18"
fill="none"
stroke="#FF8A8C"
strokeWidth="2"
opacity="0.4"
/>
{/* Features List */}
{step.features && (
<div className="grid grid-cols-2 gap-3 mb-6">
{step.features.map((feature, featureIndex) => (
<motion.div
key={featureIndex}
className="flex items-center gap-2 text-left"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: isActive ? 1 : 0.7, x: 0 }}
transition={{ duration: 0.3, delay: featureIndex * 0.1 }}
>
<div className="w-4 h-4 bg-green-100 rounded-full flex items-center justify-center flex-shrink-0">
<Check className="w-2.5 h-2.5 text-green-600" />
</div>
<span className="text-gray-700 text-sm font-light">{feature}</span>
</motion.div>
))}
</div>
)}
{/* Checkmark on pin */}
<path
d="M 393 2070 L 397 2075 L 407 2063"
stroke="white"
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
fill="none"
/>
{/* Action Button */}
{isActive && (
<motion.div
className="mt-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.3 }}
>
<Button
className="bg-primary hover:bg-primary/90 text-white px-6 py-2 rounded-xl font-normal"
onClick={onPassesClick}
>
Get Started
<ChevronRight className="w-4 h-4 ml-2" />
</Button>
</motion.div>
)}
</CardContent>
</Card>
</motion.div>
{/* Spacer for the opposite side */}
<div className={`${isLeft ? 'col-span-6 col-start-7' : 'col-span-6'}`} />
</div>
</motion.div>
);
})}
</div>
{/* Define gradients */}
<defs>
<linearGradient id="pinHeadGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="#FF6B6E" />
<stop offset="50%" stopColor="#F95F62" />
<stop offset="100%" stopColor="#E53E41" />
</linearGradient>
<linearGradient id="pinGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="#999999" />
<stop offset="100%" stopColor="#666666" />
</linearGradient>
</defs>
</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-32">
{/* Header */}
<motion.div
className="text-center mb-8"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<h1 className="font-poppins text-4xl md:text-5xl lg:text-6xl leading-tight">
<span className="font-light">How it</span>{' '}
<span className="font-semibold">works</span>
</h1>
</motion.div>
{/* Steps Container */}
<div className="space-y-32 lg:space-y-48">
{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">
{/* Make the most of every attraction section */}
<AttractionHassleFreeSection />

View File

@@ -14,6 +14,7 @@ import { MobileAppPromotion } from './MobileAppPromotion';
import { MelbourneFAQ } from './MelbourneFAQ';
import { Footer } from './Footer';
import { Layout } from '../Layout';
import { PersonalizedTourHero } from './PersonalizedTourHero';
interface User {
email: string;
@@ -179,11 +180,10 @@ function HeroBannerCarousel({ onCheckoutClick, onPassesClick, onEsimsClick, onHo
aria-label={`Go to slide ${index + 1}`}
>
<div
className={`h-2 rounded-full transition-all duration-300 ${
currentSlide === index
className={`h-2 rounded-full transition-all duration-300 ${currentSlide === index
? 'w-12'
: 'w-2 group-hover:opacity-75'
}`}
}`}
style={{
backgroundColor: currentSlide === index ? '#F95F62' : '#9CA3AF',
opacity: currentSlide === index ? 1 : 0.5
@@ -213,11 +213,10 @@ function HeroBannerCarousel({ onCheckoutClick, onPassesClick, onEsimsClick, onHo
/>
)}
<span
className={`font-poppins font-semibold transition-all duration-300 ${
currentSlide === index
className={`font-poppins font-semibold transition-all duration-300 ${currentSlide === index
? 'text-2xl'
: 'text-xl'
}`}
}`}
style={{
color: currentSlide === index ? '#F95F62' : '#000000',
opacity: currentSlide === index ? 1 : 0.6
@@ -228,9 +227,8 @@ function HeroBannerCarousel({ onCheckoutClick, onPassesClick, onEsimsClick, onHo
</div>
{/* Label */}
<div className={`transition-all duration-300 ${
currentSlide === index ? 'opacity-100' : 'opacity-70'
}`}>
<div className={`transition-all duration-300 ${currentSlide === index ? 'opacity-100' : 'opacity-70'
}`}>
<p className="font-poppins text-sm text-black/90 whitespace-nowrap max-w-[120px]">
{slide.highlight}
</p>
@@ -301,85 +299,85 @@ export function MelbournePage({
onSignOutClick={onSignOutClick}
user={user}
>
<div className="min-h-screen bg-background">
<div className="min-h-screen bg-background">
{/* Hero Banner Carousel */}
<HeroBannerCarousel
{/* Hero Banner Carousel */}
{/* <HeroBannerCarousel
onCheckoutClick={onCheckoutClick}
onPassesClick={onPassesClick}
onEsimsClick={onEsimsClick}
onHotelDiscountsClick={onHotelDiscountsClick}
/>
/> */}
<PersonalizedTourHero />
{/* Main Content */}
<main>
<div className="container mx-auto px-4 py-16">
{/* Features Grid */}
<motion.section
className="grid md:grid-cols-3 gap-8 py-16"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.8, delay: 0.4 }}
>
{[
{
title: "50+ Attractions",
description: "Access to Melbourne's top museums, galleries, and experiences",
icon: "🏛️"
},
{
title: "Instant QR Access",
description: "Skip the lines with digital tickets on your phone",
icon: "📱"
},
{
title: "Save up to 50%",
description: "Exclusive discounts and special offers for cardholders",
icon: "💰"
}
].map((feature, index) => (
<motion.div
key={feature.title}
className="text-center p-8 bg-white rounded-2xl shadow-lg border border-gray-100"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.6 + index * 0.1 }}
whileHover={{ scale: 1.02, y: -5 }}
>
<div className="text-4xl mb-4">{feature.icon}</div>
<h3 className="font-poppins text-xl font-semibold text-gray-900 mb-3">
{feature.title}
</h3>
<p className="text-gray-600">
{feature.description}
</p>
</motion.div>
))}
</motion.section>
</div>
</main>
{/* Main Content */}
<main>
<div className="container mx-auto px-4 py-16">
{/* Features Grid */}
<motion.section
className="grid md:grid-cols-3 gap-8 py-16"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.8, delay: 0.4 }}
>
{[
{
title: "50+ Attractions",
description: "Access to Melbourne's top museums, galleries, and experiences",
icon: "🏛️"
},
{
title: "Instant QR Access",
description: "Skip the lines with digital tickets on your phone",
icon: "📱"
},
{
title: "Save up to 50%",
description: "Exclusive discounts and special offers for cardholders",
icon: "💰"
}
].map((feature, index) => (
<motion.div
key={feature.title}
className="text-center p-8 bg-white rounded-2xl shadow-lg border border-gray-100"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.6 + index * 0.1 }}
whileHover={{ scale: 1.02, y: -5 }}
>
<div className="text-4xl mb-4">{feature.icon}</div>
<h3 className="font-poppins text-xl font-semibold text-gray-900 mb-3">
{feature.title}
</h3>
<p className="text-gray-600">
{feature.description}
</p>
</motion.div>
))}
</motion.section>
</div>
</main>
{/* Melbourne Attractions Section */}
<MelbourneAttractions />
{/* Melbourne Attractions Section */}
<MelbourneAttractions />
{/* Melbourne Card Comparison Section */}
<MelbourneCardComparison onCheckoutClick={onCheckoutClick} />
{/* Melbourne Card Comparison Section */}
<MelbourneCardComparison onCheckoutClick={onCheckoutClick} />
{/* Melbourne Tour Overview Section */}
<MelbourneTourOverview />
{/* Melbourne Tour Overview Section */}
<MelbourneTourOverview />
{/* Melbourne Blogs Section */}
<MelbourneBlogs />
{/* Melbourne Blogs Section */}
<MelbourneBlogs />
{/* Enhanced Testimonials Section */}
<EnhancedTestimonials />
{/* Enhanced Testimonials Section */}
<EnhancedTestimonials />
{/* Mobile App Promotion Section */}
<MobileAppPromotion />
{/* Mobile App Promotion Section */}
<MobileAppPromotion />
{/* Melbourne FAQ Section */}
<MelbourneFAQ />
</div>
{/* Melbourne FAQ Section */}
<MelbourneFAQ />
</div>
</Layout>
);
}

View File

@@ -6,7 +6,7 @@ import Frame1597884853 from '../imports/Frame1597884853';
import { Button } from './ui/button';
import { ImageWithFallback } from './figma/ImageWithFallback';
import { CTAButton } from './CTAButton';
import logoImage from '../assets/cit-logo.png';
import logoImage from '../assets/cityLogo.png';
interface NavbarProps {
activeCity: string;

View File

@@ -0,0 +1,268 @@
import { motion } from 'motion/react';
import { Check, Clock } from 'lucide-react';
import { ImageWithFallback } from './figma/ImageWithFallback';
import exampleImage from 'figma:asset/9404cd260e0be9427a5438a687bca92b4e4740ac.png';
interface PersonalizedTourHeroProps {
onCreateItineraryClick?: () => void;
}
export function PersonalizedTourHero({ onCreateItineraryClick }: PersonalizedTourHeroProps) {
// Mock activity data
const activities = [
{
id: 1,
image: 'https://images.unsplash.com/photo-1624138784614-87fd1b6528f8?w=400',
title: '1h1 Marriott Mussions With Balcony he',
time: '9 Days at 15:00',
status: 'added',
statusText: 'Added'
},
{
id: 2,
image: 'https://images.unsplash.com/photo-1513635269975-59663e0ac1ad?w=400',
title: 'Universal Studios Singapore - Universal Express',
time: '8 Days',
status: 'adding',
statusText: 'Adding...'
},
{
id: 3,
text: 'Upgrade my Buy1 At1dfls visit to the SKY',
status: 'pending'
}
];
// Left column thumbnail images
const leftThumbnails = [
'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=300',
'https://images.unsplash.com/photo-1476514525535-07fb3b4ae5f1?w=300',
'https://images.unsplash.com/photo-1551882547-ff40c63fe5fa?w=300',
'https://images.unsplash.com/photo-1469854523086-cc02fe5d8800?w=300',
'https://images.unsplash.com/photo-1530789253388-582c481c54b0?w=300',
'https://images.unsplash.com/photo-1507525428034-b723cf961d3e?w=300',
'https://images.unsplash.com/photo-1476514525535-07fb3b4ae5f1?w=300',
'https://images.unsplash.com/photo-1551882547-ff40c63fe5fa?w=300'
];
// Right column thumbnail images
const rightThumbnails = [
'https://images.unsplash.com/photo-1504893524553-b855bce32c67?w=300',
'https://images.unsplash.com/photo-1488646953014-85cb44e25828?w=300',
'https://images.unsplash.com/photo-1500835556837-99ac94a94552?w=300',
'https://images.unsplash.com/photo-1502602898657-3e91760cbb34?w=300',
'https://images.unsplash.com/photo-1493246507139-91e8fad9978e?w=300',
'https://images.unsplash.com/photo-1476514525535-07fb3b4ae5f1?w=300',
'https://images.unsplash.com/photo-1488646953014-85cb44e25828?w=300',
'https://images.unsplash.com/photo-1500835556837-99ac94a94552?w=300'
];
return (
<div
className="relative w-full min-h-screen overflow-hidden flex items-center"
style={{
backgroundImage: 'url(https://images.unsplash.com/photo-1708554139375-b8a8c00a5f57?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBza3lsaW5lJTIwY2l0eXNjYXBlfGVufDF8fHx8MTc2MTMwMjU1M3ww&ixlib=rb-4.1.0&q=80&w=1080)',
backgroundSize: 'cover',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat'
}}
>
{/* Background Overlay */}
<div className="absolute inset-0 bg-black/48 backdrop-blur-[1px]"></div>
{/* Left Thumbnail Column */}
<div className="absolute left-0 top-0 bottom-0 w-24 md:w-32 lg:w-40 overflow-hidden">
<motion.div
className="flex flex-col gap-4"
animate={{ y: [0, -50, 0] }}
transition={{
duration: 20,
repeat: Infinity,
ease: "linear"
}}
>
{[...leftThumbnails, ...leftThumbnails].map((img, index) => (
<div
key={`left-${index}`}
className="relative w-16 h-16 md:w-20 md:h-20 lg:w-24 lg:h-24 rounded-lg overflow-hidden shadow-md flex-shrink-0 mx-auto"
>
<ImageWithFallback
src={img}
alt={`Destination ${index + 1}`}
className="w-full h-full object-cover"
/>
</div>
))}
</motion.div>
</div>
{/* Right Thumbnail Column */}
<div className="absolute right-0 top-0 bottom-0 w-24 md:w-32 lg:w-40 overflow-hidden">
<motion.div
className="flex flex-col gap-4"
animate={{ y: [-50, 0, -50] }}
transition={{
duration: 20,
repeat: Infinity,
ease: "linear"
}}
>
{[...rightThumbnails, ...rightThumbnails].map((img, index) => (
<div
key={`right-${index}`}
className="relative w-16 h-16 md:w-20 md:h-20 lg:w-24 lg:h-24 rounded-lg overflow-hidden shadow-md flex-shrink-0 mx-auto"
>
<ImageWithFallback
src={img}
alt={`Destination ${index + 1}`}
className="w-full h-full object-cover"
/>
</div>
))}
</motion.div>
</div>
{/* Main Content */}
<div className="relative z-10 w-full px-4 py-16 md:py-20 lg:py-24">
<div className="max-w-5xl mx-auto">
{/* Heading */}
<motion.div
className="text-center mb-8 md:mb-12"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<h1 className="font-poppins text-3xl sm:text-4xl md:text-5xl lg:text-6xl leading-tight mb-3">
<span className="font-normal text-white">Your Tour,</span>
<br />
<span className="font-normal text-white">Perfectly </span>
<span className="font-bold italic" style={{ color: '#F95F62' }}>
Personalised!
</span>
</h1>
<p className="font-poppins text-base md:text-lg text-white font-normal mt-4">
Explore Experience, AI-powered mult-day tours.
</p>
</motion.div>
{/* Activity Cards */}
<motion.div
className="space-y-4 max-w-2xl mx-auto"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.6, delay: 0.2 }}
>
{/* Activity 1 - Added */}
<motion.div
className="bg-white rounded-2xl shadow-lg border border-gray-200 p-4 flex items-center gap-4"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: 0.3 }}
>
<div className="relative w-20 h-20 md:w-24 md:h-24 rounded-xl overflow-hidden flex-shrink-0">
<ImageWithFallback
src={activities[0].image}
alt={activities[0].title}
className="w-full h-full object-cover"
/>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<p className="font-poppins text-xs md:text-sm text-gray-500 mb-1 font-normal">
Activity
</p>
<h3 className="font-poppins text-sm md:text-base font-medium text-gray-900 mb-1 truncate">
1h1 Marriott Mussions With Balcony he
</h3>
<p className="font-poppins text-xs text-gray-500 font-normal">
9 Days at 15:00
</p>
</div>
<div className="flex items-center gap-1.5 px-3 py-1.5 rounded-full flex-shrink-0" style={{ backgroundColor: 'rgba(249, 95, 98, 0.1)' }}>
<Check className="w-3.5 h-3.5" style={{ color: '#F95F62' }} />
<span className="font-poppins text-xs font-medium" style={{ color: '#F95F62' }}>
Added
</span>
</div>
</div>
</div>
</motion.div>
{/* Activity 2 - Adding */}
<motion.div
className="bg-white rounded-2xl shadow-lg border border-gray-200 p-4 flex items-center gap-4"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: 0.4 }}
>
<div className="relative w-20 h-20 md:w-24 md:h-24 rounded-xl overflow-hidden flex-shrink-0">
<ImageWithFallback
src={activities[1].image}
alt={activities[1].title}
className="w-full h-full object-cover"
/>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<p className="font-poppins text-xs md:text-sm text-gray-500 mb-1 font-normal">
Activity
</p>
<h3 className="font-poppins text-sm md:text-base font-medium text-gray-900 mb-1 line-clamp-2">
Universal Studios Singapore - Universal Express
</h3>
<p className="font-poppins text-xs text-gray-500 font-normal">
8 Days
</p>
</div>
<div className="flex items-center gap-1.5 px-3 py-1.5 rounded-full flex-shrink-0" style={{ backgroundColor: 'rgba(249, 95, 98, 0.1)' }}>
<span className="font-poppins text-xs font-medium" style={{ color: '#F95F62' }}>
Adding...
</span>
</div>
</div>
</div>
</motion.div>
{/* Activity 3 - Text Only */}
<motion.div
className="bg-white rounded-2xl shadow-lg border-2 p-6"
style={{ borderColor: '#F95F62' }}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: 0.5 }}
>
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0" style={{ backgroundColor: '#F95F62' }}>
<Clock className="w-5 h-5 text-white" />
</div>
<p className="font-poppins text-sm md:text-base font-medium text-gray-800">
Upgrade my Buy1 At1dfls visit to the SKY
</p>
</div>
</motion.div>
</motion.div>
{/* CTA Button */}
<motion.div
className="mt-8 md:mt-12 text-center"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.6 }}
>
<button
onClick={onCreateItineraryClick}
className="group relative px-8 py-4 md:px-10 md:py-5 rounded-full flex items-center gap-3 mx-auto overflow-hidden transition-all duration-300 hover:scale-105 shadow-xl hover:shadow-2xl font-poppins font-semibold text-base md:text-lg text-white"
style={{ backgroundColor: '#F95F62' }}
>
{/* Animated shine effect */}
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent -translate-x-full group-hover:translate-x-full transition-transform duration-700" />
<span className="relative z-10">Create Your Itinerary</span>
</button>
</motion.div>
</div>
</div>
</div>
);
}