select city missing in how it works and your card

This commit is contained in:
priyanshuvish
2025-11-11 15:02:35 +05:30
parent 94c5f5641c
commit 5c0e5a09ea
8 changed files with 486 additions and 566 deletions

View File

@@ -1,6 +1,8 @@
import { ReactNode } from 'react';
import Navbar from './components/Navbar';
import { Footer } from './components/Footer';
import { ReactNode } from "react";
import { useLocation } from "react-router-dom";
import Navbar from "./components/Navbar";
import { Footer } from "./components/Footer";
import { getAutoNavigationSource } from "./utils/getAutoNavigationSource";
interface User {
email: string;
@@ -10,35 +12,37 @@ interface User {
interface LayoutProps {
children: ReactNode;
activeCity?: string;
onSignInClick?: () => void;
onSignInClick?: () => void;
onSignOutClick?: () => void;
user?: User | null;
}
export function Layout({
children,
activeCity = 'Melbourne',
export function Layout({
children,
activeCity,
onSignInClick,
onSignOutClick,
user
user,
}: LayoutProps) {
const location = useLocation();
// 🧠 Use the helper to determine which city to show
const cityToUse = activeCity || getAutoNavigationSource(location);
return (
<div className="min-h-screen bg-background flex flex-col">
{/* Navbar */}
<Navbar
activeCity={activeCity}
<Navbar
activeCity={cityToUse}
onCityChange={() => {}}
onSignInClick={() => onSignInClick?.()}
onSignInClick={() => onSignInClick?.()}
onSignOutClick={onSignOutClick}
isUserSignedIn={!!user}
user={user}
/>
{/* Main Content */}
<main className="flex-1">{children}</main>
{/* Footer */}
<Footer />
</div>
);
}
}

View File

@@ -166,7 +166,7 @@ function HeroCarousel({ onCheckoutClick, onPassesClick }: { onCheckoutClick: ()
<br />
<span className="font-bold text-gray-900">All-in-One City Pass</span>
</h1>
{/* Description */}
<motion.p
initial={{ opacity: 0, y: 20 }}
@@ -295,7 +295,7 @@ function HeroCarousel({ onCheckoutClick, onPassesClick }: { onCheckoutClick: ()
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>
@@ -327,11 +327,10 @@ function HeroCarousel({ onCheckoutClick, onPassesClick }: { onCheckoutClick: ()
aria-label={`Go to slide ${index + 1}`}
>
<div
className={`h-2 rounded-full transition-all duration-300 ${
index === currentSlide
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>
))}
@@ -446,13 +445,13 @@ const steps: Step[] = [
];
// Step Component
function StepSection({
step,
index,
function StepSection({
step,
index,
onInView
}: {
step: Step;
index: number;
}: {
step: Step;
index: number;
onInView: (index: number) => void;
}) {
const ref = useRef<HTMLDivElement>(null);
@@ -528,7 +527,7 @@ function StepSection({
</div>
</motion.div>
)}
<motion.h2
className="font-poppins text-3xl md:text-4xl lg:text-5xl"
animate={{
@@ -608,11 +607,10 @@ function StepSection({
}}
>
<motion.div
className={`relative rounded-xl overflow-hidden transition-all duration-500 ${
isInView
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)'
@@ -682,11 +680,11 @@ function StepSection({
);
}
export function HowItWorksPage({
onHomeClick,
onMelbourneClick,
export function HowItWorksPage({
onHomeClick,
onMelbourneClick,
onPassesClick,
onCheckoutClick,
onCheckoutClick,
onSignInClick,
onSignOutClick,
onAttractionsClick,
@@ -735,341 +733,341 @@ export function HowItWorksPage({
return (
<Layout
activeCity="shared"
activeCity="shared"
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
user={user}
>
<div className="min-h-screen bg-white">
{/* Hero Section - Full Carousel */}
<HeroCarousel
onCheckoutClick={onCheckoutClick}
onPassesClick={onPassesClick}
/>
<div className="min-h-screen bg-white">
{/* 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>
{/* Hero Section - Full Carousel */}
<HeroCarousel
onCheckoutClick={onCheckoutClick}
onPassesClick={onPassesClick}
/>
{/* 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"
{/* 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"
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 }}
/>
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>
{/* 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}
{/* 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>
</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 */}
<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
initial={{ opacity: 0, x: -30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
className="text-center mb-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
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>
<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>
{/* 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>
{/* 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>
</motion.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>
</section>
<div className="container mx-auto px-4">
{/* Reviews Section */}
<ReviewsSection />
</div>
</div>
</Layout>
);
}

View File

@@ -6,7 +6,7 @@ import { motion } from 'motion/react';
const cardOptions = [
{
id: 'selective',
name: 'Selective Card',
name: 'Flexi Card',
subtitle: 'Pick 5-10 things to do from a choice of 102 attractions tours and activities',
priceRange: '$89-159',
duration: '3-7 days',

View File

@@ -84,6 +84,9 @@ export default function Navbar({
const location = useLocation();
const navigate = useNavigate();
const [lastKnownCity, setLastKnownCity] = useState<'landing' | 'melbourne'>('landing');
// More flexible navigation configuration
const navigationConfig = {
landing: [
@@ -163,29 +166,59 @@ export default function Navbar({
// Check if we're on landing page
const isLandingPage = location.pathname === '/';
// Auto-detect navigation source based on activeCity and current page
// Restore from session on mount
useEffect(() => {
const savedCity = sessionStorage.getItem('lastKnownCity');
if (savedCity === 'melbourne' || savedCity === 'landing') {
setLastKnownCity(savedCity);
}
}, []);
// Save whenever it changes
useEffect(() => {
sessionStorage.setItem('lastKnownCity', lastKnownCity);
}, [lastKnownCity]);
// Update lastKnownCity automatically when route clearly belongs to one city
useEffect(() => {
const path = location.pathname;
// Melbourne-specific routes
const melbournePaths = [
'/melbourne',
'/magic-itinerary',
'/super-savings',
'/attractions'
];
if (melbournePaths.some(p => path.startsWith(p))) {
setLastKnownCity('melbourne');
}
// Landing-specific routes (root or home-like)
if (path === '/' || path.startsWith('/explore') || path.startsWith('/contact')) {
setLastKnownCity('landing');
}
}, [location.pathname]);
// ✅ Determine which navbar to show
const getAutoNavigationSource = (): 'landing' | 'melbourne' => {
// If activeCity is explicitly set to 'shared', detect from context
if (activeCity.toLowerCase() === 'shared') {
// Check if we're on Melbourne-specific pages
const isMelbournePage =
location.pathname === '/melbourne' ||
location.pathname.startsWith('/attractions') ||
location.pathname === '/magic-itinerary' ||
location.pathname === '/super-savings';
const path = location.pathname;
return isMelbournePage ? 'melbourne' : 'landing';
// Explicit routes
if (path.startsWith('/melbourne')) return 'melbourne';
if (path === '/' || path.startsWith('/explore')) return 'landing';
// Shared routes
if (['/passes', '/how-it-works'].includes(path)) {
return lastKnownCity; // ← remembers where user came from
}
// If activeCity is explicitly Melbourne, use melbourne source
if (activeCity.toLowerCase() === 'melbourne') {
return 'melbourne';
}
// Default to landing
return 'landing';
// Fallback
return lastKnownCity;
};
// Get navigation items based on current context
const getNavigationItems = (): NavigationItem[] => {
const currentSource = getAutoNavigationSource();
@@ -353,6 +386,14 @@ export default function Navbar({
return () => window.removeEventListener('scroll', handleScroll);
}, []);
useEffect(() => {
if (activeCity.toLowerCase() === 'melbourne') {
setLastKnownCity('melbourne');
} else if (activeCity.toLowerCase() === 'landing' || activeCity.toLowerCase() === 'landingpage') {
setLastKnownCity('landing');
}
}, [activeCity]);
// Handle city change for mobile dropdown
const handleMobileCityChange = (city: string) => {
console.log('City selected:', city);

View File

@@ -191,7 +191,7 @@ export function PassesPage({
return (
<Layout
activeCity="Melbourne"
activeCity="shared"
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
user={userData} // ✅ Pass the updated user data

View File

@@ -1,7 +1,8 @@
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { Sparkles, MapPin, Calendar, Wand2, Clock } from 'lucide-react';
import { ImageWithFallback } from './figma/ImageWithFallback';
// import { ImageWithFallback } from './figma/ImageWithFallback';
import cityTourVideo from '../assets/itinenary-animation-vid.mp4';
interface PersonalizedTourHeroProps {
onCreateItineraryClick?: () => void;
@@ -55,6 +56,8 @@ export function PersonalizedTourHero({ onCreateItineraryClick }: PersonalizedTou
];
const [currentCardIndex, setCurrentCardIndex] = useState(0);
const [videoLoaded, setVideoLoaded] = useState(false);
const handleVideoLoad = () => setVideoLoaded(true);
useEffect(() => {
const timer = setInterval(() => {
@@ -174,200 +177,42 @@ export function PersonalizedTourHero({ onCreateItineraryClick }: PersonalizedTou
</motion.div>
</motion.div>
{/* Right - Animated Card Stack */}
{/* Right - Video Section */}
<motion.div
initial={{ opacity: 0, x: 30 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="relative hidden lg:block"
>
<div className="relative flex justify-center items-center min-h-[500px]" style={{ perspective: '1000px' }}>
{/* Card Stack Container */}
<div className="relative w-full max-w-md" style={{ transformStyle: 'preserve-3d' }}>
{/* Third Card (Background) */}
<motion.div
className="absolute inset-0 bg-white rounded-2xl shadow-lg overflow-hidden"
style={{
zIndex: 1,
transformStyle: 'preserve-3d'
}}
animate={{
scale: 0.88,
y: 20,
opacity: 0.4,
}}
transition={{ duration: 0.3 }}
<div className="relative flex justify-center items-center min-h-[500px]">
{/* Video Wrapper */}
<div className="relative w-full max-w-md h-full rounded-2xl overflow-hidden shadow-2xl border border-gray-100">
<video
autoPlay
muted
loop
playsInline
onLoadedData={handleVideoLoad}
className="w-full h-full object-cover rounded-2xl"
>
<div className="relative h-56 overflow-hidden">
<ImageWithFallback
src={thirdCard.image}
alt={thirdCard.name}
className="w-full h-full object-cover opacity-60"
<source src={cityTourVideo} type="video/mp4" />
Your browser does not support the video tag.
</video>
{/* Loading Spinner Overlay */}
{!videoLoaded && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-900/80 backdrop-blur-sm">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
className="w-12 h-12 border-4 border-primary/30 border-t-primary rounded-full"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent" />
</div>
</motion.div>
{/* Second Card */}
<motion.div
className="absolute inset-0 bg-white rounded-2xl shadow-xl overflow-hidden"
style={{
zIndex: 2,
transformStyle: 'preserve-3d'
}}
animate={{
scale: 0.94,
y: 10,
opacity: 0.7,
}}
transition={{ duration: 0.3 }}
>
<div className="relative h-56 overflow-hidden">
<ImageWithFallback
src={nextCard.image}
alt={nextCard.name}
className="w-full h-full object-cover opacity-80"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent" />
</div>
</motion.div>
{/* Front Card (Animated) */}
<AnimatePresence mode="popLayout">
<motion.div
key={currentCard.id}
className="relative bg-white rounded-2xl shadow-2xl overflow-hidden border border-gray-100"
style={{
zIndex: 3,
transformStyle: 'preserve-3d'
}}
initial={{
scale: 0.94,
y: 10,
opacity: 0.7,
}}
animate={{
scale: 1,
opacity: 1,
y: 0,
}}
exit={{
scale: 1.05,
opacity: 0,
x: -80,
rotateZ: -5,
}}
transition={{
duration: 0.5,
ease: [0.34, 1.56, 0.64, 1],
}}
>
{/* Card Image */}
<div className="relative h-56 overflow-hidden">
<ImageWithFallback
src={currentCard.image}
alt={currentCard.name}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent" />
{/* Category Badge */}
<motion.div
className="absolute top-4 left-4 bg-white/90 backdrop-blur-sm px-3 py-1.5 rounded-full shadow-lg"
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 0.2, duration: 0.4, type: "spring" }}
>
<span className="font-poppins font-semibold text-primary text-xs">
{currentCard.category}
</span>
</motion.div>
{/* Time Badge */}
<motion.div
className="absolute top-4 right-4 bg-gradient-to-r from-primary to-orange-500 px-3 py-1.5 rounded-full shadow-lg"
initial={{ scale: 0, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 0.25, duration: 0.4, type: "spring" }}
>
<div className="flex items-center gap-1.5">
<Clock className="w-3.5 h-3.5 text-white" />
<span className="font-poppins font-semibold text-white text-xs">{currentCard.time}</span>
</div>
</motion.div>
{/* Attraction Name Overlay */}
<motion.div
className="absolute bottom-4 left-4 right-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.15, duration: 0.4 }}
>
<h3 className="font-poppins font-semibold text-xl text-white leading-tight mb-1">
{currentCard.name}
</h3>
<p className="font-poppins font-normal text-sm text-white/90 flex items-center gap-1.5">
<MapPin className="w-4 h-4" />
Melbourne, Australia
</p>
</motion.div>
</div>
{/* Card Content */}
<motion.div
className="p-5"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.25, duration: 0.4 }}
>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-gradient-to-br from-primary to-orange-500 rounded-full flex items-center justify-center">
<span className="font-poppins text-white font-semibold text-sm">
{currentCardIndex + 1}
</span>
</div>
<span className="font-poppins text-sm font-medium text-gray-600">
Day {currentCardIndex + 1} Activity
</span>
</div>
<div className="flex items-center gap-1">
<Sparkles className="w-4 h-4 text-primary" />
<span className="font-poppins text-xs font-medium text-primary">AI Selected</span>
</div>
</div>
<div className="bg-gradient-to-r from-primary/5 to-orange-50/50 rounded-xl p-4 border border-primary/10">
<p className="font-poppins text-sm font-normal text-gray-700 leading-relaxed">
Added to your personalized itinerary based on your preferences and travel style
</p>
</div>
</motion.div>
</motion.div>
</AnimatePresence>
)}
</div>
</div>
{/* Card Indicators */}
<motion.div
className="flex flex-wrap justify-center gap-2 mt-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.5 }}
>
{attractionCards.map((card, idx) => (
<button
key={card.id}
onClick={() => setCurrentCardIndex(idx)}
className={`transition-all duration-300 w-2 h-2 rounded-full ${
idx === currentCardIndex
? 'bg-gradient-to-r from-primary to-orange-500 scale-125 w-6'
: 'bg-gray-300 hover:bg-gray-400'
}`}
/>
))}
</motion.div>
</motion.div>
</div>
</div>
</div>

View File

@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import { motion } from 'motion/react';
import { Link } from 'react-router-dom';
import { Link, useLocation } from 'react-router-dom';
import { ChevronDown, MapPin, Star, Shield, Clock, Smartphone } from 'lucide-react';
import Navbar from '../components/Navbar';
import { Footer } from '../components/Footer';
@@ -17,6 +17,8 @@ import { LandingTrustSection } from '../components/LandingTrustSection';
import { LandingMobileAppSection } from '../components/LandingMobileAppSection';
import { LandingNewsletterSection } from '../components/LandingNewsletterSection';
import { CustomPostcards } from '../components/CustomPostcards';
import { Layout } from '../Layout';
import { getAutoNavigationSource } from '../utils/getAutoNavigationSource';
@@ -43,6 +45,8 @@ export function LandingPage({ onSignInClick,
onSignOutClick,
user }: LandingPageProps) {
const [currentCityIndex, setCurrentCityIndex] = useState(0);
const location = useLocation();
const activeCity = getAutoNavigationSource(location);
const cities = [
{
@@ -91,16 +95,12 @@ export function LandingPage({ onSignInClick,
return (
<div className="min-h-screen bg-white">
{/* Navbar */}
<Navbar
activeCity="Landingpage"
onCityChange={(city) => {
// Handle city change if needed, or remove this prop
}}
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
isUserSignedIn={!!user}
user={user}
/>
<Layout
activeCity={activeCity}
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
user={user} // ✅ Pass the updated user data
>
{/* City Submenu */}
{/* <CitySubmenu
@@ -209,10 +209,7 @@ export function LandingPage({ onSignInClick,
{/* Newsletter Section */}
<LandingNewsletterSection />
{/* Footer */}
<Footer
currentPage="melbourne"
/>
</Layout>
</div>
);
}

View File

@@ -0,0 +1,35 @@
import { Location } from "react-router-dom";
/**
* Determines which city layout/navigation should be active.
*
* It reads the current pathname and the last known city (from sessionStorage)
* to decide whether to show Landing or Melbourne navigation.
*/
export function getAutoNavigationSource(location: Location): "landing" | "melbourne" {
const path = location.pathname;
const storedCity = sessionStorage.getItem("lastCity");
// ✅ When user is on a Melbourne page, remember it
if (path.startsWith("/melbourne")) {
sessionStorage.setItem("lastCity", "melbourne");
return "melbourne";
}
// ✅ Shared pages — should use last city if available
if (
path.startsWith("/passes") ||
path.startsWith("/how-it-works") || // added
path.startsWith("/your-card") || // added
path.startsWith("/checkout") ||
path.startsWith("/faqs") ||
path.startsWith("/about") ||
path.startsWith("/contact")
) {
// Use stored city if exists, otherwise default to landing
return storedCity === "melbourne" ? "melbourne" : "landing";
}
// ✅ Default to landing
return "landing";
}