select city missing in how it works and your card
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -191,7 +191,7 @@ export function PassesPage({
|
||||
|
||||
return (
|
||||
<Layout
|
||||
activeCity="Melbourne"
|
||||
activeCity="shared"
|
||||
onSignInClick={onSignInClick}
|
||||
onSignOutClick={onSignOutClick}
|
||||
user={userData} // ✅ Pass the updated user data
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
35
src/utils/getAutoNavigationSource.ts
Normal file
35
src/utils/getAutoNavigationSource.ts
Normal 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";
|
||||
}
|
||||
Reference in New Issue
Block a user