new src added

This commit is contained in:
priyanshuvish
2025-10-09 19:03:24 +05:30
commit 97969c079b
224 changed files with 62327 additions and 0 deletions

32
.gitignore vendored Normal file
View File

@@ -0,0 +1,32 @@
# Node modules
node_modules/
# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Environment files
.env
.env.local
.env.*.local
# Build output
dist/
build/
.next/
out/
# IDE files
.vscode/
.idea/
*.swp
# Mac / Linux / Windows system files
.DS_Store
Thumbs.db
# Temporary
*.tmp

11
README.md Normal file
View File

@@ -0,0 +1,11 @@
# CityCards Travel 22-8-2025
This is a code bundle for CityCards Travel 22-8-2025. The original project is available at https://www.figma.com/design/faSWipJ4DA1dnh88olOVuH/CityCards-Travel-22-8-2025.
## Running the code
Run `npm i` to install the dependencies.
Run `npm run dev` to start the development server.

15
index.html Normal file
View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CityCards Travel 22-8-2025</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

4248
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

64
package.json Normal file
View File

@@ -0,0 +1,64 @@
{
"name": "CityCards Travel 22-8-2025",
"version": "0.1.0",
"private": true,
"dependencies": {
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-alert-dialog": "^1.1.6",
"@radix-ui/react-aspect-ratio": "^1.1.2",
"@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-collapsible": "^1.1.3",
"@radix-ui/react-context-menu": "^2.2.6",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-hover-card": "^1.1.6",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-menubar": "^1.1.6",
"@radix-ui/react-navigation-menu": "^1.2.5",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-progress": "^1.1.2",
"@radix-ui/react-radio-group": "^1.2.3",
"@radix-ui/react-scroll-area": "^1.2.3",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slider": "^1.2.3",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toggle": "^1.1.2",
"@radix-ui/react-toggle-group": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@tailwindcss/postcss": "^4.1.13",
"class-variance-authority": "^0.7.1",
"clsx": "*",
"cmdk": "^1.1.1",
"embla-carousel-react": "^8.6.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.487.0",
"motion": "*",
"next-themes": "^0.4.6",
"postcss": "^8.5.6",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.55.0",
"react-resizable-panels": "^2.1.7",
"recharts": "^2.15.2",
"sonner": "^2.0.3",
"tailwind-merge": "*",
"tailwindcss": "^4.1.13",
"vaul": "^1.1.2"
},
"devDependencies": {
"@types/node": "^20.10.0",
"@types/react": "^19.1.12",
"@types/react-dom": "^19.1.9",
"@vitejs/plugin-react-swc": "^3.10.2",
"vite": "6.3.5"
},
"scripts": {
"dev": "vite",
"build": "vite build"
}
}

5
postcss.config.mjs Normal file
View File

@@ -0,0 +1,5 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
}
}

1209
src/App.tsx Normal file

File diff suppressed because it is too large Load Diff

3
src/Attributions.md Normal file
View File

@@ -0,0 +1,3 @@
This Figma Make file includes components from [shadcn/ui](https://ui.shadcn.com/) used under [MIT license](https://github.com/shadcn-ui/ui/blob/main/LICENSE.md).
This Figma Make file includes photos from [Unsplash](https://unsplash.com) used under [license](https://unsplash.com/license).

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 936 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

BIN
src/assets/cit-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 991 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

View File

@@ -0,0 +1,93 @@
#!/usr/bin/env python3
import os
import re
# List of files to update
files_to_update = [
"components/BlogsPage.tsx",
"components/HowItWorksPage.tsx",
"components/ProfilePage.tsx",
"components/EsimsPage.tsx",
"components/AttractionsPage.tsx",
"components/FAQPage.tsx",
"components/MelbourneBlogs.tsx",
"components/CityAttractionsPage.tsx",
"components/BlogDetailsPage.tsx",
"components/VarietyOfAdventures.tsx",
"components/MelbourneFAQ.tsx",
"components/MelbournePage.tsx",
"components/MelbourneCardComparison.tsx",
"components/BookAttractionSection.tsx",
"components/PassesPage.tsx",
"components/CheckoutPage.tsx",
"components/DownloadAppPage.tsx",
"components/WhyChooseUsSection.tsx",
"components/PrivacyPolicyPage.tsx",
"components/MobileAppPromotion.tsx",
"components/OffersPage.tsx",
"components/ContactUsPage.tsx",
"components/WhyChooseCityCards.tsx",
"components/HotelDiscountsPage.tsx",
"components/MagicItinerary.tsx",
"components/NewsletterSection.tsx",
"components/PostCardsPage.tsx",
"components/HeroSection.tsx",
"components/MelbourneTourOverview.tsx",
"components/ItineraryViewPage.tsx",
"components/CityCardsPage.tsx",
"components/MagicItineraryPage.tsx",
"components/CreateMagicItineraryPage.tsx",
"components/SecureCheckoutPage.tsx",
"components/AttractionDetailsPage.tsx",
"components/AboutUsPage.tsx"
]
def replace_in_file(filepath):
"""Replace font-merchant with font-poppins in a file."""
try:
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
# Count replacements
count = content.count('font-merchant')
if count == 0:
print(f"{filepath} - already updated")
return 0
# Replace all instances
new_content = content.replace('font-merchant', 'font-poppins')
# Write back
with open(filepath, 'w', encoding='utf-8') as f:
f.write(new_content)
print(f"{filepath} - replaced {count} instances")
return count
except FileNotFoundError:
print(f"{filepath} - file not found")
return 0
except Exception as e:
print(f"{filepath} - error: {e}")
return 0
def main():
print("Starting batch font replacement...")
print("=" * 50)
total_replacements = 0
files_updated = 0
for filepath in files_to_update:
count = replace_in_file(filepath)
if count > 0:
files_updated += 1
total_replacements += count
print("=" * 50)
print(f"\nCompleted!")
print(f"Files updated: {files_updated}")
print(f"Total replacements: {total_replacements}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,539 @@
import React from 'react';
import { motion } from 'motion/react';
import { ArrowLeft, Heart, MapPin, Zap, Globe, Users, Camera, Coffee } from 'lucide-react';
import { Button } from './ui/button';
import { ImageWithFallback } from './figma/ImageWithFallback';
import Navbar from './Navbar';
import { Footer } from './Footer';
import { MobileAppSection } from './MobileAppSection';
import { EnhancedTestimonials } from './EnhancedTestimonials';
import { ReviewsSection } from './ReviewsSection';
interface User {
email: string;
name: string;
}
interface AboutUsPageProps {
onBackClick: () => void;
onHomeClick: () => void;
onMelbourneClick: () => void;
onPassesClick: () => void;
onCheckoutClick: () => void;
onSignInClick: () => void;
onSignOutClick: () => void;
onAttractionsClick: () => void;
onBlogsClick: () => void;
onHowItWorksClick: () => void;
onFAQClick: () => void;
onPrivacyPolicyClick: () => void;
onAboutUsClick: () => void;
onProfileClick: () => void;
onCityCardsClick: () => void;
onMagicItineraryClick: () => void;
onPostCardsClick: () => void;
onOffersClick: () => void;
onContactUsClick?: () => void;
onEsimsClick?: () => void;
onHotelDiscountsClick?: () => void;
currentPage: string;
user: User | null;
}
export function AboutUsPage({
onBackClick,
onHomeClick,
onMelbourneClick,
onPassesClick,
onCheckoutClick,
onSignInClick,
onSignOutClick,
onAttractionsClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onProfileClick,
onCityCardsClick,
onMagicItineraryClick,
onPostCardsClick,
onOffersClick,
onContactUsClick,
onEsimsClick,
onHotelDiscountsClick,
currentPage,
user
}: AboutUsPageProps) {
return (
<div className="min-h-screen bg-background">
{/* Navbar */}
<Navbar
activeCity=""
onCityChange={() => {}}
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onPassesClick={onPassesClick}
onCheckoutClick={onCheckoutClick}
onSignInClick={onSignInClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick}
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
currentPage={currentPage}
isUserSignedIn={!!user}
user={user}
/>
{/* Header Section with Floating Elements */}
<section className="pt-32 pb-16 bg-gradient-to-br from-primary/5 via-secondary/5 to-background relative overflow-hidden">
{/* Floating Travel Icons */}
<motion.div
className="absolute top-20 left-10 text-primary/20"
animate={{
y: [0, -15, 0],
rotate: [0, 5, -5, 0]
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "easeInOut"
}}
>
<MapPin className="w-12 h-12" />
</motion.div>
<motion.div
className="absolute top-32 right-16 text-secondary/20"
animate={{
y: [0, -10, 0],
x: [0, 5, 0]
}}
transition={{
duration: 3,
repeat: Infinity,
ease: "easeInOut",
delay: 0.5
}}
>
<Camera className="w-10 h-10" />
</motion.div>
<motion.div
className="absolute bottom-20 left-20 text-primary/15"
animate={{
rotate: [0, 360]
}}
transition={{
duration: 8,
repeat: Infinity,
ease: "linear"
}}
>
<Globe className="w-8 h-8" />
</motion.div>
<div className="container mx-auto px-4 pt-12 relative z-10">
{/* Back Button */}
<motion.button
onClick={onBackClick}
className="flex items-center gap-2 text-gray-600 hover:text-primary mb-8 transition-all duration-300 hover:scale-105"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5 }}
>
<ArrowLeft className="w-5 h-5" />
<span className="font-medium">Back</span>
</motion.button>
{/* Page Title with Fun Typography */}
<motion.div
className="max-w-4xl mx-auto text-center"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
>
<motion.h1
className="heading-dynamic text-4xl md:text-5xl lg:text-6xl mb-6 leading-tight"
whileInView={{
backgroundPosition: ["0% 50%", "100% 50%", "0% 50%"]
}}
transition={{ duration: 3, repeat: Infinity }}
>
<span className="font-merchant font-light" style={{ fontStretch: 'normal', fontVariant: 'normal' }}>Meet the</span>{' '}
<span className="bg-gradient-to-r from-primary via-secondary to-primary bg-[length:200%_100%] bg-clip-text text-transparent font-bold italic animate-shimmer">
Dream Team
</span>{' '}
</motion.h1>
<motion.p
className="font-poppins text-lg md:text-xl text-gray-600 leading-relaxed"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.8 }}
>
We're a bunch of travel-obsessed humans who got tired of vacation planning stress
and decided to fix it for everyone! 🌍✨
</motion.p>
</motion.div>
</div>
</section>
{/* Our Story Section */}
<section className="py-20 bg-gradient-to-b from-background to-muted/20">
<div className="container mx-auto px-4">
<div className="max-w-6xl mx-auto">
{/* Story Introduction */}
<motion.div
className="grid lg:grid-cols-2 gap-12 items-center mb-20"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8 }}
>
<div>
<motion.div
className="flex items-center gap-2 mb-6"
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ delay: 0.2 }}
>
<Heart className="w-6 h-6 text-red-500" />
<span className="font-merchant text-sm uppercase tracking-wide text-primary font-medium">Our Story</span>
</motion.div>
<h2 className="heading-dynamic text-3xl md:text-4xl mb-6 leading-tight">
<span className="font-normal">From</span>{' '}
<span className="italic font-light text-emphasis">Travel Stress</span>{' '}
<span className="font-normal">to</span>{' '}
<span className="bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent font-bold">
Travel Bliss
</span>
</h2>
<p className="font-poppins text-lg text-gray-600 leading-relaxed mb-6">
Picture this: You're on your dream vacation, but instead of exploring that amazing museum,
you're standing in a 2-hour line holding a crumpled paper ticket. Sound familiar? 😅
</p>
<p className="font-poppins text-lg text-gray-600 leading-relaxed">
That's exactly what happened to our founder during a trip to Melbourne. After missing
three attractions because of queues and ticket confusion, they had an idea:
<span className="font-medium text-primary"> "What if we could put all city attractions in one magical card?"</span>
</p>
</div>
<motion.div
className="relative"
initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.8, delay: 0.3 }}
>
<div className="absolute -inset-4 bg-gradient-to-r from-primary/20 to-secondary/20 rounded-3xl blur-2xl" />
<ImageWithFallback
src="https://images.unsplash.com/photo-1606933988011-8adbb3ee269a?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx0cmF2ZWwlMjBhZHZlbnR1cmUlMjB3b3JsZCUyMG1hcCUyMHBsYW5uaW5nfGVufDF8fHx8MTc1NzY3NjczNnww&ixlib=rb-4.1.0&q=80&w=1080"
alt="Travel planning inspiration"
className="relative rounded-2xl shadow-2xl w-full h-80 object-cover"
/>
{/* Floating emoji reactions */}
<motion.div
className="absolute -top-6 -right-6 text-4xl"
animate={{
rotate: [0, 10, -10, 0],
scale: [1, 1.1, 1]
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut"
}}
>
💡
</motion.div>
<motion.div
className="absolute -bottom-4 -left-6 text-3xl"
animate={{
y: [0, -10, 0]
}}
transition={{
duration: 1.5,
repeat: Infinity,
ease: "easeInOut",
delay: 0.5
}}
>
</motion.div>
</motion.div>
</motion.div>
{/* Our Mission Cards */}
<motion.div
className="grid md:grid-cols-3 gap-8 mb-20"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8, delay: 0.2 }}
>
{[
{
icon: <Zap className="w-8 h-8" />,
title: "Skip the Lines",
description: "Because life's too short to waste on queues! Fast-track entry to all the coolest spots.",
color: "from-yellow-400 to-orange-500",
emoji: "⚡"
},
{
icon: <MapPin className="w-8 h-8" />,
title: "Discover Hidden Gems",
description: "We'll show you secret local spots that even your guidebook doesn't know about!",
color: "from-green-400 to-blue-500",
emoji: "🗺️"
},
{
icon: <Heart className="w-8 h-8" />,
title: "Create Epic Memories",
description: "More time exploring, less time planning. That's what vacation should feel like!",
color: "from-pink-400 to-red-500",
emoji: "💖"
}
].map((card, index) => (
<motion.div
key={index}
className="relative group"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
whileHover={{ y: -8, scale: 1.02 }}
>
<div className="bg-white rounded-2xl p-8 shadow-lg border border-gray-100 relative overflow-hidden h-full">
{/* Background gradient */}
<div className={`absolute top-0 right-0 w-20 h-20 bg-gradient-to-br ${card.color} opacity-10 rounded-bl-3xl`} />
{/* Floating emoji */}
<motion.div
className="absolute top-4 right-4 text-2xl"
animate={{
rotate: [0, 10, -10, 0],
scale: [1, 1.1, 1]
}}
transition={{
duration: 2,
repeat: Infinity,
delay: index * 0.3
}}
>
{card.emoji}
</motion.div>
<div className={`w-12 h-12 bg-gradient-to-br ${card.color} rounded-xl flex items-center justify-center text-white mb-6`}>
{card.icon}
</div>
<h3 className="font-merchant text-xl mb-4 group-hover:text-primary transition-colors">
{card.title}
</h3>
<p className="font-poppins text-gray-600 leading-relaxed">
{card.description}
</p>
</div>
</motion.div>
))}
</motion.div>
{/* Team Stats Section */}
<motion.div
className="bg-gradient-to-r from-primary/5 to-secondary/5 rounded-3xl p-8 md:p-12 text-center"
initial={{ opacity: 0, scale: 0.95 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.8 }}
>
<h3 className="font-merchant text-2xl md:text-3xl mb-8">
We're a Small Team with <span className="italic font-light">Big Dreams</span> 🚀
</h3>
<div className="grid md:grid-cols-4 gap-8">
{[
{ number: "15+", label: "Cities Covered", emoji: "🌍" },
{ number: "500K+", label: "Happy Travelers", emoji: "😊" },
{ number: "1M+", label: "Lines Skipped", emoji: "⚡" },
{ number: "∞", label: "Adventures Created", emoji: "🗺️" }
].map((stat, index) => (
<motion.div
key={index}
className="text-center"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
>
<motion.div
className="text-4xl mb-2"
animate={{
scale: [1, 1.1, 1]
}}
transition={{
duration: 2,
repeat: Infinity,
delay: index * 0.2
}}
>
{stat.emoji}
</motion.div>
<div className="font-merchant text-3xl font-bold text-primary mb-2">
{stat.number}
</div>
<div className="font-poppins text-gray-600">
{stat.label}
</div>
</motion.div>
))}
</div>
</motion.div>
</div>
</div>
</section>
{/* Values Section */}
<section className="py-20">
<div className="container mx-auto px-4">
<div className="max-w-4xl mx-auto text-center mb-16">
<motion.h2
className="heading-dynamic text-3xl md:text-4xl mb-6"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
>
<span className="font-normal">What Makes Us</span>{' '}
<span className="italic font-light text-emphasis">Tick</span>{' '}
</motion.h2>
<motion.p
className="font-poppins text-lg text-gray-600"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
These aren't just buzzwords for us - they're our daily mantras!
</motion.p>
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{[
{
title: "Travel Should Be Fun",
description: "Not stressful, not complicated, just pure joy and discovery!",
icon: <Coffee className="w-6 h-6" />,
emoji: "🎉"
},
{
title: "Everyone Deserves Adventure",
description: "From budget backpackers to luxury travelers - adventure is for all!",
icon: <Users className="w-6 h-6" />,
emoji: "🌟"
},
{
title: "Local is Everything",
description: "The best experiences come from locals who know their city inside out.",
icon: <MapPin className="w-6 h-6" />,
emoji: "🏠"
},
{
title: "Innovation Over Tradition",
description: "Why stick to 'how it's always been' when we can make it better?",
icon: <Zap className="w-6 h-6" />,
emoji: "💡"
},
{
title: "Sustainability Matters",
description: "Protecting the places we love to visit for future generations.",
icon: <Globe className="w-6 h-6" />,
emoji: "🌱"
},
{
title: "Memories Over Things",
description: "The best souvenirs are the stories you'll tell forever.",
icon: <Camera className="w-6 h-6" />,
emoji: "📸"
}
].map((value, index) => (
<motion.div
key={index}
className="bg-white rounded-2xl p-6 shadow-lg border border-gray-100 hover:shadow-xl transition-all duration-300 group relative overflow-hidden"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1 }}
whileHover={{ y: -5 }}
>
<motion.div
className="absolute top-4 right-4 text-2xl"
animate={{
rotate: [0, 10, -10, 0]
}}
transition={{
duration: 2,
repeat: Infinity,
delay: index * 0.2
}}
>
{value.emoji}
</motion.div>
<div className="w-12 h-12 bg-gradient-to-br from-primary to-secondary rounded-xl flex items-center justify-center text-white mb-4 group-hover:scale-110 transition-transform">
{value.icon}
</div>
<h3 className="font-merchant text-lg mb-3 group-hover:text-primary transition-colors">
{value.title}
</h3>
<p className="font-poppins text-gray-600 text-sm leading-relaxed">
{value.description}
</p>
</motion.div>
))}
</div>
</div>
</section>
{/* What Our Travelers Say Section */}
<EnhancedTestimonials />
{/* Access All Your City Cards Section */}
<section className="py-20">
<MobileAppSection />
</section>
{/* Customer Review Section */}
<section className="py-20">
<ReviewsSection />
</section>
{/* Footer */}
<Footer
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onPassesClick={onPassesClick}
onSignInClick={onSignInClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onContactUsClick={onContactUsClick}
currentPage={currentPage}
/>
</div>
);
}

View File

@@ -0,0 +1,79 @@
import { ImageWithFallback } from './figma/ImageWithFallback';
import { Button } from './ui/button';
interface ArticleCard {
id: string;
title: string;
image: string;
}
const articles: ArticleCard[] = [
{
id: '1',
title: 'Choosing a Travel Backpack: A Comprehensive Guide!',
image: 'https://images.unsplash.com/photo-1553062407-98eeb64c6a62?w=500&h=400&fit=crop&q=80'
},
{
id: '2',
title: 'Bucket List Trip - 52 Places To Visit In Your Life...',
image: 'https://images.unsplash.com/photo-1488646953014-85cb44e25828?w=500&h=400&fit=crop&q=80'
},
{
id: '3',
title: 'The FREE Way to Travel: No Money, No Problems!',
image: 'https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=500&h=400&fit=crop&q=80'
}
];
export function ArticlesSection() {
return (
<section className="py-20 bg-white">
<div className="container mx-auto px-4">
{/* Header */}
<div className="text-center mb-16">
<h2 className="text-5xl font-bold text-gray-900 mb-6">
Our latest articles about travel
</h2>
<p className="text-xl text-gray-400 font-light">
Know the latest articles about travel
</p>
</div>
{/* Articles Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-7xl mx-auto mb-16">
{articles.map((article) => (
<div
key={article.id}
className="relative group cursor-pointer overflow-hidden rounded-3xl h-[400px] shadow-md hover:shadow-2xl transition-all duration-300 hover:-translate-y-2"
>
<ImageWithFallback
src={article.image}
alt={article.title}
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500"
/>
{/* Gradient overlay */}
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/30 to-transparent" />
{/* Article title */}
<div className="absolute bottom-0 left-0 right-0 p-8">
<h3 className="text-white text-xl font-semibold leading-tight drop-shadow-lg">
{article.title}
</h3>
</div>
</div>
))}
</div>
{/* CTA Button */}
<div className="text-center">
<Button
className="bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 text-white px-10 rounded-full font-semibold text-lg shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-105 h-14"
size="lg"
>
Explore more
</Button>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,474 @@
import { useState } from 'react';
import { motion } from 'motion/react';
import { ArrowLeft, Clock, Users, Calendar, MapPin, Star, Check, X, ChevronLeft, ChevronRight } from 'lucide-react';
import { Button } from './ui/button';
import { Badge } from './ui/badge';
import { Card, CardContent } from './ui/card';
import { Separator } from './ui/separator';
import { Calendar as CalendarComponent } from './ui/calendar';
import Navbar from './Navbar';
import { Footer } from './Footer';
import { ImageWithFallback } from './figma/ImageWithFallback';
interface AttractionDetailsPageProps {
attractionId: string;
onBackClick: () => void;
onHomeClick: () => void;
onMelbourneClick: () => void;
onPassesClick: () => void;
onCheckoutClick: () => void;
onSignInClick: () => void;
onSignOutClick?: () => void;
onAttractionsClick: () => void;
onBlogsClick: () => void;
onHowItWorksClick: () => void;
onFAQClick: () => void;
onPrivacyPolicyClick: () => void;
onAboutUsClick: () => void;
onProfileClick: () => void;
onCityCardsClick: () => void;
onMagicItineraryClick: () => void;
onPostCardsClick: () => void;
onOffersClick: () => void;
onContactUsClick?: () => void;
onEsimsClick?: () => void;
onHotelDiscountsClick?: () => void;
currentPage: string;
user?: { email: string; name: string } | null;
}
export function AttractionDetailsPage({
attractionId,
onBackClick,
onHomeClick,
onMelbourneClick,
onPassesClick,
onCheckoutClick,
onSignInClick,
onSignOutClick,
onAttractionsClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onProfileClick,
onCityCardsClick,
onMagicItineraryClick,
onPostCardsClick,
onOffersClick,
onContactUsClick,
onEsimsClick,
onHotelDiscountsClick,
currentPage,
user,
}: AttractionDetailsPageProps) {
const [date, setDate] = useState<Date | undefined>(new Date());
// Featured attraction for the main display
const featuredAttraction = {
id: 'phi-phi',
name: 'Phi Phi Islands Adventure Day Trip with Seaview Lunch by V. Marine Tour',
badges: ['Bestseller', 'Free cancellation', 'Reservation Required'],
images: {
main: 'https://images.unsplash.com/photo-1559827260-dc66d52bef19?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzbm9ya2VsaW5nJTIwdHVydGxlJTIwYWR2ZW50dXJlfGVufDF8fHx8MTc1ODEwNDkwMHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
gallery: [
'https://images.unsplash.com/photo-1559827260-dc66d52bef19?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzbm9ya2VsaW5nJTIwdHVydGxlJTIwYWR2ZW50dXJlfGVufDF8fHx8MTc1ODEwNDkwMHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxpc2xhbmQlMjB0b3VyJTIwYWRvJTIwdHJvcGljYWx8ZW58MXx8fHwxNzU4MTA0OTEwfDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxncmVhdCUyMG9jZWFuJTIwcm9hZCUyMGF1c3RyYWxpYXxlbnwxfHx8fDE3NTgxMDQ5Mzd8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
'https://images.unsplash.com/photo-1682687220742-aba13b6e50ba?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxhdHYlMjBkZXNlcnQlMjB0b3VyfGVufDF8fHx8MTc1ODEwNDg5Nnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral'
]
},
overview: {
duration: '3 days',
groupSize: '10 people',
ages: '18-99 yrs',
languages: 'English, Japanese'
},
description: 'The Phi Phi archipelago is a must-visit while in Phuket, and this speedboat trip whisks you around the islands in one day. Swim over the coral reefs of Pileh Lagoon, have lunch at Phi Phi Leh, snorkel at Bamboo Island, and visit Monkey Beach and Maya Bay, immortalized in "The Beach." Boat transfers, snacks, buffet lunch, snorkeling equipment, and Phuket hotel pickup and drop-off all included.',
highlights: [
'Experience the thrill of a speedboat to the stunning Phi Phi Islands',
'Be amazed by the variety of marine life in the archepelago',
'Enjoy relaxing in paradise with white sand beaches and azure turquoise water',
'Feel the comfort of a tour limited to 35 passengers',
'Catch a glimpse of the wild monkeys around Monkey Beach'
],
included: [
'Beverages, drinking water, morning tea and buffet lunch',
'Local taxes',
'Hotel pickup and drop-off by air-conditioned minivan',
'Insurance Transfer to a private pier',
'Soft drinks',
'Tour Guide'
],
notIncluded: [
'Towel',
'Tips',
'Alcoholic Beverages'
],
bookingOptions: [
'By Calling on 022 2645675',
'Email your details at islands.booking@mail.com',
'Via CityCards Portal'
]
};
return (
<div className="min-h-screen bg-[#f8f8f8]">
<Navbar
activeCity="Melbourne"
onCityChange={(city) => {
if (city === 'Melbourne') {
onMelbourneClick();
}
}}
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
onPassesClick={onPassesClick}
onCheckoutClick={onCheckoutClick}
onHomeClick={onHomeClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick}
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
currentPage="attractions"
isUserSignedIn={!!user}
user={user}
/>
<div className="container mx-auto px-4 pt-40 pb-16 max-w-6xl">
{/* Back Button */}
<motion.div
className="mb-8"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5 }}
>
<Button
variant="ghost"
onClick={onBackClick}
className="font-poppins font-medium text-base text-gray-600 hover:text-primary transition-colors duration-200"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Attractions
</Button>
</motion.div>
{/* Title and Badges Section */}
<div className="mb-8">
<div className="flex flex-wrap gap-3 mb-6">
{featuredAttraction.badges.map((badge, index) => (
<Badge
key={index}
variant={index === 0 ? "default" : "secondary"}
className={`px-6 py-2 rounded-full text-sm transition-all duration-200 ${
index === 0
? 'bg-primary text-white shadow-lg'
: 'bg-primary/10 text-primary border border-primary/20'
}`}
>
{badge}
</Badge>
))}
</div>
<h1 className="text-4xl font-bold text-[#2d3134] leading-tight">
<span className="bg-gradient-to-r from-primary to-primary/80 bg-clip-text text-transparent">
Phi Phi Islands Adventure
</span>{' '}
<span className="text-[#2d3134]">
Day Trip with Seaview Lunch by V. Marine Tour
</span>
</h1>
</div>
{/* Image Gallery Section */}
<div className="grid grid-cols-4 grid-rows-2 gap-4 h-[510px] mb-12">
{/* Main large image */}
<div className="col-span-2 row-span-2">
<ImageWithFallback
src={featuredAttraction.images.main}
alt="Main attraction image"
className="w-full h-full object-cover rounded-lg"
/>
</div>
{/* Gallery images */}
{featuredAttraction.images.gallery.slice(0, 4).map((image, index) => (
<div key={index} className="col-span-1 row-span-1">
<ImageWithFallback
src={image}
alt={`Gallery image ${index + 1}`}
className="w-full h-full object-cover rounded-lg"
/>
</div>
))}
</div>
{/* Main Content Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12">
{/* Left Content - Tour Details */}
<div className="lg:col-span-2 space-y-12">
{/* Overview Cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
{Object.entries(featuredAttraction.overview).map(([key, value]) => (
<Card key={key} className="p-4 text-center bg-white border border-primary/10 hover:border-primary/20 transition-all duration-200 hover:shadow-lg group">
<div className="w-12 h-12 bg-primary/10 rounded-xl mx-auto mb-3 flex items-center justify-center group-hover:bg-primary/20 transition-colors duration-200">
{key === 'duration' && <Clock className="w-6 h-6 text-primary" />}
{key === 'groupSize' && <Users className="w-6 h-6 text-primary" />}
{key === 'ages' && <Users className="w-6 h-6 text-primary" />}
{key === 'languages' && <MapPin className="w-6 h-6 text-primary" />}
</div>
<h3 className="font-normal text-primary capitalize mb-1">
{key === 'groupSize' ? 'Group Size' : key}
</h3>
<p className="text-sm text-[#717171] font-light">{value}</p>
</Card>
))}
</div>
{/* Tour Overview */}
<div>
<div className="flex items-center gap-4 mb-6">
<div className="h-1 w-12 bg-primary rounded-full"></div>
<h2 className="text-3xl font-semibold text-[#2d3134]">
Tour <span className="text-primary">Overview</span>
</h2>
</div>
<p className="text-[#2d3134] leading-relaxed text-lg font-light">
{featuredAttraction.description}
</p>
</div>
{/* Tour Highlights */}
<div>
<div className="flex items-center gap-4 mb-6">
<div className="h-1 w-12 bg-primary rounded-full"></div>
<h3 className="text-2xl font-medium text-[#2d3134]">
Tour <span className="text-primary">Highlights</span>
</h3>
</div>
<ul className="space-y-4">
{featuredAttraction.highlights.map((highlight, index) => (
<li key={index} className="flex items-start gap-3 group">
<div className="w-6 h-6 bg-primary/10 rounded-full mt-1 flex items-center justify-center flex-shrink-0 group-hover:bg-primary/20 transition-colors duration-200">
<div className="w-2 h-2 bg-primary rounded-full"></div>
</div>
<span className="text-[#2d3134] leading-relaxed font-light">{highlight}</span>
</li>
))}
</ul>
</div>
{/* What's Included/Not Included */}
<div>
<div className="flex items-center gap-4 mb-8">
<div className="h-1 w-12 bg-primary rounded-full"></div>
<h3 className="text-3xl font-semibold text-[#2d3134]">
What's <span className="text-primary">included</span>
</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{/* Included */}
<div className="space-y-4">
<h4 className="font-medium text-primary mb-4 flex items-center gap-2">
<Check className="w-5 h-5" />
Included
</h4>
{featuredAttraction.included.map((item, index) => (
<div key={index} className="flex items-start gap-3 group">
<div className="w-6 h-6 bg-primary/10 rounded-full flex items-center justify-center flex-shrink-0 mt-1 group-hover:bg-primary/20 transition-colors duration-200">
<Check className="w-3 h-3 text-primary" />
</div>
<span className="text-[#2d3134] font-light">{item}</span>
</div>
))}
</div>
{/* Not Included */}
<div className="space-y-4">
<h4 className="font-medium text-gray-600 mb-4 flex items-center gap-2">
<X className="w-5 h-5" />
Not Included
</h4>
{featuredAttraction.notIncluded.map((item, index) => (
<div key={index} className="flex items-start gap-3 group">
<div className="w-6 h-6 bg-gray-100 rounded-full flex items-center justify-center flex-shrink-0 mt-1 group-hover:bg-gray-200 transition-colors duration-200">
<X className="w-3 h-3 text-gray-500" />
</div>
<span className="text-[#2d3134] font-light">{item}</span>
</div>
))}
</div>
</div>
</div>
{/* Location on map placeholder */}
<div>
<div className="flex items-center gap-4 mb-8">
<div className="h-1 w-12 bg-primary rounded-full"></div>
<h3 className="text-3xl font-semibold text-[#2d3134]">
Location on <span className="text-primary">map</span>
</h3>
</div>
<div className="h-80 bg-gradient-to-br from-primary/5 to-primary/10 rounded-lg flex items-center justify-center border border-primary/10 hover:border-primary/20 transition-colors duration-200">
<div className="text-center">
<div className="w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-4">
<MapPin className="w-8 h-8 text-primary" />
</div>
<p className="text-lg font-medium text-primary mb-2">Interactive Map</p>
<p className="text-sm text-gray-600 font-light">Phi Phi Islands, Thailand</p>
</div>
</div>
</div>
</div>
{/* Right Sidebar - Calendar and Booking */}
<div className="lg:sticky lg:top-32 space-y-8 self-start">
{/* Calendar Widget with Custom Design */}
<Card className="p-6 bg-white shadow-lg border border-primary/10">
<div className="mb-6">
<h3 className="text-xl font-bold text-primary mb-1">Select Date</h3>
<p className="text-sm text-gray-600">Choose your preferred visit date</p>
</div>
{/* Custom Calendar Design */}
<div className="space-y-4">
{/* Calendar Header */}
<div className="flex items-center justify-between">
<button className="p-2 hover:bg-primary/10 rounded-lg transition-colors">
<ChevronLeft className="w-5 h-5 text-primary" />
</button>
<span className="font-semibold text-gray-900">September 2025</span>
<button className="p-2 hover:bg-primary/10 rounded-lg transition-colors">
<ChevronRight className="w-5 h-5 text-primary" />
</button>
</div>
{/* Days of week */}
<div className="grid grid-cols-7 gap-1 text-center text-sm font-medium text-gray-500">
<div>Su</div>
<div>Mo</div>
<div>Tu</div>
<div>We</div>
<div>Th</div>
<div>Fr</div>
<div>Sa</div>
</div>
{/* Calendar Grid */}
<div className="grid grid-cols-7 gap-1">
{/* Previous month */}
<button className="h-10 w-10 text-sm text-gray-300 hover:bg-gray-50 rounded">31</button>
{/* Current month */}
{Array.from({ length: 30 }, (_, i) => {
const day = i + 1;
const isSelected = day === 27;
const isToday = day === 15;
return (
<button
key={day}
className={`h-10 w-10 text-sm rounded font-medium transition-all duration-200 ${
isSelected
? 'bg-primary text-white shadow-lg scale-105'
: isToday
? 'bg-primary/10 text-primary border border-primary/20'
: 'text-gray-700 hover:bg-primary/5 hover:text-primary'
}`}
>
{day}
</button>
);
})}
{/* Next month */}
{Array.from({ length: 4 }, (_, i) => (
<button
key={`next-${i + 1}`}
className="h-10 w-10 text-sm text-gray-300 hover:bg-gray-50 rounded"
>
{i + 1}
</button>
))}
</div>
</div>
{/* Selected Date Display */}
<div className="mt-6 p-4 bg-primary/5 rounded-lg border border-primary/10">
<div className="flex items-center gap-3">
<Calendar className="w-5 h-5 text-primary" />
<div>
<p className="text-sm font-medium text-gray-900">Selected Date</p>
<p className="text-lg font-semibold text-primary">September 27, 2025</p>
</div>
</div>
</div>
</Card>
{/* Pricing Card */}
<Card className="p-6 bg-gradient-to-br from-primary/5 to-primary/10 border border-primary/20">
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-gray-600">Adult Ticket</span>
<span className="font-bold text-xl text-primary">$89</span>
</div>
<div className="flex items-center justify-between">
<span className="text-gray-600">Service Fee</span>
<span className="font-medium text-gray-900">$5</span>
</div>
<div className="border-t border-primary/20 pt-4">
<div className="flex items-center justify-between">
<span className="font-semibold text-gray-900">Total</span>
<span className="font-bold text-2xl text-primary">$94</span>
</div>
</div>
</div>
</Card>
{/* Confirm Booking Button */}
<Button
className="w-full bg-primary text-white hover:bg-primary/90 py-6 text-lg rounded-xl font-semibold transition-all duration-200 shadow-lg hover:shadow-xl hover:scale-[1.02] relative overflow-hidden group"
onClick={() => onCheckoutClick()}
>
<span className="relative z-10 flex items-center justify-center gap-2">
<Check className="w-5 h-5" />
Confirm Booking
</span>
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent -skew-x-12 -translate-x-full group-hover:translate-x-full transition-transform duration-700"></div>
</Button>
{/* Trust Indicators */}
<div className="flex items-center justify-center gap-4 text-sm text-gray-600">
<div className="flex items-center gap-1">
<Check className="w-4 h-4 text-primary" />
<span>Instant Confirmation</span>
</div>
<div className="flex items-center gap-1">
<X className="w-4 h-4 text-primary" />
<span>Free Cancellation</span>
</div>
</div>
</div>
</div>
</div>
<Footer
onHomeClick={onHomeClick}
onAboutUsClick={onAboutUsClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onBlogsClick={onBlogsClick}
onContactUsClick={onContactUsClick}
/>
</div>
);
}

View File

@@ -0,0 +1,225 @@
import { motion } from 'motion/react';
import earthImage from '../assets/f32abfc6ad0bc41084ce6dd6d98890502970dda7.png';
export function AttractionHassleFreeSection() {
return (
<section className="bg-background relative overflow-hidden">
{/* Subtle Background Elements */}
<div className="absolute inset-0">
<div className="absolute top-1/4 left-1/6 w-32 h-32 bg-primary/5 rounded-full blur-2xl"></div>
<div className="absolute bottom-1/3 right-1/6 w-24 h-24 bg-secondary/5 rounded-full blur-2xl"></div>
</div>
<div className="relative z-10">
<motion.div
className="flex flex-col lg:flex-row items-center py-16 lg:py-24"
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8, ease: "easeOut" }}
>
{/* Left Side - Earth Ellipse with Cloud Animation */}
<motion.div
className="w-full lg:w-1/2 relative flex items-center justify-center px-4 lg:px-8"
initial={{ opacity: 0, x: -40 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.7, ease: "easeOut" }}
>
{/* Earth Ellipse Container */}
<div className="relative w-96 h-96 lg:w-[28rem] lg:h-[28rem] xl:w-[32rem] xl:h-[32rem] flex items-center justify-center">
{/* Earth PNG Image */}
<motion.div
className="w-80 h-80 lg:w-[22rem] lg:h-[22rem] xl:w-[26rem] xl:h-[26rem] flex items-center justify-center"
animate={{
rotate: 360,
scale: [1, 1.05, 1]
}}
transition={{
rotate: { duration: 30, repeat: Infinity, ease: "linear" },
scale: { duration: 4, repeat: Infinity, ease: "easeInOut" }
}}
>
<img
src={earthImage}
alt="Earth"
className="w-full h-full object-contain drop-shadow-2xl"
/>
</motion.div>
{/* Floating Cloud Animations */}
<motion.div
className="absolute -top-6 left-12 lg:left-16 w-20 h-10 lg:w-24 lg:h-12 bg-white/80 rounded-full shadow-sm"
animate={{
x: [0, 25, 0],
y: [0, -12, 0]
}}
transition={{
duration: 6,
repeat: Infinity,
ease: "easeInOut"
}}
>
<div className="absolute top-2 left-2 w-7 h-7 lg:w-8 lg:h-8 bg-white/60 rounded-full"></div>
<div className="absolute top-0 right-2 w-10 h-10 lg:w-12 lg:h-12 bg-white/70 rounded-full"></div>
</motion.div>
<motion.div
className="absolute top-16 lg:top-20 -right-8 lg:-right-12 w-16 h-8 lg:w-20 lg:h-10 bg-white/70 rounded-full shadow-sm"
animate={{
x: [0, -20, 0],
y: [0, 10, 0]
}}
transition={{
duration: 8,
repeat: Infinity,
ease: "easeInOut",
delay: 1
}}
>
<div className="absolute top-1 left-1 w-5 h-5 lg:w-6 lg:h-6 bg-white/50 rounded-full"></div>
<div className="absolute top-0 right-1 w-8 h-8 lg:w-10 lg:h-10 bg-white/60 rounded-full"></div>
</motion.div>
<motion.div
className="absolute bottom-12 lg:bottom-16 -left-6 lg:-left-8 w-18 h-9 lg:w-22 lg:h-11 bg-white/75 rounded-full shadow-sm"
animate={{
x: [0, 30, 0],
y: [0, -8, 0]
}}
transition={{
duration: 7,
repeat: Infinity,
ease: "easeInOut",
delay: 2
}}
>
<div className="absolute top-1 left-1 w-6 h-6 lg:w-8 lg:h-8 bg-white/55 rounded-full"></div>
<div className="absolute top-0 right-1 w-9 h-9 lg:w-11 lg:h-11 bg-white/65 rounded-full"></div>
</motion.div>
<motion.div
className="absolute -bottom-4 right-16 lg:right-20 w-14 h-7 lg:w-18 lg:h-9 bg-white/65 rounded-full shadow-sm"
animate={{
x: [0, -18, 0],
y: [0, 8, 0]
}}
transition={{
duration: 5,
repeat: Infinity,
ease: "easeInOut",
delay: 0.5
}}
>
<div className="absolute top-0 left-1 w-4 h-4 lg:w-5 lg:h-5 bg-white/45 rounded-full"></div>
<div className="absolute top-0 right-1 w-7 h-7 lg:w-9 lg:h-9 bg-white/55 rounded-full"></div>
</motion.div>
{/* Orbital Ring - Centered around the Earth */}
<motion.div
className="absolute inset-0 w-96 h-96 lg:w-[28rem] lg:h-[28rem] xl:w-[32rem] xl:h-[32rem] border-2 border-dashed border-primary/20 rounded-full flex items-center justify-center"
animate={{ rotate: 360 }}
transition={{ duration: 40, repeat: Infinity, ease: "linear" }}
>
{/* Small orbiting elements */}
<motion.div
className="absolute top-0 left-1/2 w-4 h-4 lg:w-5 lg:h-5 bg-primary/40 rounded-full transform -translate-x-1/2 -translate-y-1/2"
animate={{ rotate: -360 }}
transition={{ duration: 40, repeat: Infinity, ease: "linear" }}
/>
<motion.div
className="absolute bottom-0 left-1/2 w-3 h-3 lg:w-4 lg:h-4 bg-secondary/40 rounded-full transform -translate-x-1/2 translate-y-1/2"
animate={{ rotate: -360 }}
transition={{ duration: 40, repeat: Infinity, ease: "linear" }}
/>
</motion.div>
</div>
</motion.div>
{/* Right Side - Content with Hierarchy - Full Width */}
<motion.div
className="w-full lg:w-1/2 lg:pl-16 xl:pl-24"
initial={{ opacity: 0, x: 40 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.7, ease: "easeOut", delay: 0.1 }}
>
<div className="container mx-auto px-4 lg:px-0 lg:pr-4 xl:pr-8">
{/* Main Heading */}
<motion.h1
className="text-4xl lg:text-5xl xl:text-6xl leading-tight text-foreground mb-12"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, ease: "easeOut" }}
>
<span className="font-light">Make the most of every</span>{' '}
<span className="bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent font-bold italic">
attraction
</span>
<span className="font-light">hassle-free.</span>
</motion.h1>
{/* Content Sections */}
<div className="space-y-10">
{/* Section 1 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, ease: "easeOut", delay: 0.2 }}
>
<h3 className="text-xl lg:text-2xl font-semibold text-foreground mb-4">
Explore a diverse array of travel passes
</h3>
<p className="text-lg text-muted-foreground leading-relaxed">
Explore a diverse array of travel passes, each designed to enhance your
adventure. From unlimited access to top attractions to exclusive discounts
on local experiences, our selection ensures you find the ideal pass for
your journey.
</p>
</motion.div>
{/* Section 2 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, ease: "easeOut", delay: 0.3 }}
>
<h3 className="text-xl lg:text-2xl font-semibold text-foreground mb-4">
Discover every attraction effortlessly
</h3>
<p className="text-lg text-muted-foreground leading-relaxed">
Explore a diverse array of travel passes, each designed to enhance your
adventure. From unlimited access to top attractions to exclusive discounts
on local experiences, our selection ensures you find the ideal pass for
your journey.
</p>
</motion.div>
{/* Section 3 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, ease: "easeOut", delay: 0.4 }}
>
<h3 className="text-xl lg:text-2xl font-semibold text-foreground mb-4">
Discover every attraction effortlessly
</h3>
<p className="text-lg text-muted-foreground leading-relaxed">
Explore a diverse array of travel passes, each designed to enhance your
adventure. From unlimited access to top attractions to exclusive discounts
on local experiences, our selection ensures you find the ideal pass for
your journey.
</p>
</motion.div>
</div>
</div>
</motion.div>
</motion.div>
</div>
</section>
);
}

View File

@@ -0,0 +1,604 @@
import { useState } from 'react';
import { motion } from 'motion/react';
import { Search, Star, Clock, ChevronRight } from 'lucide-react';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Card, CardContent } from './ui/card';
import { Badge } from './ui/badge';
import { Checkbox } from './ui/checkbox';
import Navbar from './Navbar';
import { CitySubmenu } from './CitySubmenu';
import { Footer } from './Footer';
import { ImageWithFallback } from './figma/ImageWithFallback';
interface User {
email: string;
name: string;
}
interface Attraction {
id: string;
name: string;
description: string;
image: string;
location: string;
duration: string;
rating: number;
price: number;
category: string;
hasReservation: boolean;
reviewCount: number;
passType: string;
}
const attractions: Attraction[] = [
{
id: '1',
name: 'Centipede Tour - Guided Arizona Desert Tour by ATV',
description: 'Experience the thrill of off-road adventure through the stunning Arizona desert landscape',
image: 'https://images.unsplash.com/photo-1682687220742-aba13b6e50ba?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxhdHYlMjBkZXNlcnQlMjB0b3VyfGVufDF8fHx8MTc1ODEwNDg5Nnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
location: 'Paris, France',
duration: '4 days',
rating: 4.8,
price: 189.25,
category: 'adventure',
hasReservation: true,
reviewCount: 243,
passType: 'unlimited'
},
{
id: '2',
name: 'Molokini and Turtle Town Snorkeling Adventure Aboard',
description: 'Snorkel in crystal-clear waters and swim alongside sea turtles in this unforgettable marine adventure',
image: 'https://images.unsplash.com/photo-1559827260-dc66d52bef19?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzbm9ya2VsaW5nJTIwdHVydGxlJTIwYWR2ZW50dXJlfGVufDF8fHx8MTc1ODEwNDkwMHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
location: 'New York, USA',
duration: '4 days',
rating: 4.8,
price: 225,
category: 'adventure',
hasReservation: false,
reviewCount: 167,
passType: 'selective'
},
{
id: '3',
name: 'Westminster Walking Tour & Westminster Abbey Entry',
description: 'Explore the heart of London with guided tours of historic Westminster and the famous Abbey',
image: 'https://images.unsplash.com/photo-1533929736458-ca588d08c8be?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx3ZXN0bWluc3RlciUyMGFiYmV5JTIwbG9uZG9ufGVufDF8fHx8MTc1ODEwNDkwNnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
location: 'London, UK',
duration: '4 days',
rating: 4.8,
price: 343,
category: 'culture',
hasReservation: true,
reviewCount: 343,
passType: 'unlimited'
},
{
id: '4',
name: 'All Inclusive Ultimate Circle Island Day Tour with Lunch',
description: 'Comprehensive island tour including all major attractions, lunch, and transportation',
image: 'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxpc2xhbmQlMjB0b3VyJTIwYWRvJTIwdHJvcGljYWx8ZW58MXx8fHwxNzU4MTA0OTEwfDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
location: 'New York, USA',
duration: '4 days',
rating: 4.8,
price: 225,
category: 'adventure',
hasReservation: false,
reviewCount: 243,
passType: 'unlimited'
},
{
id: '5',
name: 'Space Center Houston Admission Ticket',
description: 'Explore NASA\'s Johnson Space Center and discover the wonders of space exploration',
image: 'https://images.unsplash.com/photo-1446776653964-20c1d3a81b06?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzcGFjZSUyMGNlbnRlciUyMG5hc2ElMjBob3VzdG9ufGVufDF8fHx8MTc1ODEwNDkxM3ww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
location: 'Paris, France',
duration: '4 days',
rating: 4.8,
price: 225,
category: 'family',
hasReservation: true,
reviewCount: 243,
passType: 'selective'
},
{
id: '6',
name: 'Melbourne Skydeck Observatory',
description: 'Experience breathtaking 360-degree views from the Southern Hemisphere\'s highest viewing platform',
image: 'https://images.unsplash.com/photo-1677200922658-d0df5b2ac91e?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBhdHRyYWN0aW9ucyUyMGZhbW91cyUyMGxhbmRtYXJrc3xlbnwxfHx8fDE3NTc0MDEwODV8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
location: 'Melbourne CBD',
duration: '2 hours',
rating: 4.5,
price: 25,
category: 'adventure',
hasReservation: true,
reviewCount: 892,
passType: 'selective'
},
{
id: '7',
name: 'Royal Botanic Gardens Melbourne',
description: 'Explore 38 hectares of stunning gardens featuring over 8,500 species of plants',
image: 'https://images.unsplash.com/photo-1721272962395-a848331ce92d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjByb3lhbCUyMGJvdGFuaWMlMjBnYXJkZW5zfGVufDF8fHx8MTc1NzMzNzc4OXww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
location: 'South Yarra',
duration: '3 hours',
rating: 4.7,
price: 0,
category: 'nature',
hasReservation: false,
reviewCount: 1245,
passType: 'selective'
},
{
id: '8',
name: 'Federation Square Cultural Precinct',
description: 'Melbourne\'s cultural precinct featuring galleries, museums, and unique architecture',
image: 'https://images.unsplash.com/photo-1580688027085-8220709e3d84?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxmZWRlcmF0aW9uJTIwc3F1YXJlJTIwbWVsYm91cm5lfGVufDF8fHx8MTc1NzQwMTA5Mnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
location: 'Melbourne CBD',
duration: '3 hours',
rating: 4.3,
price: 0,
category: 'culture',
hasReservation: true,
reviewCount: 672,
passType: 'unlimited'
},
{
id: '9',
name: 'St Kilda Pier & Little Penguins',
description: 'Watch little penguins return home at sunset while enjoying the scenic pier',
image: 'https://images.unsplash.com/photo-1597889790884-2bb63cfbd4f6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzdCUyMGtpbGRhJTIwcGllciUyMG1lbGJvdXJuZXxlbnwxfHx8fDE3NTc0MDEwOTV8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
location: 'St Kilda',
duration: '2 hours',
rating: 4.4,
price: 0,
category: 'nature',
hasReservation: false,
reviewCount: 543,
passType: 'unlimited'
},
{
id: '10',
name: 'Queen Victoria Market Experience',
description: 'Historic market offering fresh produce, gourmet foods, and unique souvenirs',
image: 'https://images.unsplash.com/photo-1676454953709-e0be46f62490?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxxdWVlbiUyMHZpY3RvcmlhJTIwbWFya2V0JTIwbWVsYm91cm5lfGVufDF8fHx8MTc1NzQwMTA5OHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
location: 'Melbourne CBD',
duration: '2 hours',
rating: 4.6,
price: 0,
category: 'culture',
hasReservation: true,
reviewCount: 987,
passType: 'selective'
},
{
id: '11',
name: 'Melbourne Zoo Adventure',
description: 'Meet over 320 animal species from around the world in naturalistic habitats',
image: 'https://images.unsplash.com/photo-1681429477985-30dc7b88dd5b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjB6b28lMjBhbmltYWxzfGVufDF8fHx8MTc1NzMzNzgxMHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
location: 'Parkville',
duration: '4 hours',
rating: 4.5,
price: 40,
category: 'family',
hasReservation: false,
reviewCount: 1156,
passType: 'selective'
},
{
id: '12',
name: 'Great Ocean Road Day Tour',
description: 'Scenic coastal drive featuring the famous Twelve Apostles and stunning ocean views',
image: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxncmVhdCUyMG9jZWFuJTIwcm9hZCUyMGF1c3RyYWxpYXxlbnwxfHx8fDE3NTgxMDQ5Mzd8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
location: 'Great Ocean Road',
duration: '12 hours',
rating: 4.9,
price: 85,
category: 'adventure',
hasReservation: true,
reviewCount: 678,
passType: 'unlimited'
}
];
const filterCategories = [
{ value: 'with-reservation', label: 'With Reservation', count: 3 },
{ value: 'without-reservation', label: 'Without Reservation', count: 3 },
{ value: 'beach', label: 'Beach', count: 3 },
{ value: 'adventure', label: 'Adventure', count: 3 },
{ value: 'mountains', label: 'Mountains', count: 3 },
{ value: 'family', label: 'Family Friendly', count: 3 }
];
const passTypeCategories = [
{ value: 'selective', label: 'Selective Pass', count: 6 },
{ value: 'unlimited', label: 'Unlimited Pass', count: 6 }
];
interface AttractionsPageProps {
onBackClick: () => void;
onHomeClick: () => void;
onMelbourneClick: () => void;
onPassesClick: () => void;
onCheckoutClick: () => void;
onSignInClick: () => void;
onSignOutClick: () => void;
onAttractionsClick: () => void;
onBlogsClick: () => void;
onHowItWorksClick: () => void;
onFAQClick: () => void;
onPrivacyPolicyClick: () => void;
onAboutUsClick: () => void;
onProfileClick: () => void;
onCityCardsClick: () => void;
onMagicItineraryClick: () => void;
onPostCardsClick: () => void;
onOffersClick: () => void;
onContactUsClick?: () => void;
onEsimsClick?: () => void;
onHotelDiscountsClick?: () => void;
onAttractionClick?: (attractionId: string) => void;
currentPage: string;
user: User | null;
}
export function AttractionsPage({
onBackClick,
onHomeClick,
onMelbourneClick,
onPassesClick,
onCheckoutClick,
onSignInClick,
onSignOutClick,
onAttractionsClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onProfileClick,
onCityCardsClick,
onMagicItineraryClick,
onPostCardsClick,
onOffersClick,
onContactUsClick,
onEsimsClick,
onHotelDiscountsClick,
onAttractionClick,
currentPage,
user
}: AttractionsPageProps) {
const [searchQuery, setSearchQuery] = useState('');
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
const [selectedPassTypes, setSelectedPassTypes] = useState<string[]>([]);
const filteredAttractions = attractions.filter(attraction => {
const matchesSearch = attraction.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
attraction.description.toLowerCase().includes(searchQuery.toLowerCase());
const matchesCategory = selectedCategories.length === 0 ||
selectedCategories.some(cat => {
if (cat === 'with-reservation') return attraction.hasReservation;
if (cat === 'without-reservation') return !attraction.hasReservation;
return attraction.category === cat;
});
const matchesPassType = selectedPassTypes.length === 0 ||
selectedPassTypes.includes(attraction.passType);
return matchesSearch && matchesCategory && matchesPassType;
});
const toggleCategory = (category: string) => {
setSelectedCategories(prev =>
prev.includes(category)
? prev.filter(c => c !== category)
: [...prev, category]
);
};
const togglePassType = (passType: string) => {
setSelectedPassTypes(prev =>
prev.includes(passType)
? prev.filter(p => p !== passType)
: [...prev, passType]
);
};
const showingFrom = 1;
const showingTo = Math.min(12, filteredAttractions.length);
const totalItems = filteredAttractions.length;
return (
<div className="min-h-screen bg-background">
<Navbar
activeCity="Melbourne"
onCityChange={(city) => {
if (city === 'Melbourne') {
onMelbourneClick();
}
}}
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
onPassesClick={onPassesClick}
onCheckoutClick={onCheckoutClick}
onHomeClick={onHomeClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick}
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
currentPage="attractions"
isUserSignedIn={!!user}
user={user}
/>
{/* City Submenu */}
<CitySubmenu
currentPage={currentPage}
onClose={() => {}} // Empty function since submenu always shows on this page
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onAttractionsClick={onAttractionsClick}
onPassesClick={onPassesClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
/>
<div className="container mx-auto px-4 pt-56 pb-16">
{/* Breadcrumb */}
{/* Page Header */}
<div className="text-center mb-12">
<h1 className="font-merchant text-2xl md:text-3xl lg:text-4xl leading-tight text-gray-900 mb-4">
<span className="font-light">Discover</span>{' '}
<span className="font-bold italic text-gradient-primary">Melbourne's</span>{' '}
<span className="font-normal">Best</span>{' '}
<span className="font-semibold text-emphasis">Attractions</span>
</h1>
<p className="font-poppins text-xl leading-relaxed font-normal text-gray-600 max-w-3xl mx-auto">
Skip the lines and explore Melbourne's most iconic destinations with your CityCard pass
</p>
</div>
{/* City Card Promotional Banner */}
<div className="mb-8">
<Card className="bg-gradient-to-r from-primary to-primary/80 text-white p-8 rounded-xl border-none shadow-lg overflow-hidden relative">
<div className="flex flex-col gap-6">
<h2 className="font-merchant text-2xl leading-tight font-bold text-white">
Find Your Perfect Adventure
</h2>
{/* Search Bar and Button Container */}
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center">
{/* Search Bar */}
<div className="relative flex-1 max-w-lg">
<Input
placeholder="Search An Attraction"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-4 pr-12 h-[44px] bg-white/95 backdrop-blur-sm border-0 rounded-lg text-gray-800 placeholder:text-gray-500 font-poppins shadow-lg"
/>
<Search className="absolute right-4 top-1/2 transform -translate-y-1/2 text-gray-500 w-5 h-5" />
</div>
{/* Call-to-Action Button */}
<Button
className="bg-white/90 hover:bg-white text-primary border-2 border-primary hover:border-primary/80 px-8 h-[44px] rounded-lg font-semibold transition-all duration-200 hover:scale-105 font-poppins shadow-lg"
onClick={onCheckoutClick}
>
Buy Now
</Button>
</div>
</div>
{/* Decorative background elements */}
<div className="absolute top-0 right-0 w-32 h-32 bg-white/5 rounded-full -translate-y-16 translate-x-16"></div>
<div className="absolute bottom-0 left-0 w-24 h-24 bg-white/5 rounded-full translate-y-12 -translate-x-12"></div>
</Card>
</div>
<div className="flex gap-8">
{/* Left Sidebar */}
<div className="w-64 flex-shrink-0">
<Card className="p-8 sticky top-32">
<div className="space-y-6">
{/* Search by header */}
<div className="flex items-center gap-4">
<div className="h-0 w-6 border-t-[3px] border-[#2d2d2d] rotate-90"></div>
<h3 className="font-merchant text-[22px] text-[#2d2d2d] font-normal">Search by</h3>
</div>
{/* Filter categories */}
<div className="space-y-4">
{filterCategories.map(category => (
<div key={category.value} className="flex items-center gap-3">
<Checkbox
id={category.value}
checked={selectedCategories.includes(category.value)}
onCheckedChange={() => toggleCategory(category.value)}
className="border-[#bebebe]"
/>
<label
htmlFor={category.value}
className="font-poppins text-sm text-[#414141] cursor-pointer flex-1 font-normal"
>
{category.label} ({category.count})
</label>
</div>
))}
</div>
{/* Divider */}
<div className="border-t border-[#e5e5e5]"></div>
{/* Pass Type header */}
<div className="flex items-center gap-4">
<div className="h-0 w-6 border-t-[3px] border-[#2d2d2d] rotate-90"></div>
<h3 className="font-merchant text-[22px] text-[#2d2d2d] font-normal">Pass Type</h3>
</div>
{/* Pass Type filters */}
<div className="space-y-4">
{passTypeCategories.map(passType => (
<div key={passType.value} className="flex items-center gap-3">
<Checkbox
id={passType.value}
checked={selectedPassTypes.includes(passType.value)}
onCheckedChange={() => togglePassType(passType.value)}
className="border-[#bebebe]"
/>
<label
htmlFor={passType.value}
className="font-poppins text-sm text-[#414141] cursor-pointer flex-1 font-normal"
>
{passType.label} ({passType.count})
</label>
</div>
))}
</div>
</div>
</Card>
</div>
{/* Main Content */}
<div className="flex-1">
{/* Header */}
<div className="mb-8">
<h1 className="text-[48px] font-medium text-[#2d2d2d] mb-6">Attractions in Melbourne</h1>
{/* Results count */}
<p className="text-[16px] text-[#414141] mb-2">
Showing {showingFrom}-{showingTo} of {totalItems} Item(s)
</p>
</div>
{/* Attractions Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 auto-rows-fr">
{filteredAttractions.slice(0, 12).map((attraction) => (
<motion.div
key={attraction.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="h-full"
>
<Card
className="overflow-hidden hover:shadow-lg transition-all duration-200 cursor-pointer h-full flex flex-col group hover:border-primary/20"
onClick={() => onAttractionClick?.(attraction.id)}
>
<div className="aspect-[4/3] relative bg-gray-200 overflow-hidden">
<ImageWithFallback
src={attraction.image}
alt={attraction.name}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-200"
/>
{/* Pass Type Badge */}
<div className="absolute top-3 left-3 flex flex-col gap-2">
{attraction.price === 0 ? (
<Badge className="bg-primary text-white px-3 py-1 font-poppins font-semibold shadow-lg">
FREE
</Badge>
) : attraction.passType === 'unlimited' ? (
<Badge className="bg-gradient-to-r from-amber-500 to-orange-500 text-white px-3 py-1 font-poppins font-semibold shadow-lg border-0">
Unlimited Pass Exclusive
</Badge>
) : (
<Badge className="bg-gradient-to-r from-blue-500 to-cyan-500 text-white px-3 py-1 font-poppins font-semibold shadow-lg border-0">
Selective Pass
</Badge>
)}
</div>
</div>
<CardContent className="p-4 flex-1 flex flex-col">
<div className="text-sm text-muted-foreground mb-2 font-medium font-poppins">
{attraction.location}
</div>
<h3 className="font-semibold text-foreground mb-3 line-clamp-2 leading-tight min-h-[2.5rem] font-poppins">
{attraction.name}
</h3>
<div className="flex items-center gap-2 mb-3">
<div className="flex items-center gap-1">
{[...Array(5)].map((_, i) => (
<Star
key={i}
className={`w-4 h-4 ${
i < Math.floor(attraction.rating)
? 'fill-primary text-primary'
: 'text-gray-300'
}`}
/>
))}
<span className="text-sm font-medium ml-1 text-gray-700 font-poppins">
{attraction.rating} ({attraction.reviewCount})
</span>
</div>
</div>
{/* Pricing and Pass Info */}
<div className="mt-auto pt-2 space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-1 text-sm text-muted-foreground">
<Clock className="w-4 h-4 text-primary" />
<span className="font-poppins">{attraction.duration}</span>
</div>
<div className="text-right">
<div className="text-xs text-muted-foreground font-poppins font-normal">Normal visit price</div>
<div className="text-lg font-bold text-gray-400 line-through font-poppins">
${attraction.price}
</div>
</div>
</div>
{/* Included with Pass CTA */}
<div className="bg-gradient-to-r from-primary/10 to-secondary/10 border border-primary/20 rounded-lg p-2.5">
<div className="flex items-center justify-between gap-2">
<div className="flex-1">
<p className="text-xs font-poppins font-semibold text-primary uppercase">
Included with {attraction.passType === 'unlimited' ? 'Unlimited' : 'Selective'} Pass
</p>
<p className="text-xs font-poppins font-normal text-gray-600 mt-0.5">
Save ${attraction.price}
</p>
</div>
<Button
className="bg-primary hover:bg-primary/90 text-white font-poppins font-semibold text-xs px-4 min-h-[44px] min-w-[44px] h-[44px] whitespace-nowrap"
onClick={(e) => {
e.stopPropagation();
onCheckoutClick();
}}
>
Get Pass
</Button>
</div>
</div>
</div>
</CardContent>
</Card>
</motion.div>
))}
</div>
</div>
</div>
</div>
<Footer
onHomeClick={onHomeClick}
onAboutUsClick={onAboutUsClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onBlogsClick={onBlogsClick}
onContactUsClick={onContactUsClick}
/>
</div>
);
}

View File

@@ -0,0 +1,510 @@
import { useState } from 'react';
import { motion } from 'motion/react';
import { ArrowLeft, Calendar, User, Clock, Share2, BookmarkPlus, ThumbsUp, MessageSquare, Tag, MapPin } from 'lucide-react';
import { Button } from './ui/button';
import { Badge } from './ui/badge';
import { Card, CardContent } from './ui/card';
import { Separator } from './ui/separator';
import Navbar from './Navbar';
import { Footer } from './Footer';
import { ImageWithFallback } from './figma/ImageWithFallback';
interface User {
email: string;
name: string;
}
interface BlogDetailsPageProps {
blogId: string;
onBackClick: () => void;
onHomeClick: () => void;
onMelbourneClick: () => void;
onPassesClick: () => void;
onCheckoutClick: () => void;
onSignInClick: () => void;
onSignOutClick: () => void;
onAttractionsClick: () => void;
onBlogsClick: () => void;
onHowItWorksClick: () => void;
onFAQClick: () => void;
onPrivacyPolicyClick: () => void;
onAboutUsClick: () => void;
onProfileClick: () => void;
onCityCardsClick: () => void;
onMagicItineraryClick: () => void;
onPostCardsClick: () => void;
onOffersClick: () => void;
onContactUsClick?: () => void;
onEsimsClick?: () => void;
onHotelDiscountsClick?: () => void;
currentPage: string;
user: User | null;
}
// Mock blog data - in a real app this would come from an API
const getBlogById = (id: string) => {
const blogs = {
'1': {
id: '1',
title: 'Phi Phi Islands Adventure Day Trip with Seaview Lunch by V. Marine Tour',
content: `
<p>The Phi Phi archipelago is a must-visit while in Phuket, and this speedboat trip whisks you around the islands in one day. Swim over the coral reefs of Pileh Lagoon, have lunch at Phi Phi Leh, snorkel at Bamboo Island, and visit Monkey Beach and Maya Bay, immortalized in "The Beach."</p>
<p>Boat transfers, snacks, buffet lunch, snorkeling equipment, and Phuket hotel pickup and drop-off all included.</p>
<h2>Tour Overview</h2>
<p>The Phi Phi archipelago is a must-visit while in Phuket, and this speedboat trip whisks you around the islands in one day. Swim over the coral reefs of Pileh Lagoon, have lunch at Phi Phi Leh, snorkel at Bamboo Island, and visit Monkey Beach and Maya Bay, immortalized in "The Beach." Boat transfers, snacks, buffet lunch, snorkeling equipment, and Phuket hotel pickup and drop-off all included.</p>
<h3>What's included</h3>
<ul>
<li>Beverages, drinking water, morning tea and buffet lunch</li>
<li>Local taxes</li>
<li>Hotel pickup and drop-off by air-conditioned minivan</li>
<li>Insurance Transfer to a private pier</li>
<li>Soft drinks</li>
<li>Tour Guide</li>
</ul>
<h3>Location on map</h3>
<p>Explore the stunning locations of the Phi Phi Islands, including Maya Bay, Bamboo Island, and Monkey Beach. Each location offers unique experiences from snorkeling to beach relaxation.</p>
`,
author: 'Sarah Johnson',
date: 'Dec 15, 2024',
readTime: '5 min read',
category: 'Travel Guide',
image: 'https://images.unsplash.com/photo-1559827260-dc66d52bef19?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzbm9ya2VsaW5nJTIwdHVydGxlJTIwYWR2ZW50dXJlfGVufDF8fHx8MTc1ODEwNDkwMHww&ixlib=rb-4.1.0&q=80&w=1080',
gallery: [
'https://images.unsplash.com/photo-1559827260-dc66d52bef19?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzbm9ya2VsaW5nJTIwdHVydGxlJTIwYWR2ZW50dXJlfGVufDF8fHx8MTc1ODEwNDkwMHww&ixlib=rb-4.1.0&q=80&w=1080',
'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxpc2xhbmQlMjB0b3VyJTIwYWRvJTIwdHJvcGljYWx8ZW58MXx8fHwxNzU4MTA0OTEwfDA&ixlib=rb-4.1.0&q=80&w=1080',
'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxncmVhdCUyMG9jZWFuJTIwcm9hZCUyMGF1c3RyYWxpYXxlbnwxfHx8fDE3NTgxMDQ5Mzd8MA&ixlib=rb-4.1.0&q=80&w=1080',
'https://images.unsplash.com/photo-1682687220742-aba13b6e50ba?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxhdHYlMjBkZXNlcnQlMjB0b3VyfGVufDF8fHx8MTc1ODEwNDg5Nnww&ixlib=rb-4.1.0&q=80&w=1080'
],
tags: ['Adventure', 'Islands', 'Snorkeling', 'Marine Life'],
likes: 45,
comments: 12
},
'2': {
id: '2',
title: '5 best cafe spots in Melbourne',
content: `
<p>Melbourne's coffee culture is legendary, and for good reason. The city is home to some of the world's most innovative cafes, where baristas are treated as artists and coffee is considered a craft.</p>
<p>From hidden laneway gems to bustling neighborhood favorites, here are our top picks for the best cafe experiences in Melbourne.</p>
<h2>The Coffee Scene</h2>
<p>Melbourne's coffee culture began in the 1950s with Italian and Greek immigrants who brought their espresso traditions to Australia. Today, the city boasts more cafes per capita than any other city in the world.</p>
<h3>Our Top Picks</h3>
<ul>
<li>Degraves Espresso Bar - A laneway institution</li>
<li>Pellegrini's Espresso Bar - Old-school Italian charm</li>
<li>Brother Baba Budan - Single origin specialists</li>
<li>Patricia Coffee Brewers - Modern coffee artistry</li>
<li>Axil Coffee Roasters - Local roasting expertise</li>
</ul>
<h3>Coffee Culture Tips</h3>
<p>When visiting Melbourne's cafes, remember that coffee is taken seriously here. Don't be surprised if your barista asks detailed questions about your preferences - they're helping you find your perfect cup.</p>
`,
author: 'James Miller',
date: 'Dec 12, 2024',
readTime: '4 min read',
category: 'Food & Drink',
image: 'https://images.unsplash.com/photo-1554118811-1e0d58224f24?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxjb2ZmZWUlMjBzaG9wJTIwbWVsYm91cm5lfGVufDF8fHx8MTc1NjEyNDA5M3ww&ixlib=rb-4.1.0&q=80&w=1080',
gallery: [
'https://images.unsplash.com/photo-1554118811-1e0d58224f24?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxjb2ZmZWUlMjBzaG9wJTIwbWVsYm91cm5lfGVufDF8fHx8MTc1NjEyNDA5M3ww&ixlib=rb-4.1.0&q=80&w=1080',
'https://images.unsplash.com/photo-1679731980101-503d93bbec27?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBjb2ZmZWUlMjBsYW5ld2F5c3xlbnwxfHx8fDE3NTYxMjQwNzN8MA&ixlib=rb-4.1.0&q=80&w=1080'
],
tags: ['Coffee', 'Melbourne', 'Food', 'Culture'],
likes: 32,
comments: 8
}
};
return blogs[id as keyof typeof blogs] || blogs['1'];
};
export function BlogDetailsPage({
blogId,
onBackClick,
onHomeClick,
onMelbourneClick,
onPassesClick,
onCheckoutClick,
onSignInClick,
onSignOutClick,
onAttractionsClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onProfileClick,
onCityCardsClick,
onMagicItineraryClick,
onPostCardsClick,
onOffersClick,
onContactUsClick,
onEsimsClick,
onHotelDiscountsClick,
currentPage,
user
}: BlogDetailsPageProps) {
const [isLiked, setIsLiked] = useState(false);
const [isBookmarked, setIsBookmarked] = useState(false);
const blog = getBlogById(blogId);
return (
<div className="min-h-screen bg-[#f8f8f8]">
<Navbar
activeCity="Melbourne"
onCityChange={(city) => {
if (city === 'Melbourne') {
onMelbourneClick();
}
}}
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
onPassesClick={onPassesClick}
onCheckoutClick={onCheckoutClick}
onHomeClick={onHomeClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick}
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
currentPage="blogs"
isUserSignedIn={!!user}
user={user}
/>
<div className="container mx-auto px-4 pt-40 pb-16 max-w-6xl">
{/* Back Button */}
<motion.div
className="mb-8"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5 }}
>
<Button
variant="ghost"
onClick={onBackClick}
className="font-poppins font-medium text-base text-gray-600 hover:text-primary transition-colors duration-200"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Blogs
</Button>
</motion.div>
{/* Blog Header */}
<motion.div
className="mb-8"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<div className="flex flex-wrap gap-3 mb-6">
<Badge variant="default" className="bg-primary text-white px-4 py-2 rounded-full">
{blog.category}
</Badge>
{blog.tags.map((tag, index) => (
<Badge
key={index}
variant="secondary"
className="bg-primary/10 text-primary border border-primary/20 px-4 py-2 rounded-full"
>
{tag}
</Badge>
))}
</div>
<h1 className="font-merchant text-4xl md:text-5xl lg:text-6xl leading-tight mb-6">
<span className="font-light">Discover the</span>{' '}
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
Ultimate
</span>{' '}
<span className="font-normal text-[#2d3134]">{blog.title.split(' ').slice(-3).join(' ')}</span>
</h1>
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-6 text-gray-600">
<div className="flex items-center gap-2">
<User className="w-4 h-4" />
<span className="font-poppins text-base font-normal">{blog.author}</span>
</div>
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4" />
<span className="font-poppins text-base font-normal">{blog.date}</span>
</div>
<div className="flex items-center gap-2">
<Clock className="w-4 h-4" />
<span className="font-poppins text-base font-normal">{blog.readTime}</span>
</div>
</div>
<div className="flex items-center gap-3">
<Button
variant="ghost"
size="sm"
onClick={() => setIsLiked(!isLiked)}
className={`flex items-center gap-2 ${isLiked ? 'text-red-500' : 'text-gray-600'}`}
>
<ThumbsUp className={`w-4 h-4 ${isLiked ? 'fill-current' : ''}`} />
<span>{blog.likes + (isLiked ? 1 : 0)}</span>
</Button>
<Button
variant="ghost"
size="sm"
className="flex items-center gap-2 text-gray-600"
>
<MessageSquare className="w-4 h-4" />
<span>{blog.comments}</span>
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => setIsBookmarked(!isBookmarked)}
className={`${isBookmarked ? 'text-primary' : 'text-gray-600'}`}
>
<BookmarkPlus className={`w-4 h-4 ${isBookmarked ? 'fill-current' : ''}`} />
</Button>
<Button
variant="ghost"
size="sm"
className="text-gray-600"
>
<Share2 className="w-4 h-4" />
</Button>
</div>
</div>
</motion.div>
{/* Image Gallery - Masonry Layout */}
<motion.div
className="grid grid-cols-4 grid-rows-2 gap-4 h-[500px] mb-12"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
>
{/* Large featured image - Left side spanning 2 rows */}
<div className="col-span-2 row-span-2 rounded-lg overflow-hidden">
<ImageWithFallback
src={blog.image}
alt="Main blog image"
className="w-full h-full object-cover transition-transform duration-300 hover:scale-105"
/>
</div>
{/* Top right wide image */}
<div className="col-span-2 row-span-1 rounded-lg overflow-hidden">
<ImageWithFallback
src={blog.gallery[0]}
alt="Gallery image 1"
className="w-full h-full object-cover transition-transform duration-300 hover:scale-105"
/>
</div>
{/* Bottom right - First small image */}
<div className="col-span-1 row-span-1 rounded-lg overflow-hidden">
<ImageWithFallback
src={blog.gallery[1]}
alt="Gallery image 2"
className="w-full h-full object-cover transition-transform duration-300 hover:scale-105"
/>
</div>
{/* Bottom right - Second small image */}
<div className="col-span-1 row-span-1 rounded-lg overflow-hidden">
<ImageWithFallback
src="https://images.unsplash.com/photo-1669131196140-49591336b13e?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxjYWZlJTIwaW50ZXJpb3IlMjByZXN0YXVyYW50fGVufDF8fHx8MTc1OTIzNDkwNXww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral"
alt="Cafe interior"
className="w-full h-full object-cover transition-transform duration-300 hover:scale-105"
/>
</div>
</motion.div>
{/* Main Content */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
>
{/* Tour Overview Section */}
<div className="mb-16">
<h2 className="font-merchant text-2xl md:text-3xl lg:text-4xl leading-tight mb-8">
<span className="font-light">Tour</span>{' '}
<span className="font-semibold text-[#2d3134]">Overview</span>
</h2>
{/* Tour Details Cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 mb-8">
<div className="bg-gray-100 p-6 rounded-lg text-center">
<div className="w-12 h-12 bg-gray-300 rounded-lg mx-auto mb-3 flex items-center justify-center">
<Clock className="w-6 h-6 text-gray-600" />
</div>
<div className="font-poppins text-xs text-gray-500 uppercase tracking-wide mb-1 font-light">Duration</div>
<div className="font-poppins text-base font-medium text-gray-900">8 Hours</div>
</div>
<div className="bg-gray-100 p-6 rounded-lg text-center">
<div className="w-12 h-12 bg-gray-300 rounded-lg mx-auto mb-3 flex items-center justify-center">
<User className="w-6 h-6 text-gray-600" />
</div>
<div className="font-poppins text-xs text-gray-500 uppercase tracking-wide mb-1 font-light">Group Size</div>
<div className="font-poppins text-base font-medium text-gray-900">12 People</div>
</div>
<div className="bg-gray-100 p-6 rounded-lg text-center">
<div className="w-12 h-12 bg-gray-300 rounded-lg mx-auto mb-3 flex items-center justify-center">
<Tag className="w-6 h-6 text-gray-600" />
</div>
<div className="font-poppins text-xs text-gray-500 uppercase tracking-wide mb-1 font-light">Age Range</div>
<div className="font-poppins text-base font-medium text-gray-900">5-99 yrs</div>
</div>
<div className="bg-gray-100 p-6 rounded-lg text-center">
<div className="w-12 h-12 bg-gray-300 rounded-lg mx-auto mb-3 flex items-center justify-center">
<MapPin className="w-6 h-6 text-gray-600" />
</div>
<div className="font-poppins text-xs text-gray-500 uppercase tracking-wide mb-1 font-light">Languages</div>
<div className="font-poppins text-base font-medium text-gray-900">English</div>
</div>
</div>
<p className="font-poppins text-base leading-relaxed font-normal text-gray-700">
The Phi Phi archipelago is a must-visit while in Phuket, and this speedboat trip whisks you around the islands in one day. Swim over the coral reefs of Pileh Lagoon, have lunch at Phi Phi Leh, snorkel at Bamboo Island, and visit Monkey Beach and Maya Bay, immortalized in "The Beach." Boat transfers, snacks, buffet lunch, snorkeling equipment, and Phuket hotel pickup and drop-off all included.
</p>
</div>
{/* Tour Highlights Section */}
<div className="mb-16">
<h2 className="font-merchant text-2xl md:text-3xl lg:text-4xl leading-tight mb-8">
<span className="font-normal">Tour</span>{' '}
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
Highlights
</span>
</h2>
<ul className="space-y-4">
<li className="flex items-start gap-3">
<div className="w-2 h-2 bg-gray-400 rounded-full mt-2 flex-shrink-0"></div>
<span className="font-poppins text-base leading-relaxed font-normal text-gray-700">Visit the famous Maya Bay, featured in the movie "The Beach"</span>
</li>
<li className="flex items-start gap-3">
<div className="w-2 h-2 bg-gray-400 rounded-full mt-2 flex-shrink-0"></div>
<span className="font-poppins text-base leading-relaxed font-normal text-gray-700">Snorkel in the crystal-clear waters of Bamboo Island</span>
</li>
<li className="flex items-start gap-3">
<div className="w-2 h-2 bg-gray-400 rounded-full mt-2 flex-shrink-0"></div>
<span className="font-poppins text-base leading-relaxed font-normal text-gray-700">Swim in the stunning Pileh Lagoon surrounded by limestone cliffs</span>
</li>
<li className="flex items-start gap-3">
<div className="w-2 h-2 bg-gray-400 rounded-full mt-2 flex-shrink-0"></div>
<span className="font-poppins text-base leading-relaxed font-normal text-gray-700">Observe playful monkeys at Monkey Beach</span>
</li>
<li className="flex items-start gap-3">
<div className="w-2 h-2 bg-gray-400 rounded-full mt-2 flex-shrink-0"></div>
<span className="font-poppins text-base leading-relaxed font-normal text-gray-700">Enjoy a delicious buffet lunch with sea view</span>
</li>
</ul>
</div>
{/* What's Included Section */}
<div className="mb-16">
<h2 className="font-merchant text-2xl md:text-3xl lg:text-4xl leading-tight mb-8 font-semibold text-[#2d3134]">
What's Included
</h2>
<div className="grid md:grid-cols-2 gap-6">
<div>
<ul className="space-y-3">
<li className="flex items-center gap-3">
<div className="w-5 h-5 bg-green-500 rounded-full flex items-center justify-center flex-shrink-0">
<span className="font-poppins text-white text-xs font-medium">✓</span>
</div>
<span className="font-poppins text-base leading-relaxed font-normal text-gray-700">Beverages, drinking water, morning tea and buffet lunch</span>
</li>
<li className="flex items-center gap-3">
<div className="w-5 h-5 bg-green-500 rounded-full flex items-center justify-center flex-shrink-0">
<span className="font-poppins text-white text-xs font-medium">✓</span>
</div>
<span className="font-poppins text-base leading-relaxed font-normal text-gray-700">Local taxes</span>
</li>
<li className="flex items-center gap-3">
<div className="w-5 h-5 bg-green-500 rounded-full flex items-center justify-center flex-shrink-0">
<span className="font-poppins text-white text-xs font-medium">✓</span>
</div>
<span className="font-poppins text-base leading-relaxed font-normal text-gray-700">Hotel pickup and drop-off by air-conditioned minivan</span>
</li>
</ul>
</div>
<div>
<ul className="space-y-3">
<li className="flex items-center gap-3">
<div className="w-5 h-5 bg-green-500 rounded-full flex items-center justify-center flex-shrink-0">
<span className="font-poppins text-white text-xs font-medium">✓</span>
</div>
<span className="font-poppins text-base leading-relaxed font-normal text-gray-700">Insurance Transfer to a private pier</span>
</li>
<li className="flex items-center gap-3">
<div className="w-5 h-5 bg-green-500 rounded-full flex items-center justify-center flex-shrink-0">
<span className="font-poppins text-white text-xs font-medium">✓</span>
</div>
<span className="font-poppins text-base leading-relaxed font-normal text-gray-700">Soft drinks</span>
</li>
<li className="flex items-center gap-3">
<div className="w-5 h-5 bg-green-500 rounded-full flex items-center justify-center flex-shrink-0">
<span className="font-poppins text-white text-xs font-medium">✓</span>
</div>
<span className="font-poppins text-base leading-relaxed font-normal text-gray-700">Tour Guide</span>
</li>
</ul>
</div>
</div>
</div>
{/* Location on Map Section */}
<div className="mb-16">
<h2 className="font-merchant text-2xl md:text-3xl lg:text-4xl leading-tight mb-8">
<span className="font-normal">Location on</span>{' '}
<span className="font-semibold text-[#2d3134]">Map</span>
</h2>
<div className="h-96 rounded-lg overflow-hidden shadow-lg">
<ImageWithFallback
src="https://images.unsplash.com/photo-1582653402347-a59d799cbb0e?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtYXAlMjBhc2lhJTIwdGhhaWxhbmQlMjBsb2NhdGlvbnxlbnwxfHx8fDE3NTg5OTY4MTN8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral"
alt="Location map showing Phi Phi Islands"
className="w-full h-full object-cover"
/>
</div>
</div>
</motion.div>
</div>
<Footer
onHomeClick={onHomeClick}
onAboutUsClick={onAboutUsClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onBlogsClick={onBlogsClick}
onContactUsClick={onContactUsClick}
/>
</div>
);
}

View File

@@ -0,0 +1,529 @@
import { useState } from 'react';
import { motion } from 'motion/react';
import { Calendar, User, Clock, ArrowRight, Search, Tag, CreditCard, MapPin, Check, Smartphone, Star, Heart, Share2 } from 'lucide-react';
import { Button } from './ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
import { Badge } from './ui/badge';
import { Input } from './ui/input';
import Navbar from './Navbar';
import { CitySubmenu } from './CitySubmenu';
import { MobileAppSection } from './MobileAppSection';
import { WhyChooseCityCards } from './WhyChooseCityCards';
import { EnhancedTestimonials } from './EnhancedTestimonials';
import { ReviewsSection } from './ReviewsSection';
import { Footer } from './Footer';
import { ImageWithFallback } from './figma/ImageWithFallback';
import imgFrame1597884939 from "figma:asset/5da1b0444c0d21bc7ee776c49e36e2a8ea4d3e12.png";
// Blog Mobile App Section Component
function BlogMobileAppSection() {
return (
<section className="bg-muted/30 relative overflow-hidden">
{/* Subtle Background Elements */}
</section>
);
}
interface BlogsPageProps {
onBackClick: () => void;
onHomeClick: () => void;
onMelbourneClick: () => void;
onPassesClick: () => void;
onCheckoutClick: () => void;
onSignInClick: () => void;
onSignOutClick?: () => void;
onAttractionsClick: () => void;
onBlogsClick: () => void;
onHowItWorksClick: () => void;
onFAQClick: () => void;
onPrivacyPolicyClick: () => void;
onAboutUsClick: () => void;
onProfileClick: () => void;
onCityCardsClick: () => void;
onMagicItineraryClick: () => void;
onPostCardsClick: () => void;
onOffersClick: () => void;
onContactUsClick?: () => void;
onEsimsClick?: () => void;
onHotelDiscountsClick?: () => void;
onBlogClick?: (blogId: string) => void;
currentPage: string;
user?: { email: string; name: string; } | null;
}
// Mock blog data
const blogPosts = [
{
id: '1',
title: 'Ultimate Melbourne Coffee Culture Guide',
excerpt: 'Discover the hidden laneway cafes and roastery gems that make Melbourne the coffee capital of Australia.',
author: 'Sarah Johnson',
date: 'Dec 15, 2024',
readTime: '5 min read',
category: 'Food & Drink',
featured: true,
image: 'https://images.unsplash.com/photo-1514432324607-a09d9b4aefdd?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBjb2ZmZWV8ZW58MXx8fHwxNzU2MTI0MDYwfDA&ixlib=rb-4.1.0&q=80&w=1080'
},
{
id: '2',
title: 'Hidden Street Art Treasures in Melbourne',
excerpt: 'Navigate through Melbourne\'s famous laneways and discover world-class street art that transforms the city into an open-air gallery.',
author: 'Michael Chen',
date: 'Dec 12, 2024',
readTime: '4 min read',
category: 'Entertainment',
image: 'https://images.unsplash.com/photo-1523631278395-d65a85c7c3b3?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzdHJlZXQlMjBhcnQlMjBtZWxib3VybmV8ZW58MXx8fHwxNzU2MTI0MDY5fDA&ixlib=rb-4.1.0&q=80&w=1080'
},
{
id: '3',
title: 'A Foodie\'s Guide to Queen Victoria Market',
excerpt: 'From fresh produce to international delicacies, explore the vibrant flavors and culinary experiences at Melbourne\'s iconic market.',
author: 'Emma Wilson',
date: 'Dec 10, 2024',
readTime: '6 min read',
category: 'Food & Drink',
image: 'https://images.unsplash.com/photo-1567306301408-9b74a2a3e9b4?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtYXJrZXQlMjBmb29kfGVufDF8fHx8MTc1NjEyNDA3Mnww&ixlib=rb-4.1.0&q=80&w=1080'
},
{
id: '4',
title: 'Exploring Melbourne\'s Rooftop Bars',
excerpt: 'Sip cocktails with stunning city views at Melbourne\'s best rooftop venues, from hidden speakeasies to glamorous sky-high lounges.',
author: 'James Parker',
date: 'Dec 8, 2024',
readTime: '7 min read',
category: 'Adventure',
image: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxvdXRiYWNrJTIwYXVzdHJhbGlhfGVufDF8fHx8MTc1NjEyNDEwMXww&ixlib=rb-4.1.0&q=80&w=1080'
},
{
id: '5',
title: 'Secrets of the Great Ocean Road',
excerpt: 'Uncover hidden gems along one of the world\'s most scenic coastal drives, from secluded beaches to charming seaside towns.',
author: 'Lisa Rodriguez',
date: 'Dec 5, 2024',
readTime: '8 min read',
category: 'Road Trip',
image: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxncmVhdCUyMG9jZWFuJTIwcm9hZHxlbnwxfHx8fDE3NTYxMjQxMDV8MA&ixlib=rb-4.1.0&q=80&w=1080'
},
{
id: '6',
title: 'Wonders of the Great Barrier Reef',
excerpt: 'Explore the underwater paradise of the Great Barrier Reef and discover the incredible marine life that calls this UNESCO World Heritage site home.',
author: 'David Thompson',
date: 'Dec 3, 2024',
readTime: '6 min read',
category: 'Nature',
image: 'https://images.unsplash.com/photo-1559827260-dc66d52bef19?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxncmVhdCUyMGJhcnJpZXIlMjByZWVmfGVufDF8fHx8MTc1NjEyNDEwOXww&ixlib=rb-4.1.0&q=80&w=1080'
}
];
const categories = ['All', 'Travel Guide', 'Food & Drink', 'Entertainment', 'Adventure', 'Road Trip', 'Nature'];
const exploreMoreItems = [
{
title: 'Melbourne City Guide',
description: 'Complete guide to exploring Melbourne',
category: 'Guide'
},
{
title: 'Best Attractions',
description: 'Top-rated attractions in Melbourne',
category: 'Attractions'
},
{
title: 'Food & Culture',
description: 'Culinary experiences and cultural insights',
category: 'Culture'
},
{
title: 'Events Calendar',
description: 'Upcoming events and festivals',
category: 'Events'
},
{
title: 'Travel Tips',
description: 'Essential tips for visiting Melbourne',
category: 'Tips'
},
{
title: 'Photography Spots',
description: 'Instagram-worthy locations in the city',
category: 'Photography'
}
];
export function BlogsPage({
onHomeClick,
onMelbourneClick,
onPassesClick,
onCheckoutClick,
onSignInClick,
onSignOutClick,
onAttractionsClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onProfileClick,
onCityCardsClick,
onMagicItineraryClick,
onPostCardsClick,
onOffersClick,
onContactUsClick,
onEsimsClick,
onHotelDiscountsClick,
onBlogClick,
currentPage,
user
}: BlogsPageProps) {
const [selectedCategory, setSelectedCategory] = useState<string>('All');
const [searchQuery, setSearchQuery] = useState<string>('');
const filteredPosts = blogPosts.filter(post => {
const matchesCategory = selectedCategory === 'All' || post.category === selectedCategory;
const matchesSearch = post.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
post.excerpt.toLowerCase().includes(searchQuery.toLowerCase());
return matchesCategory && matchesSearch;
});
const featuredPost = blogPosts.find(post => post.featured);
const latestPosts = blogPosts.slice(0, 4);
return (
<div className="min-h-screen bg-background">
{/* Navigation */}
<Navbar
activeCity="Melbourne"
onCityChange={(city) => {
if (city === 'Melbourne') {
onMelbourneClick();
}
}}
onHomeClick={onHomeClick}
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
onPassesClick={onPassesClick}
onCheckoutClick={onCheckoutClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick}
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
onEsimsClick={onEsimsClick}
onHotelDiscountsClick={onHotelDiscountsClick}
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 text-4xl md:text-5xl lg:text-6xl leading-tight mb-4">
<span className="font-light">Explore more with the</span>{' '}
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
Blogs
</span>
</h1>
<p className="font-poppins text-xl leading-relaxed font-normal text-gray-600 max-w-3xl mx-auto">
Discover insider tips, hidden gems, and travel stories from Melbourne and beyond
</p>
</motion.div>
{/* Search and Filter Section */}
<motion.div
className="mb-12"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
>
<div className="flex flex-col md:flex-row gap-6 items-center justify-between">
{/* Search Bar */}
<div className="relative w-full md:w-96">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
<Input
placeholder="Search blogs..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="font-poppins font-normal text-base pl-10 py-3 rounded-xl border-gray-200 focus:border-primary"
/>
</div>
{/* Category Filter */}
<div className="flex gap-2 flex-wrap justify-center">
{categories.map((category) => (
<Button
key={category}
variant={selectedCategory === category ? "default" : "outline"}
size="sm"
onClick={() => setSelectedCategory(category)}
className={`font-poppins font-medium text-base rounded-full px-4 py-2 transition-all duration-200 ${
selectedCategory === category
? 'bg-primary text-white shadow-lg'
: 'hover:bg-gray-50 hover:border-primary'
}`}
>
{category}
</Button>
))}
</div>
</div>
</motion.div>
{/* Main Content Grid */}
<div className="grid lg:grid-cols-12 gap-12 mb-20">
{/* Left Side - Latest Blogs */}
<motion.div
className="lg:col-span-8"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
>
<div className="mb-8">
<h2 className="font-poppins text-2xl font-normal text-gray-900 mb-6">
Latest Blogs
</h2>
{/* Featured Blog */}
{featuredPost && (
<Card
className="mb-8 overflow-hidden shadow-lg hover:shadow-xl transition-shadow duration-300 h-[767px] flex flex-col cursor-pointer"
onClick={() => onBlogClick?.(featuredPost.id)}
>
<div className="relative h-[555px]">
<ImageWithFallback
src={featuredPost.image}
alt={featuredPost.title}
className="w-full h-full object-cover"
/>
<Badge className="absolute top-4 left-4 bg-primary text-white font-normal">
Featured
</Badge>
</div>
<div className="flex flex-col pt-[12px] px-[14px]">
<div>
<div className="flex items-center gap-2 mb-3">
<Badge variant="outline" className="text-sm font-light">
{featuredPost.category}
</Badge>
</div>
<h3 className="font-poppins font-normal text-gray-900 mb-3 line-clamp-2 text-xl">
{featuredPost.title}
</h3>
<p className="font-poppins text-base text-gray-600 mb-4 line-clamp-2 font-light">
{featuredPost.excerpt}
</p>
</div>
<div className="flex items-center justify-between text-sm text-gray-500 font-light">
<div className="flex items-center gap-3">
<div className="flex items-center gap-1">
<User className="w-3 h-3" />
<span>{featuredPost.author}</span>
</div>
<div className="flex items-center gap-1">
<Calendar className="w-3 h-3" />
<span>{featuredPost.date}</span>
</div>
</div>
<Button
variant="ghost"
size="sm"
className="text-primary hover:text-primary/80 h-auto p-1 font-normal"
onClick={(e) => {
e.stopPropagation();
onBlogClick?.(featuredPost.id);
}}
>
<ArrowRight className="w-4 h-4" />
</Button>
</div>
</div>
</Card>
)}
{/* Blog List */}
<div className="space-y-6">
{filteredPosts.filter(post => !post.featured).map((post, index) => (
<motion.div
key={post.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 * index }}
>
<Card
className="overflow-hidden hover:shadow-lg transition-shadow duration-300 cursor-pointer"
onClick={() => onBlogClick?.(post.id)}
>
<div className="md:flex">
<div className="md:w-48 md:flex-shrink-0">
<div className="relative h-48 md:h-full">
<ImageWithFallback
src={post.image}
alt={post.title}
className="w-full h-full object-cover"
/>
</div>
</div>
<div className="p-6 flex-1">
<div className="flex items-center gap-2 mb-3">
<Badge variant="outline" className="text-xs font-light">
{post.category}
</Badge>
</div>
<h3 className="font-merchant text-xl font-normal text-gray-900 mb-3 line-clamp-2">
{post.title}
</h3>
<p className="font-poppins text-gray-600 mb-4 line-clamp-2 font-light">
{post.excerpt}
</p>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4 text-sm text-gray-500 font-light">
<div className="flex items-center gap-1">
<User className="w-4 h-4" />
<span>{post.author}</span>
</div>
<div className="flex items-center gap-1">
<Calendar className="w-4 h-4" />
<span>{post.date}</span>
</div>
<div className="flex items-center gap-1">
<Clock className="w-4 h-4" />
<span>{post.readTime}</span>
</div>
</div>
<Button
variant="ghost"
size="sm"
className="text-primary hover:text-primary/80 font-normal"
onClick={(e) => {
e.stopPropagation();
onBlogClick?.(post.id);
}}
>
Read More <ArrowRight className="w-4 h-4 ml-1" />
</Button>
</div>
</div>
</div>
</Card>
</motion.div>
))}
</div>
</div>
</motion.div>
{/* Right Side - Explore More */}
<motion.div
className="lg:col-span-4"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
>
<div className="sticky top-24">
<h3 className="font-merchant text-xl font-normal text-gray-900 mb-6">
Explore More
</h3>
<div className="space-y-4">
{exploreMoreItems.map((item, index) => (
<Card key={index} className="p-4 hover:shadow-md transition-shadow duration-200 cursor-pointer">
<div className="flex items-center justify-between">
<div>
<h4 className="font-merchant font-normal text-gray-900 mb-1">
{item.title}
</h4>
<p className="font-poppins text-sm text-gray-600 font-light">
{item.description}
</p>
</div>
<ArrowRight className="w-4 h-4 text-gray-400" />
</div>
</Card>
))}
</div>
{/* Newsletter Signup */}
<Card className="mt-8 p-6 bg-gradient-to-br from-primary/5 to-secondary/5 border-primary/20">
<h4 className="font-merchant font-normal text-gray-900 mb-2">
Stay Updated
</h4>
<p className="font-poppins text-sm text-gray-600 mb-4 font-light">
Get the latest travel tips and city guides delivered to your inbox.
</p>
<div className="space-y-3">
<Input
placeholder="Enter your email"
className="bg-white border-gray-200 font-light"
/>
<Button className="w-full bg-primary hover:bg-primary/90 font-normal">
Subscribe
</Button>
</div>
</Card>
</div>
</motion.div>
</div>
{/* Access Your CityCards Section */}
<div className="my-20">
<MobileAppSection />
</div>
{/* Why Choose CityCards Section */}
<div className="my-20">
<WhyChooseCityCards />
</div>
{/* Enhanced Testimonials Section */}
<EnhancedTestimonials />
{/* Blog Mobile App Section */}
<BlogMobileAppSection />
{/* Reviews Section */}
<ReviewsSection />
</div>
<Footer
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onPassesClick={onPassesClick}
onSignInClick={onSignInClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onContactUsClick={onContactUsClick}
currentPage={currentPage}
/>
</div>
);
}
export default BlogsPage;

View File

@@ -0,0 +1,410 @@
import { useState } from 'react';
import { ChevronLeft, ChevronRight, Clock, Users, Star, Zap, CheckCircle, MapPin, Volume2, Camera } from 'lucide-react';
import { ImageWithFallback } from './figma/ImageWithFallback';
import { motion } from 'motion/react';
const attractions = [
{
id: 1,
name: "Sydney Opera House",
city: "Sydney",
country: "Australia",
image: "https://images.unsplash.com/photo-1657622884558-cc7525f93638?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzeWRuZXklMjBvcGVyYSUyMGhvdXNlJTIwaGFyYm9yJTIwYnJpZGdlfGVufDF8fHx8MTc1NjExNDMwMHww&ixlib=rb-4.1.0&q=80&w=1080",
rating: 4.8,
reviews: "12,500+",
category: "Landmarks",
originalPrice: "$89",
includedValue: "$89",
perks: [
{ icon: Zap, label: "Skip-the-line", color: "text-green-600" },
{ icon: Volume2, label: "Audio guide", color: "text-blue-600" },
{ icon: Camera, label: "Photo spots", color: "text-purple-600" }
]
},
{
id: 2,
name: "Great Ocean Road",
city: "Melbourne",
country: "Australia",
image: "https://images.unsplash.com/photo-1557544780-585e99807b15?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxncmVhdCUyMG9jZWFuJTIwcm9hZCUyMHR3ZWx2ZSUyMGFwb3N0bGVzfGVufDF8fHx8MTc1NjExNDMwNHww&ixlib=rb-4.1.0&q=80&w=1080",
rating: 4.9,
reviews: "8,200+",
category: "Nature",
originalPrice: "$125",
includedValue: "$125",
perks: [
{ icon: Users, label: "Guided tour", color: "text-blue-600" },
{ icon: MapPin, label: "Transport", color: "text-green-600" },
{ icon: Camera, label: "Photo stops", color: "text-purple-600" }
]
},
{
id: 3,
name: "Lone Pine Koala Sanctuary",
city: "Brisbane",
country: "Australia",
image: "https://images.unsplash.com/photo-1625476038303-0d3022077d39?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxsb25lJTIwcGluZSUyMGtvYWxhJTIwc2FuY3R1YXJ5JTIwYnJpc2JhbmV8ZW58MXx8fHwxNzU2MTE0MzA3fDA&ixlib=rb-4.1.0&q=80&w=1080",
rating: 4.7,
reviews: "15,800+",
category: "Wildlife",
originalPrice: "$65",
includedValue: "$65",
perks: [
{ icon: Zap, label: "Skip-the-line", color: "text-green-600" },
{ icon: Users, label: "Animal encounters", color: "text-orange-600" },
{ icon: Camera, label: "Photo opportunities", color: "text-purple-600" }
]
},
{
id: 4,
name: "Kings Park",
city: "Perth",
country: "Australia",
image: "https://images.unsplash.com/photo-1667315682754-852d9e855207?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxraW5ncyUyMHBhcmslMjBwZXJ0aCUyMGJvdGFuaWNhbCUyMGdhcmRlbnxlbnwxfHx8fDE3NTYxMTQzMTJ8MA&ixlib=rb-4.1.0&q=80&w=1080",
rating: 4.6,
reviews: "9,400+",
category: "Parks",
originalPrice: "Free",
includedValue: "$35",
perks: [
{ icon: Users, label: "Walking tours", color: "text-blue-600" },
{ icon: Volume2, label: "Audio guide", color: "text-blue-600" },
{ icon: MapPin, label: "Trail maps", color: "text-green-600" }
]
},
{
id: 5,
name: "Barossa Valley",
city: "Adelaide",
country: "Australia",
image: "https://images.unsplash.com/photo-1578274821879-08e7f9050d83?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxiYXJvc3NhJTIwdmFsbGV5JTIwdmluZXlhcmQlMjB3aW5lcnl8ZW58MXx8fHwxNzU2MTE0MzE3fDA&ixlib=rb-4.1.0&q=80&w=1080",
rating: 4.8,
reviews: "6,700+",
category: "Wine Tours",
originalPrice: "$98",
includedValue: "$98",
perks: [
{ icon: Users, label: "Wine tastings", color: "text-purple-600" },
{ icon: MapPin, label: "Transport", color: "text-green-600" },
{ icon: Volume2, label: "Expert guide", color: "text-blue-600" }
]
},
{
id: 6,
name: "Uluru",
city: "Northern Territory",
country: "Australia",
image: "https://images.unsplash.com/photo-1535960146738-4113294a306c?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx1bHVydSUyMGF5ZXJzJTIwcm9jayUyMGF1c3RyYWxpYSUyMGRlc2VydHxlbnwxfHx8fDE3NTYxMTQzMjB8MA&ixlib=rb-4.1.0&q=80&w=1080",
rating: 4.9,
reviews: "11,200+",
category: "Landmarks",
originalPrice: "$156",
includedValue: "$156",
perks: [
{ icon: Users, label: "Cultural tours", color: "text-orange-600" },
{ icon: Camera, label: "Sunrise viewing", color: "text-purple-600" },
{ icon: MapPin, label: "Transport", color: "text-green-600" }
]
},
{
id: 7,
name: "Blue Mountains",
city: "Sydney",
country: "Australia",
image: "https://images.unsplash.com/photo-1603101468073-50894cb31719?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxibHVlJTIwbW91bnRhaW5zJTIwc3lkbmV5JTIwdGhyZWUlMjBzaXN0ZXJzfGVufDF8fHx8MTc1NjExNDMyNXww&ixlib=rb-4.1.0&q=80&w=1080",
rating: 4.7,
reviews: "10,300+",
category: "Nature",
originalPrice: "$78",
includedValue: "$78",
perks: [
{ icon: MapPin, label: "Scenic railway", color: "text-green-600" },
{ icon: Users, label: "Guided walks", color: "text-blue-600" },
{ icon: Camera, label: "Lookout access", color: "text-purple-600" }
]
},
{
id: 8,
name: "Kangaroo Island",
city: "Adelaide",
country: "Australia",
image: "https://images.unsplash.com/photo-1719933564987-6c842ee2084e?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxrYW5nYXJvbyUyMGlzbGFuZCUyMGF1c3RyYWxpYSUyMHdpbGRsaWZlfGVufDF8fHx8MTc1NjExNDMyOXww&ixlib=rb-4.1.0&q=80&w=1080",
rating: 4.8,
reviews: "7,900+",
category: "Wildlife",
originalPrice: "$145",
includedValue: "$145",
perks: [
{ icon: Users, label: "Wildlife tours", color: "text-orange-600" },
{ icon: MapPin, label: "Ferry transport", color: "text-green-600" },
{ icon: Volume2, label: "Nature guide", color: "text-blue-600" }
]
}
];
const categories = ["All", "Landmarks", "Nature", "Wildlife", "Parks", "Wine Tours"];
export function BookAttractionSection() {
const [activeCategory, setActiveCategory] = useState("All");
const filteredAttractions = activeCategory === "All"
? attractions
: attractions.filter(attraction => attraction.category === activeCategory);
const AttractionCard = ({ attraction, index }: { attraction: typeof attractions[0], index: number }) => (
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
viewport={{ once: true }}
className="group relative bg-white rounded-3xl overflow-hidden shadow-lg hover:shadow-2xl transition-all duration-500 cursor-pointer border border-gray-100/50 hover:border-primary/30 hover:ring-4 hover:ring-primary/10 flex-shrink-0 w-[280px] md:w-auto md:flex-shrink"
>
{/* Image Container */}
<div className="relative h-64 md:h-72 lg:h-80 overflow-hidden">
<ImageWithFallback
src={attraction.image}
alt={attraction.name}
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-black/20 to-transparent group-hover:from-black/70 group-hover:via-black/30 transition-all duration-500" />
{/* Rating Badge */}
<div className="absolute top-4 right-4 bg-white/95 backdrop-blur-sm rounded-full px-3 py-1.5 flex items-center gap-1 shadow-lg group-hover:bg-white group-hover:shadow-xl transition-all duration-300">
<div className="w-4 h-4 bg-gradient-to-r from-yellow-400 to-yellow-500 rounded-full flex items-center justify-center">
<span className="text-white text-xs"></span>
</div>
<span className="text-sm font-medium text-gray-900">{attraction.rating}</span>
</div>
{/* Value Badge - Shows on hover */}
<motion.div
initial={{ opacity: 0, scale: 0.8, y: 10 }}
whileHover={{ opacity: 1, scale: 1, y: 0 }}
className="absolute top-4 left-4 bg-gradient-to-r from-green-500 to-emerald-600 text-white px-3 py-1.5 rounded-full text-sm font-medium shadow-lg opacity-0 group-hover:opacity-100 transition-all duration-300"
>
Included: {attraction.includedValue}
</motion.div>
{/* Overlay Content */}
<div className="absolute bottom-0 left-0 right-0 p-6 text-white transform group-hover:translate-y-0 transition-transform duration-300">
<h3 className="font-merchant text-xl leading-snug font-bold mb-1 group-hover:text-white transition-colors transform group-hover:scale-105 transition-transform duration-300">
{attraction.name}
</h3>
<p className="text-white/90 text-sm mb-2 group-hover:text-white transition-colors">
{attraction.city}, {attraction.country}
</p>
<div className="flex items-center justify-between">
<div className="flex items-center gap-1 text-white/80 text-xs group-hover:text-white/90 transition-colors">
<span>{attraction.reviews} reviews</span>
</div>
<motion.div
initial={{ opacity: 0, x: 20 }}
whileHover={{ opacity: 1, x: 0 }}
className="opacity-0 group-hover:opacity-100 transition-all duration-500"
>
<div className="flex items-center text-white text-sm">
<span className="mr-1"></span>
<span>Explore</span>
</div>
</motion.div>
</div>
</div>
</div>
{/* Hover Overlay - Included Value & Perks */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileHover={{ opacity: 1, y: 0 }}
className="absolute inset-0 bg-white/95 backdrop-blur-md opacity-0 group-hover:opacity-100 transition-all duration-500 flex flex-col justify-center items-center p-6"
>
<div className="text-center">
{/* Included Value Header */}
<motion.div
initial={{ scale: 0.8 }}
whileHover={{ scale: 1 }}
className="mb-6"
>
<div className="inline-flex items-center gap-2 bg-gradient-to-r from-green-500 to-emerald-600 text-white px-4 py-2 rounded-full mb-2">
<CheckCircle className="w-4 h-4" />
<span className="font-medium text-sm">Included Value</span>
</div>
<div className="text-3xl font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
{attraction.includedValue}
</div>
<p className="text-sm text-gray-600 mt-1">
{attraction.originalPrice === "Free" ? "Premium access included" : "Save money with CityCard"}
</p>
</motion.div>
{/* Perks Grid */}
<div className="space-y-3 mb-6">
<h4 className="font-merchant text-sm font-semibold text-gray-900">What's Included:</h4>
<div className="grid gap-2">
{attraction.perks.map((perk, perkIndex) => (
<motion.div
key={perkIndex}
initial={{ opacity: 0, x: -10 }}
whileHover={{ opacity: 1, x: 0 }}
transition={{ delay: perkIndex * 0.1 }}
className="flex items-center gap-3 p-2 bg-white/60 backdrop-blur-sm rounded-lg border border-white/40"
>
<div className={`w-8 h-8 rounded-full bg-white flex items-center justify-center shadow-sm ${perk.color}`}>
<perk.icon className="w-4 h-4" />
</div>
<span className="text-sm font-medium text-gray-800">{perk.label}</span>
</motion.div>
))}
</div>
</div>
{/* Action Button */}
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="font-poppins font-semibold w-full bg-gradient-to-r from-primary to-secondary text-white py-3 px-6 rounded-xl shadow-lg hover:shadow-xl transition-all duration-300"
>
Add to CityCard
</motion.button>
</div>
</motion.div>
{/* Enhanced Hover Gradient Overlay */}
<div className="absolute inset-0 bg-gradient-to-tr from-primary/10 via-transparent to-secondary/10 opacity-0 group-hover:opacity-100 transition-all duration-500 pointer-events-none" />
{/* Border Glow Effect */}
<div className="absolute inset-0 rounded-3xl bg-gradient-to-r from-primary to-secondary opacity-0 group-hover:opacity-20 transition-opacity duration-500 blur-sm pointer-events-none" />
</motion.div>
);
return (
<section className="py-20 bg-gradient-to-br from-gray-50 to-white relative overflow-hidden">
<div className="container mx-auto px-4">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<div className="inline-flex items-center gap-2 bg-gradient-to-r from-primary/10 to-secondary/10 px-4 py-2 rounded-full mb-6">
<div className="w-2 h-2 bg-gradient-to-r from-primary to-secondary rounded-full"></div>
<span className="font-poppins text-sm font-medium bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
Must-See Destinations
</span>
</div>
<h2 className="font-merchant text-2xl md:text-3xl lg:text-4xl leading-tight text-gray-900 mb-4">
<span className="font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent italic">
Top
</span>{' '}
<span className="font-light">Attractions</span>{' '}
<span className="font-semibold text-emphasis">Included</span>
</h2>
<p className="font-poppins text-xl leading-relaxed font-normal text-gray-600 max-w-3xl mx-auto">
Discover Australia's most iconic attractions and hidden gems across Sydney, Melbourne, Brisbane, Perth, and Adelaide - all included with your CityCard
</p>
</motion.div>
{/* Category Tabs */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
viewport={{ once: true }}
className="flex flex-wrap justify-center gap-3 mb-12"
>
{categories.map((category, index) => (
<motion.button
key={category}
initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3, delay: index * 0.05 }}
viewport={{ once: true }}
onClick={() => setActiveCategory(category)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className={`px-6 py-4 h-14 rounded-2xl font-medium transition-all duration-300 ${
activeCategory === category
? 'bg-gradient-to-r from-primary to-secondary text-white shadow-xl shadow-primary/25 ring-2 ring-primary/20'
: 'bg-white/80 backdrop-blur-sm text-gray-700 hover:text-gray-900 hover:shadow-lg border border-gray-200/50 hover:border-primary/20 hover:bg-white'
}`}
>
{category}
</motion.button>
))}
</motion.div>
{/* Mobile Horizontal Carousel */}
<div className="block md:hidden mb-8">
<div className="relative">
{/* Scroll Container */}
<div className="flex gap-6 overflow-x-auto scrollbar-hide pb-4 px-4 -mx-4">
{filteredAttractions.map((attraction, index) => (
<AttractionCard key={attraction.id} attraction={attraction} index={index} />
))}
</div>
{/* Scroll Indicators */}
<div className="flex justify-center mt-6 gap-2">
{Array.from({ length: Math.ceil(filteredAttractions.length / 2) }).map((_, index) => (
<div
key={index}
className="w-2 h-2 rounded-full bg-gray-300"
/>
))}
</div>
{/* Mobile Hint Text */}
<div className="text-center mt-4">
<p className="text-sm text-gray-500">
Swipe to explore more attractions
</p>
</div>
</div>
</div>
{/* Desktop Bento Grid */}
<div className="hidden md:block w-full">
{/* Top Row - 3 equal cards */}
<div className="grid grid-cols-3 gap-6">
{filteredAttractions.slice(0, 3).map((attraction, index) => (
<AttractionCard key={attraction.id} attraction={attraction} index={index} />
))}
</div>
{/* Consistent Vertical Spacing */}
<div className="h-6"></div>
{/* Bottom Row - 2 larger cards */}
<div className="grid grid-cols-2 gap-6">
{filteredAttractions.slice(3, 5).map((attraction, index) => (
<AttractionCard key={attraction.id} attraction={attraction} index={index + 3} />
))}
</div>
</div>
{/* Call to Action */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
viewport={{ once: true }}
className="text-center mt-12"
>
<motion.button
whileHover={{ scale: 1.05, boxShadow: "0 20px 40px rgba(99,102,241,0.3)" }}
whileTap={{ scale: 0.95 }}
className="relative bg-gradient-to-r from-primary to-secondary text-white py-4 px-12 rounded-lg text-lg shadow-xl transition-all duration-300 overflow-hidden group"
>
<span className="relative z-10">Get Your City Card</span>
{/* Shine animation */}
<div className="absolute inset-0 opacity-30">
<div className="h-full bg-gradient-to-r from-transparent via-white to-transparent animate-shine"></div>
</div>
</motion.button>
</motion.div>
</div>
</section>
);
}

View File

@@ -0,0 +1,93 @@
import { motion } from 'motion/react';
import BeforeLogin from '../imports/BeforeLogin';
import AfterLogin from '../imports/AfterLogin';
interface User {
email: string;
name: string;
}
interface CTAButtonProps {
user: User | null;
onClick: () => void;
className?: string;
}
// Helper function to generate initials from user data
const generateInitials = (user: User): string => {
if (user.name && user.name.includes(' ')) {
// If name has spaces, use first letter of first and last name
const nameParts = user.name.trim().split(' ');
return (nameParts[0]?.charAt(0) + nameParts[nameParts.length - 1]?.charAt(0)).toUpperCase();
} else if (user.name) {
// If name is single word, use first two letters
return user.name.substring(0, 2).toUpperCase();
} else {
// Fallback to email
const emailPart = user.email.split('@')[0];
if (emailPart.length >= 2) {
return emailPart.substring(0, 2).toUpperCase();
}
return emailPart.charAt(0).toUpperCase() + 'X';
}
};
// Custom AfterLogin component with dynamic initials
function CustomAfterLogin({ initials }: { initials: string }) {
return (
<div className="bg-[#f95f62] relative rounded-[3.35544e+07px] shadow-[0px_10px_15px_-3px_rgba(0,0,0,0.1),0px_4px_6px_-4px_rgba(0,0,0,0.1)] size-full" data-name="After Login">
<div className="flex flex-row items-center justify-center relative size-full">
<div className="box-border content-stretch flex gap-[4px] items-center justify-center overflow-clip px-[15px] py-[12px] relative size-full">
{/* User Initials Circle */}
<div className="box-border content-stretch flex items-center justify-center pl-0 pr-[0.016px] py-0 relative rounded-[3.35544e+07px] shrink-0 size-[28px]" style={{ backgroundImage: "linear-gradient(90deg, rgba(0, 0, 0, 0.4) 0%, rgba(0, 0, 0, 0.4) 100%), linear-gradient(90deg, rgb(249, 95, 98) 0%, rgb(249, 95, 98) 100%)" }}>
<div className="h-[14px] relative shrink-0 w-[13.703px]">
<div className="bg-clip-padding border-0 border-[transparent] border-solid box-border h-[14px] relative w-[13.703px]">
<div className="absolute font-['Poppins:Bold',_sans-serif] leading-[0] left-0 not-italic text-[10.5px] text-nowrap text-white top-0">
<p className="leading-[14px] whitespace-pre">{initials}</p>
</div>
</div>
</div>
</div>
{/* Text */}
<div className="content-stretch flex h-[17.5px] items-start relative shrink-0 w-[86.047px]">
<div className="font-['Poppins:SemiBold',_sans-serif] leading-[0] not-italic relative shrink-0 text-[12.25px] text-nowrap text-white">
<p className="leading-[17.5px] whitespace-pre">MY CITY CARD</p>
</div>
</div>
</div>
</div>
</div>
);
}
export function CTAButton({ user, onClick, className = "" }: CTAButtonProps) {
const buttonContent = user ? (
<CustomAfterLogin initials={generateInitials(user)} />
) : (
<BeforeLogin />
);
return (
<motion.button
onClick={onClick}
className={`relative w-[180px] h-[50px] cursor-pointer group ${className}`}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
transition={{ duration: 0.2 }}
>
<motion.div
key={user ? user.email : 'logged-out'}
className="w-full h-full"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3 }}
>
{buttonContent}
</motion.div>
{/* Hover glow effect */}
<div className="absolute inset-0 rounded-full opacity-0 group-hover:opacity-20 transition-opacity duration-300 bg-gradient-to-r from-primary to-secondary blur-lg -z-10" />
</motion.button>
);
}

View File

@@ -0,0 +1,767 @@
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { ArrowLeft, CreditCard, Users, Calendar, MapPin, Shield, Truck, Clock, ChevronRight, Check, ChevronDown, X, Mail, Smartphone } from 'lucide-react';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Label } from './ui/label';
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
import { Separator } from './ui/separator';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog';
import { RadioGroup, RadioGroupItem } from './ui/radio-group';
import { Checkbox } from './ui/checkbox';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import { Badge } from './ui/badge';
import { Textarea } from './ui/textarea';
import Navbar from './Navbar';
import { Footer } from './Footer';
import { ImageWithFallback } from './figma/ImageWithFallback';
interface CheckoutPageProps {
onBackClick: () => void;
onHomeClick: () => void;
onMelbourneClick: () => void;
onPassesClick: () => void;
onCheckoutClick: () => void;
onSignInClick: () => void;
onSignOutClick?: () => void;
onAttractionsClick: () => void;
onBlogsClick: () => void;
onHowItWorksClick: () => void;
onFAQClick: () => void;
onPrivacyPolicyClick: () => void;
onAboutUsClick: () => void;
onProfileClick: () => void;
onCityCardsClick: () => void;
onMagicItineraryClick: () => void;
onPostCardsClick: () => void;
onOffersClick: () => void;
onSecureCheckoutClick?: () => void;
onContactUsClick?: () => void;
onEsimsClick?: () => void;
onHotelDiscountsClick?: () => void;
currentPage: string;
user?: { email: string; name: string } | null;
}
// Mock cart data
const mockCartItems = [
{
id: '1',
name: 'Paris Unlimited Pass',
type: '7-Day Pass',
price: 79,
originalPrice: 149,
discount: 47,
attractions: 45,
validity: '7 days',
image: 'https://images.unsplash.com/photo-1502602898536-47ad22581b52?w=400',
features: ['Skip-the-line access', 'Mobile voucher', 'Free cancellation']
}
];
export function CheckoutPage({
onBackClick,
onHomeClick,
onMelbourneClick,
onPassesClick,
onCheckoutClick,
onSignInClick,
onSignOutClick,
onAttractionsClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onProfileClick,
onCityCardsClick,
onMagicItineraryClick,
onPostCardsClick,
onOffersClick,
onSecureCheckoutClick,
onContactUsClick,
onEsimsClick,
onHotelDiscountsClick,
currentPage,
user,
}: CheckoutPageProps) {
const [purchaseType, setPurchaseType] = useState<'self' | 'gift'>('self');
const [selectedPayment, setSelectedPayment] = useState('credit-card');
const [showEmailVerification, setShowEmailVerification] = useState(false);
const [verificationCode, setVerificationCode] = useState('');
const [isEmailVerified, setIsEmailVerified] = useState(false);
const [formData, setFormData] = useState({
email: '',
firstName: '',
lastName: '',
phone: '',
country: '',
address: '',
city: '',
postalCode: '',
cardNumber: '',
expiry: '',
cvv: '',
cardName: '',
agreeTerms: false,
subscribeNewsletter: false
});
const [giftData, setGiftData] = useState({
recipientName: '',
recipientPhone: '',
recipientEmail: '',
personalizedMessage: ''
});
const subtotal = mockCartItems.reduce((sum, item) => sum + item.price, 0);
const tax = Math.round(subtotal * 0.1);
const total = subtotal + tax;
const totalSavings = mockCartItems.reduce((sum, item) => sum + (item.originalPrice - item.price), 0);
const handleInputChange = (field: string, value: string | boolean) => {
setFormData(prev => ({ ...prev, [field]: value }));
// Trigger email verification when email is complete
if (field === 'email' && typeof value === 'string' && value.includes('@') && value.includes('.') && !isEmailVerified) {
setTimeout(() => {
setShowEmailVerification(true);
}, 1000);
}
};
const handleGiftInputChange = (field: string, value: string) => {
setGiftData(prev => ({ ...prev, [field]: value }));
};
const handleEmailVerification = () => {
if (verificationCode === '123456') {
setIsEmailVerified(true);
setShowEmailVerification(false);
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!isEmailVerified) {
setShowEmailVerification(true);
return;
}
const checkoutData = {
purchaseType,
formData,
...(purchaseType === 'gift' && { giftData }),
selectedPayment,
cartItems: mockCartItems
};
console.log('Processing checkout...', checkoutData);
};
const paymentMethods = [
{
id: 'credit-card',
name: 'Credit Card',
icon: <CreditCard className="w-5 h-5" />,
description: 'Visa, Mastercard, American Express'
},
{
id: 'paypal',
name: 'PayPal',
icon: <div className="w-5 h-5 bg-blue-600 rounded-sm flex items-center justify-center text-xs font-bold text-white">P</div>,
description: 'Pay with your PayPal account'
},
{
id: 'google-pay',
name: 'Google Pay',
icon: <div className="w-5 h-5 bg-green-600 rounded-sm flex items-center justify-center text-xs font-bold text-white">G</div>,
description: 'Pay with Google Pay'
}
];
return (
<div className="min-h-screen bg-background">
{/* Navbar */}
<Navbar
activeCity="Paris"
onCityChange={() => {}}
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onPassesClick={onPassesClick}
onCheckoutClick={onCheckoutClick}
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick}
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
onEsimsClick={onEsimsClick}
onHotelDiscountsClick={onHotelDiscountsClick}
currentPage={currentPage}
isUserSignedIn={!!user}
user={user}
/>
{/* Header Section */}
<section className="pt-32 pb-8 bg-gradient-to-br from-muted/30 to-background">
<div className="container mx-auto px-4 pt-8">
{/* Back Button */}
<motion.button
onClick={onBackClick}
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-6 transition-colors duration-200"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5 }}
>
<ArrowLeft className="w-5 h-5" />
<span className="font-medium">Back to Cart</span>
</motion.button>
{/* Page Title */}
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
>
<h1 className="font-merchant text-3xl md:text-4xl lg:text-5xl mb-4">
<span className="font-light">Secure</span>{' '}
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">Checkout</span>
</h1>
<p className="font-poppins text-lg text-gray-600">
Complete your purchase and start exploring Paris
</p>
</motion.div>
</div>
</section>
{/* Main Checkout Content */}
<section className="py-12">
<div className="container mx-auto px-4">
<form onSubmit={handleSubmit} className="grid grid-cols-1 lg:grid-cols-5 gap-8">
{/* Left Column - Form Inputs (3/5 width) */}
<div className="lg:col-span-3 space-y-8">
{/* Purchase Type Selection */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.1 }}
>
<Card>
<CardHeader>
<CardTitle className="font-merchant">Purchase Type</CardTitle>
</CardHeader>
<CardContent>
<RadioGroup
value={purchaseType}
onValueChange={(value) => setPurchaseType(value as 'self' | 'gift')}
className="grid grid-cols-1 md:grid-cols-2 gap-4"
>
<div className="relative">
<RadioGroupItem value="self" id="self" className="peer sr-only" />
<Label
htmlFor="self"
className="flex flex-col items-center justify-between rounded-lg border-2 border-muted bg-white p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer transition-all min-h-[44px]"
>
<div className="flex items-center gap-3 w-full">
<Users className="w-5 h-5" />
<div className="flex-1">
<p className="font-poppins font-medium">Buy for Myself</p>
<p className="text-sm text-muted-foreground font-poppins">Purchase a pass for your own use</p>
</div>
</div>
</Label>
</div>
<div className="relative">
<RadioGroupItem value="gift" id="gift" className="peer sr-only" />
<Label
htmlFor="gift"
className="flex flex-col items-center justify-between rounded-lg border-2 border-muted bg-white p-4 hover:bg-accent hover:text-accent-foreground peer-data-[state=checked]:border-primary [&:has([data-state=checked])]:border-primary cursor-pointer transition-all min-h-[44px]"
>
<div className="flex items-center gap-3 w-full">
<Mail className="w-5 h-5" />
<div className="flex-1">
<p className="font-poppins font-medium">Gift a Pass</p>
<p className="text-sm text-muted-foreground font-poppins">Send as a gift to someone special</p>
</div>
</div>
</Label>
</div>
</RadioGroup>
</CardContent>
</Card>
</motion.div>
{/* Gift Recipient Information - Only shown when gift is selected */}
{purchaseType === 'gift' && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 font-merchant">
<Mail className="w-5 h-5" />
Gift Recipient Details
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label htmlFor="recipientName" className="font-poppins">Recipient Name *</Label>
<Input
id="recipientName"
value={giftData.recipientName}
onChange={(e) => handleGiftInputChange('recipientName', e.target.value)}
placeholder="Jane Smith"
required={purchaseType === 'gift'}
className="mt-1 font-poppins"
/>
</div>
<div>
<Label htmlFor="recipientEmail" className="font-poppins">Recipient Email Address *</Label>
<Input
id="recipientEmail"
type="email"
value={giftData.recipientEmail}
onChange={(e) => handleGiftInputChange('recipientEmail', e.target.value)}
placeholder="recipient@email.com"
required={purchaseType === 'gift'}
className="mt-1 font-poppins"
/>
</div>
<div>
<Label htmlFor="recipientPhone" className="font-poppins">Recipient Phone Number *</Label>
<Input
id="recipientPhone"
type="tel"
value={giftData.recipientPhone}
onChange={(e) => handleGiftInputChange('recipientPhone', e.target.value)}
placeholder="+1 (555) 123-4567"
required={purchaseType === 'gift'}
className="mt-1 font-poppins"
/>
</div>
<div>
<Label htmlFor="personalizedMessage" className="font-poppins">Personalized Message</Label>
<Textarea
id="personalizedMessage"
value={giftData.personalizedMessage}
onChange={(e) => handleGiftInputChange('personalizedMessage', e.target.value)}
placeholder="Write a special message for your gift recipient..."
rows={4}
className="mt-1 font-poppins"
/>
<p className="text-xs text-muted-foreground mt-1 font-poppins">Optional: Add a personal touch to your gift</p>
</div>
</CardContent>
</Card>
</motion.div>
)}
{/* Contact Information */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: purchaseType === 'gift' ? 0.3 : 0.2 }}
>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 font-merchant">
<Users className="w-5 h-5" />
{purchaseType === 'gift' ? 'Your Information (Purchaser)' : 'Contact Information'}
{isEmailVerified && <Check className="w-5 h-5 text-green-600" />}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label htmlFor="email" className="font-poppins">Email Address *</Label>
<div className="relative">
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleInputChange('email', e.target.value)}
placeholder="your@email.com"
required
className={`mt-1 font-poppins ${isEmailVerified ? 'border-green-600 bg-green-50' : ''}`}
/>
{isEmailVerified && (
<Check className="absolute right-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-green-600" />
)}
</div>
{isEmailVerified && (
<p className="text-sm text-green-600 mt-1 font-poppins">Email verified </p>
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor="firstName" className="font-poppins">First Name *</Label>
<Input
id="firstName"
value={formData.firstName}
onChange={(e) => handleInputChange('firstName', e.target.value)}
placeholder="John"
required
className="mt-1 font-poppins"
/>
</div>
<div>
<Label htmlFor="lastName" className="font-poppins">Last Name *</Label>
<Input
id="lastName"
value={formData.lastName}
onChange={(e) => handleInputChange('lastName', e.target.value)}
placeholder="Doe"
required
className="mt-1 font-poppins"
/>
</div>
</div>
<div>
<Label htmlFor="phone" className="font-poppins">Phone Number</Label>
<Input
id="phone"
type="tel"
value={formData.phone}
onChange={(e) => handleInputChange('phone', e.target.value)}
placeholder="+1 (555) 123-4567"
className="mt-1 font-poppins"
/>
</div>
</CardContent>
</Card>
</motion.div>
{/* Payment Method */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.3 }}
>
</motion.div>
{/* Billing Address */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: purchaseType === 'gift' ? 0.4 : 0.3 }}
>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 font-merchant">
<MapPin className="w-5 h-5" />
Billing Address
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label htmlFor="country" className="font-poppins">Country *</Label>
<Select value={formData.country} onValueChange={(value) => handleInputChange('country', value)}>
<SelectTrigger className="mt-1">
<SelectValue placeholder="Select country" />
</SelectTrigger>
<SelectContent>
<SelectItem value="us">United States</SelectItem>
<SelectItem value="au">Australia</SelectItem>
<SelectItem value="uk">United Kingdom</SelectItem>
<SelectItem value="ca">Canada</SelectItem>
<SelectItem value="de">Germany</SelectItem>
<SelectItem value="fr">France</SelectItem>
<SelectItem value="in">India</SelectItem>
<SelectItem value="jp">Japan</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="address" className="font-poppins">Street Address *</Label>
<Input
id="address"
value={formData.address}
onChange={(e) => handleInputChange('address', e.target.value)}
placeholder="123 Main Street"
required
className="mt-1 font-poppins"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor="city" className="font-poppins">City *</Label>
<Input
id="city"
value={formData.city}
onChange={(e) => handleInputChange('city', e.target.value)}
placeholder="New York"
required
className="mt-1 font-poppins"
/>
</div>
<div>
<Label htmlFor="postalCode" className="font-poppins">Postal Code *</Label>
<Input
id="postalCode"
value={formData.postalCode}
onChange={(e) => handleInputChange('postalCode', e.target.value)}
placeholder="10001"
required
className="mt-1 font-poppins"
/>
</div>
</div>
</CardContent>
</Card>
</motion.div>
{/* Terms and Newsletter */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: purchaseType === 'gift' ? 0.5 : 0.4 }}
className="space-y-4"
>
<div className="flex items-start space-x-2">
<Checkbox
id="terms"
checked={formData.agreeTerms}
onCheckedChange={(checked) => handleInputChange('agreeTerms', checked as boolean)}
/>
<div className="grid gap-1.5 leading-none">
<label
htmlFor="terms"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 font-poppins"
>
I agree to the Terms of Service and Privacy Policy *
</label>
</div>
</div>
<div className="flex items-start space-x-2">
<Checkbox
id="newsletter"
checked={formData.subscribeNewsletter}
onCheckedChange={(checked) => handleInputChange('subscribeNewsletter', checked as boolean)}
/>
<div className="grid gap-1.5 leading-none">
<label
htmlFor="newsletter"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 font-poppins"
>
Subscribe to our newsletter for travel tips and special offers
</label>
</div>
</div>
</motion.div>
</div>
{/* Right Column - Order Summary (2/5 width) */}
<div className="lg:col-span-2">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: purchaseType === 'gift' ? 0.4 : 0.3 }}
className="sticky top-24"
>
<Card>
<CardHeader>
<CardTitle className="font-merchant flex items-center gap-2">
Order Summary
{purchaseType === 'gift' && (
<Badge variant="secondary" className="font-poppins">
<Mail className="w-3 h-3 mr-1" />
Gift Purchase
</Badge>
)}
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Gift Indicator */}
{purchaseType === 'gift' && giftData.recipientName && (
<div className="bg-gradient-to-r from-primary/10 to-secondary/10 border border-primary/20 rounded-lg p-4">
<div className="flex items-start gap-3">
<Mail className="w-5 h-5 text-primary mt-0.5" />
<div className="flex-1">
<p className="font-medium text-sm font-poppins mb-1">Gift Recipient</p>
<p className="text-sm text-gray-700 font-poppins">{giftData.recipientName}</p>
<p className="text-xs text-gray-500 font-poppins mt-1">{giftData.recipientEmail}</p>
</div>
</div>
</div>
)}
{/* Cart Items */}
<div className="space-y-4">
{mockCartItems.map((item) => (
<div key={item.id} className="flex gap-4">
<ImageWithFallback
src={item.image}
alt={item.name}
className="w-20 h-20 rounded-lg object-cover"
/>
<div className="flex-1 min-w-0">
<h4 className="font-medium text-base mb-1 font-poppins">{item.name}</h4>
<p className="text-sm text-gray-500 mb-2 font-poppins">{item.type}</p>
<div className="flex items-center gap-2 mb-2">
<span className="font-bold text-lg font-poppins">{item.price}</span>
<span className="text-sm text-gray-500 line-through font-poppins">{item.originalPrice}</span>
<Badge variant="outline" className="text-xs">
-{item.discount}%
</Badge>
</div>
<div className="text-sm text-gray-600 font-poppins">
{item.attractions} attractions Valid for {item.validity}
</div>
</div>
</div>
))}
</div>
<Separator />
{/* Features Summary */}
<div className="space-y-3">
<h4 className="font-medium font-merchant">What's Included:</h4>
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm font-poppins">
<Check className="w-4 h-4 text-green-600" />
<span>Skip-the-line access to {mockCartItems[0].attractions} attractions</span>
</div>
<div className="flex items-center gap-2 text-sm font-poppins">
<Check className="w-4 h-4 text-green-600" />
<span>Valid for {mockCartItems[0].validity}</span>
</div>
<div className="flex items-center gap-2 text-sm font-poppins">
<Check className="w-4 h-4 text-green-600" />
<span>Instant mobile delivery</span>
</div>
<div className="flex items-center gap-2 text-sm font-poppins">
<Check className="w-4 h-4 text-green-600" />
<span>Free cancellation</span>
</div>
</div>
</div>
<Separator />
{/* Pricing Breakdown */}
<div className="space-y-3">
<div className="flex justify-between text-sm font-poppins">
<span>Subtotal</span>
<span>€{subtotal}</span>
</div>
<div className="flex justify-between text-sm font-poppins">
<span>Taxes & Fees</span>
<span>€{tax}</span>
</div>
<div className="flex justify-between text-sm text-green-600 font-poppins">
<span>Total Savings</span>
<span>-€{totalSavings}</span>
</div>
<Separator />
<div className="flex justify-between font-bold text-xl font-poppins">
<span>Total</span>
<span>€{total}</span>
</div>
</div>
{/* Checkout Button */}
<Button
type="button"
onClick={() => onSecureCheckoutClick?.()}
className="w-full bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-medium py-4 text-lg font-poppins"
disabled={
!formData.agreeTerms ||
!formData.email ||
!formData.firstName ||
!formData.lastName ||
!isEmailVerified ||
(purchaseType === 'gift' && (!giftData.recipientName || !giftData.recipientEmail || !giftData.recipientPhone))
}
>
{purchaseType === 'gift' ? 'Send Gift & Proceed to Checkout' : 'Proceed to Secure Checkout'}
<ChevronRight className="w-5 h-5 ml-2" />
</Button>
{/* Security Notice */}
<div className="flex items-center gap-2 text-xs text-gray-500 justify-center font-poppins">
<Shield className="w-4 h-4" />
<span>Secure 256-bit SSL encryption</span>
</div>
</CardContent>
</Card>
</motion.div>
</div>
</form>
</div>
</section>
{/* Email Verification Popup */}
<Dialog open={showEmailVerification} onOpenChange={setShowEmailVerification}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 font-merchant">
<Mail className="w-5 h-5" />
Verify Your Email
</DialogTitle>
<DialogDescription className="font-poppins">
Enter the verification code sent to your email to continue with your booking.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<p className="text-sm text-gray-600 font-poppins">
We've sent a verification code to <strong>{formData.email}</strong>
</p>
<div>
<Label htmlFor="verification" className="font-poppins">Verification Code</Label>
<Input
id="verification"
value={verificationCode}
onChange={(e) => setVerificationCode(e.target.value)}
placeholder="Enter 6-digit code"
className="mt-1 font-poppins text-center text-lg letter-spacing-2"
maxLength={6}
/>
</div>
<div className="flex gap-3">
<Button
onClick={handleEmailVerification}
className="flex-1 bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-poppins"
disabled={verificationCode.length !== 6}
>
Verify Email
</Button>
<Button
variant="outline"
onClick={() => setShowEmailVerification(false)}
className="font-poppins"
>
Skip for now
</Button>
</div>
<p className="text-xs text-gray-500 text-center font-poppins">
Demo code: <strong>123456</strong>
</p>
</div>
</DialogContent>
</Dialog>
{/* Footer */}
<Footer
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onPassesClick={onPassesClick}
onSignInClick={onSignInClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onContactUsClick={onContactUsClick}
currentPage={currentPage}
/>
</div>
);
}

View File

@@ -0,0 +1,685 @@
import { useState, useEffect } from 'react';
import { motion } from 'motion/react';
import { Search, MapPin, Clock, Star, Users, Filter, X } from 'lucide-react';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Card, CardContent } from './ui/card';
import { Badge } from './ui/badge';
import { Checkbox } from './ui/checkbox';
import { Slider } from './ui/slider';
import Navbar from './Navbar';
import { CitySubmenu } from './CitySubmenu';
import { EnhancedTestimonials } from './EnhancedTestimonials';
import { Footer } from './Footer';
import { ImageWithFallback } from './figma/ImageWithFallback';
interface City {
id: string;
name: string;
country: string;
description: string;
image: string;
region: string;
attractionsCount: number;
rating: number;
visitDuration: string;
startingPrice: number;
currency: string;
category: string;
status: 'available' | 'coming-soon';
isPopular?: boolean;
passesAvailable: number;
}
const cities: City[] = [
{
id: 'melbourne',
name: 'Melbourne',
country: 'Australia',
description: 'Cultural capital with world-famous coffee culture, street art, and vibrant laneways',
image: 'https://images.unsplash.com/photo-1624341373902-70e3a8dc9acc?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBjaXR5JTIwc2t5bGluZXxlbnwxfHx8fDE3NTc2NjM0ODN8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
region: 'Australia & Oceania',
attractionsCount: 30,
rating: 4.8,
visitDuration: '3-7 days',
startingPrice: 49,
currency: 'AUD',
category: 'culture',
status: 'available',
isPopular: true,
passesAvailable: 3
},
{
id: 'sydney',
name: 'Sydney',
country: 'Australia',
description: 'Iconic harbor city with the Opera House, Harbour Bridge, and stunning beaches',
image: 'https://images.unsplash.com/photo-1523059623039-a9ed027e7fad?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzeWRuZXklMjBvcGVyYSUyMGhvdXNlfGVufDF8fHx8MTc1NzU5MDk3MXww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
region: 'Australia & Oceania',
attractionsCount: 25,
rating: 4.7,
visitDuration: '3-6 days',
startingPrice: 52,
currency: 'AUD',
category: 'adventure',
status: 'coming-soon',
isPopular: true,
passesAvailable: 2
},
{
id: 'paris',
name: 'Paris',
country: 'France',
description: 'City of Light with world-famous landmarks, art museums, and romantic atmosphere',
image: 'https://images.unsplash.com/photo-1431274172761-fca41d930114?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxwYXJpcyUyMGVpZmZlbCUyMHRvd2VyfGVufDF8fHx8MTc1NzYxNjQ2OXww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
region: 'Europe',
attractionsCount: 45,
rating: 4.9,
visitDuration: '4-8 days',
startingPrice: 55,
currency: 'EUR',
category: 'culture',
status: 'coming-soon',
isPopular: true,
passesAvailable: 4
},
{
id: 'london',
name: 'London',
country: 'United Kingdom',
description: 'Historic capital with royal palaces, world-class museums, and modern culture',
image: 'https://images.unsplash.com/photo-1486299267070-83823f5448dd?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxsb25kb24lMjBiaWclMjBiZW58ZW58MXx8fHwxNzU3NjQ3OTYxfDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
region: 'Europe',
attractionsCount: 38,
rating: 4.8,
visitDuration: '4-7 days',
startingPrice: 58,
currency: 'GBP',
category: 'culture',
status: 'coming-soon',
passesAvailable: 3
},
{
id: 'tokyo',
name: 'Tokyo',
country: 'Japan',
description: 'Vibrant metropolis blending ancient traditions with cutting-edge technology',
image: 'https://images.unsplash.com/photo-1588486691401-93624c48459b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx0b2t5byUyMGNpdHklMjBsYW5kc2NhcGV8ZW58MXx8fHwxNzU3NjYzNDk5fDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
region: 'Asia',
attractionsCount: 52,
rating: 4.9,
visitDuration: '5-10 days',
startingPrice: 68,
currency: 'JPY',
category: 'culture',
status: 'coming-soon',
isPopular: true,
passesAvailable: 5
},
{
id: 'rome',
name: 'Rome',
country: 'Italy',
description: 'Eternal city with ancient history, timeless beauty, and incredible cuisine',
image: 'https://images.unsplash.com/photo-1706884027668-4b2a1a9701ed?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxyb21lJTIwY29sb3NzZXVtJTIwYW5jaWVudHxlbnwxfHx8fDE3NTc2Mzk3NDN8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
region: 'Europe',
attractionsCount: 34,
rating: 4.8,
visitDuration: '3-6 days',
startingPrice: 45,
currency: 'EUR',
category: 'culture',
status: 'coming-soon',
passesAvailable: 3
},
{
id: 'barcelona',
name: 'Barcelona',
country: 'Spain',
description: 'Architectural wonder with Gaudí masterpieces, beaches, and vibrant nightlife',
image: 'https://images.unsplash.com/photo-1539037116277-4db20889f2d4?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxiYXJjZWxvbmElMjBjaXR5fGVufDF8fHx8MTc1NzY2MzUyOXww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
region: 'Europe',
attractionsCount: 29,
rating: 4.7,
visitDuration: '3-5 days',
startingPrice: 42,
currency: 'EUR',
category: 'culture',
status: 'coming-soon',
passesAvailable: 2
},
{
id: 'new-york',
name: 'New York',
country: 'United States',
description: 'The city that never sleeps with iconic skyline, Broadway, and world-class dining',
image: 'https://images.unsplash.com/photo-1496442226666-8d4d0e62e6e9?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxuZXclMjB5b3JrJTIwY2l0eXxlbnwxfHx8fDE3NTc2NjM1MzN8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
region: 'North America',
attractionsCount: 67,
rating: 4.6,
visitDuration: '4-8 days',
startingPrice: 75,
currency: 'USD',
category: 'adventure',
status: 'coming-soon',
passesAvailable: 6
}
];
interface CitiesPageProps {
onBackClick: () => void;
onHomeClick: () => void;
onMelbourneClick: () => void;
onPassesClick: () => void;
onCitiesClick: () => void;
onDealsClick: () => void;
onCheckoutClick: () => void;
onSignInClick: () => void;
onAttractionsClick: () => void;
onBlogsClick: () => void;
onHowItWorksClick: () => void;
onFAQClick: () => void;
onPrivacyPolicyClick: () => void;
onAboutUsClick: () => void;
onProfileClick: () => void;
onCityClick?: (cityId: string) => void;
currentPage: string;
}
export function CitiesPage({
onBackClick,
onHomeClick,
onMelbourneClick,
onPassesClick,
onCitiesClick,
onDealsClick,
onCheckoutClick,
onSignInClick,
onAttractionsClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onProfileClick,
onCityClick,
currentPage
}: CitiesPageProps) {
const [searchQuery, setSearchQuery] = useState('');
const [selectedRegions, setSelectedRegions] = useState<string[]>([]);
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
const [selectedStatus, setSelectedStatus] = useState<string[]>([]);
const [priceRange, setPriceRange] = useState([0, 100]);
const [showMobileFilters, setShowMobileFilters] = useState(false);
const [pageNumber, setPageNumber] = useState(1);
const [itemsPerPage] = useState(9); // Show 9 cities per page (3 rows of 3)
const regions = [
{ value: 'australia-oceania', label: 'Australia & Oceania', count: 2 },
{ value: 'europe', label: 'Europe', count: 4 },
{ value: 'asia', label: 'Asia', count: 1 },
{ value: 'north-america', label: 'North America', count: 1 }
];
const categories = [
{ value: 'culture', label: 'Culture', count: 6 },
{ value: 'adventure', label: 'Adventure', count: 2 }
];
const statuses = [
{ value: 'available', label: 'Available Now', count: 1 },
{ value: 'coming-soon', label: 'Coming Soon', count: 7 }
];
const filteredCities = cities.filter(city => {
const matchesSearch = city.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
city.country.toLowerCase().includes(searchQuery.toLowerCase()) ||
city.description.toLowerCase().includes(searchQuery.toLowerCase());
const matchesRegion = selectedRegions.length === 0 ||
selectedRegions.some(region =>
city.region.toLowerCase().replace(/[^a-z]/g, '-') === region
);
const matchesCategory = selectedCategories.length === 0 || selectedCategories.includes(city.category);
const matchesStatus = selectedStatus.length === 0 || selectedStatus.includes(city.status);
const matchesPrice = city.startingPrice >= priceRange[0] && city.startingPrice <= priceRange[1];
return matchesSearch && matchesRegion && matchesCategory && matchesStatus && matchesPrice;
});
// Pagination logic
const totalPages = Math.ceil(filteredCities.length / itemsPerPage);
const startIndex = (pageNumber - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const currentCities = filteredCities.slice(startIndex, endIndex);
const totalItems = filteredCities.length;
const showingFrom = totalItems > 0 ? startIndex + 1 : 0;
const showingTo = Math.min(endIndex, totalItems);
// Reset to first page when filters change
useEffect(() => {
setPageNumber(1);
}, [searchQuery, selectedRegions, selectedCategories, selectedStatus, priceRange]);
const toggleRegion = (region: string) => {
setSelectedRegions(prev =>
prev.includes(region)
? prev.filter(r => r !== region)
: [...prev, region]
);
};
const toggleCategory = (category: string) => {
setSelectedCategories(prev =>
prev.includes(category)
? prev.filter(c => c !== category)
: [...prev, category]
);
};
const toggleStatus = (status: string) => {
setSelectedStatus(prev =>
prev.includes(status)
? prev.filter(s => s !== status)
: [...prev, status]
);
};
const clearAllFilters = () => {
setSelectedRegions([]);
setSelectedCategories([]);
setSelectedStatus([]);
setPriceRange([0, 100]);
setSearchQuery('');
};
const FilterSidebar = ({ isMobile = false }) => (
<div className={`bg-white ${isMobile ? 'p-6' : 'p-6 sticky top-44'} rounded-lg border border-gray-100 h-fit`}>
<div className="flex items-center justify-between mb-6">
<h3 className="font-semibold text-gray-900">Filters</h3>
{isMobile && (
<Button
variant="ghost"
size="sm"
onClick={() => setShowMobileFilters(false)}
>
<X className="w-4 h-4" />
</Button>
)}
</div>
{/* Search by region */}
<div className="mb-6">
<h4 className="font-medium text-gray-900 mb-3">Region</h4>
<div className="space-y-3">
{regions.map(region => (
<div key={region.value} className="flex items-center space-x-3">
<Checkbox
id={region.value}
checked={selectedRegions.includes(region.value)}
onCheckedChange={() => toggleRegion(region.value)}
/>
<label
htmlFor={region.value}
className="text-sm text-gray-700 cursor-pointer flex-1"
>
{region.label} ({region.count})
</label>
</div>
))}
</div>
</div>
{/* Category */}
<div className="mb-6">
<h4 className="font-medium text-gray-900 mb-3">Category</h4>
<div className="space-y-3">
{categories.map(category => (
<div key={category.value} className="flex items-center space-x-3">
<Checkbox
id={category.value}
checked={selectedCategories.includes(category.value)}
onCheckedChange={() => toggleCategory(category.value)}
/>
<label
htmlFor={category.value}
className="text-sm text-gray-700 cursor-pointer flex-1"
>
{category.label} ({category.count})
</label>
</div>
))}
</div>
</div>
{/* Status */}
<div className="mb-6">
<h4 className="font-medium text-gray-900 mb-3">Availability</h4>
<div className="space-y-3">
{statuses.map(status => (
<div key={status.value} className="flex items-center space-x-3">
<Checkbox
id={status.value}
checked={selectedStatus.includes(status.value)}
onCheckedChange={() => toggleStatus(status.value)}
/>
<label
htmlFor={status.value}
className="text-sm text-gray-700 cursor-pointer flex-1"
>
{status.label} ({status.count})
</label>
</div>
))}
</div>
</div>
{/* Price Range */}
<div className="mb-6">
<h4 className="font-medium text-gray-900 mb-3">Starting Price Range</h4>
<div className="px-3">
<Slider
value={priceRange}
onValueChange={setPriceRange}
min={0}
max={100}
step={5}
className="mb-3"
/>
<div className="flex justify-between text-sm text-gray-600">
<span>${priceRange[0]}</span>
<span>${priceRange[1]}</span>
</div>
</div>
</div>
{/* Clear filters */}
<Button
variant="outline"
onClick={clearAllFilters}
className="w-full"
>
Clear All Filters
</Button>
</div>
);
const handleCityClick = (cityId: string) => {
if (cityId === 'melbourne') {
onMelbourneClick();
} else {
onCityClick?.(cityId);
}
};
return (
<div className="min-h-screen bg-background">
<Navbar
activeCity=""
onCityChange={(city) => {
if (city === 'Melbourne') {
onMelbourneClick();
}
}}
onSignInClick={onSignInClick}
onPassesClick={onPassesClick}
onCitiesClick={() => {}} // Already on cities page
onDealsClick={onDealsClick}
onCheckoutClick={onCheckoutClick}
onHomeClick={onHomeClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick}
currentPage="cities"
isUserSignedIn={!!user}
user={user}
/>
<div className="container mx-auto px-4 py-8">
<div className="flex flex-col lg:flex-row gap-8 pt-32">
{/* Desktop Sidebar */}
<div className="hidden lg:block lg:w-1/4">
<FilterSidebar />
</div>
{/* Main Content */}
<div className="flex-1">
{/* Header */}
<div className="mb-8">
<h1 className="mb-2">Explore <span className="font-bold italic">Cities</span></h1>
<p className="text-gray-600">
Discover amazing destinations around the world with our curated city passes
</p>
</div>
{/* Search Bar */}
<div className="mb-8">
<div className="relative max-w-md">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
<Input
placeholder="Search cities..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 bg-white border-gray-200"
/>
</div>
</div>
{/* Mobile Filter Button */}
<div className="lg:hidden mb-6">
<Button
variant="outline"
onClick={() => setShowMobileFilters(true)}
className="w-full"
>
<Filter className="w-4 h-4 mr-2" />
Filters ({selectedRegions.length + selectedCategories.length + selectedStatus.length})
</Button>
</div>
{/* Results Count */}
<div className="mb-6">
<p className="text-gray-600">
Showing {showingFrom}-{showingTo} of {totalItems} city(s)
</p>
</div>
{/* Cities Grid */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.2 }}
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
>
{currentCities.map((city, index) => (
<motion.div
key={city.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
<Card
className="group cursor-pointer interactive-card h-full"
onClick={() => handleCityClick(city.id)}
>
<CardContent className="p-0">
<div className="relative">
<ImageWithFallback
src={city.image}
alt={city.name}
className="w-full h-48 object-cover rounded-t-lg"
/>
{city.isPopular && (
<Badge className="absolute top-3 left-3 bg-primary text-white">
Popular
</Badge>
)}
<div className="absolute top-3 right-3 bg-white rounded-full p-2 shadow-sm">
<Star className="w-4 h-4 fill-current text-yellow-500" />
</div>
<Badge
className={`absolute bottom-3 right-3 ${
city.status === 'available'
? 'bg-green-600 text-white'
: 'bg-orange-600 text-white'
}`}
>
{city.status === 'available' ? 'Available' : 'Coming Soon'}
</Badge>
</div>
<div className="p-4">
<div className="flex items-start justify-between mb-2">
<div className="flex-1">
<p className="text-sm text-gray-500 mb-1">{city.country}</p>
<h3 className="font-semibold text-gray-900 group-hover:text-primary transition-colors">
{city.name}
</h3>
</div>
</div>
<p className="text-sm text-gray-600 mb-3 line-clamp-2">
{city.description}
</p>
<div className="flex items-center gap-4 text-sm text-gray-500 mb-3">
<div className="flex items-center gap-1">
<Users className="w-4 h-4" />
<span>{city.attractionsCount} attractions</span>
</div>
<div className="flex items-center gap-1">
<Clock className="w-4 h-4" />
<span>{city.visitDuration}</span>
</div>
</div>
<div className="flex items-center gap-4 text-sm text-gray-500 mb-3">
<div className="flex items-center gap-1">
<Star className="w-4 h-4 fill-current text-yellow-500" />
<span>{city.rating}</span>
</div>
<div className="flex items-center gap-1">
<MapPin className="w-4 h-4" />
<span>{city.region}</span>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-1">
<span className="text-sm text-gray-500">From</span>
<span className="font-semibold">${city.startingPrice} {city.currency}</span>
</div>
<Badge
variant="secondary"
className="text-xs"
>
{city.passesAvailable} {city.passesAvailable === 1 ? 'Pass' : 'Passes'}
</Badge>
</div>
</div>
</CardContent>
</Card>
</motion.div>
))}
</motion.div>
{totalItems === 0 && (
<div className="text-center py-12">
<p className="text-gray-500 mb-4">No cities found matching your criteria</p>
<Button onClick={clearAllFilters} variant="outline">
Clear All Filters
</Button>
</div>
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="mt-12 flex flex-col sm:flex-row items-center justify-between gap-4">
<div className="text-sm text-gray-600">
Page {pageNumber} of {totalPages}
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
onClick={() => setPageNumber(Math.max(1, pageNumber - 1))}
disabled={pageNumber === 1}
className="px-3 py-2"
>
Previous
</Button>
{/* Page numbers */}
<div className="flex items-center gap-1">
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
let pageNum;
if (totalPages <= 5) {
pageNum = i + 1;
} else if (pageNumber <= 3) {
pageNum = i + 1;
} else if (pageNumber >= totalPages - 2) {
pageNum = totalPages - 4 + i;
} else {
pageNum = pageNumber - 2 + i;
}
return (
<Button
key={pageNum}
variant={pageNumber === pageNum ? "default" : "outline"}
onClick={() => setPageNumber(pageNum)}
className="px-3 py-2 min-w-[40px]"
>
{pageNum}
</Button>
);
})}
</div>
<Button
variant="outline"
onClick={() => setPageNumber(Math.min(totalPages, pageNumber + 1))}
disabled={pageNumber === totalPages}
className="px-3 py-2"
>
Next
</Button>
</div>
</div>
)}
</div>
</div>
</div>
{/* Mobile Filter Modal */}
{showMobileFilters && (
<div className="fixed inset-0 z-50 lg:hidden">
<div className="fixed inset-0 bg-black bg-opacity-50" onClick={() => setShowMobileFilters(false)} />
<div className="fixed inset-y-0 left-0 w-80 bg-white overflow-y-auto">
<FilterSidebar isMobile />
</div>
</div>
)}
{/* Enhanced Testimonials Section */}
<EnhancedTestimonials />
<Footer
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onPassesClick={onPassesClick}
onSignInClick={onSignInClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
currentPage={currentPage}
/>
</div>
);
}

View File

@@ -0,0 +1,747 @@
import { useState, useEffect } from 'react';
import { motion } from 'motion/react';
import { Search, MapPin, Clock, Star, DollarSign, Filter, X, ArrowLeft } from 'lucide-react';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Card, CardContent } from './ui/card';
import { Badge } from './ui/badge';
import { Checkbox } from './ui/checkbox';
import { Slider } from './ui/slider';
import Navbar from './Navbar';
import { EnhancedTestimonials } from './EnhancedTestimonials';
import { Footer } from './Footer';
import { ImageWithFallback } from './figma/ImageWithFallback';
interface Attraction {
id: string;
name: string;
description: string;
image: string;
location: string;
duration: string;
rating: number;
price: number;
category: string;
passType: 'unlimited' | 'selective' | 'both';
isPopular?: boolean;
}
interface CityData {
id: string;
name: string;
country: string;
hero_image: string;
currency: string;
description: string;
attractions: Attraction[];
}
const cityAttractions: Record<string, CityData> = {
paris: {
id: 'paris',
name: 'Paris',
country: 'France',
hero_image: 'https://images.unsplash.com/photo-1652254693457-5f6d7db674c6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxwYXJpcyUyMHRvdXJpc20lMjBhdHRyYWN0aW9uc3xlbnwxfHx8fDE3NTc2NjQwMjB8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
currency: 'EUR',
description: 'City of Light with world-famous landmarks, art museums, and romantic atmosphere',
attractions: [
{
id: 'eiffel-tower',
name: 'Eiffel Tower',
description: 'Iconic iron tower offering breathtaking views of Paris from its observation decks',
image: 'https://images.unsplash.com/photo-1431274172761-fca41d930114?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxwYXJpcyUyMGVpZmZlbCUyMHRvd2VyfGVufDF8fHx8MTc1NzYxNjQ2OXww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
location: 'Champ de Mars',
duration: '2-3 hours',
rating: 4.5,
price: 29,
category: 'culture',
passType: 'unlimited',
isPopular: true
},
{
id: 'louvre-museum',
name: 'Louvre Museum',
description: 'World\'s largest art museum housing the Mona Lisa and countless masterpieces',
image: 'https://images.unsplash.com/photo-1566139536744-6270a2e5a3a3?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxsb3V2cmUlMjBtdXNldW18ZW58MXx8fHwxNzU3NjY0MDY3fDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
location: 'Palais Royal',
duration: '3-4 hours',
rating: 4.7,
price: 17,
category: 'culture',
passType: 'unlimited',
isPopular: true
},
{
id: 'notre-dame',
name: 'Notre-Dame Cathedral',
description: 'Gothic masterpiece and historic cathedral in the heart of Paris',
image: 'https://images.unsplash.com/photo-1539650116574-75c0c6d0d0e3?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxub3RyZSUyMGRhbWUlMjBwYXJpc3xlbnwxfHx8fDE3NTc2NjQwNzF8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
location: 'Île de la Cité',
duration: '1-2 hours',
rating: 4.6,
price: 0,
category: 'culture',
passType: 'unlimited'
},
{
id: 'arc-de-triomphe',
name: 'Arc de Triomphe',
description: 'Monumental arch honoring those who fought for France, with panoramic city views',
image: 'https://images.unsplash.com/photo-1598975106642-dc3a8c79c854?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxhcmMlMjBkZSUyMHRyaW9tcGhlfGVufDF8fHx8MTc1NzY2NDA3NXww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
location: 'Champs-Élysées',
duration: '1-2 hours',
rating: 4.4,
price: 13,
category: 'culture',
passType: 'both'
},
{
id: 'seine-cruise',
name: 'Seine River Cruise',
description: 'Scenic boat tour along the Seine passing famous Parisian landmarks',
image: 'https://images.unsplash.com/photo-1502602898536-47ad22581b52?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzZWluZSUyMHJpdmVyJTIwY3J1aXNlfGVufDF8fHx8MTc1NzY2NDA3OXww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
location: 'Various Docks',
duration: '1 hour',
rating: 4.3,
price: 15,
category: 'adventure',
passType: 'both'
},
{
id: 'montmartre',
name: 'Montmartre & Sacré-Cœur',
description: 'Bohemian hilltop district with the stunning Sacré-Cœur Basilica',
image: 'https://images.unsplash.com/photo-1471939743851-c4df8b6ee c95?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtb250bWFydHJlJTIwc2FjcmUlMjBjb2V1cnxlbnwxfHx8fDE3NTc2NjQwODN8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
location: 'Montmartre',
duration: '2-4 hours',
rating: 4.5,
price: 0,
category: 'culture',
passType: 'unlimited'
}
]
},
tokyo: {
id: 'tokyo',
name: 'Tokyo',
country: 'Japan',
hero_image: 'https://images.unsplash.com/photo-1713263367828-9eafd7fc3797?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx0b2t5byUyMGF0dHJhY3Rpb25zJTIwY2l0eXNjYXBlfGVufDF8fHx8MTc1NzY2NDAyNHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
currency: 'JPY',
description: 'Vibrant metropolis blending ancient traditions with cutting-edge technology',
attractions: [
{
id: 'tokyo-tower',
name: 'Tokyo Tower',
description: 'Iconic red tower offering spectacular city views and broadcasting facilities',
image: 'https://images.unsplash.com/photo-1598976838083-ad00095bb123?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx0b2t5byUyMHRvd2VyfGVufDF8fHx8MTc1NzY2NDEwOHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
location: 'Minato',
duration: '2-3 hours',
rating: 4.4,
price: 25,
category: 'adventure',
passType: 'unlimited',
isPopular: true
},
{
id: 'sensoji-temple',
name: 'Sensoji Temple',
description: 'Ancient Buddhist temple and Tokyo\'s oldest temple with traditional atmosphere',
image: 'https://images.unsplash.com/photo-1503899036084-c55cdd92da26?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzZW5zb2ppJTIwdGVtcGxlfGVufDF8fHx8MTc1NzY2NDExMnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
location: 'Asakusa',
duration: '1-2 hours',
rating: 4.6,
price: 0,
category: 'culture',
passType: 'unlimited'
},
{
id: 'shibuya-crossing',
name: 'Shibuya Crossing',
description: 'World\'s busiest pedestrian crossing and symbol of modern Tokyo',
image: 'https://images.unsplash.com/photo-1542051841857-5f90071e7989?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzaGlidXlhJTIwY3Jvc3Npbmd8ZW58MXx8fHwxNzU3NjY0MTE2fDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
location: 'Shibuya',
duration: '1 hour',
rating: 4.3,
price: 0,
category: 'adventure',
passType: 'unlimited',
isPopular: true
},
{
id: 'tokyo-skytree',
name: 'Tokyo Skytree',
description: 'World\'s second tallest structure with observation decks and stunning views',
image: 'https://images.unsplash.com/photo-1576538509036-c47f3604da84?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx0b2t5byUyMHNreXRyZWV8ZW58MXx8fHwxNzU3NjY0MTIwfDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
location: 'Sumida',
duration: '2-3 hours',
rating: 4.5,
price: 32,
category: 'adventure',
passType: 'selective',
isPopular: true
}
]
},
london: {
id: 'london',
name: 'London',
country: 'United Kingdom',
hero_image: 'https://images.unsplash.com/photo-1645544865499-8b768cf70562?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxsb25kb24lMjBsYW5kbWFya3MlMjB0b3VyaXNtfGVufDF8fHx8MTc1NzY2NDAyOHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
currency: 'GBP',
description: 'Historic capital with royal palaces, world-class museums, and modern culture',
attractions: [
{
id: 'big-ben',
name: 'Big Ben & Parliament',
description: 'Iconic clock tower and seat of British government with guided tours',
image: 'https://images.unsplash.com/photo-1486299267070-83823f5448dd?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxsb25kb24lMjBiaWclMjBiZW58ZW58MXx8fHwxNzU3NjQ3OTYxfDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
location: 'Westminster',
duration: '2 hours',
rating: 4.7,
price: 28,
category: 'culture',
passType: 'unlimited',
isPopular: true
},
{
id: 'british-museum',
name: 'British Museum',
description: 'World-renowned museum with artifacts from across human history',
image: 'https://images.unsplash.com/photo-1603838354146-f5ffc0b45cd9?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxicml0aXNoJTIwbXVzZXVtfGVufDF8fHx8MTc1NzY2NDE0NHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
location: 'Bloomsbury',
duration: '3-4 hours',
rating: 4.6,
price: 0,
category: 'culture',
passType: 'unlimited'
},
{
id: 'tower-bridge',
name: 'Tower Bridge',
description: 'Victorian bascule bridge with glass floor and panoramic walkways',
image: 'https://images.unsplash.com/photo-1513635269975-59663e0ac1ad?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx0b3dlciUyMGJyaWRnZSUyMGxvbmRvbnxlbnwxfHx8fDE3NTc2NjQxNDh8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
location: 'Tower Hamlets',
duration: '1-2 hours',
rating: 4.4,
price: 12,
category: 'culture',
passType: 'both',
isPopular: true
},
{
id: 'buckingham-palace',
name: 'Buckingham Palace',
description: 'Official residence of the British monarch with state rooms tours',
image: 'https://images.unsplash.com/photo-1529655683826-ac6bbde65424?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxidWNraW5naGFtJTIwcGFsYWNlfGVufDF8fHx8MTc1NzY2NDE1Mnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral',
location: 'Westminster',
duration: '2-3 hours',
rating: 4.3,
price: 30,
category: 'culture',
passType: 'selective'
}
]
}
};
interface CityAttractionsPageProps {
cityId: string;
onBackClick: () => void;
onHomeClick: () => void;
onMelbourneClick: () => void;
onPassesClick: () => void;
onCitiesClick: () => void;
onSignInClick: () => void;
onAttractionsClick: () => void;
onBlogsClick: () => void;
onHowItWorksClick: () => void;
onFAQClick: () => void;
onPrivacyPolicyClick: () => void;
onAboutUsClick: () => void;
onAttractionClick?: (attractionId: string) => void;
currentPage: string;
}
export function CityAttractionsPage({
cityId,
onBackClick,
onHomeClick,
onMelbourneClick,
onPassesClick,
onCitiesClick,
onSignInClick,
onAttractionsClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onAttractionClick,
currentPage
}: CityAttractionsPageProps) {
const [searchQuery, setSearchQuery] = useState('');
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
const [selectedPassTypes, setSelectedPassTypes] = useState<string[]>([]);
const [priceRange, setPriceRange] = useState([0, 50]);
const [showMobileFilters, setShowMobileFilters] = useState(false);
const [pageNumber, setPageNumber] = useState(1);
const [itemsPerPage] = useState(12);
// Get city data
const cityData = cityAttractions[cityId];
if (!cityData) {
return (
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold mb-4">City Not Found</h1>
<p className="text-gray-600 mb-4">Sorry, this city is not available yet.</p>
<Button onClick={onBackClick}>Go Back</Button>
</div>
</div>
);
}
const categories = [
{ value: 'adventure', label: 'Adventure', count: cityData.attractions.filter(a => a.category === 'adventure').length },
{ value: 'culture', label: 'Culture', count: cityData.attractions.filter(a => a.category === 'culture').length },
{ value: 'family', label: 'Family Friendly', count: cityData.attractions.filter(a => a.category === 'family').length }
].filter(cat => cat.count > 0);
const passTypes = [
{ value: 'unlimited', label: 'Unlimited', count: cityData.attractions.filter(a => a.passType === 'unlimited').length },
{ value: 'selective', label: 'Selective', count: cityData.attractions.filter(a => a.passType === 'selective').length },
{ value: 'both', label: 'Both', count: cityData.attractions.filter(a => a.passType === 'both').length }
].filter(type => type.count > 0);
const filteredAttractions = cityData.attractions.filter(attraction => {
const matchesSearch = attraction.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
attraction.description.toLowerCase().includes(searchQuery.toLowerCase());
const matchesCategory = selectedCategories.length === 0 || selectedCategories.includes(attraction.category);
const matchesPassType = selectedPassTypes.length === 0 || selectedPassTypes.includes(attraction.passType);
const matchesPrice = attraction.price >= priceRange[0] && attraction.price <= priceRange[1];
return matchesSearch && matchesCategory && matchesPassType && matchesPrice;
});
// Pagination logic
const totalPages = Math.ceil(filteredAttractions.length / itemsPerPage);
const startIndex = (pageNumber - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const currentAttractions = filteredAttractions.slice(startIndex, endIndex);
const totalItems = filteredAttractions.length;
const showingFrom = totalItems > 0 ? startIndex + 1 : 0;
const showingTo = Math.min(endIndex, totalItems);
// Reset to first page when filters change
useEffect(() => {
setPageNumber(1);
}, [searchQuery, selectedCategories, selectedPassTypes, priceRange]);
const toggleCategory = (category: string) => {
setSelectedCategories(prev =>
prev.includes(category)
? prev.filter(c => c !== category)
: [...prev, category]
);
};
const togglePassType = (passType: string) => {
setSelectedPassTypes(prev =>
prev.includes(passType)
? prev.filter(p => p !== passType)
: [...prev, passType]
);
};
const clearAllFilters = () => {
setSelectedCategories([]);
setSelectedPassTypes([]);
setPriceRange([0, 50]);
setSearchQuery('');
};
const FilterSidebar = ({ isMobile = false }) => (
<div className={`bg-white ${isMobile ? 'p-6' : 'p-6 sticky top-44'} rounded-lg border border-gray-100 h-fit`}>
<div className="flex items-center justify-between mb-6">
<h3 className="font-semibold text-gray-900">Filters</h3>
{isMobile && (
<Button
variant="ghost"
size="sm"
onClick={() => setShowMobileFilters(false)}
>
<X className="w-4 h-4" />
</Button>
)}
</div>
{/* Categories */}
{categories.length > 0 && (
<div className="mb-6">
<h4 className="font-medium text-gray-900 mb-3">Category</h4>
<div className="space-y-3">
{categories.map(category => (
<div key={category.value} className="flex items-center space-x-3">
<Checkbox
id={category.value}
checked={selectedCategories.includes(category.value)}
onCheckedChange={() => toggleCategory(category.value)}
/>
<label
htmlFor={category.value}
className="text-sm text-gray-700 cursor-pointer flex-1"
>
{category.label} ({category.count})
</label>
</div>
))}
</div>
</div>
)}
{/* Pass Types */}
{passTypes.length > 0 && (
<div className="mb-6">
<h4 className="font-medium text-gray-900 mb-3">Pass Type</h4>
<div className="space-y-3">
{passTypes.map(passType => (
<div key={passType.value} className="flex items-center space-x-3">
<Checkbox
id={passType.value}
checked={selectedPassTypes.includes(passType.value)}
onCheckedChange={() => togglePassType(passType.value)}
/>
<label
htmlFor={passType.value}
className="text-sm text-gray-700 cursor-pointer flex-1"
>
{passType.label} ({passType.count})
</label>
</div>
))}
</div>
</div>
)}
{/* Price Range */}
<div className="mb-6">
<h4 className="font-medium text-gray-900 mb-3">Price Range ({cityData.currency})</h4>
<div className="px-3">
<Slider
value={priceRange}
onValueChange={setPriceRange}
min={0}
max={50}
step={5}
className="mb-3"
/>
<div className="flex justify-between text-sm text-gray-600">
<span>{priceRange[0]} {cityData.currency}</span>
<span>{priceRange[1]} {cityData.currency}</span>
</div>
</div>
</div>
{/* Clear filters */}
<Button
variant="outline"
onClick={clearAllFilters}
className="w-full"
>
Clear All Filters
</Button>
</div>
);
return (
<div className="min-h-screen bg-background">
<Navbar
activeCity=""
onCityChange={() => {}}
onSignInClick={onSignInClick}
onPassesClick={onPassesClick}
onCitiesClick={onCitiesClick}
onHomeClick={onHomeClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
currentPage="cities"
isUserSignedIn={!!user}
user={user}
/>
{/* Hero Section */}
<div
className="relative w-full h-[50vh] bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `linear-gradient(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.4)), url('${cityData.hero_image}')`
}}
>
<div className="absolute inset-0 flex items-center justify-center">
<div className="text-center text-white">
<motion.button
onClick={onBackClick}
className="absolute top-8 left-8 flex items-center gap-2 text-white hover:text-gray-200 transition-colors duration-200"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5 }}
>
<ArrowLeft className="w-5 h-5" />
<span>Back to Cities</span>
</motion.button>
<motion.h1
className="font-merchant text-4xl md:text-5xl lg:text-6xl mb-4"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<span className="font-light">Explore</span>{' '}
<span className="font-bold italic">{cityData.name}</span>
</motion.h1>
<motion.p
className="text-xl md:text-2xl max-w-3xl mx-auto"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
>
{cityData.description}
</motion.p>
</div>
</div>
</div>
<div className="container mx-auto px-4 py-8">
<div className="flex flex-col lg:flex-row gap-8">
{/* Desktop Sidebar */}
<div className="hidden lg:block lg:w-1/4">
<FilterSidebar />
</div>
{/* Main Content */}
<div className="flex-1">
{/* Header */}
<div className="mb-8">
<h2 className="mb-2">{cityData.name} Attractions</h2>
<p className="text-gray-600">
Discover the best attractions and experiences in {cityData.name}
</p>
</div>
{/* Search Bar */}
<div className="mb-8">
<div className="relative max-w-md">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
<Input
placeholder="Search attractions..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 bg-white border-gray-200"
/>
</div>
</div>
{/* Mobile Filter Button */}
<div className="lg:hidden mb-6">
<Button
variant="outline"
onClick={() => setShowMobileFilters(true)}
className="w-full"
>
<Filter className="w-4 h-4 mr-2" />
Filters ({selectedCategories.length + selectedPassTypes.length})
</Button>
</div>
{/* Results Count */}
<div className="mb-6">
<p className="text-gray-600">
Showing {showingFrom}-{showingTo} of {totalItems} attraction(s)
</p>
</div>
{/* Attractions Grid */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5, delay: 0.2 }}
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
>
{currentAttractions.map((attraction, index) => (
<motion.div
key={attraction.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
<Card
className="group cursor-pointer interactive-card h-full"
onClick={() => onAttractionClick?.(attraction.id)}
>
<CardContent className="p-0">
<div className="relative">
<ImageWithFallback
src={attraction.image}
alt={attraction.name}
className="w-full h-48 object-cover rounded-t-lg"
/>
{attraction.isPopular && (
<Badge className="absolute top-3 left-3 bg-primary text-white">
Popular
</Badge>
)}
<div className="absolute top-3 right-3 bg-white rounded-full p-2 shadow-sm">
<Star className="w-4 h-4 fill-current text-yellow-500" />
</div>
</div>
<div className="p-4">
<div className="flex items-start justify-between mb-2">
<div className="flex-1">
<p className="text-sm text-gray-500 mb-1">{attraction.location}</p>
<h3 className="font-semibold text-gray-900 group-hover:text-primary transition-colors">
{attraction.name}
</h3>
</div>
</div>
<p className="text-sm text-gray-600 mb-3 line-clamp-2">
{attraction.description}
</p>
<div className="flex items-center gap-4 text-sm text-gray-500 mb-3">
<div className="flex items-center gap-1">
<Clock className="w-4 h-4" />
<span>{attraction.duration}</span>
</div>
<div className="flex items-center gap-1">
<Star className="w-4 h-4 fill-current text-yellow-500" />
<span>{attraction.rating}</span>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-1">
{attraction.price > 0 ? (
<>
<DollarSign className="w-4 h-4 text-gray-500" />
<span className="font-semibold">{attraction.price} {cityData.currency}</span>
</>
) : (
<span className="font-semibold text-green-600">Free</span>
)}
</div>
<Badge
variant={attraction.passType === 'unlimited' ? 'default' : 'secondary'}
className="text-xs"
>
{attraction.passType === 'unlimited' ? 'Unlimited' :
attraction.passType === 'selective' ? 'Selective' : 'Both'}
</Badge>
</div>
</div>
</CardContent>
</Card>
</motion.div>
))}
</motion.div>
{totalItems === 0 && (
<div className="text-center py-12">
<p className="text-gray-500 mb-4">No attractions found matching your criteria</p>
<Button onClick={clearAllFilters} variant="outline">
Clear All Filters
</Button>
</div>
)}
{/* Pagination */}
{totalPages > 1 && (
<div className="mt-12 flex flex-col sm:flex-row items-center justify-between gap-4">
<div className="text-sm text-gray-600">
Page {pageNumber} of {totalPages}
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
onClick={() => setPageNumber(Math.max(1, pageNumber - 1))}
disabled={pageNumber === 1}
className="px-3 py-2"
>
Previous
</Button>
{/* Page numbers */}
<div className="flex items-center gap-1">
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
let pageNum;
if (totalPages <= 5) {
pageNum = i + 1;
} else if (pageNumber <= 3) {
pageNum = i + 1;
} else if (pageNumber >= totalPages - 2) {
pageNum = totalPages - 4 + i;
} else {
pageNum = pageNumber - 2 + i;
}
return (
<Button
key={pageNum}
variant={pageNumber === pageNum ? "default" : "outline"}
onClick={() => setPageNumber(pageNum)}
className="px-3 py-2 min-w-[40px]"
>
{pageNum}
</Button>
);
})}
</div>
<Button
variant="outline"
onClick={() => setPageNumber(Math.min(totalPages, pageNumber + 1))}
disabled={pageNumber === totalPages}
className="px-3 py-2"
>
Next
</Button>
</div>
</div>
)}
</div>
</div>
</div>
{/* Mobile Filter Modal */}
{showMobileFilters && (
<div className="fixed inset-0 z-50 lg:hidden">
<div className="fixed inset-0 bg-black bg-opacity-50" onClick={() => setShowMobileFilters(false)} />
<div className="fixed inset-y-0 left-0 w-80 bg-white overflow-y-auto">
<FilterSidebar isMobile />
</div>
</div>
)}
{/* Enhanced Testimonials Section */}
<EnhancedTestimonials />
<Footer
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onPassesClick={onPassesClick}
onSignInClick={onSignInClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
currentPage={currentPage}
/>
</div>
);
}

View File

@@ -0,0 +1,391 @@
import React from 'react';
import { motion } from 'motion/react';
import { ArrowLeft, Star, MapPin, Clock, CreditCard, Users, Shield, Smartphone, Check } from 'lucide-react';
import { Button } from './ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
import { Badge } from './ui/badge';
import Navbar from './Navbar';
import SubNavbar from './SubNavbar';
import { Footer } from './Footer';
import { MobileAppSection } from './MobileAppSection';
import { EnhancedTestimonials } from './EnhancedTestimonials';
import { FAQPage } from './FAQPage';
import { HowItWorks } from './HowItWorks';
import { WhyChooseCityCards } from './WhyChooseCityCards';
import { ImageWithFallback } from './figma/ImageWithFallback';
interface User {
email: string;
name: string;
}
interface CityCardsPageProps {
onBackClick: () => void;
onHomeClick: () => void;
onMelbourneClick: () => void;
onPassesClick: () => void;
onCheckoutClick: () => void;
onSignInClick: () => void;
onSignOutClick?: () => void;
onAttractionsClick: () => void;
onBlogsClick: () => void;
onHowItWorksClick: () => void;
onFAQClick: () => void;
onPrivacyPolicyClick: () => void;
onAboutUsClick: () => void;
onProfileClick: () => void;
onCityCardsClick: () => void;
onMagicItineraryClick: () => void;
onPostCardsClick: () => void;
onOffersClick: () => void;
onContactUsClick?: () => void;
onEsimsClick?: () => void;
onHotelDiscountsClick?: () => void;
currentPage: string;
user: User | null;
}
export function CityCardsPage({
onBackClick,
onHomeClick,
onMelbourneClick,
onPassesClick,
onCheckoutClick,
onSignInClick,
onSignOutClick,
onAttractionsClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onProfileClick,
onCityCardsClick,
onMagicItineraryClick,
onPostCardsClick,
onOffersClick,
onContactUsClick,
onEsimsClick,
onHotelDiscountsClick,
currentPage,
user
}: CityCardsPageProps) {
return (
<div className="min-h-screen bg-background">
{/* Navbar */}
<Navbar
activeCity=""
onCityChange={() => {}}
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
onPassesClick={onPassesClick}
onCheckoutClick={onCheckoutClick}
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick}
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
onEsimsClick={onEsimsClick}
onHotelDiscountsClick={onHotelDiscountsClick}
currentPage="citycards"
isUserSignedIn={!!user}
user={user}
/>
{/* Sub Navbar */}
<SubNavbar
activeTab="citycards"
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
onEsimsClick={onEsimsClick}
onHotelDiscountsClick={onHotelDiscountsClick}
/>
{/* Hero Section */}
<section className="relative pt-52 pb-20 overflow-hidden">
{/* Background gradient */}
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-secondary/5 to-background"></div>
<div className="container mx-auto px-4 relative z-10">
<motion.div
className="max-w-4xl mx-auto text-center"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<h1 className="font-merchant text-4xl md:text-5xl lg:text-6xl leading-tight mb-6">
<span className="font-light">What Are</span>{' '}
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
City Cards?
</span>
</h1>
<p className="font-poppins text-xl leading-relaxed font-normal text-gray-600 mb-8 max-w-2xl mx-auto">
CityCards are your all-in-one pass to explore cities like never before. Get access to top attractions,
skip the lines, and save money while discovering amazing experiences.
</p>
</motion.div>
</div>
{/* Decorative elements */}
<div className="absolute top-20 left-10 w-20 h-20 bg-primary/10 rounded-full blur-xl"></div>
<div className="absolute bottom-20 right-10 w-32 h-32 bg-secondary/10 rounded-full blur-xl"></div>
</section>
{/* Get Your City Cards Section */}
<section className="py-16">
<div className="container mx-auto px-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="text-center mb-12"
>
<h2 className="font-merchant text-3xl mb-4">Get your City Cards</h2>
<p className="text-gray-600 font-poppins max-w-2xl mx-auto">
Choose from our range of passes designed for every type of traveler and every budget
</p>
</motion.div>
<div className="grid md:grid-cols-2 gap-8 max-w-5xl mx-auto">
{/* Selective Pass */}
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
className="relative flex"
>
<Card className="relative transition-all duration-300 cursor-pointer flex flex-col w-full border-gray-200 shadow-md hover:shadow-lg hover:border-primary/30">
<CardHeader className="text-center pb-6 pt-8 flex-shrink-0">
<CardTitle className="font-poppins text-2xl font-bold mb-2 text-gray-900">
SELECTIVE PASS
</CardTitle>
<CardDescription className="text-gray-600 mb-6 leading-relaxed font-poppins h-[48px] flex items-center justify-center">
Perfect for travelers who want to explore selected attractions at their own pace with essential features.
</CardDescription>
{/* Pricing */}
<div className="mb-6">
<div className="flex items-baseline justify-center gap-2 mb-2">
<span className="text-4xl font-bold text-gray-900">$59.99</span>
<span className="text-gray-500 font-poppins">/ per person</span>
</div>
<div className="text-sm text-gray-500 font-poppins h-[20px]">
<span className="line-through mr-2">$89.99</span>
<span className="text-green-600 font-medium">Save 33%</span>
</div>
</div>
</CardHeader>
<CardContent className="pt-0 flex flex-col flex-1">
{/* Features List - Fixed Height */}
<div className="space-y-3 mb-8 flex-1 min-h-[200px]">
{[
'Access to selected attractions',
'Limited number of attractions per pass',
'Flexible validity period',
'Priority entry where available',
'Mobile ticket delivery',
'Standard customer support'
].map((feature, index) => (
<div key={index} className="flex items-start gap-3">
<Check className="w-5 h-5 text-green-500 mt-0.5 flex-shrink-0" />
<span className="text-sm text-gray-700 font-poppins leading-relaxed">{feature}</span>
</div>
))}
</div>
{/* CTA Button - Fixed at bottom */}
<div className="mt-auto flex-shrink-0">
<Button
className="w-full py-4 px-8 rounded-xl font-semibold text-lg transition-all duration-300 bg-gray-900 hover:bg-primary text-white hover:shadow-lg font-poppins"
onClick={onCheckoutClick}
>
PURCHASE NOW
</Button>
<p className="text-xs text-gray-500 text-center mt-4 font-poppins h-[32px] flex items-center justify-center">
Free cancellation up to 24 hours Instant delivery
</p>
</div>
</CardContent>
</Card>
</motion.div>
{/* Unlimited Pass */}
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.4 }}
className="relative flex"
>
<Card className="relative transition-all duration-300 cursor-pointer flex flex-col w-full ring-2 ring-primary shadow-xl">
{/* Popular Badge */}
<div className="absolute -top-4 left-1/2 transform -translate-x-1/2 z-10">
<Badge className="bg-gradient-to-r from-yellow-400 to-orange-500 text-black px-6 py-2 font-semibold shadow-lg">
Most Popular
</Badge>
</div>
<CardHeader className="text-center pb-6 pt-8 flex-shrink-0">
<CardTitle className="font-poppins text-2xl font-bold mb-2 text-gray-900">
UNLIMITED CARD
</CardTitle>
<CardDescription className="text-gray-600 mb-6 leading-relaxed font-poppins h-[48px] flex items-center justify-center">
The ultimate experience for adventure seekers who want unlimited access to all attractions with premium features.
</CardDescription>
{/* Pricing */}
<div className="mb-6">
<div className="flex items-baseline justify-center gap-2 mb-2">
<span className="text-4xl font-bold text-gray-900">$89.99</span>
<span className="text-gray-500 font-poppins">/ per person</span>
</div>
<div className="text-sm text-gray-500 font-poppins h-[20px]">
<span className="line-through mr-2">$149.99</span>
<span className="text-green-600 font-medium">Save 40%</span>
</div>
</div>
</CardHeader>
<CardContent className="pt-0 flex flex-col flex-1">
{/* Features List - Fixed Height */}
<div className="space-y-3 mb-8 flex-1 min-h-[200px]">
{[
'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, index) => (
<div key={index} className="flex items-start gap-3">
<Check className="w-5 h-5 text-green-500 mt-0.5 flex-shrink-0" />
<span className="text-sm text-gray-700 font-poppins leading-relaxed">{feature}</span>
</div>
))}
</div>
{/* CTA Button - Fixed at bottom */}
<div className="mt-auto flex-shrink-0">
<Button
className="w-full py-4 px-8 rounded-xl font-semibold text-lg transition-all duration-300 bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white shadow-lg hover:shadow-xl font-poppins"
onClick={onCheckoutClick}
>
PURCHASE NOW
</Button>
<p className="text-xs text-gray-500 text-center mt-4 font-poppins h-[32px] flex items-center justify-center">
Free cancellation up to 24 hours Instant delivery
</p>
</div>
</CardContent>
</Card>
</motion.div>
</div>
</div>
</section>
{/* How It Works Section */}
<HowItWorks />
{/* Why Choose CityCards Section */}
<WhyChooseCityCards />
{/* Benefits Section */}
{/* Ready to Explore Melbourne Section */}
{/* FAQ Section */}
<section className="py-16 bg-gray-50">
<div className="container mx-auto px-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="text-center mb-12"
>
<h2 className="font-merchant text-3xl mb-4">Frequently Asked Questions</h2>
<p className="text-gray-600 font-poppins max-w-2xl mx-auto">
Everything you need to know about CityCards
</p>
</motion.div>
<div className="max-w-3xl mx-auto">
<div className="space-y-4">
{[
{
question: "How do CityCards work?",
answer: "CityCards are digital passes that give you access to multiple attractions in a city. Simply download our app, activate your card, and show it at participating attractions for instant entry."
},
{
question: "Can I use my CityCard for multiple days?",
answer: "Yes! Depending on the pass you choose, CityCards can be valid for 1, 2, 3, 5, or 7 consecutive days from your first use."
},
{
question: "What's included in an Unlimited Pass?",
answer: "The Unlimited Pass includes access to all participating attractions in your chosen city, plus additional perks like discounts at restaurants and shops."
},
{
question: "Do I need to book attractions in advance?",
answer: "Most attractions allow walk-in access with your CityCard, but some popular attractions may require advance booking through our app to guarantee entry."
},
{
question: "What if I don't use all my attractions?",
answer: "There's no pressure to visit every attraction. Your CityCard gives you the flexibility to explore at your own pace and choose what interests you most."
}
].map((faq, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 + index * 0.1 }}
>
<Card className="hover:shadow-md transition-shadow duration-300">
<CardContent className="p-6">
<h3 className="font-merchant text-lg mb-3">{faq.question}</h3>
<p className="text-gray-600 font-poppins">{faq.answer}</p>
</CardContent>
</Card>
</motion.div>
))}
</div>
</div>
</div>
</section>
{/* Mobile App Section */}
<MobileAppSection />
{/* Customer Reviews */}
<EnhancedTestimonials />
{/* Footer */}
<Footer
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onPassesClick={onPassesClick}
onSignInClick={onSignInClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onContactUsClick={onContactUsClick}
currentPage={currentPage}
/>
</div>
);
}

View File

@@ -0,0 +1,285 @@
import { useState, useEffect } from 'react';
import { motion } from 'motion/react';
interface CitySubmenuProps {
onClose: () => void;
currentPage?: string;
onHomeClick?: () => void;
onMelbourneClick?: () => void;
onAttractionsClick?: () => void;
onPassesClick?: () => void;
onBlogsClick?: () => void;
onHowItWorksClick?: () => void;
}
interface SubmenuItem {
id: string;
label: string;
isCity?: boolean;
action?: () => void;
}
export function CitySubmenu({
onClose,
currentPage,
onHomeClick,
onMelbourneClick,
onAttractionsClick,
onPassesClick,
onBlogsClick,
onHowItWorksClick
}: CitySubmenuProps) {
const [selectedItem, setSelectedItem] = useState<string | null>(null);
const [isScrolled, setIsScrolled] = useState(false);
// Handle scroll effects to match main navbar behavior
useEffect(() => {
const handleScroll = () => {
const scrolled = window.scrollY > 20;
setIsScrolled(scrolled);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
const submenuItems: SubmenuItem[] = [
{
id: 'melbourne',
label: 'Melbourne',
isCity: true,
action: () => {
onMelbourneClick?.();
onClose();
}
},
{
id: 'attractions',
label: 'Attractions',
action: () => {
onAttractionsClick?.();
onClose();
}
},
{
id: 'buy-now',
label: 'Buy Now',
action: () => {
onPassesClick?.();
onClose();
}
},
{
id: 'blogs',
label: 'Blogs',
action: () => {
onBlogsClick?.();
onClose();
}
},
{
id: 'how-it-works',
label: 'How It Works',
action: () => {
onHowItWorksClick?.();
onClose();
}
}
];
// Update selection based on current page
useEffect(() => {
if (currentPage === 'melbourne') {
setSelectedItem('melbourne');
} else if (currentPage === 'attractions') {
setSelectedItem('attractions');
} else if (currentPage === 'blogs') {
setSelectedItem('blogs');
} else if (currentPage === 'how-it-works') {
setSelectedItem('how-it-works');
} else if (currentPage === 'passes') {
setSelectedItem('buy-now');
} else {
setSelectedItem(null);
}
}, [currentPage]);
const handleItemClick = (item: SubmenuItem) => {
if (item.isCity) {
setSelectedItem(item.id);
}
item.action?.();
};
const isItemActive = (item: SubmenuItem) => {
return (item.isCity && currentPage === 'melbourne' && item.id === 'melbourne') ||
(item.id === 'attractions' && currentPage === 'attractions') ||
(item.id === 'buy-now' && currentPage === 'passes') ||
(item.id === 'blogs' && currentPage === 'blogs') ||
(item.id === 'how-it-works' && currentPage === 'how-it-works');
};
return (
<>
{/* Desktop Submenu */}
<motion.div
className="fixed top-[120px] left-1/2 transform -translate-x-1/2 z-30 hidden lg:block"
initial={{ y: -20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{
duration: 0.6,
ease: [0.25, 0.1, 0.25, 1],
delay: 0.4
}}
>
<motion.div
className="bg-white rounded-full px-2 py-2 shadow-lg border border-gray-200"
initial={{ scale: 0.9 }}
animate={{
scale: isScrolled ? 0.95 : 1,
y: isScrolled ? 2 : 0,
boxShadow: isScrolled
? "0 20px 25px -5px rgba(0, 0, 0, 0.08), 0 10px 10px -5px rgba(0, 0, 0, 0.04)"
: "0 10px 15px -3px rgba(0, 0, 0, 0.05), 0 4px 6px -2px rgba(0, 0, 0, 0.03)"
}}
transition={{ duration: 0.3, ease: [0.25, 0.1, 0.25, 1] }}
>
<div className="flex items-center gap-1">
{submenuItems.map((item) => (
<motion.button
key={item.id}
onClick={() => handleItemClick(item)}
className={`font-poppins font-medium text-base relative px-4 py-2.5 transition-all duration-300 whitespace-nowrap rounded-full ${
isItemActive(item)
? 'bg-primary text-white shadow-md'
: 'text-gray-700 hover:text-white hover:bg-gray-800'
}`}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
{item.label}
{/* Hover effect for non-active items */}
{!isItemActive(item) && (
<motion.div
className="absolute inset-0 bg-gray-800 rounded-full -z-10"
initial={{ opacity: 0, scale: 0.9 }}
whileHover={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.2 }}
/>
)}
</motion.button>
))}
</div>
</motion.div>
</motion.div>
{/* Mobile Submenu */}
<motion.div
className="fixed top-[100px] left-4 right-4 z-30 md:hidden"
initial={{ y: -20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{
duration: 0.6,
ease: [0.25, 0.1, 0.25, 1],
delay: 0.4
}}
>
<motion.div
className="bg-white rounded-2xl px-3 py-3 shadow-lg border border-gray-200"
initial={{ scale: 0.9 }}
animate={{
scale: isScrolled ? 0.95 : 1,
y: isScrolled ? 2 : 0,
boxShadow: isScrolled
? "0 20px 25px -5px rgba(0, 0, 0, 0.08), 0 10px 10px -5px rgba(0, 0, 0, 0.04)"
: "0 10px 15px -3px rgba(0, 0, 0, 0.05), 0 4px 6px -2px rgba(0, 0, 0, 0.03)"
}}
transition={{ duration: 0.3, ease: [0.25, 0.1, 0.25, 1] }}
>
<div className="flex items-center gap-2 overflow-x-auto scrollbar-hide">
{submenuItems.map((item) => (
<motion.button
key={item.id}
onClick={() => handleItemClick(item)}
className={`relative px-3 py-2 text-sm font-medium transition-all duration-300 whitespace-nowrap rounded-xl flex-shrink-0 ${
isItemActive(item)
? 'bg-primary text-white shadow-md'
: 'text-gray-700 hover:text-white hover:bg-gray-800'
}`}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
{item.label}
{/* Hover effect for non-active items */}
{!isItemActive(item) && (
<motion.div
className="absolute inset-0 bg-gray-800 rounded-xl -z-10"
initial={{ opacity: 0, scale: 0.9 }}
whileHover={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.2 }}
/>
)}
</motion.button>
))}
</div>
</motion.div>
</motion.div>
{/* Medium Screen Submenu */}
<motion.div
className="fixed top-[110px] left-1/2 transform -translate-x-1/2 z-30 hidden md:block lg:hidden"
initial={{ y: -20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{
duration: 0.6,
ease: [0.25, 0.1, 0.25, 1],
delay: 0.4
}}
>
<motion.div
className="bg-white rounded-full px-2 py-2 shadow-lg border border-gray-200"
initial={{ scale: 0.9 }}
animate={{
scale: isScrolled ? 0.95 : 1,
y: isScrolled ? 2 : 0,
boxShadow: isScrolled
? "0 20px 25px -5px rgba(0, 0, 0, 0.08), 0 10px 10px -5px rgba(0, 0, 0, 0.04)"
: "0 10px 15px -3px rgba(0, 0, 0, 0.05), 0 4px 6px -2px rgba(0, 0, 0, 0.03)"
}}
transition={{ duration: 0.3, ease: [0.25, 0.1, 0.25, 1] }}
>
<div className="flex items-center gap-1">
{submenuItems.map((item) => (
<motion.button
key={item.id}
onClick={() => handleItemClick(item)}
className={`relative px-4 py-2.5 text-sm font-medium transition-all duration-300 whitespace-nowrap rounded-full ${
isItemActive(item)
? 'bg-primary text-white shadow-md'
: 'text-gray-700 hover:text-white hover:bg-gray-800'
}`}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
{item.label}
{/* Hover effect for non-active items */}
{!isItemActive(item) && (
<motion.div
className="absolute inset-0 bg-gray-800 rounded-full -z-10"
initial={{ opacity: 0, scale: 0.9 }}
whileHover={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.2 }}
/>
)}
</motion.button>
))}
</div>
</motion.div>
</motion.div>
</>
);
}

View File

@@ -0,0 +1,490 @@
import { useState } from 'react';
import { motion } from 'motion/react';
import {
Mail,
Phone,
MessageCircle,
HelpCircle,
FileText,
CreditCard,
Upload,
Send,
ChevronRight,
CheckCircle
} from 'lucide-react';
import { Button } from './ui/button';
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
import { Input } from './ui/input';
import { Label } from './ui/label';
import { Textarea } from './ui/textarea';
import Navbar from './Navbar';
import { Footer } from './Footer';
interface ContactUsPageProps {
onBackClick: () => void;
onHomeClick: () => void;
onPassesClick: () => void;
onCheckoutClick: () => void;
onSignInClick: () => void;
onSignOutClick?: () => void;
onAttractionsClick: () => void;
onBlogsClick: () => void;
onHowItWorksClick: () => void;
onFAQClick: () => void;
onPrivacyPolicyClick: () => void;
onAboutUsClick: () => void;
onProfileClick: () => void;
onCityCardsClick: () => void;
onMagicItineraryClick: () => void;
onPostCardsClick: () => void;
onOffersClick: () => void;
onEsimsClick: () => void;
onHotelDiscountsClick: () => void;
currentPage?: string;
user?: { email: string; name: string } | null;
}
interface CommonQuestion {
id: string;
question: string;
category: string;
icon: typeof HelpCircle;
iconBgColor: string;
}
export function ContactUsPage({
onBackClick,
onHomeClick,
onPassesClick,
onCheckoutClick,
onSignInClick,
onSignOutClick,
onAttractionsClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onProfileClick,
onCityCardsClick,
onMagicItineraryClick,
onPostCardsClick,
onOffersClick,
onEsimsClick,
onHotelDiscountsClick,
currentPage,
user
}: ContactUsPageProps) {
const [formData, setFormData] = useState({
subject: '',
description: '',
email: user?.email || '',
phone: ''
});
const [fileName, setFileName] = useState<string>('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSubmitted, setIsSubmitted] = useState(false);
const commonQuestions: CommonQuestion[] = [
{
id: '1',
question: 'How to redeem passes?',
category: 'Guide',
icon: HelpCircle,
iconBgColor: 'bg-blue-100 text-blue-600'
},
{
id: '2',
question: 'Refund Policy',
category: 'Policy',
icon: FileText,
iconBgColor: 'bg-orange-100 text-orange-600'
},
{
id: '3',
question: 'Payment Issues',
category: 'Billing and Payments',
icon: CreditCard,
iconBgColor: 'bg-green-100 text-green-600'
},
{
id: '4',
question: 'Refund Policy',
category: 'Policy',
icon: FileText,
iconBgColor: 'bg-orange-100 text-orange-600'
},
{
id: '5',
question: 'How to redeem passes?',
category: 'Guide',
icon: HelpCircle,
iconBgColor: 'bg-blue-100 text-blue-600'
},
{
id: '6',
question: 'Payment Issues',
category: 'Billing and Payments',
icon: CreditCard,
iconBgColor: 'bg-green-100 text-green-600'
},
{
id: '7',
question: 'Refund Policy',
category: 'Policy',
icon: FileText,
iconBgColor: 'bg-orange-100 text-orange-600'
}
];
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setFileName(file.name);
}
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setIsSubmitting(true);
// Simulate API call
setTimeout(() => {
setIsSubmitting(false);
setIsSubmitted(true);
// Reset form after 3 seconds
setTimeout(() => {
setIsSubmitted(false);
setFormData({
subject: '',
description: '',
email: user?.email || '',
phone: ''
});
setFileName('');
}, 3000);
}, 1500);
};
return (
<div className="min-h-screen bg-gray-50">
<Navbar
activeCity="Paris"
onCityChange={() => {}}
onHomeClick={onHomeClick}
onPassesClick={onPassesClick}
onCheckoutClick={onCheckoutClick}
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick}
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
onEsimsClick={onEsimsClick}
onHotelDiscountsClick={onHotelDiscountsClick}
currentPage={currentPage}
user={user}
/>
{/* Hero Section */}
<section className="pt-32 pb-16 bg-gradient-to-br from-primary/5 via-white to-secondary/5">
<div className="container mx-auto px-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="text-center max-w-3xl mx-auto"
>
<div className="inline-flex items-center justify-center w-20 h-20 bg-primary/10 rounded-full mb-6">
<MessageCircle className="w-10 h-10 text-primary" />
</div>
<h1 className="font-merchant text-4xl md:text-5xl lg:text-6xl mb-4">
<span className="font-medium">Help &</span>{' '}
<span className="font-semibold text-primary">Support</span>
</h1>
<p className="font-poppins text-xl text-gray-600 font-normal">
Get assistance with your CityCards experience
</p>
</motion.div>
</div>
</section>
{/* Main Content */}
<section className="py-16">
<div className="container mx-auto px-4">
<div className="grid lg:grid-cols-2 gap-12 max-w-7xl mx-auto">
{/* Left Column - Common Questions */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
>
<div className="mb-8">
<h2 className="font-merchant text-3xl md:text-4xl mb-3">
<span className="font-medium">Common</span>{' '}
<span className="font-semibold text-primary">Questions</span>
</h2>
<p className="font-poppins text-base text-gray-600 font-normal">
Quick answers to frequently asked questions
</p>
</div>
<div className="space-y-4 mb-8">
{commonQuestions.map((question, index) => (
<motion.div
key={question.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.1 * index }}
>
<Card className="cursor-pointer hover:shadow-lg transition-all duration-300 group border-2 hover:border-primary/20">
<CardContent className="p-5">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4 flex-1">
<div className={`w-12 h-12 rounded-full ${question.iconBgColor} flex items-center justify-center flex-shrink-0`}>
<question.icon className="w-6 h-6" />
</div>
<div className="flex-1">
<h3 className="font-poppins font-medium text-base mb-1">
{question.question}
</h3>
<p className="font-poppins text-sm text-gray-500 font-normal">
{question.category}
</p>
</div>
</div>
<ChevronRight className="w-5 h-5 text-gray-400 group-hover:text-primary transition-colors" />
</div>
</CardContent>
</Card>
</motion.div>
))}
</div>
<div className="flex flex-col sm:flex-row gap-4">
<Button
onClick={onFAQClick}
className="flex-1 h-14 font-poppins font-semibold bg-primary hover:bg-primary/90"
>
Browse FAQ
</Button>
</div>
</motion.div>
{/* Right Column - Support Form */}
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
>
<Card className="shadow-xl border-2">
<CardHeader className="border-b bg-gray-50/50">
<CardTitle className="font-merchant text-2xl font-semibold">
Contact Support
</CardTitle>
<p className="font-poppins text-sm text-gray-600 font-normal mt-2">
Need help? We're here for you. Raise a ticket and our support team will get back to you shortly
</p>
</CardHeader>
<CardContent className="p-6">
{isSubmitted ? (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="text-center py-12"
>
<div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
<CheckCircle className="w-10 h-10 text-green-600" />
</div>
<h3 className="font-merchant text-2xl font-semibold mb-2">
Ticket Submitted!
</h3>
<p className="font-poppins text-base text-gray-600 font-normal">
We'll get back to you shortly
</p>
</motion.div>
) : (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Subject */}
<div>
<Label className="font-poppins font-normal text-base mb-2 block">
Subject
</Label>
<Input
type="text"
placeholder="Enter Subject"
value={formData.subject}
onChange={(e) => setFormData({ ...formData, subject: e.target.value })}
required
className="h-12 font-poppins font-normal"
/>
</div>
{/* Description */}
<div>
<Label className="font-poppins font-normal text-base mb-2 block">
Description
</Label>
<Textarea
placeholder="Enter Description"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
required
rows={6}
className="font-poppins font-normal resize-none"
/>
</div>
{/* File Upload */}
<div>
<Label className="font-poppins font-normal text-base mb-2 block">
File upload
</Label>
<div className="relative">
<Input
type="file"
onChange={handleFileChange}
className="hidden"
id="file-upload"
accept="image/*,.pdf,.doc,.docx"
/>
<label
htmlFor="file-upload"
className="flex items-center justify-between h-12 px-4 border-2 border-gray-200 rounded-lg cursor-pointer hover:border-primary/30 transition-colors bg-white"
>
<span className="font-poppins font-normal text-base text-gray-500">
{fileName || 'Upload File'}
</span>
<Upload className="w-5 h-5 text-gray-400" />
</label>
</div>
</div>
{/* Contact Details */}
<div className="pt-4 border-t">
<h3 className="font-merchant text-xl font-semibold mb-4">
Contact Details
</h3>
<div className="space-y-4">
{/* Email */}
<div className="flex items-center gap-3 p-4 bg-gray-50 rounded-lg">
<Mail className="w-5 h-5 text-primary flex-shrink-0" />
<div className="flex-1">
<p className="font-poppins text-sm text-gray-500 font-normal mb-1">
Email
</p>
{user ? (
<p className="font-poppins font-medium text-base">
{user.email}
</p>
) : (
<Input
type="email"
placeholder="your.email@example.com"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
required
className="h-10 font-poppins font-normal mt-1"
/>
)}
</div>
</div>
{/* Phone */}
<div className="flex items-center gap-3 p-4 bg-gray-50 rounded-lg">
<Phone className="w-5 h-5 text-primary flex-shrink-0" />
<div className="flex-1">
<p className="font-poppins text-sm text-gray-500 font-normal mb-1">
Phone
</p>
<Input
type="tel"
placeholder="(+971) 050 4245 564"
value={formData.phone}
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
className="h-10 font-poppins font-normal mt-1"
/>
</div>
</div>
</div>
</div>
{/* Submit Button */}
<Button
type="submit"
disabled={isSubmitting}
className="w-full h-14 font-poppins font-semibold bg-primary hover:bg-primary/90 text-lg"
>
{isSubmitting ? (
<>
<span className="animate-spin mr-2"></span>
Submitting...
</>
) : (
<>
<Send className="w-5 h-5 mr-2" />
Submit Ticket
</>
)}
</Button>
</form>
)}
</CardContent>
</Card>
{/* Additional Contact Info */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.5 }}
className="mt-6 p-6 bg-gradient-to-br from-primary/5 to-secondary/5 rounded-lg"
>
<h3 className="font-merchant text-lg font-semibold mb-3">
Other Ways to Reach Us
</h3>
<div className="space-y-3">
<div className="flex items-center gap-3">
<Mail className="w-5 h-5 text-primary" />
<a href="mailto:support@citycards.com" className="font-poppins text-base font-normal hover:text-primary transition-colors">
support@citycards.com
</a>
</div>
<div className="flex items-center gap-3">
<Phone className="w-5 h-5 text-primary" />
<a href="tel:+97105042455564" className="font-poppins text-base font-normal hover:text-primary transition-colors">
(+971) 050 4245 564
</a>
</div>
</div>
</motion.div>
</motion.div>
</div>
</div>
</section>
<Footer
onHomeClick={onHomeClick}
onPassesClick={onPassesClick}
onCheckoutClick={onCheckoutClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onContactUsClick={() => {}}
/>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,842 @@
import { useState, useRef, useEffect } from 'react';
import { Camera, ArrowRight, Edit3, Upload, Type, Calendar } from 'lucide-react';
import { Button } from './ui/button';
import { ImageWithFallback } from './figma/ImageWithFallback';
import { motion, useMotionValue, useSpring, useTransform, useInView } from 'motion/react';
import { HandwrittenText, useHandwrittenText } from './HandwrittenText';
import vintageVector from 'figma:asset/e8091276ed2c976b5c975f21687b0d1702bd9c90.png';
interface EditableCardProps {
isEditing: boolean;
onEdit: () => void;
children: React.ReactNode;
className?: string;
style?: React.CSSProperties;
editIcon?: React.ReactNode;
}
export function CustomPostcards() {
const [editingCard, setEditingCard] = useState<string | null>(null);
const postcardRef = useRef<HTMLDivElement>(null);
const sectionRef = useRef<HTMLDivElement>(null);
// 3D tilt effect using mouse position
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);
// Spring animations for smooth mouse following
const rotateX = useSpring(useTransform(mouseY, [-0.5, 0.5], [5, -5]), {
stiffness: 100,
damping: 15
});
const rotateY = useSpring(useTransform(mouseX, [-0.5, 0.5], [-5, 5]), {
stiffness: 100,
damping: 15
});
// Detect when section is in view to trigger handwriting
const isInView = useInView(sectionRef, {
once: true,
margin: "-100px",
amount: 0.3
});
// Handwritten text control
const handwrittenControl = useHandwrittenText(false);
// Handle mouse movement for 3D effect
const handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
if (!postcardRef.current) return;
const rect = postcardRef.current.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const x = (event.clientX - centerX) / (rect.width / 2);
const y = (event.clientY - centerY) / (rect.height / 2);
mouseX.set(x);
mouseY.set(y);
};
const handleMouseLeave = () => {
mouseX.set(0);
mouseY.set(0);
};
const [postcardData, setPostcardData] = useState({
photo: "https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400&h=600&fit=crop&crop=center",
message: "Greetings from paradise!\nThe beaches here are absolutely\nbreathtaking. Wish you were\nhere to enjoy this amazing\nsunset with me.",
date: "July 2024",
addressLabel: "POSTCARD"
});
const handleCreatePostcard = () => {
console.log('Navigate to postcard creation page...');
};
// Start handwriting animation when section comes into view
useEffect(() => {
if (isInView && !editingCard) {
// Delay the start of handwriting to let the postcard animation settle
const timer = setTimeout(() => {
handwrittenControl.start();
}, 1200);
return () => clearTimeout(timer);
}
}, [isInView, editingCard, handwrittenControl]);
// Reset handwriting when editing
useEffect(() => {
if (editingCard === 'message') {
handwrittenControl.reset();
}
}, [editingCard, handwrittenControl]);
const EditableCard = ({ isEditing, onEdit, children, className = "", style = {}, editIcon }: EditableCardProps) => {
return (
<motion.div
className={`relative group cursor-pointer ${className}`}
style={style}
onClick={(e) => {
e.stopPropagation();
onEdit();
}}
whileHover={{
scale: 1.02,
transition: { duration: 0.2 }
}}
whileTap={{ scale: 0.98 }}
animate={isEditing ? {
boxShadow: "0 0 0 2px rgba(99, 102, 241, 0.5), 0 8px 16px rgba(99, 102, 241, 0.3)"
} : {}}
>
{children}
{/* Animated Edit overlay */}
<motion.div
className="absolute inset-0 bg-black rounded-md flex items-center justify-center"
initial={{ opacity: 0, backgroundColor: "rgba(0, 0, 0, 0)" }}
animate={isEditing ? {
opacity: 1,
backgroundColor: "rgba(0, 0, 0, 0.2)"
} : {
opacity: 0,
backgroundColor: "rgba(0, 0, 0, 0)"
}}
whileHover={!isEditing ? {
opacity: 1,
backgroundColor: "rgba(0, 0, 0, 0.1)"
} : {}}
transition={{ duration: 0.2 }}
>
<motion.div
className="bg-white bg-opacity-90 px-2 py-1 rounded-md shadow-lg border border-gray-200 flex items-center gap-1"
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
whileHover={{ scale: 1.05 }}
transition={{ duration: 0.2 }}
>
<motion.div
animate={isEditing ? { rotate: [0, 10, -10, 0] } : {}}
transition={{ duration: 2, repeat: isEditing ? Infinity : 0 }}
>
{editIcon}
</motion.div>
<motion.span
className="text-xs text-gray-700 font-medium"
animate={isEditing ? {
color: ["#374151", "#6366f1", "#374151"]
} : {}}
transition={{ duration: 1, repeat: isEditing ? Infinity : 0 }}
>
{isEditing ? 'Editing...' : 'Edit'}
</motion.span>
</motion.div>
</motion.div>
</motion.div>
);
};
// Ultra-realistic vintage postcard with responsive scaling and animations
const PostcardFrame = () => {
return (
<motion.div
className="relative shadow-2xl transform rotate-[0.5deg] w-full h-full"
initial={{ opacity: 0, scale: 0.9, rotate: 0 }}
animate={{ opacity: 1, scale: 1, rotate: 0.5 }}
transition={{ duration: 0.8, ease: "easeOut" }}
style={{
background: `
radial-gradient(circle at 20% 80%, rgba(210, 180, 140, 0.15) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(160, 130, 100, 0.1) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(190, 160, 120, 0.08) 0%, transparent 50%),
linear-gradient(135deg, #f8f4e6 0%, #f0e7d3 25%, #ede2d0 50%, #e8dcc8 75%, #e3d5c2 100%)
`,
borderRadius: '10px',
boxShadow: `
inset 0px 1px 30px rgba(139, 115, 85, 0.2),
inset 0px -1px 20px rgba(160, 130, 100, 0.15),
0px 12px 40px rgba(0, 0, 0, 0.15),
0px 4px 12px rgba(0, 0, 0, 0.1)
`,
border: '1px solid rgba(139, 115, 85, 0.2)'
}}
>
{/* Realistic paper texture with variations */}
<div
className="absolute inset-0 opacity-40 rounded-[10px] pointer-events-none"
style={{
backgroundImage: `
url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23d4af9a' fill-opacity='0.08'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"),
radial-gradient(circle at 25% 75%, rgba(180, 150, 120, 0.1) 0%, transparent 40%),
radial-gradient(circle at 75% 25%, rgba(160, 130, 100, 0.08) 0%, transparent 35%)
`
}}
/>
{/* Age spots and stains */}
<div className="absolute inset-0 rounded-[10px] pointer-events-none opacity-30">
<div
className="absolute w-8 h-6 rounded-full"
style={{
background: 'radial-gradient(ellipse, rgba(160, 120, 80, 0.15) 0%, transparent 70%)',
top: '15%',
right: '20%',
transform: 'rotate(-15deg)'
}}
/>
<div
className="absolute w-6 h-8 rounded-full"
style={{
background: 'radial-gradient(ellipse, rgba(140, 110, 70, 0.12) 0%, transparent 60%)',
bottom: '25%',
left: '15%',
transform: 'rotate(25deg)'
}}
/>
<div
className="absolute w-4 h-4 rounded-full"
style={{
background: 'radial-gradient(circle, rgba(180, 140, 100, 0.2) 0%, transparent 50%)',
top: '60%',
right: '10%'
}}
/>
</div>
{/* Corner wear and creases */}
<div
className="absolute top-0 right-0 w-12 h-12 opacity-25"
style={{
background: 'radial-gradient(circle at top right, rgba(139, 115, 85, 0.3) 20%, rgba(160, 130, 100, 0.2) 40%, transparent 70%)',
borderTopRightRadius: '10px'
}}
/>
<div
className="absolute bottom-0 left-0 w-16 h-16 opacity-20"
style={{
background: 'radial-gradient(circle at bottom left, rgba(120, 95, 70, 0.25) 25%, rgba(140, 115, 85, 0.15) 50%, transparent 75%)',
borderBottomLeftRadius: '10px'
}}
/>
{/* Subtle crease lines */}
<div
className="absolute w-full h-px bg-gradient-to-r from-transparent via-amber-800/10 to-transparent opacity-40"
style={{
top: '35%',
transform: 'rotate(-0.5deg)'
}}
/>
{/* Animated Vintage Vector Logo - Top Right - Mobile Optimized */}
<motion.div
className="absolute opacity-60 pointer-events-none"
style={{
top: '4.3%',
right: '3.5%',
width: '7.5%',
height: '11.5%',
filter: 'sepia(30%) saturate(120%) hue-rotate(15deg) brightness(85%) contrast(110%)'
}}
animate={{
rotate: [5, 7, 3, 5],
scale: [1, 1.02, 0.98, 1]
}}
transition={{
duration: 8,
repeat: Infinity,
ease: "easeInOut"
}}
>
<img
src={vintageVector}
alt="Vintage logo"
className="w-full h-full object-contain"
style={{
mixBlendMode: 'multiply',
opacity: 0.8
}}
/>
{/* Subtle aging overlay for the vector */}
<div
className="absolute inset-0 rounded-full"
style={{
background: `
radial-gradient(circle at 30% 30%, rgba(160, 130, 90, 0.2) 0%, transparent 60%),
radial-gradient(circle at 70% 70%, rgba(140, 110, 70, 0.15) 0%, transparent 50%)
`,
mixBlendMode: 'multiply'
}}
/>
</motion.div>
{/* Editable Photo Card with realistic mounting - Responsive */}
<EditableCard
isEditing={editingCard === 'photo'}
onEdit={() => setEditingCard(editingCard === 'photo' ? null : 'photo')}
style={{
position: 'absolute',
width: '33.3%',
height: '72.3%',
left: '4.9%',
top: '7.4%',
transform: 'rotate(-0.8deg)'
}}
editIcon={<Upload className="w-3 h-3 text-gray-600" />}
>
<div
className="w-full h-full rounded-lg overflow-hidden shadow-lg relative"
style={{
border: '4px solid #ffffff',
boxShadow: `
0px 6px 25px rgba(0, 0, 0, 0.25),
inset 0px 1px 3px rgba(255, 255, 255, 0.8),
inset 0px -1px 2px rgba(160, 130, 100, 0.2)
`
}}
>
<ImageWithFallback
src={postcardData.photo}
alt="Beautiful tropical beach"
className="w-full h-full object-cover"
style={{
filter: 'sepia(8%) saturate(110%) contrast(102%) brightness(98%)'
}}
/>
{/* Photo aging overlay */}
<div
className="absolute inset-0 pointer-events-none"
style={{
background: `
radial-gradient(circle at 20% 20%, rgba(255, 235, 205, 0.15) 0%, transparent 40%),
radial-gradient(circle at 80% 80%, rgba(200, 170, 130, 0.1) 0%, transparent 35%)
`,
mixBlendMode: 'multiply'
}}
/>
</div>
</EditableCard>
{/* Realistic vertical divider with ink bleeding - Responsive */}
<div
className="absolute"
style={{
top: '10.6%',
bottom: '10.6%',
left: '43.1%',
width: '2px',
background: `
linear-gradient(to bottom,
transparent 0%,
rgba(101, 84, 63, 0.4) 10%,
rgba(101, 84, 63, 0.6) 30%,
rgba(101, 84, 63, 0.8) 50%,
rgba(101, 84, 63, 0.6) 70%,
rgba(101, 84, 63, 0.4) 90%,
transparent 100%
)
`,
filter: 'blur(0.3px)'
}}
/>
{/* Ink bleed effect around divider - Responsive */}
<div
className="absolute opacity-20"
style={{
top: '10.6%',
bottom: '10.6%',
left: '42.8%',
width: '6px',
background: `
linear-gradient(to bottom,
transparent 0%,
rgba(101, 84, 63, 0.1) 20%,
rgba(101, 84, 63, 0.15) 50%,
rgba(101, 84, 63, 0.1) 80%,
transparent 100%
)
`,
filter: 'blur(1px)'
}}
/>
{/* Right side content area - Responsive */}
<div
className="absolute pointer-events-none"
style={{
top: '10.6%',
left: '47.2%',
width: '47.2%',
height: '78.7%'
}}
>
{/* Editable Address Label Card - Responsive */}
<EditableCard
isEditing={editingCard === 'label'}
onEdit={() => setEditingCard(editingCard === 'label' ? null : 'label')}
style={{
position: 'absolute',
top: '4.1%',
left: '0%',
pointerEvents: 'auto',
transform: 'rotate(-0.3deg)'
}}
editIcon={<Type className="w-3 h-3 text-gray-600" />}
>
<div
className="px-2 py-1 rounded text-xs md:text-sm"
style={{
fontWeight: '600',
letterSpacing: '2px',
textTransform: 'uppercase',
color: 'rgba(101, 84, 63, 0.8)',
textShadow: '0px 1px 2px rgba(0, 0, 0, 0.1)'
}}
>
{postcardData.addressLabel}
</div>
</EditableCard>
{/* Realistic horizontal ruled lines with ink bleeding - Responsive */}
<div
className="absolute pointer-events-none"
style={{
top: '14.9%',
left: '0%',
width: '94.1%',
height: '59.5%'
}}
>
{[...Array(9)].map((_, i) => (
<div key={i} className="relative" style={{ marginBottom: '6.5%' }}>
{/* Main line */}
<div
style={{
height: '1px',
width: i % 2 === 0 ? '100%' : '92%',
background: `linear-gradient(to right,
rgba(101, 84, 63, 0.3) 0%,
rgba(101, 84, 63, 0.5) 20%,
rgba(101, 84, 63, 0.6) 50%,
rgba(101, 84, 63, 0.4) 80%,
rgba(101, 84, 63, 0.2) 95%,
transparent 100%
)`,
transform: `rotate(${(Math.random() - 0.5) * 0.5}deg)`
}}
/>
{/* Ink bleed effect */}
<div
className="absolute top-0 left-0 opacity-30"
style={{
height: '3px',
width: i % 2 === 0 ? '98%' : '90%',
background: `linear-gradient(to right,
rgba(101, 84, 63, 0.1) 0%,
rgba(101, 84, 63, 0.2) 30%,
rgba(101, 84, 63, 0.15) 70%,
transparent 100%
)`,
filter: 'blur(1px)',
transform: 'translateY(-1px)'
}}
/>
</div>
))}
</div>
{/* Editable Message Card with Handwritten Animation - Responsive */}
<EditableCard
isEditing={editingCard === 'message'}
onEdit={() => setEditingCard(editingCard === 'message' ? null : 'message')}
style={{
position: 'absolute',
top: '17.6%',
left: '4.4%',
width: '88.2%',
pointerEvents: 'auto',
transform: 'rotate(-0.7deg)'
}}
editIcon={<Edit3 className="w-3 h-3 text-gray-600" />}
>
<div className="px-2 py-2 rounded leading-relaxed">
{editingCard === 'message' ? (
// Show static text when editing
<div
style={{
fontFamily: "'Dancing Script', 'Brush Script MT', cursive",
fontSize: 'clamp(14px, 3.6vw, 26px)',
lineHeight: '1.7',
color: 'rgba(85, 70, 50, 0.9)',
textShadow: `
1px 1px 2px rgba(0, 0, 0, 0.08),
0px 0px 3px rgba(101, 84, 63, 0.1)
`,
whiteSpace: 'pre-line',
filter: 'contrast(110%) brightness(98%)'
}}
>
{postcardData.message}
</div>
) : (
// Show animated handwritten text when not editing
<HandwrittenText
text={postcardData.message}
speed={6}
startDelay={0}
autoStart={false}
onComplete={handwrittenControl.onComplete}
style={{
fontFamily: "'Dancing Script', 'Brush Script MT', cursive",
fontSize: 'clamp(14px, 3.6vw, 26px)',
lineHeight: '1.7',
color: 'rgba(85, 70, 50, 0.9)',
textShadow: `
1px 1px 2px rgba(0, 0, 0, 0.08),
0px 0px 3px rgba(101, 84, 63, 0.1)
`,
filter: 'contrast(110%) brightness(98%)'
}}
/>
)}
</div>
</EditableCard>
{/* Editable Date Card - Responsive */}
<EditableCard
isEditing={editingCard === 'date'}
onEdit={() => setEditingCard(editingCard === 'date' ? null : 'date')}
style={{
position: 'absolute',
bottom: '16.2%',
right: '7.4%',
pointerEvents: 'auto',
transform: 'rotate(-1.5deg)'
}}
editIcon={<Calendar className="w-3 h-3 text-gray-600" />}
>
<div
className="px-2 py-1 rounded text-xs"
style={{
fontWeight: '500',
letterSpacing: '1px',
color: 'rgba(101, 84, 63, 0.7)',
textShadow: '0px 1px 1px rgba(0, 0, 0, 0.05)',
fontFamily: "'Poppins', sans-serif"
}}
>
{postcardData.date}
</div>
</EditableCard>
</div>
{/* Ultra-realistic stamp - Responsive */}
<EditableCard
isEditing={editingCard === 'stamp'}
onEdit={() => setEditingCard(editingCard === 'stamp' ? null : 'stamp')}
style={{
position: 'absolute',
width: '12.5%',
height: '19.1%',
right: '3.5%',
bottom: '5.3%',
transform: 'rotate(-12deg)'
}}
editIcon={<Edit3 className="w-3 h-3 text-gray-600" />}
>
<div
className="w-full h-full border-2 border-dashed rounded-full flex items-center justify-center relative"
style={{
borderColor: 'rgba(139, 115, 85, 0.8)',
background: `
radial-gradient(circle at 30% 30%, rgba(220, 190, 150, 0.95) 0%, rgba(200, 170, 130, 0.9) 40%, rgba(180, 150, 110, 0.85) 100%),
linear-gradient(135deg, rgba(240, 220, 180, 0.3) 0%, transparent 50%)
`,
boxShadow: `
inset 0px 2px 8px rgba(0, 0, 0, 0.25),
inset 0px -1px 4px rgba(255, 255, 255, 0.2),
0px 4px 12px rgba(0, 0, 0, 0.15)
`
}}
>
{/* Stamp aging and wear */}
<div
className="absolute inset-0 rounded-full pointer-events-none"
style={{
background: `
radial-gradient(circle at 70% 20%, rgba(160, 130, 90, 0.3) 0%, transparent 40%),
radial-gradient(circle at 20% 80%, rgba(140, 110, 70, 0.2) 0%, transparent 35%)
`,
mixBlendMode: 'multiply'
}}
/>
{/* Stamp inner details */}
<div className="absolute inset-3 border border-amber-800/50 rounded-full" />
<div className="absolute inset-5 border border-amber-800/30 rounded-full" />
{/* Stamp text */}
<div className="text-center leading-tight z-10" style={{ color: 'rgba(101, 84, 63, 0.9)' }}>
<div style={{ fontSize: 'clamp(6px, 1.25vw, 9px)', fontWeight: '700', letterSpacing: '1px' }}>
TRAVEL
</div>
<div style={{ fontSize: 'clamp(5px, 0.97vw, 7px)', fontWeight: '600', marginTop: '2px', letterSpacing: '0.5px' }}>
MEMORIES
</div>
<div style={{ fontSize: 'clamp(5px, 0.97vw, 7px)', fontWeight: '500', marginTop: '1px' }}>
2024
</div>
</div>
{/* Stamp perforations with realistic variations */}
{[...Array(20)].map((_, i) => (
<div
key={i}
className="absolute rounded-full"
style={{
width: '2px',
height: '2px',
backgroundColor: 'rgba(101, 84, 63, 0.4)',
top: '50%',
left: '50%',
transform: `translate(-50%, -50%) rotate(${i * 18}deg) translateY(-42px)`,
opacity: Math.random() > 0.1 ? 1 : 0.3 // Random missing perforations
}}
/>
))}
{/* Stamp smudge mark */}
<div
className="absolute w-3 h-2 opacity-25"
style={{
background: 'radial-gradient(ellipse, rgba(101, 84, 63, 0.4) 0%, transparent 70%)',
bottom: '15%',
right: '20%',
transform: 'rotate(20deg)',
filter: 'blur(0.5px)'
}}
/>
</div>
</EditableCard>
{/* Additional realistic aging effects */}
<div
className="absolute w-3 h-1 opacity-15"
style={{
background: 'linear-gradient(45deg, rgba(120, 100, 70, 0.3), transparent)',
top: '20%',
left: '60%',
transform: 'rotate(45deg)',
filter: 'blur(0.5px)'
}}
/>
</motion.div>
);
};
return (
<section ref={sectionRef} className="py-12 md:py-16 lg:py-20 bg-white relative overflow-hidden">
{/* Background decorations */}
<div className="absolute inset-0 opacity-10 overflow-hidden">
{/* Vintage Stamps */}
<div className="absolute top-20 left-20 w-16 h-20 bg-gradient-to-br from-primary/30 to-secondary/30 rounded-sm rotate-12 border-2 border-primary/20"></div>
<div className="absolute top-40 right-32 w-12 h-16 bg-gradient-to-br from-secondary/30 to-primary/30 rounded-sm -rotate-6 border-2 border-secondary/20"></div>
<div className="absolute bottom-32 left-40 w-14 h-18 bg-gradient-to-br from-primary/20 to-secondary/20 rounded-sm rotate-45 border-2 border-primary/15"></div>
{/* Paper Textures */}
<div className="absolute top-1/3 right-1/4 w-32 h-32 bg-gradient-to-tr from-primary/10 to-transparent rounded-full blur-2xl"></div>
<div className="absolute bottom-1/3 left-1/4 w-40 h-40 bg-gradient-to-bl from-secondary/10 to-transparent rounded-full blur-2xl"></div>
{/* Ink Splatters */}
<div className="absolute top-1/2 left-1/2 w-8 h-8 bg-primary/20 rounded-full blur-sm"></div>
<div className="absolute top-1/4 right-1/3 w-6 h-6 bg-secondary/20 rounded-full blur-sm"></div>
<div className="absolute bottom-1/4 left-1/3 w-4 h-4 bg-primary/15 rounded-full blur-sm"></div>
</div>
<div className="container mx-auto px-4 relative z-10">
{/* Header */}
<div className="text-left mb-12 md:mb-16">
<div className="inline-flex items-center gap-2 bg-gray-50 px-4 py-2 rounded-full mb-4">
<Camera className="w-4 h-4 text-primary" />
<span className="text-sm text-gray-700">Custom Memories</span>
</div>
<h2 className="text-3xl md:text-4xl lg:text-5xl xl:text-6xl text-gray-900 mb-6">
<span className="font-light">How to create your</span>
<span className="block">
<span className="font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent italic">
custom
</span>{' '}
<span className="font-normal">postcard?</span>
</span>
</h2>
<p className="text-lg md:text-xl text-gray-600 max-w-3xl leading-relaxed">
Transform your travel experiences into beautiful, personalized postcards with authentic handwritten messages. Share your journey in a way that feels truly personal.
</p>
</div>
{/* Main Content - Responsive Grid */}
<div className="grid lg:grid-cols-2 gap-8 lg:gap-16 xl:gap-20 items-center max-w-7xl mx-auto">
{/* Left Column - Interactive Postcard with 3D Effect */}
<motion.div
className="relative flex items-center justify-center order-2 lg:order-1"
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
onClick={() => setEditingCard(null)}
>
{/* Main Postcard Container with 3D Transform */}
<motion.div
ref={postcardRef}
className="relative w-full max-w-lg mx-auto"
style={{
perspective: '1000px',
aspectRatio: '1.6/1'
}}
>
<motion.div
className="w-full h-full"
style={{
rotateX,
rotateY,
transformStyle: 'preserve-3d'
}}
>
<PostcardFrame />
</motion.div>
</motion.div>
{/* Floating Elements around postcard */}
<motion.div
className="absolute -top-6 -right-6 w-16 h-16 bg-primary/10 rounded-full blur-lg"
animate={{
scale: [1, 1.2, 1],
opacity: [0.3, 0.6, 0.3]
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "easeInOut"
}}
/>
<motion.div
className="absolute -bottom-8 -left-8 w-20 h-20 bg-secondary/10 rounded-full blur-lg"
animate={{
scale: [1.2, 1, 1.2],
opacity: [0.4, 0.2, 0.4]
}}
transition={{
duration: 5,
repeat: Infinity,
ease: "easeInOut"
}}
/>
</motion.div>
{/* Right Column - Features and CTA */}
<div className="space-y-8 order-1 lg:order-2">
{/* Features List */}
<div className="space-y-6">
{[
{
icon: Camera,
title: "Capture & Customize",
description: "Upload your travel photos and customize every detail of your postcard."
},
{
icon: Edit3,
title: "Handwritten Messages",
description: "Add personal messages that appear in beautiful handwritten script."
},
{
icon: ArrowRight,
title: "Share Instantly",
description: "Send digital postcards or order physical prints delivered worldwide."
}
].map((feature, index) => (
<motion.div
key={index}
className="flex items-start gap-4 p-4 bg-gray-50/50 rounded-xl border border-gray-100/50 hover:border-primary/20 hover:bg-gray-50 transition-all duration-300"
initial={{ opacity: 0, x: 20 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
viewport={{ once: true }}
>
<div className="w-12 h-12 bg-gradient-to-br from-primary/10 to-secondary/10 rounded-xl flex items-center justify-center flex-shrink-0">
<feature.icon className="w-6 h-6 text-primary" />
</div>
<div className="flex-1">
<h4 className="font-semibold text-gray-900 mb-2">{feature.title}</h4>
<p className="text-gray-600 leading-relaxed">{feature.description}</p>
</div>
</motion.div>
))}
</div>
{/* CTA Section */}
<div className="space-y-4">
<Button
onClick={handleCreatePostcard}
size="xl"
className="w-full md:w-auto h-14 px-8 rounded-xl bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 shadow-lg hover:shadow-xl transition-all duration-300 group"
>
<Camera className="w-5 h-5 mr-2 group-hover:scale-110 transition-transform" />
Create Your Postcard
<ArrowRight className="w-5 h-5 ml-2 group-hover:translate-x-1 transition-transform" />
</Button>
<p className="text-sm text-gray-500 leading-relaxed">
Turn your travel memories into timeless keepsakes. Click anywhere on the postcard above to start customizing.
</p>
</div>
{/* Interactive Hint */}
{!editingCard && (
<motion.div
className="flex items-center gap-2 text-sm text-primary/70 bg-primary/5 p-3 rounded-lg border border-primary/10"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 2 }}
>
<motion.div
animate={{ scale: [1, 1.2, 1] }}
transition={{ duration: 2, repeat: Infinity }}
>
</motion.div>
<span>Hover over the postcard and click any element to edit it!</span>
</motion.div>
)}
</div>
</div>
</div>
</section>
);
}

1175
src/components/DealsPage.tsx Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,768 @@
import { useState } from 'react';
import { motion } from 'motion/react';
import { ArrowLeft, ChevronRight, QrCode, CreditCard, Calendar, MapPin, Star, CheckCircle, Sparkles, Users, Clock, Gift, Ticket } from 'lucide-react';
import { Button } from './ui/button';
import { Card, CardContent } from './ui/card';
import { Badge } from './ui/badge';
import Navbar from './Navbar';
import { Footer } from './Footer';
import { ImageWithFallback } from './figma/ImageWithFallback';
interface User {
email: string;
name: string;
}
interface DownloadAppPageProps {
onBackClick: () => void;
onHomeClick: () => void;
onMelbourneClick: () => void;
onPassesClick: () => void;
onCheckoutClick: () => void;
onSignInClick: () => void;
onSignOutClick: () => void;
onAttractionsClick: () => void;
onBlogsClick: () => void;
onHowItWorksClick: () => void;
onFAQClick: () => void;
onPrivacyPolicyClick: () => void;
onAboutUsClick: () => void;
onProfileClick: () => void;
onCityCardsClick: () => void;
onMagicItineraryClick: () => void;
onPostCardsClick: () => void;
onOffersClick: () => void;
onContactUsClick?: () => void;
onEsimsClick?: () => void;
onHotelDiscountsClick?: () => void;
currentPage: string;
user: User | null;
}
export function DownloadAppPage({
onBackClick,
onHomeClick,
onMelbourneClick,
onPassesClick,
onCheckoutClick,
onSignInClick,
onSignOutClick,
onAttractionsClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onProfileClick,
onCityCardsClick,
onMagicItineraryClick,
onPostCardsClick,
onOffersClick,
onContactUsClick,
onEsimsClick,
onHotelDiscountsClick,
currentPage,
user
}: DownloadAppPageProps) {
// Card type state
const [cardType, setCardType] = useState<'unlimited' | 'selective'>('unlimited');
// Mock data for the current pass
const currentPass = {
name: 'Melbourne- Unlimited Card',
status: 'Active',
date: '22/12/2024',
duration: '2 Days',
adults: 3,
kids: 3
};
// Mock attractions data
const attractions = [
{ name: 'Colosseum', tours: '100+ Tours', image: 'https://images.unsplash.com/photo-1552832230-c0197dd311b5?w=100' },
{ name: 'Colosseum', tours: '100+ Tours', image: 'https://images.unsplash.com/photo-1552832230-c0197dd311b5?w=100' },
{ name: 'Colosseum', tours: '100+ Tours', image: 'https://images.unsplash.com/photo-1552832230-c0197dd311b5?w=100' },
{ name: 'Colosseum', tours: '100+ Tours', image: 'https://images.unsplash.com/photo-1552832230-c0197dd311b5?w=100' },
{ name: 'Colosseum', tours: '100+ Tours', image: 'https://images.unsplash.com/photo-1552832230-c0197dd311b5?w=100' }
];
// Mock user profiles
const userProfiles = [
'https://images.unsplash.com/photo-1494790108755-2616b332b467?w=60',
'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=60',
'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=60',
'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=60',
'https://images.unsplash.com/photo-1544005313-94ddf0286df2?w=60'
];
// Generate QR code pattern
const generateQRPattern = () => {
const size = 17;
const pattern = [];
for (let i = 0; i < size * size; i++) {
const row = Math.floor(i / size);
const col = i % size;
// Corner squares (5x5 for smaller QR)
const isCornerSquare =
(row < 5 && col < 5) || // Top-left
(row < 5 && col >= 12) || // Top-right
(row >= 12 && col < 5); // Bottom-left
// Finder patterns within corner squares
const isFinderPattern = isCornerSquare && (
(row === 0 || row === 4 || col === 0 || col === 4) ||
(row >= 2 && row <= 2 && col >= 2 && col <= 2)
);
// Timing patterns
const isTimingPattern = (row === 4 && col >= 6 && col <= 10) || (col === 4 && row >= 6 && row <= 10);
// Random data pattern for other areas
const isDataPattern = !isCornerSquare && !isTimingPattern && Math.random() > 0.4;
pattern.push(isFinderPattern || isTimingPattern || isDataPattern);
}
return pattern;
};
const qrPattern = generateQRPattern();
return (
<div className="min-h-screen bg-[#f8f8f8]">
<Navbar
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onPassesClick={onPassesClick}
onCheckoutClick={onCheckoutClick}
onSignInClick={onSignInClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick}
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
currentPage={currentPage}
isUserSignedIn={!!user}
user={user}
/>
<div className="container mx-auto px-4 pt-40 pb-6">
{/* Back Button */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
className="mb-6"
>
<Button
variant="ghost"
onClick={onBackClick}
className="p-0 hover:bg-transparent"
>
<div className="text-gray-600 hover:text-primary transition-colors duration-200 flex items-center gap-2">
<ArrowLeft className="w-4 h-4" />
<span>Back</span>
</div>
</Button>
</motion.div>
{/* Pass Card - Redesigned */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-8"
>
<Card className="border-0 shadow-lg overflow-hidden">
{/* Gradient Header Bar */}
<div className="h-2 bg-gradient-to-r from-primary via-secondary to-primary"></div>
<CardContent className="p-6">
{/* Card Header */}
<div className="flex items-start justify-between mb-6">
<div className="flex items-start gap-4">
{/* Icon */}
<div className="w-14 h-14 rounded-xl bg-gradient-to-br from-primary/10 to-secondary/10 flex items-center justify-center flex-shrink-0">
<Ticket className="w-7 h-7 text-primary" />
</div>
{/* Title and Status */}
<div>
<h2 className="font-merchant text-xl font-semibold text-gray-900 mb-2">
{currentPass.name}
</h2>
<Badge className="bg-gradient-to-r from-green-500 to-green-600 text-white px-3 py-1 rounded-full">
<div className="w-2 h-2 bg-white rounded-full mr-2 animate-pulse"></div>
{currentPass.status}
</Badge>
</div>
</div>
{/* Date Badge */}
<div className="bg-gradient-to-br from-primary/5 to-secondary/5 px-4 py-2 rounded-lg">
<div className="flex items-center gap-2 text-primary">
<Calendar className="w-4 h-4" />
<span className="font-poppins font-medium text-sm">{currentPass.date}</span>
</div>
</div>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-3 gap-4">
{/* Duration */}
<div className="bg-gray-50 rounded-lg p-4 text-center">
<div className="w-10 h-10 rounded-full bg-white shadow-sm flex items-center justify-center mx-auto mb-2">
<Clock className="w-5 h-5 text-primary" />
</div>
<div className="font-poppins text-xs text-gray-500 mb-1">Duration</div>
<div className="font-poppins font-semibold text-gray-900">{currentPass.duration}</div>
</div>
{/* Adults */}
<div className="bg-gray-50 rounded-lg p-4 text-center">
<div className="w-10 h-10 rounded-full bg-white shadow-sm flex items-center justify-center mx-auto mb-2">
<Users className="w-5 h-5 text-primary" />
</div>
<div className="font-poppins text-xs text-gray-500 mb-1">Adults</div>
<div className="font-poppins font-semibold text-gray-900">{currentPass.adults}</div>
</div>
{/* Kids */}
<div className="bg-gray-50 rounded-lg p-4 text-center">
<div className="w-10 h-10 rounded-full bg-white shadow-sm flex items-center justify-center mx-auto mb-2">
<Star className="w-5 h-5 text-primary" />
</div>
<div className="font-poppins text-xs text-gray-500 mb-1">Kids</div>
<div className="font-poppins font-semibold text-gray-900">{currentPass.kids}</div>
</div>
</div>
</CardContent>
</Card>
</motion.div>
{/* Attractions Section - Redesigned */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="mb-8"
>
<Card className="border-0 shadow-lg">
<CardContent className="p-6">
{/* Section Header */}
<div className="flex items-center justify-between mb-5">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-primary to-secondary flex items-center justify-center">
<MapPin className="w-5 h-5 text-white" />
</div>
<div>
<h3 className="font-merchant text-lg font-semibold text-gray-900">
Included Attractions
</h3>
<p className="font-poppins text-xs text-gray-500">
Explore amazing places
</p>
</div>
</div>
<Badge className="bg-primary/10 text-primary border-0 px-3 py-1">
<span className="font-poppins font-medium">25+ Places</span>
</Badge>
</div>
{/* Attractions Scroll */}
<div className="flex items-center gap-4 overflow-x-auto pb-2 scrollbar-hide mb-4">
{attractions.map((attraction, index) => (
<motion.div
key={index}
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.1 * index }}
className="flex-shrink-0"
>
<Card className="w-40 hover:shadow-lg transition-all duration-300 border-0 bg-gradient-to-br from-gray-50 to-white cursor-pointer group">
<CardContent className="p-3">
{/* Image */}
<div className="w-full h-24 bg-gray-200 rounded-lg mb-3 overflow-hidden">
<ImageWithFallback
src={attraction.image}
alt={attraction.name}
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-300"
/>
</div>
{/* Info */}
<div>
<h4 className="font-poppins font-semibold text-sm text-gray-900 mb-1 truncate">
{attraction.name}
</h4>
<div className="flex items-center gap-1 text-xs text-primary">
<Ticket className="w-3 h-3" />
<span className="font-poppins font-medium">{attraction.tours}</span>
</div>
</div>
</CardContent>
</Card>
</motion.div>
))}
</div>
{/* View All Button */}
<Button
variant="outline"
className="w-full border-2 border-dashed border-primary/30 hover:border-primary hover:bg-primary/5 text-primary font-poppins font-medium transition-all duration-300"
onClick={onAttractionsClick}
>
<span>View All Attractions</span>
<ChevronRight className="w-4 h-4 ml-2" />
</Button>
</CardContent>
</Card>
</motion.div>
{/* Special Benefits & Offers - Redesigned */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="mb-8"
>
<Card className="border-0 shadow-lg overflow-hidden bg-gradient-to-br from-primary/5 via-white to-secondary/5">
<CardContent className="p-6">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-primary to-secondary flex items-center justify-center">
<Gift className="w-6 h-6 text-white" />
</div>
<div>
<h3 className="font-merchant text-xl font-semibold text-gray-900">
Exclusive Offers
</h3>
<p className="font-poppins text-sm text-gray-600 font-normal">
50+ special deals for cardholders
</p>
</div>
</div>
<Badge className="bg-gradient-to-r from-green-500 to-green-600 text-white border-0 px-3 py-1">
<Sparkles className="w-3 h-3 mr-1" />
<span className="font-poppins font-medium">Limited Time</span>
</Badge>
</div>
{/* Featured Offers Grid */}
<div className="space-y-4 mb-5">
{/* Offer 1 - Restaurant Discount */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
className="bg-gradient-to-br from-orange-50 to-red-50 rounded-xl p-4 border border-orange-200 hover:border-orange-300 transition-all duration-300 cursor-pointer group hover:shadow-lg"
onClick={onOffersClick}
>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-orange-400 to-red-500 flex items-center justify-center">
<Gift className="w-5 h-5 text-white" />
</div>
<div>
<h4 className="font-poppins font-semibold text-base text-gray-900">
Restaurant Discounts
</h4>
<p className="font-poppins text-xs text-gray-600 font-normal">
At partner restaurants
</p>
</div>
</div>
<div className="bg-white rounded-lg px-3 py-2 shadow-sm">
<span className="font-merchant text-2xl font-semibold text-orange-600">25%</span>
<span className="font-poppins text-xs text-gray-600 ml-1">OFF</span>
</div>
</div>
<div className="flex items-center justify-between">
<p className="font-poppins text-xs text-gray-700 font-normal">
Valid at 30+ dining locations
</p>
<ChevronRight className="w-4 h-4 text-orange-600 group-hover:translate-x-1 transition-transform" />
</div>
</motion.div>
{/* Offer 2 - Hotel Savings */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="bg-gradient-to-br from-blue-50 to-purple-50 rounded-xl p-4 border border-blue-200 hover:border-blue-300 transition-all duration-300 cursor-pointer group hover:shadow-lg"
onClick={onOffersClick}
>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-blue-400 to-purple-500 flex items-center justify-center">
<Star className="w-5 h-5 text-white" />
</div>
<div>
<h4 className="font-poppins font-semibold text-base text-gray-900">
Hotel Bookings
</h4>
<p className="font-poppins text-xs text-gray-600 font-normal">
Premium accommodations
</p>
</div>
</div>
<div className="bg-white rounded-lg px-3 py-2 shadow-sm">
<span className="font-merchant text-2xl font-semibold text-blue-600">15%</span>
<span className="font-poppins text-xs text-gray-600 ml-1">OFF</span>
</div>
</div>
<div className="flex items-center justify-between">
<p className="font-poppins text-xs text-gray-700 font-normal">
Exclusive rates on 100+ hotels
</p>
<ChevronRight className="w-4 h-4 text-blue-600 group-hover:translate-x-1 transition-transform" />
</div>
</motion.div>
{/* Offer 3 - Shopping Deals */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="bg-gradient-to-br from-pink-50 to-purple-50 rounded-xl p-4 border border-pink-200 hover:border-pink-300 transition-all duration-300 cursor-pointer group hover:shadow-lg"
onClick={onOffersClick}
>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-pink-400 to-purple-500 flex items-center justify-center">
<Sparkles className="w-5 h-5 text-white" />
</div>
<div>
<h4 className="font-poppins font-semibold text-base text-gray-900">
Shopping Vouchers
</h4>
<p className="font-poppins text-xs text-gray-600 font-normal">
Top retail stores
</p>
</div>
</div>
<div className="bg-white rounded-lg px-3 py-2 shadow-sm">
<span className="font-merchant text-2xl font-semibold text-pink-600">20%</span>
<span className="font-poppins text-xs text-gray-600 ml-1">OFF</span>
</div>
</div>
<div className="flex items-center justify-between">
<p className="font-poppins text-xs text-gray-700 font-normal">
Redeemable at 50+ stores
</p>
<ChevronRight className="w-4 h-4 text-pink-600 group-hover:translate-x-1 transition-transform" />
</div>
</motion.div>
</div>
{/* View All Offers CTA */}
<div className="bg-gradient-to-r from-primary/10 via-secondary/10 to-primary/10 rounded-xl p-5 border border-primary/20">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{/* User Avatars - Social Proof */}
<div className="flex items-center -space-x-2">
{userProfiles.slice(0, 3).map((profile, index) => (
<div
key={index}
className="w-9 h-9 rounded-full border-2 border-white bg-gray-200 overflow-hidden shadow-sm"
style={{ zIndex: 10 - index }}
>
<ImageWithFallback
src={profile}
alt={`User ${index + 1}`}
className="w-full h-full object-cover"
/>
</div>
))}
<div className="w-9 h-9 rounded-full border-2 border-white bg-gradient-to-br from-primary to-secondary flex items-center justify-center text-white font-poppins font-semibold text-xs shadow-sm">
+10K
</div>
</div>
{/* Text */}
<div>
<h4 className="font-poppins font-semibold text-sm text-gray-900">
10,000+ Users Saved
</h4>
<p className="font-poppins text-xs text-gray-600 font-normal">
Over $2M in total savings
</p>
</div>
</div>
{/* View All Offers Button */}
<Button
size="sm"
onClick={onOffersClick}
className="bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-poppins font-semibold shadow-lg hover:shadow-xl transition-all duration-300"
>
View All 50+ Offers
<ChevronRight className="w-4 h-4 ml-1" />
</Button>
</div>
</div>
</CardContent>
</Card>
</motion.div>
{/* Main Content Grid */}
<div className="grid lg:grid-cols-2 gap-12 items-center">
{/* Left Content - Dynamic Card Selection */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.4 }}
className="space-y-8"
>
{/* Toggle between Unlimited and Selective */}
<div className="flex gap-3 mb-6">
<Button
variant={cardType === 'unlimited' ? 'default' : 'outline'}
onClick={() => setCardType('unlimited')}
className="flex-1 font-poppins font-medium"
>
Unlimited Card
</Button>
<Button
variant={cardType === 'selective' ? 'default' : 'outline'}
onClick={() => setCardType('selective')}
className="flex-1 font-poppins font-medium"
>
Selective Card
</Button>
</div>
{cardType === 'unlimited' ? (
<>
<div className="space-y-4">
<h1 className="heading-dynamic font-merchant text-4xl lg:text-5xl leading-tight">
<span className="font-normal">Get</span>{' '}
<span className="font-semibold text-gradient-primary">Melbourne Unlimited Card</span>
</h1>
<p className="font-poppins text-lg text-gray-600 leading-relaxed font-normal">
Unlimited access to 25+ top Melbourne attractions. Visit as many places as you want
with one simple card and save up to 40% compared to individual tickets.
</p>
</div>
{/* Benefits List */}
<div className="space-y-4">
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-primary to-secondary flex items-center justify-center flex-shrink-0">
<CreditCard className="w-6 h-6 text-white" />
</div>
<div>
<h3 className="font-merchant text-lg font-medium mb-1">Unlimited Entries</h3>
<p className="font-poppins text-sm text-gray-600 font-normal">Visit all 25+ attractions as many times as you want</p>
</div>
</div>
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-primary to-secondary flex items-center justify-center flex-shrink-0">
<Calendar className="w-6 h-6 text-white" />
</div>
<div>
<h3 className="font-merchant text-lg font-medium mb-1">7-Day Validity</h3>
<p className="font-poppins text-sm text-gray-600 font-normal">Explore at your own pace for 7 consecutive days</p>
</div>
</div>
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-primary to-secondary flex items-center justify-center flex-shrink-0">
<Star className="w-6 h-6 text-white" />
</div>
<div>
<h3 className="font-merchant text-lg font-medium mb-1">Skip the Queue</h3>
<p className="font-poppins text-sm text-gray-600 font-normal">Fast-track entry at major attractions</p>
</div>
</div>
</div>
{/* CTA Buttons */}
<div className="flex gap-4">
<Button
onClick={onPassesClick}
className="flex-1 bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-poppins font-semibold h-12"
>
Purchase Now
</Button>
<Button
variant="outline"
onClick={onCityCardsClick}
className="font-poppins font-medium"
>
Learn More
</Button>
</div>
</>
) : (
<>
<div className="space-y-4">
<h1 className="heading-dynamic font-merchant text-4xl lg:text-5xl leading-tight">
<span className="font-normal">Get</span>{' '}
<span className="font-semibold text-gradient-primary">Selective Card</span>{' '}
<span className="font-normal">now</span>
</h1>
<p className="font-poppins text-lg text-gray-600 leading-relaxed font-normal">
Choose your own adventure with 12 hand-picked attractions. Perfect for visitors
who want flexibility and great value for money.
</p>
</div>
{/* Benefits List */}
<div className="space-y-4">
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-primary to-secondary flex items-center justify-center flex-shrink-0">
<CheckCircle className="w-6 h-6 text-white" />
</div>
<div>
<h3 className="font-merchant text-lg font-medium mb-1">12 Curated Attractions</h3>
<p className="font-poppins text-sm text-gray-600 font-normal">Choose from the best Melbourne has to offer</p>
</div>
</div>
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-primary to-secondary flex items-center justify-center flex-shrink-0">
<Calendar className="w-6 h-6 text-white" />
</div>
<div>
<h3 className="font-merchant text-lg font-medium mb-1">Flexible 7-Day Period</h3>
<p className="font-poppins text-sm text-gray-600 font-normal">Plan your visits within a week</p>
</div>
</div>
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-full bg-gradient-to-br from-primary to-secondary flex items-center justify-center flex-shrink-0">
<Sparkles className="w-6 h-6 text-white" />
</div>
<div>
<h3 className="font-merchant text-lg font-medium mb-1">40% Savings</h3>
<p className="font-poppins text-sm text-gray-600 font-normal">Save big on combined ticket prices</p>
</div>
</div>
</div>
{/* CTA Buttons */}
<div className="flex gap-4">
<Button
onClick={onPassesClick}
className="flex-1 bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-poppins font-semibold h-12"
>
Purchase Now
</Button>
<Button
variant="outline"
onClick={onAttractionsClick}
className="font-poppins font-medium"
>
View Attractions
</Button>
</div>
</>
)}
</motion.div>
{/* Right Content - Card Preview */}
<motion.div
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.5 }}
className="flex justify-center lg:justify-end"
>
<Card className="w-full max-w-md overflow-hidden border-2 border-primary/20 shadow-2xl">
<div className={`h-3 bg-gradient-to-r ${cardType === 'unlimited' ? 'from-yellow-400 via-primary to-secondary' : 'from-blue-400 via-primary to-purple-500'}`}></div>
<CardContent className="p-8 space-y-6">
{/* Card Header */}
<div className="flex items-start justify-between">
<div>
<h3 className="font-merchant text-2xl font-semibold mb-2">
{cardType === 'unlimited' ? 'Melbourne Unlimited' : 'Melbourne Selective'}
</h3>
<Badge className="bg-gradient-to-r from-primary to-secondary text-white">
{cardType === 'unlimited' ? 'Most Popular' : 'Great Value'}
</Badge>
</div>
<CreditCard className="w-12 h-12 text-primary" />
</div>
{/* Pricing */}
<div className="bg-gradient-to-br from-primary/5 to-secondary/5 rounded-lg p-6 text-center">
<div className="text-4xl font-merchant font-semibold text-primary mb-2">
${cardType === 'unlimited' ? '149' : '89'}
</div>
<div className="text-sm font-poppins text-gray-600 font-normal line-through mb-1">
${cardType === 'unlimited' ? '249' : '149'}
</div>
<Badge variant="secondary" className="font-poppins font-medium">
Save {cardType === 'unlimited' ? '40' : '40'}%
</Badge>
</div>
{/* Features */}
<div className="space-y-3">
<div className="flex items-center gap-3 text-sm font-poppins font-normal">
<CheckCircle className="w-5 h-5 text-primary flex-shrink-0" />
<span>{cardType === 'unlimited' ? '25+' : '12'} Amazing Attractions</span>
</div>
<div className="flex items-center gap-3 text-sm font-poppins font-normal">
<CheckCircle className="w-5 h-5 text-primary flex-shrink-0" />
<span>Valid for 7 Days</span>
</div>
<div className="flex items-center gap-3 text-sm font-poppins font-normal">
<CheckCircle className="w-5 h-5 text-primary flex-shrink-0" />
<span>{cardType === 'unlimited' ? 'Unlimited visits' : 'One visit per attraction'}</span>
</div>
<div className="flex items-center gap-3 text-sm font-poppins font-normal">
<CheckCircle className="w-5 h-5 text-primary flex-shrink-0" />
<span>Digital & Physical Card</span>
</div>
</div>
{/* Visual Decoration */}
<div className="flex justify-between items-center pt-4 border-t border-gray-200">
<MapPin className="w-6 h-6 text-gray-400" />
<div className="flex gap-2">
<div className="w-8 h-8 rounded-full bg-primary/10"></div>
<div className="w-8 h-8 rounded-full bg-secondary/10"></div>
<div className="w-8 h-8 rounded-full bg-primary/20"></div>
</div>
</div>
</CardContent>
</Card>
</motion.div>
</div>
</div>
<Footer
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onPassesClick={onPassesClick}
onCheckoutClick={onCheckoutClick}
onSignInClick={onSignInClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick}
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
onContactUsClick={onContactUsClick}
currentPage={currentPage}
/>
</div>
);
}

View File

@@ -0,0 +1,323 @@
import { useState } from 'react';
import { motion } from 'motion/react';
const QuoteStart = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
<path d="M6.5 10c-.223 0-.437.034-.65.065.069-.232.14-.468.254-.68.114-.308.292-.575.469-.844.148-.291.409-.488.601-.737.201-.242.475-.403.692-.604.213-.21.492-.315.714-.463.232-.133.434-.28.65-.35l.539-.222.474-.197-.485-1.938-.597.144c-.191.048-.424.104-.689.171-.271.05-.56.187-.882.312-.318.142-.686.238-1.028.466-.344.218-.738.411-1.091.746-.363.334-.738.632-1.062 1.004-.324.363-.61.747-.822 1.131-.424.199-.924.471-1.397.991C.598 7.391.047 8.26.01 9.444c-.037 1.218.5 2.407 1.351 3.202.851.794 2.024 1.203 3.197 1.116 1.173-.087 2.317-.68 3.035-1.603.718-.924.968-2.19.68-3.364-.287-1.174-1.239-2.206-2.474-2.583C5.799 5.787 4.74 6.27 4.027 7.104c-.712.834-.91 1.97-.537 2.972z"/>
</svg>
);
const QuoteEnd = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
<path d="M17.5 14c.223 0 .437-.034.65-.065-.069.232-.14.468-.254.68-.114.308-.292.575-.469.844-.148.291-.409.488-.601.737-.201.242-.475.403-.692.604-.213.21-.492.315-.714.463-.232.133-.434.28-.65.35l-.539.222-.474.197.485 1.938.597-.144c.191-.048.424-.104.689-.171.271-.05.56-.187.882-.312.318-.142.686-.238 1.028-.466.344-.218.738-.411 1.091-.746.363-.334.738-.632 1.062-1.004.324-.363.61-.747.822-1.131.424-.199.924-.471 1.397-.991 1.019.388 1.57-1.481 1.607-2.665.037-1.218-.5-2.407-1.351-3.202-.851-.794-2.024-1.203-3.197-1.116-1.173.087-2.317.68-3.035 1.603-.718.924-.968 2.19-.68 3.364.287 1.174 1.239 2.206 2.474 2.583z"/>
</svg>
);
interface Testimonial {
id: number;
quote: string;
name: string;
company: string;
signature: string;
}
const testimonials: Testimonial[] = [
{
id: 1,
quote: "CityCards transformed our Melbourne trip into an unforgettable adventure. The seamless access to attractions and insider recommendations made every moment magical.",
name: "Sarah Chen",
company: "Travel Blogger",
signature: "Sarah"
},
{
id: 2,
quote: "As a frequent business traveler, CityCards saves me time and money. The convenience of having everything in one place is incredible - no more queuing or hassle!",
name: "Michael Torres",
company: "Business Executive",
signature: "Michael"
},
{
id: 3,
quote: "The family pass was perfect for our vacation. Our kids loved the instant access to attractions, and we loved the savings. Highly recommended for family trips!",
name: "Emma Wilson",
company: "Marketing Manager",
signature: "Emma"
},
{
id: 4,
quote: "CityCards made exploring Melbourne so easy and affordable. The local insights and recommendations helped us discover hidden gems we never would have found otherwise.",
name: "James Rodriguez",
company: "Photographer",
signature: "James"
},
{
id: 5,
quote: "Incredible value and convenience! The mobile app worked flawlessly, and having all attraction entries pre-paid meant we could focus on enjoying our honeymoon.",
name: "Lisa Thompson",
company: "Teacher",
signature: "Lisa"
}
];
export function EnhancedTestimonials() {
const [hoveredCard, setHoveredCard] = useState<number | null>(null);
// Calculate dynamic card rotation and offset
const getCardStyle = (index: number) => {
const baseRotation = (index % 3 - 1) * 1.5; // -1.5, 0, 1.5 degrees
const hoverRotation = hoveredCard === testimonials[index].id ? 0 : baseRotation;
const cardRotation = hoverRotation + (Math.sin(index * 0.5) * 0.8);
const cardOffset = Math.cos(index * 0.7) * 2;
return { cardRotation, cardOffset };
};
return (
<section className="py-20 lg:py-32 bg-gradient-to-b from-muted/20 to-background relative overflow-hidden">
{/* Background decorative elements */}
<div className="absolute inset-0 opacity-30">
<div className="absolute top-20 left-20 w-32 h-32 bg-primary/5 rounded-full blur-3xl" />
<div className="absolute bottom-20 right-20 w-40 h-40 bg-secondary/5 rounded-full blur-3xl" />
</div>
<div className="container mx-auto px-4 relative z-10">
{/* Section Header */}
<motion.div
className="text-center mb-16 lg:mb-20"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, ease: "easeOut" }}
>
<h2 className="text-4xl lg:text-5xl xl:text-6xl mb-6">
<span className="font-light">What our</span>{' '}
<span className="bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent font-bold italic">
travelers
</span>{' '}
<span className="font-normal">say</span>
</h2>
<p className="text-lg text-muted-foreground max-w-2xl mx-auto leading-relaxed">
Real stories from travelers who've discovered the magic of seamless city exploration with CityCards
</p>
</motion.div>
{/* Testimonials Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 lg:gap-12 max-w-4xl mx-auto">
{testimonials.slice(0, 2).map((testimonial, index) => {
const { cardRotation, cardOffset } = getCardStyle(index);
return (
<motion.div
key={testimonial.id}
className="flex justify-center"
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{
duration: 0.7,
delay: index * 0.1,
ease: "easeOut"
}}
onMouseEnter={() => setHoveredCard(testimonial.id)}
onMouseLeave={() => setHoveredCard(null)}
>
<div
className="relative bg-white rounded-lg p-8"
style={{
transform: `rotate(${cardRotation}deg) translateY(${cardOffset}px)`,
transformOrigin: 'center center',
minHeight: '480px',
background: `
radial-gradient(circle at 20% 80%, rgba(255, 248, 235, 0.8) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(250, 245, 230, 0.6) 0%, transparent 50%),
linear-gradient(145deg, #ffffff 0%, #fefefe 25%, #fdfdfd 50%, #fcfcfc 75%, #fbfbfb 100%)
`,
boxShadow: `
0 8px 32px rgba(0, 0, 0, 0.12),
0 4px 16px rgba(0, 0, 0, 0.08),
0 2px 8px rgba(0, 0, 0, 0.06),
inset 0 1px 0 rgba(255, 255, 255, 0.8),
inset 0 -1px 0 rgba(0, 0, 0, 0.02)
`,
border: '1px solid rgba(0, 0, 0, 0.04)',
filter: hoveredCard === testimonial.id ? 'brightness(1.02)' : 'brightness(1)',
transition: 'all 0.3s ease'
}}
>
{/* Enhanced paper texture */}
<div
className="absolute inset-0 rounded-lg opacity-40 pointer-events-none"
style={{
backgroundImage: `
url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23f5f0e8' fill-opacity='0.4'%3E%3Cpath d='M10 10h1v1h-1zM20 15h1v1h-1zM30 25h1v1h-1zM40 30h1v1h-1zM50 40h1v1h-1zM15 50h1v1h-1z'/%3E%3C/g%3E%3C/svg%3E"),
radial-gradient(circle at 25% 75%, rgba(245, 240, 232, 0.3) 0%, transparent 40%),
radial-gradient(circle at 75% 25%, rgba(250, 245, 235, 0.2) 0%, transparent 35%)
`,
mixBlendMode: 'multiply'
}}
/>
{/* Paper creases and folds */}
<div
className="absolute inset-0 rounded-lg opacity-20 pointer-events-none"
style={{
background: `
linear-gradient(135deg, transparent 40%, rgba(0,0,0,0.02) 45%, rgba(0,0,0,0.01) 55%, transparent 60%),
linear-gradient(45deg, transparent 30%, rgba(0,0,0,0.015) 35%, transparent 40%)
`
}}
/>
{/* Corner fold effect */}
<div
className="absolute top-0 right-0 w-12 h-12 opacity-15 pointer-events-none"
style={{
background: `
linear-gradient(-45deg,
transparent 40%,
rgba(0,0,0,0.08) 45%,
rgba(0,0,0,0.12) 50%,
rgba(0,0,0,0.08) 55%,
transparent 60%
)
`,
borderTopRightRadius: '8px',
clipPath: 'polygon(60% 0%, 100% 0%, 100% 60%)'
}}
/>
{/* Paperclip decoration */}
<div
className="absolute -top-2 -right-2 w-8 h-12 opacity-60 pointer-events-none z-10"
style={{
background: `
linear-gradient(145deg, #e0e0e0 0%, #d0d0d0 50%, #c8c8c8 100%)
`,
borderRadius: '2px 2px 4px 4px',
boxShadow: `
0 2px 4px rgba(0,0,0,0.1),
inset 0 1px 0 rgba(255,255,255,0.5),
inset 0 -1px 0 rgba(0,0,0,0.1)
`,
transform: 'rotate(8deg)'
}}
>
<div
className="absolute inset-1 border border-gray-400 rounded-sm"
style={{
background: 'transparent',
borderStyle: 'solid',
borderWidth: '1px'
}}
/>
</div>
{/* Tape effect on left edge */}
<div
className="absolute -left-1 top-16 w-4 h-16 opacity-30 pointer-events-none"
style={{
background: `
linear-gradient(90deg,
rgba(255,255,220,0.8) 0%,
rgba(255,255,220,0.6) 50%,
rgba(255,255,220,0.4) 100%
)
`,
borderRadius: '2px',
boxShadow: 'inset 0 1px 2px rgba(0,0,0,0.1)',
transform: 'rotate(-2deg)'
}}
/>
{/* Enhanced quotation marks */}
<div className="mb-6">
<QuoteStart className="w-8 h-8 text-amber-700/30 mb-4" />
<p
className="text-gray-700 leading-relaxed text-base relative z-10 font-poppins"
>
{testimonial.quote}
</p>
<div className="flex justify-end mt-2">
<QuoteEnd className="w-6 h-6 text-amber-700/30" />
</div>
</div>
{/* Enhanced Profile Section with sticker effect */}
<div className="mb-8 relative z-10">
<div className="font-semibold text-gray-900 text-lg font-poppins">
{testimonial.name}
</div>
<div className="text-gray-600 text-sm font-poppins">
{testimonial.company}
</div>
</div>
{/* Enhanced signature with writing animation */}
<div className="flex justify-end relative z-10">
<motion.div
className="text-right transform -rotate-1"
initial={{ pathLength: 0, opacity: 0 }}
whileInView={{
pathLength: 1,
opacity: 1,
transition: {
pathLength: { duration: 2, delay: index * 0.1 },
opacity: { duration: 0.5, delay: index * 0.1 }
}
}}
viewport={{ once: true }}
style={{
fontFamily: "'Dancing Script', 'Brush Script MT', cursive",
fontSize: '32px',
color: 'rgba(101, 84, 63, 0.8)',
textShadow: `
1px 1px 2px rgba(0, 0, 0, 0.1),
0 0 4px rgba(101, 84, 63, 0.2)
`,
filter: 'drop-shadow(0 1px 2px rgba(0,0,0,0.1))'
}}
>
{testimonial.signature}
</motion.div>
</div>
{/* Subtle aging spots */}
<div
className="absolute w-3 h-3 rounded-full opacity-8 pointer-events-none"
style={{
background: 'radial-gradient(circle, rgba(160, 120, 80, 0.15) 0%, transparent 70%)',
top: '15%',
right: '20%'
}}
/>
<div
className="absolute w-2 h-2 rounded-full opacity-8 pointer-events-none"
style={{
background: 'radial-gradient(circle, rgba(140, 110, 70, 0.12) 0%, transparent 70%)',
bottom: '25%',
left: '15%'
}}
/>
{/* Pin shadow effect */}
{index % 3 === 0 && (
<div
className="absolute w-2 h-2 rounded-full opacity-20 pointer-events-none"
style={{
background: 'radial-gradient(circle, rgba(0,0,0,0.3) 0%, transparent 70%)',
top: '8px',
left: '50%',
transform: 'translateX(-50%)',
filter: 'blur(1px)'
}}
/>
)}
</div>
</motion.div>
);
})}
</div>
</div>
</section>
);
}

1481
src/components/EsimsPage.tsx Normal file

File diff suppressed because it is too large Load Diff

511
src/components/FAQPage.tsx Normal file
View File

@@ -0,0 +1,511 @@
import { useState, useMemo } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { Search, ChevronDown, ChevronUp } from 'lucide-react';
import { Input } from './ui/input';
import { Badge } from './ui/badge';
import { Card, CardContent } from './ui/card';
import Navbar from './Navbar';
import { CitySubmenu } from './CitySubmenu';
import { Footer } from './Footer';
import { MobileAppSection } from './MobileAppSection';
import { WhyChooseCityCards } from './WhyChooseCityCards';
import { EnhancedTestimonials } from './EnhancedTestimonials';
import { ReviewsSection } from './ReviewsSection';
interface User {
email: string;
name: string;
}
interface FAQPageProps {
onBackClick: () => void;
onHomeClick: () => void;
onMelbourneClick: () => void;
onPassesClick: () => void;
onCheckoutClick: () => void;
onSignInClick: () => void;
onSignOutClick: () => void;
onAttractionsClick: () => void;
onBlogsClick: () => void;
onHowItWorksClick: () => void;
onFAQClick: () => void;
onPrivacyPolicyClick: () => void;
onAboutUsClick: () => void;
onProfileClick: () => void;
onCityCardsClick: () => void;
onMagicItineraryClick: () => void;
onPostCardsClick: () => void;
onOffersClick: () => void;
onContactUsClick?: () => void;
onEsimsClick?: () => void;
onHotelDiscountsClick?: () => void;
currentPage: string;
user: User | null;
}
interface FAQItem {
id: string;
question: string;
answer: string;
category: string;
}
const categories = [
'General',
'Bookings',
'Passes',
'Mobile App',
'Payment',
'Cancellation',
'Support'
];
const faqData: FAQItem[] = [
{
id: '1',
question: 'How can I book my CityCards pass?',
answer: 'You can easily book your CityCards pass through our website or mobile app. Simply select your preferred city, choose between our Selective or Unlimited pass options, and complete your purchase with instant digital delivery. Your pass will be available immediately for use.',
category: 'Bookings'
},
{
id: '2',
question: 'What\'s the difference between Selective and Unlimited passes?',
answer: 'Our Selective pass allows you to choose specific attractions you want to visit, perfect for targeted exploration. The Unlimited pass gives you access to all participating attractions in the city during your pass validity period, ideal for comprehensive city discovery.',
category: 'Passes'
},
{
id: '3',
question: 'How do I use my digital pass at attractions?',
answer: 'Simply show your digital pass on your mobile device at the attraction entrance. Each pass includes a unique QR code that attraction staff will scan for quick and easy entry. No printed tickets required - it\'s all digital and hassle-free.',
category: 'Mobile App'
},
{
id: '4',
question: 'Can I cancel or refund my CityCards pass?',
answer: 'Yes, we offer flexible cancellation policies. Unused passes can be cancelled within 24 hours of purchase for a full refund. For passes used partially, refund eligibility depends on the specific terms of your pass type and usage.',
category: 'Cancellation'
},
{
id: '5',
question: 'Which cities are currently available?',
answer: 'We currently offer comprehensive CityCards experiences in Melbourne, with more exciting destinations coming soon. Each city features carefully curated attractions, local experiences, and insider recommendations to help you make the most of your visit.',
category: 'General'
},
{
id: '6',
question: 'What payment methods do you accept?',
answer: 'We accept all major credit cards (Visa, Mastercard, American Express), PayPal, Apple Pay, and Google Pay. All transactions are secured with industry-standard encryption to protect your payment information.',
category: 'Payment'
},
{
id: '7',
question: 'How long are CityCards passes valid?',
answer: 'Pass validity varies by type and city. Most passes are valid for 30 days from the date of first use, giving you plenty of flexibility to explore at your own pace. Check your specific pass details for exact validity periods.',
category: 'Passes'
},
{
id: '8',
question: 'Do I need an internet connection to use my pass?',
answer: 'While an internet connection is recommended for the best experience, our mobile app includes offline functionality. You can download your pass and attraction information for offline use, ensuring access even without internet connectivity.',
category: 'Mobile App'
},
{
id: '9',
question: 'Can I share my pass with friends or family?',
answer: 'CityCards passes are non-transferable and designed for individual use only. Each pass is linked to the purchaser\'s account and cannot be shared. For group visits, each person needs their own pass.',
category: 'General'
},
{
id: '10',
question: 'What if I have issues with my booking?',
answer: 'Our customer support team is available 24/7 to help with any booking issues or questions. Contact us through the app, website chat, email, or phone, and we\'ll resolve your concern quickly and efficiently.',
category: 'Support'
},
{
id: '11',
question: 'Are there any additional fees or hidden costs?',
answer: 'No hidden fees! The price you see is the price you pay. Your CityCards pass includes all listed attractions and experiences with no additional booking fees or surprise charges at the attractions.',
category: 'Payment'
},
{
id: '12',
question: 'How do I get customer support while traveling?',
answer: 'Access 24/7 customer support directly through our mobile app, website chat, or by calling our support hotline. Our team is always ready to assist with any questions or issues during your city exploration.',
category: 'Support'
}
];
export function FAQPage({
onHomeClick,
onMelbourneClick,
onPassesClick,
onCheckoutClick,
onSignInClick,
onSignOutClick,
onAttractionsClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onProfileClick,
onCityCardsClick,
onMagicItineraryClick,
onPostCardsClick,
onOffersClick,
onContactUsClick,
onEsimsClick,
onHotelDiscountsClick,
currentPage,
user
}: FAQPageProps) {
const [searchQuery, setSearchQuery] = useState('');
const [selectedCategory, setSelectedCategory] = useState('All');
const [expandedFAQs, setExpandedFAQs] = useState<Set<string>>(new Set());
const toggleFAQ = (id: string) => {
const newExpanded = new Set(expandedFAQs);
if (newExpanded.has(id)) {
newExpanded.delete(id);
} else {
newExpanded.add(id);
}
setExpandedFAQs(newExpanded);
};
const filteredFAQs = useMemo(() => {
return faqData.filter(faq => {
const matchesSearch = faq.question.toLowerCase().includes(searchQuery.toLowerCase()) ||
faq.answer.toLowerCase().includes(searchQuery.toLowerCase());
const matchesCategory = selectedCategory === 'All' || faq.category === selectedCategory;
return matchesSearch && matchesCategory;
});
}, [searchQuery, selectedCategory]);
// Group FAQs into pairs for 2-column layout
const faqPairs = [];
for (let i = 0; i < filteredFAQs.length; i += 2) {
faqPairs.push(filteredFAQs.slice(i, i + 2));
}
return (
<div className="min-h-screen bg-background">
{/* Navigation */}
<Navbar
activeCity="Melbourne"
onCityChange={() => {}}
onHomeClick={onHomeClick}
onSignInClick={onSignInClick}
onPassesClick={onPassesClick}
onCheckoutClick={onCheckoutClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick}
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
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">
{/* Page Header */}
<motion.div
className="text-center mb-12"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<motion.div
className="text-sm tracking-wider text-primary mb-4"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.4, delay: 0.2 }}
>
GET STARTED NOW
</motion.div>
<h1 className="font-merchant font-light text-4xl md:text-5xl lg:text-6xl text-gray-900 mb-6">
Frequently Asked{' '}
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
Questions
</span>
</h1>
<p className="font-poppins text-xl leading-relaxed font-normal text-gray-600 max-w-3xl mx-auto">
Find answers to the most common questions about CityCards, bookings, and your city exploration experience
</p>
</motion.div>
{/* Search and Filter Section */}
<motion.div
className="mb-12 max-w-4xl mx-auto"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
>
{/* Search Bar */}
<div className="relative mb-8">
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
<Input
type="text"
placeholder="Search for questions..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-12 pr-4 py-6 text-lg bg-white border-2 border-gray-200 rounded-2xl focus:border-primary focus:ring-4 focus:ring-primary/10 transition-all duration-200"
/>
</div>
{/* Category Tags */}
<div className="flex flex-wrap gap-3 justify-center">
<Badge
variant={selectedCategory === 'All' ? 'default' : 'secondary'}
className={`px-6 py-3 text-sm cursor-pointer transition-all duration-200 hover:scale-105 ${
selectedCategory === 'All'
? 'bg-primary text-white shadow-lg'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
onClick={() => setSelectedCategory('All')}
>
All
</Badge>
{categories.map((category) => (
<Badge
key={category}
variant={selectedCategory === category ? 'default' : 'secondary'}
className={`px-6 py-3 text-sm cursor-pointer transition-all duration-200 hover:scale-105 ${
selectedCategory === category
? 'bg-primary text-white shadow-lg'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
onClick={() => setSelectedCategory(category)}
>
{category}
</Badge>
))}
</div>
</motion.div>
{/* FAQ Content */}
<div className="w-full">
{faqPairs.length === 0 ? (
<motion.div
className="text-center py-12"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.4 }}
>
<p className="font-poppins text-xl leading-relaxed font-normal text-gray-500">No questions found matching your search.</p>
</motion.div>
) : (
<div className="space-y-8">
{faqPairs.map((pair, pairIndex) => (
<motion.div
key={pairIndex}
className="grid grid-cols-1 lg:grid-cols-2 gap-8"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: pairIndex * 0.1 }}
>
{pair.map((faq) => {
const isExpanded = expandedFAQs.has(faq.id);
return (
<div
key={faq.id}
className="faq-card bg-white rounded-2xl shadow-sm hover:shadow-md transition-all duration-300 relative overflow-hidden"
>
<button
onClick={() => toggleFAQ(faq.id)}
className="w-full p-8 text-left hover:bg-gray-100/50 transition-colors duration-200 focus:outline-none focus:ring-4 focus:ring-primary/10"
>
<div className="flex items-start gap-4 mb-4">
<span className="w-10 h-10 bg-primary text-white rounded-full flex items-center justify-center font-bold flex-shrink-0">
{faq.id}
</span>
<div className="flex-1">
<h3 className="font-merchant font-medium text-xl text-gray-900 leading-tight mb-2">
{faq.question}
</h3>
<div className="flex items-center justify-between">
<Badge variant="secondary" className="text-xs px-3 py-1 bg-gray-100 text-gray-600">
{faq.category}
</Badge>
<div className="flex-shrink-0">
{isExpanded ? (
<ChevronUp className="w-5 h-5 text-primary" />
) : (
<ChevronDown className="w-5 h-5 text-gray-400" />
)}
</div>
</div>
</div>
</div>
</button>
{/* Answer Content */}
<AnimatePresence>
{isExpanded && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.3, ease: [0.25, 0.1, 0.25, 1] }}
className="overflow-hidden"
>
<div className="px-8 pb-8">
<div className="border-t border-gray-200 pt-6">
<p className="text-gray-600 leading-relaxed">
{faq.answer}
</p>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
})}
{/* If odd number of FAQs, add empty space for the last row */}
{pair.length === 1 && (
<div className="hidden lg:block" />
)}
</motion.div>
))}
</div>
)}
</div>
{/* Pagination Section */}
<motion.div
className="mt-16 flex flex-col items-center gap-6"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.4 }}
>
<div className="text-center text-muted-foreground">
Showing 1-12 of 24 item(s)
</div>
<button className="bg-gradient-to-r from-primary to-secondary text-white px-12 py-4 rounded-full hover:shadow-lg hover:shadow-primary/25 transition-all duration-300 font-medium text-base hover:-translate-y-0.5">
Load More
</button>
</motion.div>
{/* Mobile App Section */}
<motion.div
className="mt-20"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.5 }}
>
<div className="bg-muted/30 relative overflow-hidden rounded-3xl">
{/* Subtle Background Elements */}
<div className="absolute inset-0">
<div className="absolute top-1/3 left-1/6 w-64 h-64 bg-primary/3 rounded-full blur-3xl"></div>
<div className="absolute bottom-1/2 right-1/6 w-48 h-48 bg-secondary/3 rounded-full blur-3xl"></div>
</div>
<MobileAppSection />
</div>
</motion.div>
{/* Why Choose Us Section */}
<motion.div
className="mt-20"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.6 }}
>
<WhyChooseCityCards />
</motion.div>
{/* Enhanced Testimonials Section */}
<motion.div
className="mt-20"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.65 }}
>
<EnhancedTestimonials />
</motion.div>
{/* Customer Reviews Section */}
<motion.div
className="mt-20"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.7 }}
>
<ReviewsSection />
</motion.div>
{/* Contact Support Section */}
<motion.div
className="mt-20 text-center"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.6 }}
>
<Card className="max-w-2xl mx-auto bg-gradient-to-br from-primary/5 to-secondary/5 border-2 border-primary/20 rounded-3xl p-8">
<CardContent className="p-0">
<h3 className="font-merchant font-medium text-2xl text-gray-900 mb-4">
Still have questions?
</h3>
<p className="text-gray-600 mb-6 leading-relaxed">
Our support team is available 24/7 to help you with any questions or concerns about your CityCards experience.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<a
href="mailto:support@citycards.com"
className="px-8 py-3 bg-primary text-white rounded-xl hover:bg-primary/90 transition-colors duration-200 font-medium"
>
Email Support
</a>
<a
href="tel:+1-800-CITYCARD"
className="px-8 py-3 bg-white text-primary border-2 border-primary rounded-xl hover:bg-primary/5 transition-colors duration-200 font-medium"
>
Call Us
</a>
</div>
</CardContent>
</Card>
</motion.div>
</div>
<Footer
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onPassesClick={onPassesClick}
onSignInClick={onSignInClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onContactUsClick={onContactUsClick}
currentPage={currentPage}
/>
</div>
);
}
export default FAQPage;

View File

@@ -0,0 +1,246 @@
import { useState } from 'react';
import { Button } from './ui/button';
import { Card, CardContent } from './ui/card';
import { Badge } from './ui/badge';
import { ChevronLeft, ChevronRight, MapPin, Star, Clock } from 'lucide-react';
import { ImageWithFallback } from './figma/ImageWithFallback';
interface City {
id: string;
name: string;
country: string;
image: string;
attractions: number;
rating: number;
duration: string;
startingPrice: number;
popular?: boolean;
description: string;
}
const cities: City[] = [
{
id: 'paris',
name: 'Paris',
country: 'France',
image: 'https://images.unsplash.com/photo-1431274172761-fca41d930114?q=80&w=600&auto=format&fit=crop',
attractions: 45,
rating: 4.9,
duration: '3-7 days',
startingPrice: 49,
popular: true,
description: 'City of Light with world-famous landmarks and art'
},
{
id: 'london',
name: 'London',
country: 'United Kingdom',
image: 'https://images.unsplash.com/photo-1513635269975-59663e0ac1ad?q=80&w=600&auto=format&fit=crop',
attractions: 38,
rating: 4.8,
duration: '3-7 days',
startingPrice: 55,
description: 'Historic capital with royal palaces and modern culture'
},
{
id: 'tokyo',
name: 'Tokyo',
country: 'Japan',
image: 'https://images.unsplash.com/photo-1540959733332-eab4deabeeaf?q=80&w=600&auto=format&fit=crop',
attractions: 52,
rating: 4.9,
duration: '5-10 days',
startingPrice: 68,
popular: true,
description: 'Vibrant metropolis blending tradition with innovation'
},
{
id: 'barcelona',
name: 'Barcelona',
country: 'Spain',
image: 'https://images.unsplash.com/photo-1539037116277-4db20889f2d4?q=80&w=600&auto=format&fit=crop',
attractions: 29,
rating: 4.7,
duration: '3-5 days',
startingPrice: 42,
description: 'Architectural wonder with beaches and vibrant culture'
},
{
id: 'rome',
name: 'Rome',
country: 'Italy',
image: 'https://images.unsplash.com/photo-1552832230-c0197dd311b5?q=80&w=600&auto=format&fit=crop',
attractions: 34,
rating: 4.8,
duration: '3-6 days',
startingPrice: 45,
description: 'Eternal city with ancient history and timeless beauty'
},
{
id: 'new-york',
name: 'New York',
country: 'United States',
image: 'https://images.unsplash.com/photo-1496442226666-8d4d0e62e6e9?q=80&w=600&auto=format&fit=crop',
attractions: 67,
rating: 4.6,
duration: '4-8 days',
startingPrice: 75,
description: 'The city that never sleeps with iconic skyline'
}
];
export function FeaturedCities() {
const [currentSlide, setCurrentSlide] = useState(0);
const itemsPerSlide = 3;
const totalSlides = Math.ceil(cities.length / itemsPerSlide);
const nextSlide = () => {
setCurrentSlide((prev) => (prev + 1) % totalSlides);
};
const prevSlide = () => {
setCurrentSlide((prev) => (prev - 1 + totalSlides) % totalSlides);
};
const visibleCities = cities.slice(
currentSlide * itemsPerSlide,
(currentSlide + 1) * itemsPerSlide
);
return (
<section className="py-16 md:py-24">
<div className="container mx-auto px-4">
<div className="flex items-center justify-between mb-12">
<div className="space-y-2">
<h2 className="text-3xl md:text-4xl font-bold">
Featured Destinations
</h2>
<p className="text-muted-foreground">
Discover the world's most incredible cities with our curated passes
</p>
</div>
<div className="hidden md:flex items-center space-x-2">
<Button
variant="outline"
className="w-16 h-16"
onClick={prevSlide}
disabled={currentSlide === 0}
>
<ChevronLeft className="w-8 h-8" />
</Button>
<Button
variant="outline"
className="w-16 h-16"
onClick={nextSlide}
disabled={currentSlide === totalSlides - 1}
>
<ChevronRight className="w-8 h-8" />
</Button>
</div>
</div>
{/* Desktop Grid */}
<div className="hidden md:grid md:grid-cols-3 gap-6 mb-8">
{visibleCities.map((city) => (
<CityCard key={city.id} city={city} />
))}
</div>
{/* Mobile Carousel */}
<div className="md:hidden">
<div className="overflow-hidden">
<div
className="flex transition-transform duration-300 ease-in-out"
style={{ transform: `translateX(-${currentSlide * 100}%)` }}
>
{cities.map((city) => (
<div key={city.id} className="w-full flex-shrink-0 px-2">
<CityCard city={city} />
</div>
))}
</div>
</div>
{/* Mobile Navigation */}
<div className="flex items-center justify-center space-x-2 mt-6">
{Array.from({ length: cities.length }).map((_, index) => (
<button
key={index}
onClick={() => setCurrentSlide(index)}
className={`w-2 h-2 rounded-full transition-colors ${
index === currentSlide ? 'bg-primary' : 'bg-muted'
}`}
/>
))}
</div>
</div>
<div className="text-center mt-8">
<Button size="lg" variant="outline" className="h-14">
View All Cities
</Button>
</div>
</div>
</section>
);
}
function CityCard({ city }: { city: City }) {
return (
<Card className="group cursor-pointer transition-all duration-300 hover:shadow-xl hover:-translate-y-1">
<div className="relative overflow-hidden rounded-t-lg">
{city.popular && (
<Badge className="absolute top-3 left-3 z-10 bg-gradient-to-r from-primary to-secondary text-white">
Popular
</Badge>
)}
<ImageWithFallback
src={city.image}
alt={`${city.name}, ${city.country}`}
className="w-full h-64 object-cover group-hover:scale-105 transition-transform duration-300"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent" />
<div className="absolute bottom-3 left-3 text-white">
<h3 className="font-semibold">{city.name}</h3>
<p className="text-sm opacity-90">{city.country}</p>
</div>
</div>
<CardContent className="p-4 space-y-3">
<p className="text-sm text-muted-foreground">{city.description}</p>
<div className="flex items-center justify-between text-sm">
<div className="flex items-center space-x-1">
<MapPin className="w-4 h-4 text-muted-foreground" />
<span>{city.attractions} attractions</span>
</div>
<div className="flex items-center space-x-1">
<Star className="w-4 h-4 text-yellow-500" />
<span>{city.rating}</span>
</div>
</div>
<div className="flex items-center justify-between text-sm">
<div className="flex items-center space-x-1">
<Clock className="w-4 h-4 text-muted-foreground" />
<span>{city.duration}</span>
</div>
<div className="font-semibold">
From €{city.startingPrice}
</div>
</div>
<Button className="w-full h-14" variant="outline">
Explore {city.name}
</Button>
</CardContent>
</Card>
);
}

97
src/components/Footer.tsx Normal file
View File

@@ -0,0 +1,97 @@
import { ImageWithFallback } from './figma/ImageWithFallback';
import { FooterBrand } from './FooterBrand';
import { FooterNavigation } from './FooterNavigation';
import { FooterBottom } from './FooterBottom';
interface FooterProps {
onHomeClick?: () => void;
onMelbourneClick?: () => void;
onPassesClick?: () => void;
onSignInClick?: () => void;
onAttractionsClick?: () => void;
onBlogsClick?: () => void;
onHowItWorksClick?: () => void;
onFAQClick?: () => void;
onPrivacyPolicyClick?: () => void;
onAboutUsClick?: () => void;
onContactUsClick?: () => void;
currentPage?: string;
}
export function Footer({
onHomeClick,
onMelbourneClick,
onPassesClick,
onSignInClick,
onAttractionsClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onContactUsClick,
currentPage
}: FooterProps) {
return (
<>
{/* Footer with Nature Background */}
<footer className="relative text-white overflow-hidden">
{/* Background Image */}
<div className="absolute inset-0 z-0">
<ImageWithFallback
src="https://images.unsplash.com/photo-1469474968028-56623f02e42e?w=1920&h=1080&fit=crop&q=80"
alt="Beautiful mountain landscape with lake and forest - travel destination"
className="w-full h-full object-cover"
/>
{/* Enhanced White Gradient Overlay at Top */}
<div className="absolute top-0 left-0 right-0 h-48 bg-gradient-to-b from-white via-white/95 via-white/80 via-white/60 via-white/40 to-transparent z-10" />
{/* Additional Smooth Transition Layer */}
<div className="absolute top-0 left-0 right-0 h-24 bg-gradient-to-b from-white via-white/90 to-white/70 z-10" />
{/* Dark overlay for text readability */}
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-black/30 to-black/70 z-20" />
{/* Additional atmospheric effects */}
<div className="absolute inset-0 z-15">
<div className="absolute top-1/3 left-1/4 w-96 h-96 bg-white/5 rounded-full blur-3xl" />
<div className="absolute bottom-1/4 right-1/4 w-80 h-80 bg-primary/10 rounded-full blur-3xl" />
</div>
</div>
{/* Content Overlay */}
<div className="relative z-30 py-24">
<div className="container mx-auto px-4">
{/* Footer Content Grid */}
<div className="w-full mt-48 bg-primary/10 backdrop-blur-lg rounded-[10px] border border-white/10 p-12">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 mb-16">
{/* Logo and Description - Left Side */}
<FooterBrand />
{/* Navigation Links - Center to Right */}
<FooterNavigation
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onPassesClick={onPassesClick}
onSignInClick={onSignInClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onContactUsClick={onContactUsClick}
currentPage={currentPage}
/>
</div>
{/* Bottom Section - Copyright, Legal Links, and Social Icons */}
<FooterBottom onPrivacyPolicyClick={onPrivacyPolicyClick} />
</div>
</div>
</div>
</footer>
</>
);
}

View File

@@ -0,0 +1,72 @@
import { motion } from 'motion/react';
import { Facebook, Twitter, Instagram, Youtube } from 'lucide-react';
interface FooterBottomProps {
onPrivacyPolicyClick?: () => void;
}
export function FooterBottom({ onPrivacyPolicyClick }: FooterBottomProps) {
return (
<motion.div
className="border-t border-white/20 pt-8"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.5 }}
>
<div className="flex flex-col lg:flex-row justify-between items-center space-y-6 lg:space-y-0">
{/* Copyright */}
<p className="text-white/60 text-sm">
© 2024 CityCards. All rights reserved.
</p>
{/* Right Section - Legal Links and Social Icons */}
<div className="flex flex-col md:flex-row items-center space-y-4 md:space-y-0 md:space-x-8">
{/* Legal Links */}
<div className="flex space-x-6 text-sm">
<motion.button
onClick={onPrivacyPolicyClick}
className="text-white/70 hover:text-white transition-colors duration-200"
whileHover={{ y: -1 }}
whileTap={{ scale: 0.95 }}
>
Privacy Policy
</motion.button>
<motion.a
href="#"
className="text-white/70 hover:text-white transition-colors duration-200"
whileHover={{ y: -1 }}
whileTap={{ scale: 0.95 }}
>
Terms of Service
</motion.a>
<motion.a
href="#"
className="text-white/70 hover:text-white transition-colors duration-200"
whileHover={{ y: -1 }}
whileTap={{ scale: 0.95 }}
>
Cookie Policy
</motion.a>
</div>
{/* Social Icons - Horizontal Layout */}
<div className="flex space-x-3">
{[Facebook, Twitter, Instagram, Youtube].map((Icon, index) => (
<motion.a
key={index}
href="#"
className="text-white/70 hover:text-white transition-colors duration-200 p-2 rounded-lg hover:bg-white/10"
whileHover={{ scale: 1.1, y: -2 }}
whileTap={{ scale: 0.95 }}
transition={{ duration: 0.2, delay: index * 0.05 }}
>
<Icon className="w-5 h-5" />
</motion.a>
))}
</div>
</div>
</div>
</motion.div>
);
}

View File

@@ -0,0 +1,68 @@
import image_bc70aef6686e5f4d059b5ef3380fd4f44bb9f4c6 from '../assets/bc70aef6686e5f4d059b5ef3380fd4f44bb9f4c6.png';
import { motion } from 'motion/react';
import { Apple, Play } from 'lucide-react';
import { ImageWithFallback } from './figma/ImageWithFallback';
import cityCardsLogo from 'figma:asset/66c6d4d2fa6c02d9f60e12fcde70c13d4a78d0b7.png';
export function FooterBrand() {
return (
<motion.div
className="lg:col-span-4 space-y-4"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5 }}
>
<div className="flex items-center">
<img
src={image_bc70aef6686e5f4d059b5ef3380fd4f44bb9f4c6}
alt="CityCards Logo"
className="h-12 w-auto"
/>
</div>
<p className="font-poppins text-white/80 text-sm leading-relaxed font-normal max-w-xs">
Discover the best of every city with our curated experiences and attractions.
Your gateway to unforgettable urban adventures.
</p>
{/* Get Application Section */}
<div className="pt-4">
<div className="flex items-center space-x-4">
<p className="font-poppins text-white/80 text-sm font-normal whitespace-nowrap">Get Application:</p>
<div className="flex space-x-3">
{/* iOS App Store Button */}
<motion.a
href="#"
className="flex items-center justify-center w-12 h-12 bg-white/15 rounded-full hover:bg-white/25 transition-all duration-300 border border-white/20 hover:border-white/40 relative group"
whileHover={{ scale: 1.1, y: -2 }}
whileTap={{ scale: 0.95 }}
transition={{ duration: 0.2, ease: "easeOut" }}
title="Download for iOS"
>
<Apple className="w-6 h-6 text-white fill-current" />
<div className="absolute -top-8 left-1/2 transform -translate-x-1/2 bg-black/80 text-white text-xs px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap">
iOS
</div>
</motion.a>
{/* Android App Store Button */}
<motion.a
href="#"
className="flex items-center justify-center w-12 h-12 bg-white/15 rounded-full hover:bg-white/25 transition-all duration-300 border border-white/20 hover:border-white/40 relative group"
whileHover={{ scale: 1.1, y: -2 }}
whileTap={{ scale: 0.95 }}
transition={{ duration: 0.2, ease: "easeOut" }}
title="Download for Android"
>
<Play className="w-6 h-6 text-white fill-current" />
<div className="absolute -top-8 left-1/2 transform -translate-x-1/2 bg-black/80 text-white text-xs px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap">
Android
</div>
</motion.a>
</div>
</div>
</div>
</motion.div>
);
}

View File

@@ -0,0 +1,97 @@
import { motion } from 'motion/react';
import { footerSections } from '../utils/footerConstants';
interface FooterNavigationProps {
onHomeClick?: () => void;
onMelbourneClick?: () => void;
onPassesClick?: () => void;
onSignInClick?: () => void;
onAttractionsClick?: () => void;
onBlogsClick?: () => void;
onHowItWorksClick?: () => void;
onFAQClick?: () => void;
onPrivacyPolicyClick?: () => void;
onAboutUsClick?: () => void;
onContactUsClick?: () => void;
currentPage?: string;
}
export function FooterNavigation({
onHomeClick,
onMelbourneClick,
onPassesClick,
onSignInClick,
onAttractionsClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onContactUsClick,
currentPage
}: FooterNavigationProps) {
return (
<div className="lg:col-span-8 grid grid-cols-2 md:grid-cols-4 gap-8">
{Object.entries(footerSections).map(([key, section]) => (
<motion.div
key={key}
className="space-y-4"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{
duration: 0.5,
delay: Object.keys(footerSections).indexOf(key) * 0.1 + 0.1
}}
>
<h4 className="font-semibold text-white">{section.title}</h4>
<ul className="space-y-3">
{section.links.map((link, index) => {
const getClickHandler = () => {
switch (link) {
case 'Home': return onHomeClick;
case 'Melbourne': return onMelbourneClick;
case 'Passes': return onPassesClick;
case 'Sign In': return onSignInClick;
case 'Attractions': return onAttractionsClick;
case 'Blog': return onBlogsClick;
case 'How It Works': return onHowItWorksClick;
case 'FAQ': return onFAQClick;
case 'Privacy Policy': return onPrivacyPolicyClick;
case 'Contact Us': return onContactUsClick;
default: return undefined;
}
};
const clickHandler = getClickHandler();
return (
<li key={link}>
{clickHandler ? (
<motion.button
onClick={(e) => {
e.preventDefault();
clickHandler();
}}
className="text-white/80 hover:text-white transition-colors duration-200 text-sm text-left"
whileHover={{ x: 4 }}
transition={{ duration: 0.2 }}
>
{link}
</motion.button>
) : (
<motion.span
className="text-white/80 cursor-default text-sm"
>
{link}
</motion.span>
)}
</li>
);
})}
</ul>
</motion.div>
))}
</div>
);
}

View File

@@ -0,0 +1,227 @@
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'motion/react';
interface HandwrittenTextProps {
text: string;
className?: string;
style?: React.CSSProperties;
speed?: number; // Characters per second
startDelay?: number; // Delay before animation starts (ms)
onComplete?: () => void;
autoStart?: boolean;
}
export function HandwrittenText({
text,
className = "",
style = {},
speed = 8,
startDelay = 0,
onComplete,
autoStart = true
}: HandwrittenTextProps) {
const [displayedText, setDisplayedText] = useState('');
const [currentIndex, setCurrentIndex] = useState(0);
const [isWriting, setIsWriting] = useState(false);
const [showCursor, setShowCursor] = useState(false);
// Split text into characters, preserving line breaks
const characters = text.split('');
useEffect(() => {
if (!autoStart) return;
const startTimer = setTimeout(() => {
setIsWriting(true);
setShowCursor(true);
}, startDelay);
return () => clearTimeout(startTimer);
}, [autoStart, startDelay]);
useEffect(() => {
if (!isWriting || currentIndex >= characters.length) {
if (currentIndex >= characters.length) {
// Hide cursor after a delay
const cursorTimer = setTimeout(() => {
setShowCursor(false);
setIsWriting(false);
onComplete?.();
}, 800);
return () => clearTimeout(cursorTimer);
}
return;
}
const char = characters[currentIndex];
const baseDelay = 1000 / speed;
// Variable delays for more natural writing
let delay = baseDelay;
if (char === ' ') {
delay = baseDelay * 0.5; // Spaces are quicker
} else if (char === '\n') {
delay = baseDelay * 2; // Line breaks take longer
} else if (['.', '!', '?'].includes(char)) {
delay = baseDelay * 1.5; // Punctuation takes a bit longer
} else if ([',', ';', ':'].includes(char)) {
delay = baseDelay * 1.2;
} else {
// Add some randomness to letter timing
delay = baseDelay * (0.8 + Math.random() * 0.4);
}
const timer = setTimeout(() => {
setDisplayedText(prev => prev + char);
setCurrentIndex(prev => prev + 1);
}, delay);
return () => clearTimeout(timer);
}, [isWriting, currentIndex, characters, speed, onComplete]);
// Reset function for external control
const reset = () => {
setDisplayedText('');
setCurrentIndex(0);
setIsWriting(false);
setShowCursor(false);
};
const start = () => {
reset();
setTimeout(() => {
setIsWriting(true);
setShowCursor(true);
}, startDelay);
};
// Expose control methods
useEffect(() => {
if (typeof window !== 'undefined') {
(window as any).handwrittenTextControls = { reset, start };
}
}, []);
return (
<div className={`relative w-fit h-fit inline-block ${className}`} style={style}>
{/* Main text with character-by-character animation */}
<motion.div
className="relative"
style={{ whiteSpace: 'pre-line' }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
{displayedText.split('').map((char, index) => (
<motion.span
key={`${index}-${char}`}
initial={{
opacity: 0,
scale: 0.8,
y: 2
}}
animate={{
opacity: 1,
scale: 1,
y: 0
}}
transition={{
duration: 0.2,
ease: "easeOut",
delay: 0
}}
style={{
display: char === '\n' ? 'block' : 'inline',
width: char === '\n' ? '100%' : 'auto',
height: char === '\n' ? '0' : 'auto'
}}
>
{char === '\n' ? '' : char}
</motion.span>
))}
{/* Writing cursor/pen effect */}
<AnimatePresence>
{showCursor && (
<motion.span
className="inline-block"
initial={{ opacity: 0, scale: 0 }}
animate={{
opacity: [0.3, 1, 0.3],
scale: [0.8, 1.1, 0.8],
rotate: [0, 2, -2, 0]
}}
exit={{ opacity: 0, scale: 0 }}
transition={{
opacity: {
duration: 0.8,
repeat: Infinity,
ease: "easeInOut"
},
scale: {
duration: 0.8,
repeat: Infinity,
ease: "easeInOut"
},
rotate: {
duration: 1.2,
repeat: Infinity,
ease: "easeInOut"
}
}}
style={{
color: 'rgba(85, 70, 50, 0.6)',
marginLeft: '1px'
}}
>
|
</motion.span>
)}
</AnimatePresence>
</motion.div>
{/* Ink flow effect */}
<motion.div
className="absolute inset-0 pointer-events-none"
style={{
background: `
radial-gradient(ellipse at 20% 30%, rgba(85, 70, 50, 0.05) 0%, transparent 60%),
radial-gradient(ellipse at 80% 70%, rgba(101, 84, 63, 0.04) 0%, transparent 50%)
`,
filter: 'blur(2px)',
borderRadius: '4px'
}}
initial={{ opacity: 0 }}
animate={{ opacity: isWriting ? 0.3 : 0 }}
transition={{ duration: 0.5 }}
/>
</div>
);
}
// Hook for controlling the animation externally
export function useHandwrittenText(autoStart = true) {
const [isComplete, setIsComplete] = useState(false);
const reset = () => {
setIsComplete(false);
if (typeof window !== 'undefined' && (window as any).handwrittenTextControls) {
(window as any).handwrittenTextControls.reset();
}
};
const start = () => {
setIsComplete(false);
if (typeof window !== 'undefined' && (window as any).handwrittenTextControls) {
(window as any).handwrittenTextControls.start();
}
};
return {
isComplete,
reset,
start,
onComplete: () => setIsComplete(true)
};
}

134
src/components/Header.tsx Normal file
View File

@@ -0,0 +1,134 @@
import { useState } from 'react';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Sheet, SheetContent, SheetTrigger } from './ui/sheet';
import { Search, Menu, MapPin } from 'lucide-react';
interface HeaderProps {
activeCity?: string;
onCityChange?: (city: string) => void;
}
export function Header({ activeCity, onCityChange }: HeaderProps) {
const [isSearchOpen, setIsSearchOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const cities = [
'Paris', 'London', 'New York', 'Tokyo', 'Barcelona', 'Rome'
];
const filteredCities = cities.filter(city =>
city.toLowerCase().includes(searchQuery.toLowerCase())
);
const handleCitySelect = (city: string) => {
onCityChange?.(city);
setSearchQuery('');
setIsSearchOpen(false);
};
return (
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container flex h-16 items-center justify-between px-4">
{/* Logo */}
<div className="flex items-center space-x-2">
<div className="flex items-center space-x-2">
<div className="w-8 h-8 bg-gradient-to-r from-primary to-secondary rounded-lg flex items-center justify-center">
<MapPin className="w-5 h-5 text-white" />
</div>
<span className="font-merchant text-lg font-semibold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
CityCards
</span>
</div>
</div>
{/* Desktop Navigation */}
<nav className="hidden md:flex items-center space-x-6">
<a href="#" className="hover:text-primary transition-colors">Cities</a>
<a href="#" className="hover:text-primary transition-colors">Passes</a>
<a href="#" className="hover:text-primary transition-colors">How it Works</a>
<a href="#" className="hover:text-primary transition-colors">Support</a>
</nav>
{/* Search and Actions */}
<div className="flex items-center space-x-3">
{/* City Search */}
<div className="relative hidden md:block">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
<Input
placeholder="Search cities..."
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
setIsSearchOpen(e.target.value.length > 0);
}}
onFocus={() => setIsSearchOpen(true)}
className="pl-10 w-64"
/>
</div>
{isSearchOpen && (
<div className="absolute top-full mt-1 w-full bg-popover border rounded-md shadow-lg z-50">
{filteredCities.length > 0 ? (
filteredCities.map((city) => (
<button
key={city}
onClick={() => handleCitySelect(city)}
className="w-full text-left px-4 py-2 hover:bg-accent transition-colors first:rounded-t-md last:rounded-b-md"
>
{city}
</button>
))
) : (
<div className="px-4 py-2 text-muted-foreground">No cities found</div>
)}
</div>
)}
</div>
<Button variant="outline" className="hidden md:inline-flex">
Sign In
</Button>
<Button className="hidden md:inline-flex bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90">
Get Started
</Button>
{/* Mobile Menu */}
<Sheet>
<SheetTrigger asChild>
<Button variant="outline" size="icon" className="md:hidden">
<Menu className="h-4 w-4" />
<span className="sr-only">Toggle menu</span>
</Button>
</SheetTrigger>
<SheetContent side="right" className="w-80">
<div className="flex flex-col space-y-4 mt-6">
{/* Mobile Search */}
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
<Input placeholder="Search cities..." className="pl-10" />
</div>
{/* Mobile Navigation */}
<nav className="flex flex-col space-y-3">
<a href="#" className="py-2 hover:text-primary transition-colors">Cities</a>
<a href="#" className="py-2 hover:text-primary transition-colors">Passes</a>
<a href="#" className="py-2 hover:text-primary transition-colors">How it Works</a>
<a href="#" className="py-2 hover:text-primary transition-colors">Support</a>
</nav>
<div className="flex flex-col space-y-2 pt-4 border-t">
<Button variant="outline">Sign In</Button>
<Button className="bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90">
Get Started
</Button>
</div>
</div>
</SheetContent>
</Sheet>
</div>
</div>
</header>
);
}

View File

@@ -0,0 +1,713 @@
import { useState, useEffect, useRef, forwardRef } from "react";
import {
Star,
Menu,
X,
ShoppingBag,
ChevronDown,
Globe,
} from "lucide-react";
import { motion, AnimatePresence } from "motion/react";
import { ImageWithFallback } from "./figma/ImageWithFallback";
import { Button } from "./ui/button";
import { CitySubmenu } from "./CitySubmenu";
import imgRectangle4429 from "figma:asset/43f3bc1f9c8cc5b8f60f3f6be0bc1ad29eded0d7.png";
import cityCardsLogo from "figma:asset/e961451f70697dd054c4240fc1dcad81e08ce31e.png";
// City list data for right sidebar
const cityList = [
{
id: 1,
name: "Sydney",
attractions: 65,
image:
"https://images.unsplash.com/photo-1695018228065-2e0026c654af?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzeWRuZXklMjBoYXJib3IlMjBicmlkZ2UlMjBvcGVyYSUyMGhvdXNlfGVufDF8fHx8MTc1NjEyNDA2NXww&ixlib=rb-4.1.0&q=80&w=1080",
},
{
id: 2,
name: "Melbourne",
attractions: 48,
image:
"https://images.unsplash.com/photo-1679731980101-503d93bbec27?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBjb2ZmZWUlMjBsYW5ld2F5c3xlbnwxfHx8fDE3NTYxMjQwNzN8MA&ixlib=rb-4.1.0&q=80&w=1080",
},
{
id: 3,
name: "Brisbane",
attractions: 32,
image:
"https://images.unsplash.com/photo-1548661625-a30d197ce439?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxicmlzYmFuZSUyMHJpdmVyJTIwY2l0eSUyMHNreWxpbmV8ZW58MXx8fHwxNzU2MTI0MDc3fDA&ixlib=rb-4.1.0&q=80&w=1080",
},
];
// Interface for dropdown items
interface DropdownItem {
id: string;
label: string;
badge?: string;
icon?: React.ReactNode;
action?: () => void;
}
// Forward ref for dropdown component
const Dropdown = forwardRef<
HTMLDivElement,
{
isOpen: boolean;
onToggle: () => void;
items: DropdownItem[];
title: string;
trigger: React.ReactNode;
}
>(({ isOpen, onToggle, items, title, trigger }, ref) => {
return (
<div ref={ref} className="relative">
<div onClick={onToggle}>{trigger}</div>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, y: 10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 10, scale: 0.95 }}
transition={{
duration: 0.15,
ease: [0.25, 0.1, 0.25, 1],
}}
className="absolute right-0 mt-2 w-64 bg-white/90 backdrop-blur-md rounded-2xl shadow-xl border border-white/20 py-2 z-50"
>
<div className="px-4 py-2 border-b border-gray-200/50">
<h3 className="font-poppins font-medium text-gray-900 text-sm">
{title}
</h3>
</div>
{items.map((item) => (
<div
key={item.id}
onClick={item.action}
className="px-4 py-3 hover:bg-gray-50/50 cursor-pointer transition-colors duration-150 flex items-center justify-between"
>
<div className="flex items-center gap-2">
{item.icon}
<span className="font-poppins text-gray-700 text-sm font-normal">
{item.label}
</span>
</div>
{item.badge && (
<span className="bg-primary/10 text-primary text-xs px-2 py-1 rounded-full font-medium">
{item.badge}
</span>
)}
</div>
))}
</motion.div>
)}
</AnimatePresence>
</div>
);
});
Dropdown.displayName = "Dropdown";
interface HeroSectionProps {
onSignInClick?: () => void;
onPassesClick?: () => void;
onMelbourneClick?: () => void;
onAttractionsClick?: () => void;
onHomeClick?: () => void;
onBlogsClick?: () => void;
onHowItWorksClick?: () => void;
currentPage?: string;
}
export function HeroSection({
onSignInClick,
onPassesClick,
onMelbourneClick,
onAttractionsClick,
onHomeClick,
onBlogsClick,
onHowItWorksClick,
currentPage,
}: HeroSectionProps) {
const [hoveredCity, setHoveredCity] = useState<number | null>(
null,
);
const [isScrolled, setIsScrolled] = useState(false);
const [activeLanguageDropdown, setActiveLanguageDropdown] =
useState(false);
const [activeCartDropdown, setActiveCartDropdown] =
useState(false);
const [scrollY, setScrollY] = useState(0);
// Refs for dropdowns and parallax
const languageRef = useRef<HTMLDivElement>(null);
const cartRef = useRef<HTMLDivElement>(null);
const heroRef = useRef<HTMLDivElement>(null);
// Sample cart data
const [cartItems] = useState([
{
id: "1",
name: "Melbourne City Card",
price: "$49",
quantity: 1,
},
{
id: "2",
name: "Sydney City Card",
price: "$59",
quantity: 1,
},
]);
const cartTotal = cartItems.reduce(
(sum, item) =>
sum + parseFloat(item.price.replace("$", "")),
0,
);
// Language options
const languages = [
{
id: "en",
label: "English",
action: () => console.log("English selected"),
},
{
id: "es",
label: "Español",
action: () => console.log("Spanish selected"),
},
{
id: "fr",
label: "Français",
action: () => console.log("French selected"),
},
{
id: "de",
label: "Deutsch",
action: () => console.log("German selected"),
},
];
// Navigation handlers
const handleNavClick = (action: string) => {
switch (action) {
case "about":
console.log("Navigate to About Us");
break;
case "products":
console.log("Navigate to Cities");
break;
case "attractions":
if (onAttractionsClick) {
onAttractionsClick();
}
break;
case "card":
if (onPassesClick) onPassesClick();
break;
case "offer":
console.log("Navigate to Deals");
break;
default:
break;
}
};
const isNavItemActive = (action: string) => {
if (action === "card" && currentPage === "passes")
return true;
if (action === "about" && currentPage === "home")
return true;
if (action === "attractions" && currentPage === "attractions")
return true;
return false;
};
// Handle scroll effects with parallax (throttled for performance)
useEffect(() => {
let ticking = false;
const handleScroll = () => {
if (!ticking) {
requestAnimationFrame(() => {
const currentScrollY = window.scrollY;
setIsScrolled(currentScrollY > 20);
setScrollY(currentScrollY);
ticking = false;
});
ticking = true;
}
};
window.addEventListener("scroll", handleScroll, {
passive: true,
});
return () =>
window.removeEventListener("scroll", handleScroll);
}, []);
// Calculate parallax values
const scrollProgress = Math.min(
scrollY / window.innerHeight,
1,
);
const backgroundScale = 1.2 - scrollProgress * 0.2; // Scale from 1.2 to 1
const backgroundY = scrollY * 0.5; // Parallax movement
const borderOpacity = 1 - scrollProgress * 0.3; // Border fade effect
// Close dropdowns when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
languageRef.current &&
!languageRef.current.contains(event.target as Node)
) {
setActiveLanguageDropdown(false);
}
if (
cartRef.current &&
!cartRef.current.contains(event.target as Node)
) {
setActiveCartDropdown(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () =>
document.removeEventListener(
"mousedown",
handleClickOutside,
);
}, []);
return (
<div
ref={heroRef}
className="bg-[#ffffff] relative min-h-screen overflow-hidden"
data-name="MacBook Pro 14' - 2"
>
{/* Background Image with Parallax */}
<motion.div
className="absolute bg-[position:50%_50%] bg-size-cover h-full left-0 top-0 w-full origin-center"
style={{
backgroundImage: `url('${imgRectangle4429}')`,
scale: backgroundScale,
y: backgroundY,
willChange: "transform",
}}
/>
{/* Black Gradient Overlay */}
<div className="absolute h-full left-0 top-0 w-full bg-gradient-to-b from-black/0 to-black/55" />
{/* Main Content Frame with Border Animation */}
<motion.div
className="absolute h-[840.118px] rounded-[56px] top-[100.11px] translate-x-[-50%] w-[1449.56px] max-w-[95vw]"
style={{ left: "calc(50% + 7.086px)" }}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{
duration: 1.2,
ease: [0.25, 0.1, 0.25, 1],
}}
>
<motion.div
aria-hidden="true"
className="absolute border-2 border-[#ffffff] border-solid inset-0 pointer-events-none rounded-[56px]"
style={{
opacity: borderOpacity,
willChange: "opacity",
}}
initial={{
clipPath: "polygon(0 0, 0 0, 0 100%, 0 100%)", // Start from top-left
}}
animate={{
clipPath: "polygon(0 0, 100% 0, 100% 100%, 0 100%)", // Expand to full
}}
transition={{
duration: 1.2,
ease: [0.25, 0.1, 0.25, 1],
delay: 0.3,
}}
/>
</motion.div>
{/* Glassmorphism Navigation Bar with Menu */}
<motion.nav
className="absolute top-[17.79px] left-[38.31px] right-[38.31px] z-40"
initial={{ y: -100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{
duration: 0.6,
ease: [0.25, 0.1, 0.25, 1],
delay: 0.2,
}}
>
<motion.div
className="backdrop-blur-[10px] backdrop-filter bg-[rgba(255,255,255,0.2)] h-[66.549px] rounded-[33.275px] w-full px-8 py-4 shadow-lg shadow-black/5 border border-white/20"
animate={{
scale: isScrolled ? 0.98 : 1,
y: isScrolled ? 2 : 0,
boxShadow: isScrolled
? "0 20px 25px -5px rgba(0, 0, 0, 0.08), 0 10px 10px -5px rgba(0, 0, 0, 0.04)"
: "0 10px 15px -3px rgba(0, 0, 0, 0.05), 0 4px 6px -2px rgba(0, 0, 0, 0.03)",
}}
transition={{
duration: 0.3,
ease: [0.25, 0.1, 0.25, 1],
}}
>
{/* Navigation Content */}
<div className="flex items-center justify-between w-full h-full">
{/* CityCards Logo Section */}
<motion.div
className="flex items-center cursor-pointer flex-shrink-0"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => onHomeClick?.()}
>
<img
src={cityCardsLogo}
alt="CityCards"
className="h-10 w-auto"
/>
</motion.div>
{/* Navigation Links */}
<div className="hidden md:flex items-center gap-[30px] lg:gap-[51px]">
{[
{ label: "About Us", action: "about" },
{ label: "Cities", action: "products" },
{ label: "Attractions", action: "attractions" },
{ label: "Your Card", action: "card" },
{ label: "Deals", action: "offer" },
].map((item) => (
<motion.button
key={item.action}
onClick={() => handleNavClick(item.action)}
className={`relative px-0 py-2 text-base font-medium transition-all duration-200 whitespace-nowrap group capitalize ${
isNavItemActive(item.action)
? "text-white"
: "text-white/80 hover:text-white"
}`}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
{item.label}
{/* Active indicator */}
<motion.div
className="absolute bottom-0 left-0 h-0.5 bg-gradient-to-r from-primary to-secondary rounded-full"
initial={{ width: 0 }}
animate={{
width: isNavItemActive(item.action)
? "100%"
: 0,
opacity: isNavItemActive(item.action)
? 1
: 0,
}}
whileHover={{
width: "100%",
opacity: 1,
}}
transition={{ duration: 0.2 }}
/>
{/* Hover background */}
<motion.div
className="absolute inset-0 bg-white/10 rounded-lg -z-10"
initial={{ scale: 0, opacity: 0 }}
whileHover={{ scale: 1, opacity: 0.5 }}
transition={{ duration: 0.2 }}
/>
</motion.button>
))}
</div>
{/* Right Section */}
<div className="flex items-center gap-5">
{/* Language Dropdown */}
<div className="hidden lg:block">
<Dropdown
ref={languageRef}
isOpen={activeLanguageDropdown}
onToggle={() =>
setActiveLanguageDropdown(
!activeLanguageDropdown,
)
}
items={languages}
title="Select Language"
trigger={
<div className="flex items-center space-x-2 text-white/80 hover:text-white px-0 py-2 text-base font-medium transition-colors duration-200 cursor-pointer rounded-lg hover:bg-white/10 uppercase">
<Globe className="w-5 h-5" />
<span>ENG</span>
<ChevronDown
className={`w-4 h-4 transition-transform duration-200 ${activeLanguageDropdown ? "rotate-180" : ""}`}
/>
</div>
}
/>
</div>
{/* Shopping Cart */}
<div className="hidden lg:block">
<Dropdown
ref={cartRef}
isOpen={activeCartDropdown}
onToggle={() =>
setActiveCartDropdown(!activeCartDropdown)
}
items={[
...cartItems.map((item) => ({
id: item.id,
label: `${item.name} - ${item.price}`,
badge: `${item.quantity}x`,
})),
{
id: "total",
label: `Total: $${cartTotal.toFixed(2)}`,
icon: <ShoppingBag className="w-4 h-4" />,
},
{
id: "checkout",
label: "Proceed to Checkout",
action: () => console.log("Checkout"),
},
]}
title="Shopping Cart"
trigger={
<div className="relative text-white/80 hover:text-white p-0 transition-colors duration-200 rounded-lg hover:bg-white/10 cursor-pointer">
<ShoppingBag className="w-7 h-7" />
<motion.div
className="absolute -top-2 -right-2 w-6 h-6 bg-primary rounded-full flex items-center justify-center"
animate={{ scale: [1, 1.1, 1] }}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut",
}}
>
<span className="text-xs text-primary-foreground font-bold">
{cartItems.length}
</span>
</motion.div>
</div>
}
/>
</div>
{/* Mobile Menu Button */}
<div className="md:hidden">
<button
onClick={() => console.log('Mobile menu toggled')}
className="text-white/80 hover:text-white p-2"
>
<Menu className="w-6 h-6" />
</button>
</div>
{/* CTA Button */}
<Button
onClick={onSignInClick}
withShine={true}
className="hidden md:block h-[52px] min-w-[120px] lg:min-w-[180px] px-4 lg:px-6 py-4 rounded-full text-white font-medium bg-gradient-to-r from-primary to-secondary text-sm lg:text-base"
>
GET A CITY CARD
</Button>
</div>
</div>
</motion.div>
</motion.nav>
{/* City Submenu */}
<CitySubmenu
onClose={() => {}} // Empty function since it's always shown on homepage
currentPage={currentPage}
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onAttractionsClick={onAttractionsClick}
onPassesClick={onPassesClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
/>
{/* Main Content Container */}
<div className="relative z-10 min-h-screen flex items-center">
<div className="container mx-auto px-4 sm:px-6 lg:px-12">
<div className="flex justify-between items-center w-full">
{/* Left Content */}
<div className="max-w-4xl xl:max-w-5xl 2xl:max-w-6xl flex-1">
{/* Main Headline */}
<motion.div
className="absolute content-stretch flex flex-col gap-7 items-start justify-start leading-[0] left-[91.29px] not-italic top-[582px] w-[849.639px] max-w-[70vw]"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.8,
delay: 0.5,
ease: [0.25, 0.1, 0.25, 1],
}}
style={{
y: scrollY * -0.1, // Subtle counter-parallax for content
willChange: "transform",
}}
>
<div className="font-merchant leading-[64px] relative shrink-0 text-[#ffffff] text-[48px] sm:text-[52px] md:text-[60px] lg:text-[64px] w-[771.315px] max-w-full">
<p className="mb-0">
<span className="font-light">Melbourne</span>{' '}
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">City</span>{' '}
<span className="font-normal">Card.</span>
</p>
<p>
<span className="font-light">See</span>{' '}
<span className="font-semibold text-emphasis">More</span>{', '}
<span className="font-light">Spend</span>{' '}
<span className="font-bold">Less</span>{'.'}
</p>
</div>
<div
className="font-poppins font-normal min-w-full relative shrink-0 text-[22px] sm:text-[18px] md:text-[20px] lg:text-[22px] text-[rgba(255,255,255,0.74)]"
style={{ width: "min-content" }}
>
<p className="leading-[40px]">
Instant QR access to 40+ attractions,
exclusive perks, and savings up to 30%
</p>
</div>
</motion.div>
{/* CTA Button */}
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 0.8,
delay: 0.7,
ease: [0.25, 0.1, 0.25, 1],
}}
className="absolute left-[91.29px] top-[834.87px]"
style={{
y: scrollY * -0.05, // Minimal counter-parallax for button
willChange: "transform",
}}
>
<Button
withShine={true}
className="font-poppins font-semibold h-[61.805px] min-w-[262.349px] px-8 py-4 rounded-full text-white bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 transition-all duration-200 text-[20px]"
>
Explore Melbourne
</Button>
</motion.div>
</div>
{/* Right Side City List */}
<div className="hidden xl:block absolute right-[100px] top-[640px]">
<motion.div
className="space-y-6"
initial={{ opacity: 0, x: 30 }}
animate={{ opacity: 1, x: 0 }}
transition={{
duration: 0.8,
delay: 0.6,
ease: [0.25, 0.1, 0.25, 1],
}}
style={{
y: scrollY * -0.08, // Subtle counter-parallax for city list
willChange: "transform",
}}
>
{cityList.map((city, index) => (
<motion.div
key={city.id}
className="relative"
onMouseEnter={() => setHoveredCity(city.id)}
onMouseLeave={() => setHoveredCity(null)}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{
duration: 0.6,
delay: 0.6 + index * 0.1,
}}
>
{/* City Name */}
<div className="flex items-center cursor-pointer group">
<span className="font-poppins text-white/90 group-hover:text-white transition-colors duration-300 text-lg font-semibold">
{city.name}
</span>
</div>
{/* Hover Card */}
<AnimatePresence>
{hoveredCity === city.id && (
<motion.div
className="absolute right-full top-0 mr-6 z-30"
initial={{
opacity: 0,
scale: 0.9,
x: 10,
}}
animate={{
opacity: 1,
scale: 1,
x: 0,
}}
exit={{
opacity: 0,
scale: 0.9,
x: 10,
}}
transition={{
duration: 0.2,
ease: [0.16, 1, 0.3, 1],
}}
>
<div className="bg-white/95 backdrop-blur-md rounded-2xl p-4 shadow-2xl border border-white/20 min-w-64">
{/* Image */}
<div className="relative h-32 mb-3 rounded-xl overflow-hidden">
<ImageWithFallback
src={city.image}
alt={city.name}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/30 to-transparent" />
</div>
{/* Content */}
<div className="space-y-2">
<h4 className="text-lg font-semibold text-gray-900">
{city.name}
</h4>
<div className="flex items-center gap-2 text-sm text-gray-600">
<Star className="w-4 h-4 text-primary fill-current" />
<span className="font-medium">
{city.attractions} attractions
</span>
</div>
<p className="text-xs text-gray-500">
Discover the best experiences
</p>
</div>
</div>
{/* Arrow Pointer */}
<div className="absolute right-0 top-1/2 transform -translate-y-1/2 translate-x-2">
<div className="w-0 h-0 border-t-8 border-b-8 border-l-8 border-transparent border-l-white/95" />
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
))}
</motion.div>
</div>
</div>
</div>
</div>
</div>
);
}

108
src/components/HomePage.tsx Normal file
View File

@@ -0,0 +1,108 @@
import { motion, useScroll, useSpring, useTransform } from 'motion/react';
import { HeroSection } from './HeroSection';
import { WhyChooseCityCards } from './WhyChooseCityCards';
import { VarietyOfAdventures } from './VarietyOfAdventures';
import { MobileAppSection } from './MobileAppSection';
import { MagicItinerary } from './MagicItinerary';
import { ScrollAnimatedJourney } from './ScrollAnimatedJourney';
import { CustomPostcards } from './CustomPostcards';
import { BookAttractionSection } from './BookAttractionSection';
import { UpcomingCities } from './UpcomingCities';
import { TrustSection } from './TrustSection';
import { Footer } from './Footer';
import { SectionWrapper } from './SectionWrapper';
import { sectionsConfig } from '../utils/sections';
import {
heroVariants,
staggerContainer,
backgroundVariants
} from '../utils/animations';
interface HomePageProps {
isMobile: boolean;
onSignInClick?: () => void;
onPassesClick?: () => void;
currentPage?: string;
}
export function HomePage({ isMobile, onSignInClick, onPassesClick, currentPage }: HomePageProps) {
// Smooth scroll progress for global effects
const { scrollYProgress } = useScroll();
const scaleX = useSpring(scrollYProgress, {
stiffness: 100,
damping: 30,
restDelta: 0.001
});
// Parallax effect for scroll progress
const progressOpacity = useTransform(scrollYProgress, [0, 0.1], [0, 1]);
const sectionComponents = [
WhyChooseCityCards,
VarietyOfAdventures,
ScrollAnimatedJourney,
MagicItinerary,
BookAttractionSection,
CustomPostcards,
UpcomingCities,
TrustSection,
MobileAppSection
];
return (
<>
{/* Scroll Progress Indicator */}
<motion.div
className="fixed top-0 left-0 right-0 h-1 bg-gradient-to-r from-primary to-secondary origin-left z-40"
style={{
scaleX,
opacity: progressOpacity
}}
/>
{/* Main Content - No padding needed as capsule navbar floats */}
<main>
{/* 1. Hero Section - Immediate Load Animation */}
<motion.section
id="hero-section"
initial="hidden"
animate="visible"
variants={staggerContainer}
className="relative overflow-hidden"
>
<motion.div variants={backgroundVariants}>
<motion.div variants={heroVariants}>
<HeroSection
onSignInClick={onSignInClick}
onPassesClick={onPassesClick}
currentPage={currentPage}
/>
</motion.div>
</motion.div>
</motion.section>
{/* 2-10. All Other Sections */}
{sectionsConfig.map((config, index) => {
const Component = sectionComponents[index];
return (
<SectionWrapper
key={config.id}
id={config.id}
containerType={config.containerType}
backgroundGradient={config.backgroundGradient}
className={config.className}
variantType={config.variantType}
isMobile={isMobile}
>
<Component />
</SectionWrapper>
);
})}
</main>
{/* 11. Footer */}
<Footer />
</>
);
}

View File

@@ -0,0 +1 @@
// This file has been removed - consolidated into HowItWorksPage.tsx

View File

@@ -0,0 +1,875 @@
import { useState } from 'react';
import { motion } from 'motion/react';
import {
Search,
MapPin,
Calendar,
Users,
ChevronRight,
ChevronLeft,
Star,
Wifi,
Utensils,
Car,
Dumbbell,
Waves,
X,
Check,
Crown,
UserPlus,
Mail,
Smartphone,
BadgePercent,
Clock,
Shield,
ChevronDown,
SlidersHorizontal
} from 'lucide-react';
import { Button } from './ui/button';
import { Card, CardContent } from './ui/card';
import { Input } from './ui/input';
import { Badge } from './ui/badge';
import { Slider } from './ui/slider';
import { Checkbox } from './ui/checkbox';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import { ImageWithFallback } from './figma/ImageWithFallback';
import Navbar from './Navbar';
import { SubNavbar } from './SubNavbar';
import { Footer } from './Footer';
interface HotelDiscountsPageProps {
onBackClick: () => void;
onHomeClick: () => void;
onMelbourneClick: () => void;
onPassesClick: () => void;
onCheckoutClick: () => void;
onSignInClick: () => void;
onSignOutClick?: () => void;
onAttractionsClick: () => void;
onBlogsClick: () => void;
onHowItWorksClick: () => void;
onFAQClick: () => void;
onPrivacyPolicyClick: () => void;
onAboutUsClick: () => void;
onProfileClick: () => void;
onCityCardsClick: () => void;
onMagicItineraryClick: () => void;
onPostCardsClick: () => void;
onOffersClick: () => void;
onContactUsClick?: () => void;
onEsimsClick?: () => void;
onHotelDiscountsClick?: () => void;
currentPage?: string;
user?: { email: string; name: string } | null;
}
interface HotelDeal {
id: string;
name: string;
location: string;
image: string;
discount: number;
originalPrice: number;
salePrice: number;
rating: number;
reviews: number;
features: string[];
category: string;
}
interface City {
name: string;
image: string;
fromPrice: number;
}
export function HotelDiscountsPage({
onBackClick,
onHomeClick,
onMelbourneClick,
onPassesClick,
onCheckoutClick,
onSignInClick,
onSignOutClick,
onAttractionsClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onProfileClick,
onCityCardsClick,
onMagicItineraryClick,
onPostCardsClick,
onOffersClick,
onContactUsClick,
onEsimsClick,
onHotelDiscountsClick,
currentPage,
user
}: HotelDiscountsPageProps) {
const [selectedCategory, setSelectedCategory] = useState<string>('flash-sales');
const [featuredScrollPosition, setFeaturedScrollPosition] = useState(0);
const [priceRange, setPriceRange] = useState([50, 500]);
const [selectedStars, setSelectedStars] = useState<number[]>([]);
const [selectedAmenities, setSelectedAmenities] = useState<string[]>([]);
const [selectedFilter, setSelectedFilter] = useState('popular');
const [showFilters, setShowFilters] = useState(false);
const hotelDeals: HotelDeal[] = [
{
id: '1',
name: 'The Grand Palace Hotel',
location: 'Paris, France',
image: 'https://images.unsplash.com/photo-1694433053047-247d40a5f31b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxwYXJpcyUyMGhvdGVsJTIwZWlmZmVsfGVufDF8fHx8MTc1OTMyNDY0OHww&ixlib=rb-4.1.0&q=80&w=1080',
discount: 60,
originalPrice: 450,
salePrice: 180,
rating: 4.8,
reviews: 1234,
features: ['Free WiFi', 'Pool', 'Spa', 'Restaurant'],
category: 'luxury'
},
{
id: '2',
name: 'Tokyo Skyline Suites',
location: 'Tokyo, Japan',
image: 'https://images.unsplash.com/photo-1706985003864-43b6a85103ec?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx0b2t5byUyMGhvdGVsJTIwc2t5bGluZXxlbnwxfHx8fDE3NTkzMjQ2NTF8MA&ixlib=rb-4.1.0&q=80&w=1080',
discount: 45,
originalPrice: 320,
salePrice: 176,
rating: 4.9,
reviews: 892,
features: ['City View', 'Gym', 'WiFi', 'Parking'],
category: 'business'
},
{
id: '3',
name: 'Manhattan Luxury Resort',
location: 'New York, USA',
image: 'https://images.unsplash.com/photo-1479502806991-251c94be6b15?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxuZXclMjB5b3JrJTIwaG90ZWx8ZW58MXx8fHwxNzU5MzI0NjU0fDA&ixlib=rb-4.1.0&q=80&w=1080',
discount: 35,
originalPrice: 550,
salePrice: 358,
rating: 4.7,
reviews: 2103,
features: ['Rooftop Bar', 'Spa', 'Restaurant', 'WiFi'],
category: 'luxury'
},
{
id: '4',
name: 'London Thames View Hotel',
location: 'London, UK',
image: 'https://images.unsplash.com/photo-1500025188590-2faf31cc956c?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxsb25kb24lMjBob3RlbCUyMHZpZXd8ZW58MXx8fHwxNzU5MzI0NjU2fDA&ixlib=rb-4.1.0&q=80&w=1080',
discount: 50,
originalPrice: 380,
salePrice: 190,
rating: 4.6,
reviews: 1567,
features: ['River View', 'Breakfast', 'WiFi', 'Bar'],
category: 'boutique'
},
{
id: '5',
name: 'Resort Paradise Retreat',
location: 'Bali, Indonesia',
image: 'https://images.unsplash.com/photo-1570213489059-0aac6626cade?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxob3RlbCUyMHBvb2wlMjByZXNvcnR8ZW58MXx8fHwxNzU5MzE0NzM4fDA&ixlib=rb-4.1.0&q=80&w=1080',
discount: 55,
originalPrice: 280,
salePrice: 126,
rating: 4.9,
reviews: 3421,
features: ['Beach Access', 'Pool', 'Spa', 'All-Inclusive'],
category: 'family'
},
{
id: '6',
name: 'Boutique Central Hotel',
location: 'Melbourne, Australia',
image: 'https://images.unsplash.com/photo-1682221568203-16f33b35e57d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxib3V0aXF1ZSUyMGhvdGVsJTIwbG9iYnl8ZW58MXx8fHwxNzU5MjQ3NDUwfDA&ixlib=rb-4.1.0&q=80&w=1080',
discount: 40,
originalPrice: 220,
salePrice: 132,
rating: 4.8,
reviews: 756,
features: ['City Center', 'WiFi', 'Breakfast', 'Gym'],
category: 'boutique'
}
];
const cities: City[] = [
{ name: 'Paris', image: 'https://images.unsplash.com/photo-1694433053047-247d40a5f31b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxwYXJpcyUyMGhvdGVsJTIwZWlmZmVsfGVufDF8fHx8MTc1OTMyNDY0OHww&ixlib=rb-4.1.0&q=80&w=1080', fromPrice: 120 },
{ name: 'Tokyo', image: 'https://images.unsplash.com/photo-1706985003864-43b6a85103ec?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx0b2t5byUyMGhvdGVsJTIwc2t5bGluZXxlbnwxfHx8fDE3NTkzMjQ2NTF8MA&ixlib=rb-4.1.0&q=80&w=1080', fromPrice: 150 },
{ name: 'New York', image: 'https://images.unsplash.com/photo-1479502806991-251c94be6b15?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxuZXclMjB5b3JrJTIwaG90ZWx8ZW58MXx8fHwxNzU5MzI0NjU0fDA&ixlib=rb-4.1.0&q=80&w=1080', fromPrice: 200 },
{ name: 'London', image: 'https://images.unsplash.com/photo-1500025188590-2faf31cc956c?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxsb25kb24lMjBob3RlbCUyMHZpZXd8ZW58MXx8fHwxNzU5MzI0NjU2fDA&ixlib=rb-4.1.0&q=80&w=1080', fromPrice: 180 },
{ name: 'Bali', image: 'https://images.unsplash.com/photo-1570213489059-0aac6626cade?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxob3RlbCUyMHBvb2wlMjByZXNvcnR8ZW58MXx8fHwxNzU5MzE0NzM4fDA&ixlib=rb-4.1.0&q=80&w=1080', fromPrice: 80 },
{ name: 'Melbourne', image: 'https://images.unsplash.com/photo-1682221568203-16f33b35e57d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxib3V0aXF1ZSUyMGhvdGVsJTIwbG9iYnl8ZW58MXx8fHwxNzU5MjQ3NDUwfDA&ixlib=rb-4.1.0&q=80&w=1080', fromPrice: 110 },
{ name: 'Dubai', image: 'https://images.unsplash.com/photo-1655292912612-bb5b1bda9355?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtb2Rlcm4lMjBob3RlbCUyMGJlZHJvb218ZW58MXx8fHwxNzU5MzIxNDI4fDA&ixlib=rb-4.1.0&q=80&w=1080', fromPrice: 220 },
{ name: 'Singapore', image: 'https://images.unsplash.com/photo-1741003089080-eed43492dd54?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxsdXh1cnklMjBob3RlbCUyMGNpdHlzY2FwZXxlbnwxfHx8fDE3NTkzMjQ2NDJ8MA&ixlib=rb-4.1.0&q=80&w=1080', fromPrice: 160 },
{ name: 'Barcelona', image: 'https://images.unsplash.com/photo-1682221568203-16f33b35e57d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxib3V0aXF1ZSUyMGhvdGVsJTIwbG9iYnl8ZW58MXx8fHwxNzU5MjQ3NDUwfDA&ixlib=rb-4.1.0&q=80&w=1080', fromPrice: 130 },
{ name: 'Rome', image: 'https://images.unsplash.com/photo-1570213489059-0aac6626cade?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxob3RlbCUyMHBvb2wlMjByZXNvcnR8ZW58MXx8fHwxNzU5MzE0NzM4fDA&ixlib=rb-4.1.0&q=80&w=1080', fromPrice: 140 },
{ name: 'Amsterdam', image: 'https://images.unsplash.com/photo-1655292912612-bb5b1bda9355?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtb2Rlcm4lMjBob3RlbCUyMGJlZHJvb218ZW58MXx8fHwxNzU5MzIxNDI4fDA&ixlib=rb-4.1.0&q=80&w=1080', fromPrice: 170 },
{ name: 'Bangkok', image: 'https://images.unsplash.com/photo-1741003089080-eed43492dd54?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxsdXh1cnklMjBob3RlbCUyMGNpdHlzY2FwZXxlbnwxfHx8fDE3NTkzMjQ2NDJ8MA&ixlib=rb-4.1.0&q=80&w=1080', fromPrice: 70 }
];
const dealCategories = [
{ id: 'flash-sales', name: 'Flash Sales', icon: BadgePercent, count: 45 },
{ id: 'last-minute', name: 'Last Minute', icon: Clock, count: 32 },
{ id: 'early-bird', name: 'Early Bird', icon: Calendar, count: 28 },
{ id: 'weekend', name: 'Weekend Getaways', icon: Star, count: 51 },
{ id: 'member', name: 'Member Exclusive', icon: Crown, count: 19 },
{ id: 'package', name: 'Package Deals', icon: Shield, count: 37 },
{ id: 'long-stay', name: 'Long Stay', icon: Calendar, count: 24 },
{ id: 'group', name: 'Group Bookings', icon: Users, count: 16 }
];
const amenities = [
{ id: 'wifi', name: 'Free WiFi', icon: Wifi },
{ id: 'pool', name: 'Swimming Pool', icon: Waves },
{ id: 'gym', name: 'Fitness Center', icon: Dumbbell },
{ id: 'parking', name: 'Free Parking', icon: Car },
{ id: 'restaurant', name: 'Restaurant', icon: Utensils }
];
const scrollFeatured = (direction: 'left' | 'right') => {
const container = document.getElementById('featured-deals-container');
if (container) {
const scrollAmount = 400;
const newPosition = direction === 'left'
? Math.max(0, featuredScrollPosition - scrollAmount)
: Math.min(container.scrollWidth - container.clientWidth, featuredScrollPosition + scrollAmount);
container.scrollTo({ left: newPosition, behavior: 'smooth' });
setFeaturedScrollPosition(newPosition);
}
};
const toggleAmenity = (amenityId: string) => {
setSelectedAmenities(prev =>
prev.includes(amenityId)
? prev.filter(id => id !== amenityId)
: [...prev, amenityId]
);
};
const toggleStar = (stars: number) => {
setSelectedStars(prev =>
prev.includes(stars)
? prev.filter(s => s !== stars)
: [...prev, stars]
);
};
return (
<div className="min-h-screen bg-background">
{/* Navbar */}
<Navbar
activeCity="Paris"
onCityChange={() => {}}
onHomeClick={onHomeClick}
onPassesClick={onPassesClick}
onCheckoutClick={onCheckoutClick}
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick}
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
onEsimsClick={onEsimsClick}
onHotelDiscountsClick={onHotelDiscountsClick}
currentPage={currentPage}
isUserSignedIn={!!user}
user={user}
/>
{/* Sub Navbar */}
<SubNavbar
activePage="hotel-discounts"
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
onEsimsClick={onEsimsClick}
onHotelDiscountsClick={onHotelDiscountsClick}
/>
{/* Hero Section */}
<section className="relative pt-64 pb-20 overflow-hidden">
{/* Background Image with Overlay */}
<div className="absolute inset-0 z-0">
<ImageWithFallback
src="https://images.unsplash.com/photo-1741003089080-eed43492dd54?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxsdXh1cnklMjBob3RlbCUyMGNpdHlzY2FwZXxlbnwxfHx8fDE3NTkzMjQ2NDJ8MA&ixlib=rb-4.1.0&q=80&w=1080"
alt="Hotel Cityscape"
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-b from-black/60 via-black/40 to-background" />
</div>
<div className="container mx-auto px-4 relative z-10">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="text-center mb-12"
>
<h1 className="font-merchant mb-4">
<span className="text-white block text-4xl md:text-5xl lg:text-6xl leading-tight">
Exclusive Hotel Deals in Every City
</span>
</h1>
<p className="font-poppins text-xl text-white/90 max-w-2xl mx-auto font-normal">
Save up to 60% on handpicked accommodations
</p>
</motion.div>
{/* Search Widget */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
>
<Card className="max-w-4xl mx-auto shadow-2xl">
<CardContent className="p-6">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{/* Destination */}
<div className="relative">
<label className="font-poppins text-sm mb-2 block font-normal">Destination</label>
<div className="relative">
<MapPin className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
<Select>
<SelectTrigger className="pl-10 h-12 font-poppins font-normal">
<SelectValue placeholder="Select city" />
</SelectTrigger>
<SelectContent>
{cities.map((city) => (
<SelectItem key={city.name} value={city.name.toLowerCase()} className="font-poppins font-normal">
{city.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* Check-in Date */}
<div>
<label className="font-poppins text-sm mb-2 block font-normal">Check-in</label>
<div className="relative">
<Calendar className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
<Input type="date" className="pl-10 h-12 font-poppins font-normal" />
</div>
</div>
{/* Check-out Date */}
<div>
<label className="font-poppins text-sm mb-2 block font-normal">Check-out</label>
<div className="relative">
<Calendar className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
<Input type="date" className="pl-10 h-12 font-poppins font-normal" />
</div>
</div>
{/* Guests */}
<div>
<label className="font-poppins text-sm mb-2 block font-normal">Guests</label>
<div className="relative">
<Users className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
<Select>
<SelectTrigger className="pl-10 h-12 font-poppins font-normal">
<SelectValue placeholder="2 adults" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1" className="font-poppins font-normal">1 adult</SelectItem>
<SelectItem value="2" className="font-poppins font-normal">2 adults</SelectItem>
<SelectItem value="3" className="font-poppins font-normal">3 adults</SelectItem>
<SelectItem value="4" className="font-poppins font-normal">4+ adults</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
<Button className="w-full mt-6 h-12 font-poppins font-semibold bg-primary hover:bg-primary/90">
<Search className="w-5 h-5 mr-2" />
Search Deals
</Button>
</CardContent>
</Card>
</motion.div>
</div>
</section>
{/* Featured Deals Carousel */}
<section className="py-16 bg-gray-50">
<div className="container mx-auto px-4">
<div className="flex items-center justify-between mb-8">
<div>
<h2 className="font-merchant text-3xl md:text-4xl mb-2">
<span className="font-medium">Featured</span>{' '}
<span className="font-semibold text-primary">Deals</span>
</h2>
<div className="flex gap-2 mt-4">
{['flash-sales', 'last-minute', 'early-bird', 'weekend'].map((cat) => {
const category = dealCategories.find(c => c.id === cat);
return (
<Button
key={cat}
variant={selectedCategory === cat ? 'default' : 'outline'}
size="sm"
onClick={() => setSelectedCategory(cat)}
className="font-poppins font-medium"
>
{category?.name}
</Button>
);
})}
</div>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="icon"
onClick={() => scrollFeatured('left')}
className="rounded-full"
>
<ChevronLeft className="w-5 h-5" />
</Button>
<Button
variant="outline"
size="icon"
onClick={() => scrollFeatured('right')}
className="rounded-full"
>
<ChevronRight className="w-5 h-5" />
</Button>
</div>
</div>
{/* Carousel */}
<div
id="featured-deals-container"
className="flex gap-6 overflow-x-auto scrollbar-hide scroll-smooth pb-4"
onScroll={(e) => setFeaturedScrollPosition(e.currentTarget.scrollLeft)}
>
{hotelDeals.map((deal, index) => (
<motion.div
key={deal.id}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
className="flex-shrink-0 w-80"
>
<Card className="overflow-hidden h-full hover:shadow-xl transition-shadow duration-300">
<div className="relative h-48">
<ImageWithFallback
src={deal.image}
alt={deal.name}
className="w-full h-full object-cover"
/>
{/* Discount Badge */}
<div className="absolute top-4 right-4">
<div className="bg-primary text-white rounded-full w-16 h-16 flex items-center justify-center shadow-lg">
<div className="text-center">
<div className="font-poppins font-bold">{deal.discount}%</div>
<div className="font-poppins text-xs">OFF</div>
</div>
</div>
</div>
</div>
<CardContent className="p-4">
<h3 className="font-merchant text-xl mb-1 font-medium font-bold font-normal">{deal.name}</h3>
<p className="font-poppins text-sm text-gray-600 mb-3 font-normal flex items-center gap-1">
<MapPin className="w-4 h-4" />
{deal.location}
</p>
{/* Rating */}
<div className="flex items-center gap-2 mb-3">
<div className="flex">
{[...Array(5)].map((_, i) => (
<Star
key={i}
className={`w-4 h-4 ${i < Math.floor(deal.rating) ? 'fill-yellow-400 text-yellow-400' : 'text-gray-300'}`}
/>
))}
</div>
<span className="font-poppins text-sm font-normal text-gray-600">
{deal.rating} ({deal.reviews} reviews)
</span>
</div>
{/* Features */}
<div className="flex flex-wrap gap-2 mb-4">
{deal.features.slice(0, 3).map((feature) => (
<Badge key={feature} variant="secondary" className="font-poppins text-xs font-normal">
{feature}
</Badge>
))}
</div>
{/* Price */}
<div className="flex items-end justify-between">
<div>
<p className="font-poppins text-sm text-gray-500 line-through font-normal">
${deal.originalPrice}
</p>
<p className="font-poppins text-2xl font-bold text-primary">
${deal.salePrice}
<span className="text-sm font-normal text-gray-600">/night</span>
</p>
</div>
<Button className="font-poppins font-semibold">Book Now</Button>
</div>
</CardContent>
</Card>
</motion.div>
))}
</div>
</div>
</section>
{/* Deal Categories Grid */}
<section className="py-16">
<div className="container mx-auto px-4">
<h2 className="font-merchant text-3xl md:text-4xl mb-8 text-center">
<span className="font-medium">Browse by</span>{' '}
<span className="font-semibold text-primary">Category</span>
</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 max-w-5xl mx-auto">
{dealCategories.map((category, index) => (
<motion.div
key={category.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.05 }}
>
<Card className="cursor-pointer hover:shadow-lg hover:scale-105 transition-all duration-300 h-full">
<CardContent className="p-6 text-center">
<category.icon className="w-12 h-12 mx-auto mb-3 text-primary" />
<h3 className="font-merchant text-lg mb-2 font-medium">{category.name}</h3>
<p className="font-poppins text-sm text-gray-600 font-normal">{category.count} deals</p>
<Button variant="ghost" className="mt-3 w-full font-poppins font-medium">
View Deals
<ChevronRight className="w-4 h-4 ml-1" />
</Button>
</CardContent>
</Card>
</motion.div>
))}
</div>
</div>
</section>
{/* Destination Spotlight */}
<section className="py-16 bg-gray-50">
<div className="container mx-auto px-4">
<h2 className="font-merchant text-3xl md:text-4xl mb-2 text-center">
<span className="font-medium">Destination</span>{' '}
<span className="font-semibold text-primary">Spotlight</span>
</h2>
<p className="font-poppins text-center text-gray-600 mb-8 font-normal">
Discover amazing hotel deals in top destinations worldwide
</p>
{/* Filter Chips */}
<div className="flex flex-wrap justify-center gap-2 mb-8">
{['popular', 'seasonal', 'hidden-gems', 'city-pass'].map((filter) => (
<Button
key={filter}
variant={selectedFilter === filter ? 'default' : 'outline'}
size="sm"
onClick={() => setSelectedFilter(filter)}
className="font-poppins font-medium capitalize"
>
{filter.replace('-', ' ')}
</Button>
))}
</div>
{/* Cities Grid */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
{cities.map((city, index) => (
<motion.div
key={city.name}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{
duration: 0.4,
delay: Math.min(index * 0.05, 0.3),
ease: [0.23, 1, 0.32, 1]
}}
>
<Card className="overflow-hidden cursor-pointer group h-full shadow-md hover:shadow-2xl transition-shadow duration-300">
<div className="relative h-48 overflow-hidden">
<ImageWithFallback
src={city.image}
alt={city.name}
className="w-full h-full object-cover transition-transform duration-500 ease-out group-hover:scale-110"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/30 to-transparent" />
<div className="absolute bottom-0 left-0 right-0 p-4 text-white">
<h3 className="font-merchant text-xl mb-1 font-medium">{city.name}</h3>
<p className="font-poppins text-sm font-normal">From ${city.fromPrice}/night</p>
</div>
</div>
</Card>
</motion.div>
))}
</div>
</div>
</section>
{/* Filter & Results Section */}
<section className="py-16">
<div className="container mx-auto px-4">
<div className="flex items-center justify-between mb-8">
<h2 className="font-merchant text-3xl md:text-4xl">
<span className="font-medium">All</span>{' '}
<span className="font-semibold text-primary">Hotel Deals</span>
</h2>
{/* Mobile Filter Toggle */}
<Button
variant="outline"
onClick={() => setShowFilters(!showFilters)}
className="md:hidden font-poppins font-medium"
>
<SlidersHorizontal className="w-4 h-4 mr-2" />
Filters
</Button>
</div>
<div className="grid md:grid-cols-4 gap-8">
{/* Filters Sidebar */}
<div className={`${showFilters ? 'block' : 'hidden'} md:block`}>
<Card className="sticky top-24">
<CardContent className="p-6 space-y-6">
{/* Mobile Close Button */}
<div className="flex items-center justify-between md:hidden">
<h3 className="font-merchant text-xl font-medium">Filters</h3>
<Button
variant="ghost"
size="icon"
onClick={() => setShowFilters(false)}
>
<X className="w-5 h-5" />
</Button>
</div>
{/* Price Range */}
<div>
<h3 className="font-poppins font-semibold mb-4">Price Range</h3>
<Slider
value={priceRange}
onValueChange={setPriceRange}
min={0}
max={1000}
step={10}
className="mb-2"
/>
<div className="flex justify-between font-poppins text-sm font-normal text-gray-600">
<span>${priceRange[0]}</span>
<span>${priceRange[1]}</span>
</div>
</div>
{/* Star Rating */}
<div>
<h3 className="font-poppins font-semibold mb-4">Star Rating</h3>
<div className="space-y-2">
{[5, 4, 3, 2].map((stars) => (
<div key={stars} className="flex items-center gap-2">
<Checkbox
checked={selectedStars.includes(stars)}
onCheckedChange={() => toggleStar(stars)}
/>
<div className="flex">
{[...Array(stars)].map((_, i) => (
<Star key={i} className="w-4 h-4 fill-yellow-400 text-yellow-400" />
))}
</div>
</div>
))}
</div>
</div>
{/* Amenities */}
<div>
<h3 className="font-poppins font-semibold mb-4">Amenities</h3>
<div className="space-y-3">
{amenities.map((amenity) => (
<div key={amenity.id} className="flex items-center gap-2">
<Checkbox
checked={selectedAmenities.includes(amenity.id)}
onCheckedChange={() => toggleAmenity(amenity.id)}
/>
<amenity.icon className="w-4 h-4 text-gray-600" />
<span className="font-poppins text-sm font-normal">{amenity.name}</span>
</div>
))}
</div>
</div>
<Button className="w-full font-poppins font-semibold">Apply Filters</Button>
</CardContent>
</Card>
</div>
{/* Results Grid */}
<div className="md:col-span-3">
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{hotelDeals.map((deal, index) => (
<motion.div
key={deal.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
<Card className="overflow-hidden h-full hover:shadow-xl transition-all duration-300 group">
<div className="relative h-48">
<ImageWithFallback
src={deal.image}
alt={deal.name}
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-300"
/>
<div className="absolute top-3 right-3">
<div className="bg-primary text-white rounded-full w-14 h-14 flex items-center justify-center shadow-lg">
<div className="text-center">
<div className="font-poppins font-bold text-sm">{deal.discount}%</div>
<div className="font-poppins text-xs">OFF</div>
</div>
</div>
</div>
<Badge className="absolute bottom-3 left-3 bg-green-500 font-poppins text-xs font-normal">
Free Cancellation
</Badge>
</div>
<CardContent className="p-4">
<h3 className="font-merchant text-lg mb-1 font-medium">{deal.name}</h3>
<p className="font-poppins text-sm text-gray-600 mb-2 font-normal flex items-center gap-1">
<MapPin className="w-4 h-4" />
{deal.location}
</p>
<div className="flex items-center gap-2 mb-3">
<div className="flex">
{[...Array(5)].map((_, i) => (
<Star
key={i}
className={`w-3 h-3 ${i < Math.floor(deal.rating) ? 'fill-yellow-400 text-yellow-400' : 'text-gray-300'}`}
/>
))}
</div>
<span className="font-poppins text-xs font-normal text-gray-600">
({deal.reviews})
</span>
</div>
<div className="flex flex-wrap gap-1 mb-3">
{deal.features.slice(0, 2).map((feature) => (
<Badge key={feature} variant="secondary" className="font-poppins text-xs font-normal">
{feature}
</Badge>
))}
</div>
<div className="flex items-end justify-between pt-3 border-t">
<div>
<p className="font-poppins text-xs text-gray-500 line-through font-normal">
${deal.originalPrice}
</p>
<p className="font-poppins text-xl font-bold text-primary">
${deal.salePrice}
<span className="text-xs font-normal text-gray-600">/night</span>
</p>
</div>
<Button size="sm" className="font-poppins font-semibold">View Deal</Button>
</div>
</CardContent>
</Card>
</motion.div>
))}
</div>
</div>
</div>
</div>
</section>
{/* Special Offers Section */}
<section className="py-16 bg-gradient-to-br from-primary/5 to-secondary/5">
<div className="container mx-auto px-4">
<h2 className="font-merchant text-3xl md:text-4xl mb-12 text-center">
<span className="font-medium">Special</span>{' '}
<span className="font-semibold text-primary">Offers</span>
</h2>
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6 max-w-6xl mx-auto">
{[
{ icon: Crown, title: 'Member Exclusive', description: 'Extra 10% off for members', color: 'from-yellow-400 to-orange-500' },
{ icon: UserPlus, title: 'Refer Friends', description: 'Get $50 credit per referral', color: 'from-blue-400 to-purple-500' },
{ icon: Mail, title: 'Newsletter Deals', description: 'Exclusive subscriber offers', color: 'from-green-400 to-emerald-500' },
{ icon: Smartphone, title: 'App-Only Sales', description: 'Flash deals on mobile app', color: 'from-pink-400 to-rose-500' }
].map((offer, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
<Card className="h-full hover:shadow-xl transition-all duration-300 hover:scale-105 cursor-pointer">
<CardContent className="p-6 text-center">
<div className={`w-16 h-16 mx-auto mb-4 rounded-full bg-gradient-to-br ${offer.color} flex items-center justify-center`}>
<offer.icon className="w-8 h-8 text-white" />
</div>
<h3 className="font-merchant text-xl mb-2 font-medium">{offer.title}</h3>
<p className="font-poppins text-sm text-gray-600 mb-4 font-normal">{offer.description}</p>
<Button variant="outline" className="w-full font-poppins font-medium">
Learn More
</Button>
</CardContent>
</Card>
</motion.div>
))}
</div>
</div>
</section>
{/* Footer */}
<Footer
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onPassesClick={onPassesClick}
onCheckoutClick={onCheckoutClick}
onSignInClick={onSignInClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick}
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
onContactUsClick={onContactUsClick}
currentPage={currentPage}
/>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,406 @@
import { useState, useEffect, useRef } from 'react';
import { motion } from 'motion/react';
import { Check, ChevronRight, MapPin, Calendar, Smartphone, CreditCard } from 'lucide-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 { 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;
name: string;
}
interface HowItWorksPageProps {
onBackClick: () => void;
onHomeClick: () => void;
onMelbourneClick: () => void;
onPassesClick: () => void;
onCheckoutClick: () => void;
onSignInClick: () => void;
onSignOutClick: () => void;
onAttractionsClick: () => void;
onBlogsClick: () => void;
onHowItWorksClick: () => void;
onFAQClick: () => void;
onPrivacyPolicyClick: () => void;
onAboutUsClick: () => void;
onProfileClick: () => void;
onCityCardsClick: () => void;
onMagicItineraryClick: () => void;
onPostCardsClick: () => void;
onOffersClick: () => void;
onContactUsClick?: () => void;
onEsimsClick?: () => void;
onHotelDiscountsClick?: () => void;
currentPage: string;
user: User | null;
}
interface StepItem {
id: string;
title: string;
description: string;
icon: React.ReactNode;
isMainStep: boolean;
parentStep?: string;
image?: string;
features?: string[];
}
const steps: StepItem[] = [
{
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']
},
{
id: 'plan-itinerary',
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']
},
{
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']
}
];
export function HowItWorksPage({
onHomeClick,
onMelbourneClick,
onPassesClick,
onCheckoutClick,
onSignInClick,
onSignOutClick,
onAttractionsClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onProfileClick,
onCityCardsClick,
onMagicItineraryClick,
onPostCardsClick,
onOffersClick,
onContactUsClick,
onEsimsClick,
onHotelDiscountsClick,
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);
// 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));
setScrollProgress(progress);
};
window.addEventListener('scroll', handleScroll, { passive: true });
handleScroll(); // Initial call
return () => window.removeEventListener('scroll', handleScroll);
}, [activeStep, mainSteps]);
return (
<div className="min-h-screen bg-background">
{/* Navigation */}
<Navbar
activeCity="Melbourne"
onCityChange={() => {}}
onHomeClick={onHomeClick}
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
onPassesClick={onPassesClick}
onCheckoutClick={onCheckoutClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick}
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
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%" }}
animate={{
height: `${Math.min(100, scrollProgress * 100)}%`
}}
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] }}
/>
)}
</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}`;
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>
{/* 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>
{/* Step Title */}
<h3 className="font-poppins font-normal text-2xl text-gray-900 mb-4">
{step.title}
</h3>
{/* Step Description */}
<p className="text-gray-600 leading-relaxed mb-6 text-center font-light">
{step.description}
</p>
{/* 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>
)}
{/* 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>
</div>
{/* Make the most of every attraction section */}
<AttractionHassleFreeSection />
{/* Mobile App Section */}
<MobileAppSection />
{/* Why Choose CityCards Section */}
<div className="my-20">
<WhyChooseCityCards />
</div>
{/* Enhanced Testimonials Section */}
<EnhancedTestimonials />
{/* Reviews Section */}
<ReviewsSection />
</div>
<Footer
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onPassesClick={onPassesClick}
onSignInClick={onSignInClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onContactUsClick={onContactUsClick}
currentPage={currentPage}
/>
</div>
);
}
export default HowItWorksPage;

View File

@@ -0,0 +1,732 @@
import React, { useState } from 'react';
import { motion } from 'motion/react';
import { ArrowLeft, Calendar, Clock, MapPin, Users, Star, Heart, Share2, Download, CheckCircle, Navigation, Cloud, Sun } from 'lucide-react';
import { Button } from './ui/button';
import { Card, CardContent } from './ui/card';
import { Badge } from './ui/badge';
import Navbar from './Navbar';
import { Footer } from './Footer';
import { ImageWithFallback } from './figma/ImageWithFallback';
interface ItineraryViewPageProps {
onBackClick: () => void;
onHomeClick: () => void;
onMelbourneClick: () => void;
onPassesClick: () => void;
onCheckoutClick: () => void;
onSignInClick: () => void;
onSignOutClick: () => void;
onAttractionsClick: () => void;
onBlogsClick: () => void;
onHowItWorksClick: () => void;
onFAQClick: () => void;
onPrivacyPolicyClick: () => void;
onAboutUsClick: () => void;
onProfileClick: () => void;
onCityCardsClick: () => void;
onMagicItineraryClick: () => void;
onPostCardsClick: () => void;
onOffersClick: () => void;
onCreateItineraryClick: () => void;
onContactUsClick?: () => void;
onEsimsClick?: () => void;
onHotelDiscountsClick?: () => void;
currentPage: string;
user?: { email: string; name: string; } | null;
}
// Enhanced activity type with more details
interface Activity {
time: string;
activity: string;
location: string;
address: string;
image: string;
categories: string[];
description: string[];
isFavorite?: boolean;
}
export function ItineraryViewPage({
onBackClick,
onHomeClick,
onMelbourneClick,
onPassesClick,
onCheckoutClick,
onSignInClick,
onSignOutClick,
onAttractionsClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onProfileClick,
onCityCardsClick,
onMagicItineraryClick,
onPostCardsClick,
onOffersClick,
onCreateItineraryClick,
onContactUsClick,
onEsimsClick,
onHotelDiscountsClick,
currentPage,
user
}: ItineraryViewPageProps) {
const [viewMode, setViewMode] = useState<'daily' | 'summary'>('daily');
const [favorites, setFavorites] = useState<Set<string>>(new Set());
const toggleFavorite = (activityKey: string) => {
setFavorites(prev => {
const newSet = new Set(prev);
if (newSet.has(activityKey)) {
newSet.delete(activityKey);
} else {
newSet.add(activityKey);
}
return newSet;
});
};
// Enhanced itinerary data with images, addresses, and detailed info
const generatedItinerary = {
destination: {
name: 'Melbourne',
country: 'Australia',
weather: '18°C, Sunny',
image: 'https://images.unsplash.com/photo-1514395462725-fb4566210144?w=400&h=300&fit=crop'
},
totalDays: 3,
estimatedCost: '$450 AUD',
includedActivities: 18,
dailyPlans: [
{
day: 1,
title: "City Center & Culture",
activities: [
{
time: '8:00 am',
activity: 'The Langham Melbourne',
location: 'The Langham Melbourne',
address: '1 Southgate Avenue, Southbank VIC 3006',
image: 'https://images.unsplash.com/photo-1566073771259-6a8506099945?w=800&h=600&fit=crop',
categories: ['Accommodation', 'Luxury'],
description: [
'Check-in at luxury riverside hotel',
'Enjoy complimentary breakfast',
'Relax at the spa facilities',
'Explore the surrounding Southbank area'
]
},
{
time: '10:00 am',
activity: 'Federation Square',
location: 'Federation Square',
address: 'Corner Swanston & Flinders Streets, Melbourne VIC 3000',
image: 'https://images.unsplash.com/photo-1514395462725-fb4566210144?w=800&h=600&fit=crop',
categories: ['Culture', 'Landmark'],
description: [
'Explore Melbourne\'s cultural precinct',
'Visit the ACMI museum',
'Enjoy street performances',
'Take photos at iconic locations'
]
},
{
time: '12:00 pm',
activity: 'Degrave Street Café',
location: 'Degrave Street Espresso Bar',
address: '23-25 Degraves Street, Melbourne VIC 3000',
image: 'https://images.unsplash.com/photo-1554118811-1e0d58224f24?w=800&h=600&fit=crop',
categories: ['Food', 'Drinks', 'Culture'],
description: [
'Coffee at Pellegrini\'s Espresso Bar (iconic old-school cafe)',
'Try the famous jam doughnuts',
'Shop for fresh produce in the Deli Hall',
'Pick up unique souvenirs in the General Merchandise section'
]
},
{
time: '2:00 pm',
activity: 'Royal Botanic Gardens',
location: 'Royal Botanic Gardens Victoria',
address: 'Birdwood Avenue, South Yarra VIC 3141',
image: 'https://images.unsplash.com/photo-1585320806297-9794b3e4eeae?w=800&h=600&fit=crop',
categories: ['Nature', 'Culture'],
description: [
'Stroll through stunning landscaped gardens',
'Visit the Australian Forest Walk',
'Relax by the Ornamental Lake',
'Join a free guided walking tour'
]
},
{
time: '4:00 pm',
activity: 'National Gallery of Victoria',
location: 'NGV International',
address: '180 St Kilda Road, Melbourne VIC 3006',
image: 'https://images.unsplash.com/photo-1564399577149-749794d74eee?w=800&h=600&fit=crop',
categories: ['Culture', 'Art'],
description: [
'Explore Australia\'s oldest art museum',
'View international and Australian art collections',
'Visit the stunning water wall entrance',
'Browse the NGV design store'
]
},
{
time: '7:00 pm',
activity: 'Dinner at Chin Chin',
location: 'Chin Chin Restaurant',
address: '125 Flinders Lane, Melbourne VIC 3000',
image: 'https://images.unsplash.com/photo-1552566626-52f8b828add9?w=800&h=600&fit=crop',
categories: ['Food', 'Drinks'],
description: [
'Experience modern Thai cuisine',
'Try signature dishes like the Betel Leaf',
'Enjoy the vibrant atmosphere',
'Book ahead or walk-in for bar seating'
]
}
]
},
{
day: 2,
title: "Markets & Neighborhoods",
activities: [
{
time: '8:00 am',
activity: 'Queen Victoria Market',
location: 'Queen Victoria Market',
address: 'Queen Street, Melbourne VIC 3000',
image: 'https://images.unsplash.com/photo-1555939594-58d7cb561ad1?w=800&h=600&fit=crop',
categories: ['Food', 'Shopping', 'Culture'],
description: [
'Explore Melbourne\'s historic market (since 1878)',
'Sample fresh local produce',
'Shop for artisan goods and souvenirs',
'Grab breakfast at the Deli Hall'
]
},
{
time: '10:30 am',
activity: 'Fitzroy Street Art Tour',
location: 'Fitzroy Arts Precinct',
address: 'Gertrude Street, Fitzroy VIC 3065',
image: 'https://images.unsplash.com/photo-1499781350541-7783f6c6a0c8?w=800&h=600&fit=crop',
categories: ['Culture', 'Art'],
description: [
'Walk through famous street art laneways',
'Discover works by renowned artists',
'Visit independent galleries',
'Explore vintage and record stores'
]
},
{
time: '12:30 pm',
activity: 'Brunswick Street Lunch',
location: 'Brunswick Street Precinct',
address: 'Brunswick Street, Fitzroy VIC 3065',
image: 'https://images.unsplash.com/photo-1517248135467-4c7edcad34c4?w=800&h=600&fit=crop',
categories: ['Food', 'Drinks'],
description: [
'Choose from diverse dining options',
'Try local cafes and restaurants',
'Explore bookshops and boutiques',
'Enjoy the vibrant neighborhood atmosphere'
]
},
{
time: '2:30 pm',
activity: 'Carlton Gardens',
location: 'Carlton Gardens',
address: 'Carlton Gardens, Carlton VIC 3053',
image: 'https://images.unsplash.com/photo-1519331379826-f10be5486c6f?w=800&h=600&fit=crop',
categories: ['Nature', 'Culture', 'Landmark'],
description: [
'Visit the UNESCO World Heritage site',
'See the Royal Exhibition Building',
'Stroll through Victorian-era gardens',
'Relax by the ornamental fountains'
]
},
{
time: '4:00 pm',
activity: 'Melbourne Museum',
location: 'Melbourne Museum',
address: '11 Nicholson Street, Carlton VIC 3053',
image: 'https://images.unsplash.com/photo-1566127992631-137a642a90f4?w=800&h=600&fit=crop',
categories: ['Culture', 'Museum'],
description: [
'Explore natural and cultural history',
'Visit the Bunjilaka Aboriginal Centre',
'See the Forest Gallery living ecosystem',
'Discover Melbourne\'s story exhibition'
]
},
{
time: '7:00 pm',
activity: 'Rooftop Bar Experience',
location: 'Naked for Satan',
address: '285 Brunswick Street, Fitzroy VIC 3065',
image: 'https://images.unsplash.com/photo-1514933651103-005eec06c04b?w=800&h=600&fit=crop',
categories: ['Drinks', 'Food'],
description: [
'Enjoy sunset views from the rooftop',
'Try Spanish-style pintxos',
'Sample craft cocktails and local beers',
'Experience Melbourne\'s bar culture'
]
}
]
},
{
day: 3,
title: "Coastal Adventure",
activities: [
{
time: '8:00 am',
activity: 'St. Kilda Beach',
location: 'St. Kilda Beach',
address: 'Jacka Boulevard, St Kilda VIC 3182',
image: 'https://images.unsplash.com/photo-1505142468610-359e7d316be0?w=800&h=600&fit=crop',
categories: ['Nature', 'Beach'],
description: [
'Morning walk along the iconic beach',
'Visit the historic St Kilda Pier',
'See the little penguins at sunset',
'Explore the Sunday Esplanade Market (weekends)'
]
},
{
time: '10:00 am',
activity: 'Acland Street Cafes',
location: 'Acland Street',
address: 'Acland Street, St Kilda VIC 3182',
image: 'https://images.unsplash.com/photo-1495474472287-4d71bcdd2085?w=800&h=600&fit=crop',
categories: ['Food', 'Drinks'],
description: [
'Brunch at famous cake shops',
'Try traditional European pastries',
'Visit Lentil as Anything (pay-as-you-feel)',
'Browse vintage shops and bookstores'
]
},
{
time: '12:00 pm',
activity: 'Luna Park Melbourne',
location: 'Luna Park Melbourne',
address: '18 Lower Esplanade, St Kilda VIC 3182',
image: 'https://images.unsplash.com/photo-1513026705753-bc3fffca8bf4?w=800&h=600&fit=crop',
categories: ['Entertainment', 'Landmark'],
description: [
'Visit Melbourne\'s iconic amusement park',
'Ride the historic Scenic Railway (1912)',
'Take photos at Mr Moon entrance',
'Enjoy carnival games and rides'
]
},
{
time: '2:00 pm',
activity: 'Brighton Beach Boxes',
location: 'Brighton Beach',
address: 'Esplanade, Brighton VIC 3186',
image: 'https://images.unsplash.com/photo-1520208422220-d12a3c588e6c?w=800&h=600&fit=crop',
categories: ['Culture', 'Landmark'],
description: [
'Photograph the famous colorful bathing boxes',
'Walk along the pristine beach',
'Learn about the heritage structures',
'Relax in the beachside atmosphere'
]
},
{
time: '4:00 pm',
activity: 'Southbank Promenade',
location: 'Southbank',
address: 'Southbank Promenade, Southbank VIC 3006',
image: 'https://images.unsplash.com/photo-1559827260-dc66d52bef19?w=800&h=600&fit=crop',
categories: ['Culture', 'Shopping'],
description: [
'Stroll along the Yarra River',
'Visit arts and craft markets',
'Explore restaurants and cafes',
'Enjoy river views and street performers'
]
},
{
time: '7:00 pm',
activity: 'Farewell Dinner at Vue de Monde',
location: 'Vue de Monde',
address: 'Level 55, Rialto, 525 Collins Street, Melbourne VIC 3000',
image: 'https://images.unsplash.com/photo-1414235077428-338989a2e8c0?w=800&h=600&fit=crop',
categories: ['Food', 'Drinks', 'Luxury'],
description: [
'Experience fine dining at 55th floor',
'Enjoy panoramic Melbourne views',
'Taste modern Australian cuisine',
'Celebrate the end of your journey'
]
}
]
}
]
};
return (
<div className="min-h-screen bg-background">
{/* Navbar */}
<Navbar
activeCity=""
onCityChange={() => {}}
onSignInClick={onSignInClick}
onPassesClick={onPassesClick}
onCheckoutClick={onCheckoutClick}
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick}
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
currentPage="itinerary-view"
isUserSignedIn={!!user}
user={user}
/>
{/* Header Section */}
<section className="pt-32 pb-8 bg-gradient-to-br from-primary/5 to-secondary/5">
<div className="container mx-auto px-4 pt-32">
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="text-center"
>
<Button
variant="ghost"
onClick={onBackClick}
className="mb-6 hover:bg-primary/5 font-poppins font-medium"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Magic Itinerary
</Button>
<div className="flex items-center justify-center gap-2 text-primary mb-4">
<Star className="w-6 h-6 fill-current" />
<h1 className="font-merchant text-4xl md:text-5xl lg:text-6xl leading-tight">
<span className="font-light">Your</span>{' '}
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">Magic Itinerary</span>
</h1>
<Star className="w-6 h-6 fill-current" />
</div>
<p className="font-poppins text-xl leading-relaxed font-normal text-gray-600 max-w-2xl mx-auto">
Here's your personalized {generatedItinerary.totalDays}-day adventure in {generatedItinerary.destination.name}!
</p>
</motion.div>
</div>
</section>
{/* Main Content */}
<section className="py-8">
<div className="container mx-auto px-4">
<div className="max-w-6xl mx-auto space-y-8">
{/* View Toggle */}
<div className="flex justify-center">
<div className="bg-muted p-1 rounded-lg">
<Button
variant={viewMode === 'daily' ? 'default' : 'ghost'}
size="sm"
onClick={() => setViewMode('daily')}
className="rounded-md font-poppins font-medium"
>
Daily View
</Button>
<Button
variant={viewMode === 'summary' ? 'default' : 'ghost'}
size="sm"
onClick={() => setViewMode('summary')}
className="rounded-md font-poppins font-medium"
>
Summary
</Button>
</div>
</div>
{/* Itinerary Overview */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
>
<Card className="p-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="text-center">
<div className="font-merchant text-3xl text-primary mb-2">{generatedItinerary.totalDays}</div>
<div className="font-poppins text-sm text-muted-foreground font-normal">Days</div>
</div>
<div className="text-center">
<div className="font-merchant text-3xl text-primary mb-2">{generatedItinerary.includedActivities}</div>
<div className="font-poppins text-sm text-muted-foreground font-normal">Activities</div>
</div>
<div className="text-center">
<div className="font-merchant text-3xl text-primary mb-2">{generatedItinerary.estimatedCost}</div>
<div className="font-poppins text-sm text-muted-foreground font-normal">Estimated Cost</div>
</div>
</div>
</Card>
</motion.div>
{/* Daily Plans - Enhanced View */}
{viewMode === 'daily' && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
className="space-y-12"
>
{generatedItinerary.dailyPlans.map((day, dayIndex) => (
<div key={dayIndex} className="space-y-6">
{/* Location Header with Weather - Only show for first day or when location changes */}
{(dayIndex === 0 ||
(dayIndex > 0 &&
generatedItinerary.dailyPlans[dayIndex - 1] &&
(generatedItinerary.dailyPlans[dayIndex - 1] as any).destination?.name !== generatedItinerary.destination.name)) && (
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: 0.4 + dayIndex * 0.1 }}
className="bg-gray-50 rounded-2xl p-6 shadow-sm border border-gray-200"
>
<div className="flex items-center justify-between">
<div>
<h2 className="font-merchant text-3xl md:text-4xl leading-tight mb-2">
{generatedItinerary.destination.name}, {generatedItinerary.destination.country}
</h2>
<p className="font-poppins text-base text-primary font-medium">{generatedItinerary.destination.weather}</p>
</div>
<div className="flex items-center gap-2">
<Sun className="w-10 h-10 text-amber-500" />
</div>
</div>
</motion.div>
)}
{/* Day Header */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: 0.5 + dayIndex * 0.1 }}
className="flex items-center gap-4 pl-2"
>
<div className="bg-gradient-to-br from-primary to-secondary text-white w-16 h-16 rounded-full flex items-center justify-center shadow-lg">
<span className="font-merchant text-2xl font-semibold">{day.day}</span>
</div>
<div>
<h3 className="font-merchant text-2xl md:text-3xl leading-snug font-semibold">Day {day.day}</h3>
<p className="font-poppins text-base text-muted-foreground font-normal">{day.title}</p>
</div>
</motion.div>
{/* GMT Label */}
<div className="pl-2">
<p className="font-poppins text-sm text-gray-500 font-normal">GMT</p>
</div>
{/* Activity Cards - Desktop Grid Layout */}
<div className="space-y-8">
{day.activities.map((activity, actIndex) => {
const activityKey = `day${day.day}-act${actIndex}`;
const isFavorite = favorites.has(activityKey);
return (
<motion.div
key={actIndex}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.6 + dayIndex * 0.1 + actIndex * 0.05 }}
className="flex gap-6"
>
{/* Time Column */}
<div className="flex-shrink-0 w-24 pt-2">
<div className="font-poppins text-base font-medium text-gray-700">{activity.time}</div>
</div>
{/* Activity Card */}
<div className="flex-1">
<Card className="overflow-hidden hover:shadow-xl transition-shadow duration-300 border-2 border-gray-100">
<CardContent className="p-0">
{/* Hero Image with Overlay Buttons */}
<div className="relative h-64 md:h-72 bg-gray-200">
<ImageWithFallback
src={activity.image}
alt={activity.activity}
className="w-full h-full object-cover"
/>
{/* Favorite Heart Button - Top Right */}
<div className="absolute top-4 right-4">
<Button
size="icon"
variant="secondary"
className="bg-white/95 hover:bg-white shadow-lg backdrop-blur-sm rounded-full w-12 h-12"
onClick={() => toggleFavorite(activityKey)}
>
<Heart className={`w-5 h-5 ${isFavorite ? 'fill-primary text-primary' : 'text-gray-700'}`} />
</Button>
</div>
{/* Get Directions Button - Bottom Left */}
<div className="absolute bottom-4 left-4">
<Button
className="bg-primary hover:bg-primary/90 text-white font-poppins font-semibold shadow-lg px-6 py-3 rounded-xl"
>
<Navigation className="w-4 h-4 mr-2" />
Get Directions
</Button>
</div>
</div>
{/* Content Section */}
<div className="p-6 space-y-4">
{/* Location Name & Address */}
<div className="space-y-2">
<h4 className="font-merchant text-xl md:text-2xl leading-snug font-semibold text-gray-900">
{activity.activity}
</h4>
<div className="flex items-start gap-2 text-gray-600">
<MapPin className="w-4 h-4 mt-1 flex-shrink-0 text-primary" />
<span className="font-poppins text-sm font-normal leading-relaxed">{activity.address}</span>
</div>
</div>
{/* Category Badges */}
<div className="flex flex-wrap gap-2">
{activity.categories.map((category, catIndex) => (
<Badge
key={catIndex}
variant="secondary"
className="font-poppins font-normal text-sm bg-primary/10 text-primary hover:bg-primary/20 px-3 py-1"
>
{category}
</Badge>
))}
</div>
{/* Activity Details - Bullet Points */}
<div className="space-y-2 pt-2">
{activity.description.map((detail, detailIndex) => (
<div key={detailIndex} className="flex items-start gap-3">
<span className="text-primary font-semibold mt-1.5 flex-shrink-0">•</span>
<span className="font-poppins text-sm font-normal text-gray-600 leading-relaxed">{detail}</span>
</div>
))}
</div>
</div>
</CardContent>
</Card>
</div>
</motion.div>
);
})}
</div>
</div>
))}
</motion.div>
)}
{/* Summary View */}
{viewMode === 'summary' && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
className="space-y-4"
>
<h3 className="font-merchant text-2xl md:text-3xl text-center mb-8 leading-tight font-semibold">Trip Summary</h3>
<Card className="p-6">
<div className="space-y-6">
{generatedItinerary.dailyPlans.map((day, index) => (
<div key={index} className="border-l-4 border-primary pl-6">
<div className="flex items-center gap-2 mb-3">
<Calendar className="w-5 h-5 text-primary" />
<h4 className="font-merchant text-lg md:text-xl leading-snug font-semibold">Day {day.day}: {day.title}</h4>
</div>
<div className="space-y-2">
{day.activities.map((activity, actIndex) => (
<div key={actIndex} className="flex items-start gap-3 text-sm">
<CheckCircle className="w-4 h-4 text-green-500 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<span className="font-poppins text-gray-700 font-medium">{activity.activity}</span>
<div className="flex items-center gap-2 mt-1">
<span className="font-poppins text-gray-500 text-xs font-normal">{activity.time}</span>
<span className="text-gray-400"></span>
<span className="font-poppins text-gray-500 text-xs font-normal">{activity.location}</span>
</div>
</div>
</div>
))}
</div>
</div>
))}
</div>
</Card>
</motion.div>
)}
{/* Action Buttons */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.5 }}
className="flex flex-col sm:flex-row gap-4 justify-center pt-8"
>
<Button
variant="outline"
onClick={onCreateItineraryClick}
className="font-poppins font-medium px-8 py-3 text-lg"
>
<Heart className="w-5 h-5 mr-2" />
Create Another
</Button>
<Button
className="bg-primary hover:bg-primary/90 font-poppins font-semibold px-8 py-3 text-lg"
>
<Download className="w-5 h-5 mr-2" />
Save Itinerary
</Button>
<Button
variant="outline"
className="font-poppins font-medium px-8 py-3 text-lg"
>
<Share2 className="w-5 h-5 mr-2" />
Share Trip
</Button>
</motion.div>
</div>
</div>
</section>
{/* Footer */}
<Footer
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onPassesClick={onPassesClick}
onSignInClick={onSignInClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onContactUsClick={onContactUsClick}
currentPage={currentPage}
/>
</div>
);
}

View File

@@ -0,0 +1,285 @@
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { X } from 'lucide-react';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Label } from './ui/label';
interface LoginModalProps {
isOpen: boolean;
onClose: () => void;
onLoginSuccess: (userData: { email: string; name: string }) => void;
}
export function LoginModal({ isOpen, onClose, onLoginSuccess }: LoginModalProps) {
const [step, setStep] = useState<'email' | 'otp'>('email');
const [email, setEmail] = useState('');
const [otp, setOtp] = useState(['', '', '', '', '', '']);
const [countdown, setCountdown] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const [helperText, setHelperText] = useState('');
// Reset modal state when closed
useEffect(() => {
if (!isOpen) {
setStep('email');
setEmail('');
setOtp(['', '', '', '', '', '']);
setCountdown(0);
setHelperText('');
}
}, [isOpen]);
// Countdown timer for OTP resend
useEffect(() => {
if (countdown > 0) {
const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
return () => clearTimeout(timer);
}
}, [countdown]);
const handleSendOTP = async () => {
if (!email || !email.includes('@')) {
setHelperText('Please enter a valid email address');
return;
}
setIsLoading(true);
setHelperText('');
// Simulate API call
setTimeout(() => {
setStep('otp');
setCountdown(120); // 2 minutes countdown
setIsLoading(false);
setHelperText('OTP sent successfully');
}, 1500);
};
const handleOTPChange = (index: number, value: string) => {
if (value.length > 1) return; // Only allow single digit
const newOtp = [...otp];
newOtp[index] = value;
setOtp(newOtp);
// Auto-focus next input
if (value && index < 5) {
const nextInput = document.querySelector(`input[data-otp-index="${index + 1}"]`) as HTMLInputElement;
nextInput?.focus();
}
};
const handleOTPKeyDown = (index: number, e: React.KeyboardEvent) => {
if (e.key === 'Backspace' && !otp[index] && index > 0) {
const prevInput = document.querySelector(`input[data-otp-index="${index - 1}"]`) as HTMLInputElement;
prevInput?.focus();
}
};
const handleVerifyLogin = async () => {
const otpString = otp.join('');
if (otpString.length !== 6) {
setHelperText('Please enter complete OTP');
return;
}
setIsLoading(true);
setHelperText('');
// Simulate API call
setTimeout(() => {
// Generate name from email for demo
const emailParts = email.split('@')[0];
const name = emailParts.charAt(0).toUpperCase() + emailParts.slice(1);
onLoginSuccess({
email,
name: name.length > 8 ? name.substring(0, 8) : name
});
setIsLoading(false);
onClose();
}, 1500);
};
const formatCountdown = (seconds: number) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
return (
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50"
onClick={onClose}
/>
{/* Modal */}
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
transition={{ duration: 0.3, ease: "easeOut" }}
className="fixed inset-0 flex items-center justify-center z-50 p-4"
>
<div className="bg-white rounded-3xl shadow-2xl w-full max-w-md mx-auto overflow-hidden">
{/* Header */}
<div className="relative px-8 pt-8 pb-4">
<button
onClick={onClose}
className="absolute top-6 right-6 w-8 h-8 flex items-center justify-center rounded-full bg-gray-100 hover:bg-gray-200 transition-colors"
>
<X className="w-4 h-4 text-gray-600" />
</button>
<h2 className="font-merchant text-2xl font-semibold text-gray-900 mb-2">
Login
</h2>
<p className="font-poppins text-sm text-gray-600">
Enter your email Id and verify with OTP sent on it.
</p>
</div>
{/* Content */}
<div className="px-8 pb-8">
{step === 'email' ? (
<div className="space-y-6">
{/* Email Input */}
<div className="space-y-2">
<Label className="font-poppins text-sm font-medium text-gray-700">
Email
</Label>
<Input
type="email"
placeholder="Name@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400"
onKeyDown={(e) => e.key === 'Enter' && handleSendOTP()}
/>
{helperText && (
<p className={`font-poppins text-xs ${helperText.includes('success') ? 'text-green-600' : 'text-red-500'}`}>
{helperText}
</p>
)}
</div>
{/* Send OTP Button */}
<Button
onClick={handleSendOTP}
disabled={isLoading}
className="w-full h-12 bg-gray-800 hover:bg-gray-900 text-white font-poppins font-semibold rounded-xl transition-colors"
>
{isLoading ? (
<div className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin" />
Sending OTP...
</div>
) : (
<>
Send OTP
<svg className="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</>
)}
</Button>
</div>
) : (
<div className="space-y-6">
{/* Email Display */}
<div className="space-y-2">
<Label className="font-poppins text-sm font-medium text-gray-700">
Email
</Label>
<div className="h-12 bg-gray-50 rounded-xl flex items-center px-4">
<span className="font-poppins text-base text-gray-600">{email}</span>
</div>
{helperText && (
<p className={`font-poppins text-xs ${helperText.includes('success') ? 'text-green-600' : 'text-red-500'}`}>
{helperText}
</p>
)}
</div>
{/* OTP Input */}
<div className="space-y-3">
<Label className="font-poppins text-sm font-medium text-gray-700">
Enter OTP
</Label>
<div className="flex gap-3 justify-between">
{otp.map((digit, index) => (
<input
key={index}
type="text"
inputMode="numeric"
pattern="[0-9]*"
maxLength={1}
value={digit}
onChange={(e) => handleOTPChange(index, e.target.value.replace(/\D/g, ''))}
onKeyDown={(e) => handleOTPKeyDown(index, e)}
data-otp-index={index}
className="w-12 h-12 text-center font-poppins font-semibold text-lg bg-gray-50 border-0 rounded-xl focus:bg-white focus:ring-2 focus:ring-primary focus:outline-none transition-colors"
/>
))}
</div>
{/* Countdown */}
{countdown > 0 && (
<p className="font-poppins text-xs text-gray-500 text-center">
{formatCountdown(countdown)}
</p>
)}
</div>
{/* Verify Button */}
<Button
onClick={handleVerifyLogin}
disabled={isLoading || otp.join('').length !== 6}
className="w-full h-12 bg-gray-800 hover:bg-gray-900 text-white font-poppins font-semibold rounded-xl transition-colors disabled:opacity-50"
>
{isLoading ? (
<div className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-white/20 border-t-white rounded-full animate-spin" />
Verifying...
</div>
) : (
<>
Verify and Login
<svg className="w-4 h-4 ml-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</>
)}
</Button>
{/* Resend OTP */}
{countdown === 0 && (
<button
onClick={() => {
setStep('email');
setOtp(['', '', '', '', '', '']);
}}
className="w-full font-poppins text-sm text-gray-600 hover:text-gray-800 transition-colors"
>
Didn't receive OTP? Send again
</button>
)}
</div>
)}
</div>
</div>
</motion.div>
</>
)}
</AnimatePresence>
);
}

View File

@@ -0,0 +1,565 @@
import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { Sparkles, Clock, MapPin, Coffee, Camera, UtensilsCrossed, TreePine, Building2, Star, Navigation, ChevronRight, Heart, Users } from 'lucide-react';
import { Button } from './ui/button';
import { ImageWithFallback } from './figma/ImageWithFallback';
interface MagicItineraryProps {
onCreateItineraryClick?: () => void;
}
export function MagicItinerary({ onCreateItineraryClick }: MagicItineraryProps) {
const [currentTime, setCurrentTime] = useState('9:41');
const [activeItineraryStep, setActiveItineraryStep] = useState(0);
// Animate through itinerary steps
useEffect(() => {
const interval = setInterval(() => {
setActiveItineraryStep(prev => (prev + 1) % 6);
}, 3000);
return () => clearInterval(interval);
}, []);
const itinerarySteps = [
{
time: '9:00 AM',
duration: '2 hours',
title: 'Royal Botanic Gardens',
category: 'Nature & Parks',
description: 'Start your day with a peaceful walk through stunning gardens',
image: 'https://images.unsplash.com/photo-1670027537688-77def132d556?w=300&h=200&fit=crop',
icon: TreePine,
color: 'from-green-500 to-emerald-600',
bgColor: 'bg-green-100',
textColor: 'text-green-700',
status: 'upcoming'
},
{
time: '11:30 AM',
duration: '1.5 hours',
title: 'Coffee at Centre Place',
category: 'Coffee Culture',
description: 'Experience Melbourne\'s famous coffee culture',
image: 'https://images.unsplash.com/photo-1583569695977-1e5758f793d1?w=300&h=200&fit=crop',
icon: Coffee,
color: 'from-amber-500 to-orange-600',
bgColor: 'bg-amber-100',
textColor: 'text-amber-700',
status: 'current'
},
{
time: '1:00 PM',
duration: '2 hours',
title: 'Queen Victoria Market',
category: 'Food & Markets',
description: 'Lunch and shopping at Melbourne\'s iconic market',
image: 'https://images.unsplash.com/photo-1708903965305-f8439248cebd?w=300&h=200&fit=crop',
icon: UtensilsCrossed,
color: 'from-red-500 to-pink-600',
bgColor: 'bg-red-100',
textColor: 'text-red-700',
status: 'upcoming'
},
{
time: '3:30 PM',
duration: '1.5 hours',
title: 'Street Art Tour',
category: 'Art & Culture',
description: 'Explore famous laneways and street art',
image: 'https://images.unsplash.com/photo-1582076197789-5c2af0bb51fd?w=300&h=200&fit=crop',
icon: Camera,
color: 'from-purple-500 to-violet-600',
bgColor: 'bg-purple-100',
textColor: 'text-purple-700',
status: 'upcoming'
},
{
time: '5:00 PM',
duration: '2 hours',
title: 'Eureka Skydeck',
category: 'Views & Landmarks',
description: 'Sunset views from Melbourne\'s highest observation deck',
image: 'https://images.unsplash.com/photo-1664738512177-f682a194a91c?w=300&h=200&fit=crop',
icon: Building2,
color: 'from-blue-500 to-indigo-600',
bgColor: 'bg-blue-100',
textColor: 'text-blue-700',
status: 'upcoming'
},
{
time: '7:30 PM',
duration: '2 hours',
title: 'Dinner at Southbank',
category: 'Fine Dining',
description: 'End your day with dinner by the Yarra River',
image: 'https://images.unsplash.com/photo-1722943661451-1a439d092ff2?w=300&h=200&fit=crop',
icon: UtensilsCrossed,
color: 'from-orange-500 to-red-600',
bgColor: 'bg-orange-100',
textColor: 'text-orange-700',
status: 'upcoming'
}
];
const features = [
{
icon: Sparkles,
title: 'AI-Powered Recommendations',
description: 'Our smart algorithm analyzes your preferences to create the perfect itinerary'
},
{
icon: Clock,
title: 'Optimized Timing',
description: 'Every minute planned to perfection with travel time and queue predictions'
},
{
icon: Navigation,
title: 'Real-Time Navigation',
description: 'Turn-by-turn directions and live updates keep you on track'
}
];
return (
<section className="relative py-20 lg:py-32 overflow-hidden bg-gradient-to-br from-gray-50 via-white to-blue-50/30">
{/* Animated Background Elements */}
<div className="absolute inset-0 overflow-hidden">
<motion.div
className="absolute top-20 -left-20 w-96 h-96 bg-gradient-to-r from-primary/5 to-secondary/5 rounded-full blur-3xl"
animate={{
x: [0, 100, 0],
y: [0, -50, 0],
}}
transition={{
duration: 20,
repeat: Infinity,
ease: "linear"
}}
/>
<motion.div
className="absolute bottom-20 -right-20 w-96 h-96 bg-gradient-to-r from-secondary/5 to-primary/5 rounded-full blur-3xl"
animate={{
x: [0, -100, 0],
y: [0, 50, 0],
}}
transition={{
duration: 25,
repeat: Infinity,
ease: "linear"
}}
/>
</div>
<div className="container mx-auto px-4 relative z-10">
{/* Header Section */}
<div className="text-center mb-16 max-w-5xl mx-auto">
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.6, ease: "easeOutBack" }}
viewport={{ once: true }}
className="inline-flex items-center gap-3 bg-gradient-to-r from-primary/10 to-secondary/10 px-6 py-3 rounded-full border border-primary/20 mb-8"
>
<motion.div
animate={{ rotate: [0, 360] }}
transition={{ duration: 3, repeat: Infinity, ease: "linear" }}
>
<Sparkles className="w-6 h-6 text-primary" />
</motion.div>
<span className="font-poppins font-medium text-base text-primary">Magic Itinerary</span>
<motion.div
className="px-3 py-1 bg-gradient-to-r from-primary to-secondary text-white text-sm rounded-full"
animate={{
boxShadow: [
'0 0 0 rgba(99,102,241,0)',
'0 0 20px rgba(99,102,241,0.4)',
'0 0 0 rgba(99,102,241,0)'
]
}}
transition={{ duration: 2, repeat: Infinity }}
>
Core Feature
</motion.div>
</motion.div>
<motion.h2
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
viewport={{ once: true }}
className="font-merchant text-2xl md:text-3xl lg:text-4xl leading-tight mb-6"
>
<span className="font-light">A Perfect</span>{' '}
<span className="font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent italic">Melbourne</span>{' '}
<span className="font-normal">Plan,</span>{' '}
<span className="font-bold">Instantly.</span>
</motion.h2>
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.4 }}
viewport={{ once: true }}
className="font-poppins text-xl leading-relaxed font-normal text-gray-600 max-w-4xl mx-auto"
>
Our AI creates personalized Melbourne itineraries in seconds. Just tell us your interests,
and watch as we craft the perfect day filled with attractions, dining, and experiences
tailored specifically for you.
</motion.p>
</div>
{/* Main Content Grid */}
<div className="grid lg:grid-cols-2 gap-12 lg:gap-20 items-center max-w-7xl mx-auto">
{/* Left: Phone Mockup with Sample Itinerary */}
<motion.div
initial={{ opacity: 0, x: -50 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, delay: 0.3 }}
viewport={{ once: true }}
className="relative"
>
{/* Phone Frame */}
<div className="relative mx-auto w-80 h-[640px]">
{/* Phone Shadow */}
<motion.div
className="absolute inset-0 bg-gradient-to-br from-gray-900/20 to-gray-600/20 rounded-[3rem] blur-2xl"
animate={{
scale: [1, 1.02, 1],
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "easeInOut"
}}
style={{ transform: 'translateY(20px) translateX(10px)' }}
/>
{/* Phone Body */}
<motion.div
className="relative w-full h-full bg-gray-900 rounded-[3rem] p-2"
animate={{
boxShadow: [
'0 25px 50px rgba(0,0,0,0.3)',
'0 35px 70px rgba(99,102,241,0.2)',
'0 25px 50px rgba(0,0,0,0.3)'
]
}}
transition={{
duration: 4,
repeat: Infinity,
ease: "easeInOut"
}}
>
{/* Screen */}
<div className="w-full h-full bg-white rounded-[2.5rem] overflow-hidden relative">
{/* Status Bar */}
<div className="flex items-center justify-between px-6 pt-4 pb-2 bg-white">
<span className="text-sm font-semibold text-gray-900">{currentTime}</span>
<div className="flex items-center gap-1">
<div className="flex gap-1">
<div className="w-1 h-1 bg-gray-900 rounded-full"></div>
<div className="w-1 h-1 bg-gray-900 rounded-full"></div>
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
<div className="w-1 h-1 bg-gray-400 rounded-full"></div>
</div>
<div className="w-6 h-3 border border-gray-900 rounded-sm ml-2">
<div className="w-4 h-full bg-green-500 rounded-sm"></div>
</div>
</div>
</div>
{/* App Header */}
<div className="px-6 py-4 bg-gradient-to-r from-primary to-secondary">
<div className="flex items-center justify-between">
<div>
<h3 className="text-white font-bold text-lg">Your Melbourne Day</h3>
<p className="text-white/90 text-sm">6 stops 10 hours</p>
</div>
<motion.div
className="w-10 h-10 bg-white/20 backdrop-blur-sm rounded-full flex items-center justify-center"
animate={{ rotate: [0, 360] }}
transition={{ duration: 20, repeat: Infinity, ease: "linear" }}
>
<Sparkles className="w-5 h-5 text-white" />
</motion.div>
</div>
</div>
{/* Itinerary List */}
<div className="flex-1 overflow-hidden">
<div className="p-4 space-y-3 h-full overflow-y-auto">
{itinerarySteps.map((step, index) => (
<motion.div
key={index}
className={`relative rounded-2xl border-2 overflow-hidden transition-all duration-500 ${
index === activeItineraryStep
? 'border-primary/30 bg-gradient-to-r from-primary/5 to-secondary/5 scale-[1.02] shadow-lg'
: 'border-gray-200 bg-white hover:border-gray-300'
}`}
animate={{
y: index === activeItineraryStep ? -2 : 0,
}}
transition={{ duration: 0.3 }}
>
{/* Current Activity Indicator */}
{index === activeItineraryStep && (
<motion.div
className="absolute left-0 top-0 bottom-0 w-1 bg-gradient-to-b from-primary to-secondary"
initial={{ height: 0 }}
animate={{ height: '100%' }}
transition={{ duration: 0.5 }}
/>
)}
<div className="flex items-center gap-3 p-3">
{/* Time and Icon */}
<div className="flex-shrink-0">
<div className={`w-12 h-12 rounded-xl flex items-center justify-center ${
index === activeItineraryStep ? step.bgColor : 'bg-gray-100'
}`}>
<step.icon className={`w-6 h-6 ${
index === activeItineraryStep ? step.textColor : 'text-gray-600'
}`} />
</div>
</div>
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1">
<span className={`text-xs font-medium ${
index === activeItineraryStep ? 'text-primary' : 'text-gray-500'
}`}>
{step.time} {step.duration}
</span>
{index === activeItineraryStep && (
<motion.div
className="flex items-center gap-1 text-green-600"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2 }}
>
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span className="text-xs font-medium">Now</span>
</motion.div>
)}
</div>
<h4 className={`font-semibold mb-1 ${
index === activeItineraryStep ? 'text-gray-900' : 'text-gray-800'
}`}>
{step.title}
</h4>
<p className="text-xs text-gray-600 mb-2 line-clamp-2">
{step.description}
</p>
<div className="flex items-center justify-between">
<span className={`text-xs px-2 py-1 rounded-full ${
index === activeItineraryStep
? 'bg-primary/10 text-primary'
: 'bg-gray-100 text-gray-600'
}`}>
{step.category}
</span>
<div className="flex items-center gap-1">
<Star className="w-3 h-3 text-yellow-500 fill-current" />
<span className="text-xs text-gray-600">4.8</span>
</div>
</div>
</div>
{/* Thumbnail */}
<div className="flex-shrink-0 w-16 h-16 rounded-xl overflow-hidden">
<ImageWithFallback
src={step.image}
alt={step.title}
className="w-full h-full object-cover"
/>
</div>
</div>
</motion.div>
))}
</div>
</div>
{/* Bottom Navigation */}
<div className="flex items-center justify-around py-4 bg-white border-t border-gray-100">
{[
{ icon: MapPin, label: 'Map', active: true },
{ icon: Clock, label: 'Schedule', active: false },
{ icon: Heart, label: 'Saved', active: false },
{ icon: Users, label: 'Share', active: false }
].map((nav, idx) => (
<div key={idx} className={`flex flex-col items-center gap-1 ${
nav.active ? 'text-primary' : 'text-gray-400'
}`}>
<nav.icon className="w-5 h-5" />
<span className="text-xs">{nav.label}</span>
</div>
))}
</div>
</div>
</motion.div>
{/* Floating Action Buttons */}
<motion.div
className="absolute -right-4 top-32 bg-gradient-to-r from-primary to-secondary text-white p-3 rounded-full shadow-lg"
animate={{
y: [0, -10, 0],
boxShadow: [
'0 10px 25px rgba(99,102,241,0.3)',
'0 20px 40px rgba(99,102,241,0.5)',
'0 10px 25px rgba(99,102,241,0.3)'
]
}}
transition={{
duration: 3,
repeat: Infinity,
ease: "easeInOut"
}}
>
<Navigation className="w-5 h-5" />
</motion.div>
<motion.div
className="absolute -left-4 bottom-32 bg-green-500 text-white p-3 rounded-full shadow-lg"
animate={{
y: [0, 10, 0],
boxShadow: [
'0 10px 25px rgba(34,197,94,0.3)',
'0 20px 40px rgba(34,197,94,0.5)',
'0 10px 25px rgba(34,197,94,0.3)'
]
}}
transition={{
duration: 3,
repeat: Infinity,
delay: 1.5,
ease: "easeInOut"
}}
>
<Heart className="w-5 h-5" />
</motion.div>
</div>
</motion.div>
{/* Right: Features and CTA */}
<motion.div
initial={{ opacity: 0, x: 50 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.8, delay: 0.5 }}
viewport={{ once: true }}
className="space-y-8"
>
{/* Features List */}
<div className="space-y-6">
{features.map((feature, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.6 + index * 0.1 }}
viewport={{ once: true }}
className="flex items-start gap-4 p-6 bg-white/80 backdrop-blur-sm rounded-2xl border border-white/40 hover:border-primary/20 hover:bg-white/90 transition-all duration-300"
>
<motion.div
className="w-12 h-12 bg-gradient-to-br from-primary/10 to-secondary/10 rounded-xl flex items-center justify-center flex-shrink-0"
whileHover={{
scale: 1.1,
background: 'linear-gradient(135deg, rgb(99 102 241 / 0.2), rgb(168 85 247 / 0.2))'
}}
transition={{ duration: 0.2 }}
>
<feature.icon className="w-6 h-6 text-primary" />
</motion.div>
<div className="flex-1">
<h4 className="font-semibold text-gray-900 mb-2">{feature.title}</h4>
<p className="text-gray-600 leading-relaxed">{feature.description}</p>
</div>
</motion.div>
))}
</div>
{/* Stats */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.9 }}
viewport={{ once: true }}
className="grid grid-cols-3 gap-4 p-6 bg-gradient-to-r from-primary/5 to-secondary/5 rounded-2xl border border-primary/10"
>
{[
{ number: '15K+', label: 'Itineraries Created' },
{ number: '4.9★', label: 'User Rating' },
{ number: '30s', label: 'Average Plan Time' }
].map((stat, index) => (
<div key={index} className="text-center">
<motion.div
className="text-2xl font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"
initial={{ scale: 0 }}
whileInView={{ scale: 1 }}
transition={{ duration: 0.5, delay: 1 + index * 0.1 }}
viewport={{ once: true }}
>
{stat.number}
</motion.div>
<div className="text-sm text-gray-600">{stat.label}</div>
</div>
))}
</motion.div>
{/* CTA Buttons */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 1.2 }}
viewport={{ once: true }}
className="flex flex-col sm:flex-row gap-4"
>
<Button
withShine={true}
size="xl"
className="flex-1 h-14 rounded-2xl text-lg group"
onClick={onCreateItineraryClick}
>
<Sparkles className="w-5 h-5 mr-2 group-hover:animate-spin" />
Create My Itinerary
</Button>
<Button
variant="outline"
size="xl"
className="flex-1 h-14 rounded-2xl text-lg border-2 hover:border-primary/50"
>
See Sample Plans
<ChevronRight className="w-5 h-5 ml-2" />
</Button>
</motion.div>
{/* Trust Indicator */}
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
transition={{ duration: 0.6, delay: 1.4 }}
viewport={{ once: true }}
className="flex items-center gap-3 text-sm text-gray-600"
>
<div className="flex items-center gap-1">
{[...Array(5)].map((_, i) => (
<Star key={i} className="w-4 h-4 text-yellow-500 fill-current" />
))}
</div>
<span>Trusted by 50,000+ travelers worldwide</span>
</motion.div>
</motion.div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,398 @@
import React from 'react';
import { motion } from 'motion/react';
import { ArrowLeft, Sparkles, MapPin, Clock, Users, Calendar, Star, Zap, Heart, Camera } from 'lucide-react';
import { Button } from './ui/button';
import { Card, CardContent } from './ui/card';
import { Badge } from './ui/badge';
import Navbar from './Navbar';
import SubNavbar from './SubNavbar';
import { Footer } from './Footer';
import { MobileAppSection } from './MobileAppSection';
import { EnhancedTestimonials } from './EnhancedTestimonials';
import { HowItWorks } from './HowItWorks';
import { ImageWithFallback } from './figma/ImageWithFallback';
interface User {
email: string;
name: string;
}
interface MagicItineraryPageProps {
onBackClick: () => void;
onHomeClick: () => void;
onMelbourneClick: () => void;
onPassesClick: () => void;
onCheckoutClick: () => void;
onSignInClick: () => void;
onSignOutClick?: () => void;
onAttractionsClick: () => void;
onBlogsClick: () => void;
onHowItWorksClick: () => void;
onFAQClick: () => void;
onPrivacyPolicyClick: () => void;
onAboutUsClick: () => void;
onProfileClick: () => void;
onCityCardsClick: () => void;
onMagicItineraryClick: () => void;
onPostCardsClick: () => void;
onOffersClick: () => void;
onContactUsClick?: () => void;
onEsimsClick?: () => void;
onHotelDiscountsClick?: () => void;
onCreateItineraryClick: () => void;
onViewItineraryClick: () => void;
currentPage: string;
user: User | null;
}
export function MagicItineraryPage({
onBackClick,
onHomeClick,
onMelbourneClick,
onPassesClick,
onCheckoutClick,
onSignInClick,
onSignOutClick,
onAttractionsClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onProfileClick,
onCityCardsClick,
onMagicItineraryClick,
onPostCardsClick,
onOffersClick,
onContactUsClick,
onEsimsClick,
onHotelDiscountsClick,
onCreateItineraryClick,
onViewItineraryClick,
currentPage,
user
}: MagicItineraryPageProps) {
return (
<div className="min-h-screen bg-background">
{/* Navbar */}
<Navbar
activeCity=""
onCityChange={() => {}}
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
onPassesClick={onPassesClick}
onCheckoutClick={onCheckoutClick}
onHomeClick={onHomeClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick}
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
onEsimsClick={onEsimsClick}
onHotelDiscountsClick={onHotelDiscountsClick}
currentPage="magic-itinerary"
isUserSignedIn={!!user}
user={user}
/>
{/* Sub Navbar */}
<SubNavbar
activeTab="magic-itinerary"
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
onEsimsClick={onEsimsClick}
onHotelDiscountsClick={onHotelDiscountsClick}
/>
{/* Hero Section */}
<section className="relative pt-52 pb-20 overflow-hidden">
{/* Background gradient */}
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-secondary/5 to-background"></div>
<div className="container mx-auto px-4 relative z-10">
<motion.div
className="max-w-4xl mx-auto text-center"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<h1 className="font-merchant text-4xl md:text-5xl lg:text-6xl leading-tight mb-6">
<span className="font-light">Plan Your Perfect</span>{' '}
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
Melbourne Adventure
</span>
</h1>
<p className="font-poppins text-xl leading-relaxed font-normal text-gray-600 mb-8 max-w-2xl mx-auto">
Let our AI create a personalized itinerary just for you. Answer a few questions about your preferences,
and we'll craft the perfect Melbourne experience tailored to your interests.
</p>
<Button
onClick={onCreateItineraryClick}
className="bg-primary hover:bg-primary/90 text-white px-8 py-6 font-poppins font-semibold text-lg"
>
Create My Magic Itinerary
</Button>
</motion.div>
</div>
{/* Decorative elements */}
<div className="absolute top-20 left-10 w-20 h-20 bg-primary/10 rounded-full blur-xl"></div>
<div className="absolute bottom-20 right-10 w-32 h-32 bg-secondary/10 rounded-full blur-xl"></div>
</section>
{/* How It Works Section */}
<HowItWorks />
{/* Features Section */}
<section className="py-16 bg-gradient-to-br from-gray-50 to-gray-100">
<div className="container mx-auto px-4">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
{/* Left side - Features */}
<motion.div
initial={{ opacity: 0, x: -30 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
>
<h2 className="font-merchant text-3xl mb-6">
<span className="text-gray-900">Smart Features for</span><br />
<span className="bg-gradient-to-r from-purple-600 to-pink-600 bg-clip-text text-transparent">Smart Travelers</span>
</h2>
<div className="space-y-6">
{[
{
icon: <Sparkles className="w-6 h-6 text-purple-600" />,
title: 'AI-Powered Recommendations',
description: 'Our advanced AI analyzes your preferences to suggest the perfect experiences'
},
{
icon: <Clock className="w-6 h-6 text-pink-600" />,
title: 'Optimized Scheduling',
description: 'Smart timing that considers opening hours, travel time, and crowd patterns'
},
{
icon: <MapPin className="w-6 h-6 text-purple-600" />,
title: 'Location-Based Planning',
description: 'Efficiently planned routes that minimize travel time and maximize experiences'
},
{
icon: <Camera className="w-6 h-6 text-pink-600" />,
title: 'Instagram-Worthy Spots',
description: 'Discover hidden gems and perfect photo opportunities along your journey'
}
].map((feature, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 + index * 0.1 }}
className="flex items-start space-x-4"
>
<div className="flex-shrink-0 w-12 h-12 bg-white rounded-lg flex items-center justify-center shadow-md">
{feature.icon}
</div>
<div>
<h3 className="font-merchant text-lg mb-2">{feature.title}</h3>
<p className="text-gray-600 font-poppins">{feature.description}</p>
</div>
</motion.div>
))}
</div>
</motion.div>
{/* Right side - Visual */}
<motion.div
initial={{ opacity: 0, x: 30 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: 0.4 }}
className="relative"
>
<div className="bg-gradient-to-br from-purple-500 to-pink-500 rounded-2xl p-8 text-white">
<h3 className="font-merchant text-2xl mb-6">Sample Itinerary</h3>
<div className="space-y-4">
{[
{ time: '9:00 AM', activity: 'Coffee at Degraves Street', duration: '30 min' },
{ time: '10:00 AM', activity: 'Royal Botanic Gardens', duration: '2 hours' },
{ time: '1:00 PM', activity: 'Lunch at Queen Victoria Market', duration: '1 hour' },
{ time: '3:00 PM', activity: 'Street Art Tour in Hosier Lane', duration: '1.5 hours' },
{ time: '6:00 PM', activity: 'Sunset at Eureka Skydeck', duration: '1 hour' }
].map((item, index) => (
<div key={index} className="flex items-center space-x-4 bg-white/10 rounded-lg p-3">
<div className="text-sm font-bold bg-white/20 px-2 py-1 rounded">
{item.time}
</div>
<div className="flex-1">
<div className="font-medium">{item.activity}</div>
<div className="text-xs text-white/80">{item.duration}</div>
</div>
</div>
))}
</div>
</div>
</motion.div>
</div>
</div>
</section>
{/* Benefits Section */}
<section className="py-16">
<div className="container mx-auto px-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="text-center mb-12"
>
<h2 className="font-merchant text-3xl mb-4">Why Use Magic Itinerary?</h2>
<p className="text-gray-600 font-poppins max-w-2xl mx-auto">
Save time, discover more, and create unforgettable memories with personalized planning
</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{[
{
icon: <Clock className="w-12 h-12 text-purple-600" />,
title: 'Save Planning Time',
description: 'Skip hours of research. Get a complete itinerary in under 5 minutes.',
stat: '90% faster than manual planning'
},
{
icon: <Star className="w-12 h-12 text-pink-600" />,
title: 'Discover Hidden Gems',
description: 'Find unique experiences and local favorites you might have missed.',
stat: '50+ curated hidden spots'
},
{
icon: <Heart className="w-12 h-12 text-purple-600" />,
title: 'Personalized Experience',
description: 'Every itinerary is unique, tailored specifically to your preferences.',
stat: '1000+ possible combinations'
}
].map((benefit, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 + index * 0.1 }}
>
<Card className="h-full text-center hover:shadow-lg transition-shadow duration-300">
<CardContent className="p-8">
<div className="mb-4">
{benefit.icon}
</div>
<h3 className="font-merchant text-xl mb-3">{benefit.title}</h3>
<p className="text-gray-600 font-poppins mb-4">{benefit.description}</p>
<Badge className="bg-gradient-to-r from-purple-100 to-pink-100 text-purple-700 border-none">
{benefit.stat}
</Badge>
</CardContent>
</Card>
</motion.div>
))}
</div>
</div>
</section>
{/* What's Included Section */}
<section className="py-16 bg-gradient-to-br from-purple-50 to-pink-50">
<div className="container mx-auto px-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="text-center mb-12"
>
<h2 className="font-merchant text-3xl mb-4">What's Included</h2>
<p className="text-gray-600 font-poppins max-w-2xl mx-auto">
Your Magic Itinerary comes with everything you need for an amazing Melbourne experience
</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{[
{
title: 'Detailed Timeline',
description: 'Hour-by-hour schedule with optimal timing'
},
{
title: 'Transportation Tips',
description: 'Best routes and transport options between locations'
},
{
title: 'Local Recommendations',
description: 'Insider tips on food, shopping, and experiences'
},
{
title: 'Budget Planning',
description: 'Estimated costs and money-saving suggestions'
},
{
title: 'Weather Backup Plans',
description: 'Alternative indoor activities for rainy days'
},
{
title: 'Photo Opportunities',
description: 'Best spots and times for Instagram-worthy shots'
},
{
title: 'Cultural Insights',
description: 'Local history and interesting facts about each location'
},
{
title: 'Real-time Updates',
description: 'Live information on closures, events, and crowds'
}
].map((item, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 + index * 0.05 }}
>
<Card className="h-full hover:shadow-md transition-shadow duration-300">
<CardContent className="p-6 text-center">
<h3 className="font-merchant text-lg mb-2">{item.title}</h3>
<p className="text-gray-600 font-poppins text-sm">{item.description}</p>
</CardContent>
</Card>
</motion.div>
))}
</div>
</div>
</section>
{/* CTA Section */}
{/* Mobile App Section */}
<MobileAppSection />
{/* Customer Reviews */}
<EnhancedTestimonials />
{/* Footer */}
<Footer
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onPassesClick={onPassesClick}
onSignInClick={onSignInClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onContactUsClick={onContactUsClick}
currentPage={currentPage}
/>
</div>
);
}

View File

@@ -0,0 +1,397 @@
import { useState } from 'react';
import { ChevronLeft, ChevronRight, Clock, Users, Star, Zap, CheckCircle, MapPin, Volume2, Camera, Coffee, Palette, Eye } from 'lucide-react';
import { ImageWithFallback } from './figma/ImageWithFallback';
import { motion } from 'motion/react';
const melbourneAttractions = [
{
id: 1,
name: "Royal Botanic Gardens",
city: "Melbourne",
country: "Australia",
image: "https://images.unsplash.com/photo-1721272962395-a848331ce92d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjByb3lhbCUyMGJvdGFuaWMlMjBnYXJkZW5zfGVufDF8fHx8MTc1NzMzNzc4OXww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
rating: 4.8,
reviews: "15,600+",
category: "Gardens",
originalPrice: "Free",
includedValue: "$25",
perks: [
{ icon: Volume2, label: "Audio garden tour", color: "text-green-600" },
{ icon: MapPin, label: "Garden maps", color: "text-blue-600" },
{ icon: Camera, label: "Photo spots guide", color: "text-purple-600" }
]
},
{
id: 2,
name: "Federation Square",
city: "Melbourne",
country: "Australia",
image: "https://images.unsplash.com/photo-1639655001512-e4b58d4874b8?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBmZWRlcmF0aW9uJTIwc3F1YXJlfGVufDF8fHx8MTc1NzMzNzc5Mnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
rating: 4.6,
reviews: "22,400+",
category: "Landmarks",
originalPrice: "Free",
includedValue: "$35",
perks: [
{ icon: Volume2, label: "Cultural tours", color: "text-orange-600" },
{ icon: Eye, label: "Gallery access", color: "text-blue-600" },
{ icon: Users, label: "Event access", color: "text-purple-600" }
]
},
{
id: 3,
name: "Queen Victoria Market",
city: "Melbourne",
country: "Australia",
image: "https://images.unsplash.com/photo-1676454953709-e0be46f62490?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBxdWVlbiUyMHZpY3RvcmlhJTIwbWFya2V0fGVufDF8fHx8MTc1NzMzNzc5Nnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
rating: 4.7,
reviews: "18,200+",
category: "Markets",
originalPrice: "$45",
includedValue: "$45",
perks: [
{ icon: Users, label: "Food tours", color: "text-orange-600" },
{ icon: Coffee, label: "Tastings", color: "text-brown-600" },
{ icon: Volume2, label: "History guide", color: "text-blue-600" }
]
},
{
id: 4,
name: "Eureka Skydeck",
city: "Melbourne",
country: "Australia",
image: "https://images.unsplash.com/photo-1629677713183-29248e1268d7?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBldXJla2ElMjB0b3dlcnxlbnwxfHx8fDE3NTczMzc4MDB8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
rating: 4.9,
reviews: "11,800+",
category: "Views",
originalPrice: "$32",
includedValue: "$32",
perks: [
{ icon: Zap, label: "Skip-the-line", color: "text-green-600" },
{ icon: Eye, label: "360° views", color: "text-purple-600" },
{ icon: Camera, label: "Photo experiences", color: "text-blue-600" }
]
},
{
id: 5,
name: "St Kilda Beach & Pier",
city: "Melbourne",
country: "Australia",
image: "https://images.unsplash.com/photo-1674732954456-159835c0a46b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBzdCUyMGtpbGRhJTIwYmVhY2h8ZW58MXx8fHwxNzU3MzM3ODAzfDA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
rating: 4.5,
reviews: "14,300+",
category: "Beach",
originalPrice: "Free",
includedValue: "$20",
perks: [
{ icon: Users, label: "Penguin tours", color: "text-blue-600" },
{ icon: MapPin, label: "Beach activities", color: "text-green-600" },
{ icon: Camera, label: "Sunset spots", color: "text-purple-600" }
]
},
{
id: 6,
name: "Melbourne Laneways",
city: "Melbourne",
country: "Australia",
image: "https://images.unsplash.com/photo-1705120624704-0970afc29fea?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBsYW5ld2F5cyUyMHN0cmVldCUyMGFydHxlbnwxfHx8fDE3NTczMzc4MDd8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
rating: 4.8,
reviews: "19,500+",
category: "Street Art",
originalPrice: "$55",
includedValue: "$55",
perks: [
{ icon: Palette, label: "Art tours", color: "text-pink-600" },
{ icon: Coffee, label: "Café stops", color: "text-brown-600" },
{ icon: Camera, label: "Photo walks", color: "text-purple-600" }
]
},
{
id: 7,
name: "Melbourne Zoo",
city: "Melbourne",
country: "Australia",
image: "https://images.unsplash.com/photo-1681429477985-30dc7b88dd5b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjB6b28lMjBhbmltYWxzfGVufDF8fHx8MTc1NzMzNzgxMHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
rating: 4.7,
reviews: "13,900+",
category: "Wildlife",
originalPrice: "$42",
includedValue: "$42",
perks: [
{ icon: Zap, label: "Skip-the-line", color: "text-green-600" },
{ icon: Users, label: "Animal encounters", color: "text-orange-600" },
{ icon: Volume2, label: "Keeper talks", color: "text-blue-600" }
]
},
{
id: 8,
name: "Royal Exhibition Building",
city: "Melbourne",
country: "Australia",
image: "https://images.unsplash.com/photo-1720523794299-c3b445d71a51?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjByb3lhbCUyMGV4aGliaXRpb24lMjBidWlsZGluZ3xlbnwxfHx8fDE3NTczMzc4MTR8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
rating: 4.6,
reviews: "8,700+",
category: "Heritage",
originalPrice: "$25",
includedValue: "$25",
perks: [
{ icon: Volume2, label: "Audio tours", color: "text-blue-600" },
{ icon: Eye, label: "Exhibitions", color: "text-purple-600" },
{ icon: MapPin, label: "Heritage walks", color: "text-green-600" }
]
}
];
const categories = ["All", "Landmarks", "Gardens", "Markets", "Views", "Beach", "Street Art", "Wildlife", "Heritage"];
export function MelbourneAttractions() {
const [activeCategory, setActiveCategory] = useState("All");
const filteredAttractions = activeCategory === "All"
? melbourneAttractions
: melbourneAttractions.filter(attraction => attraction.category === activeCategory);
const AttractionCard = ({ attraction, index }: { attraction: typeof melbourneAttractions[0], index: number }) => (
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
viewport={{ once: true }}
className="group cursor-pointer flex-shrink-0 w-[280px] md:w-auto md:flex-shrink h-96 flip-card-container"
>
{/* 3D Flip Container */}
<div className="flip-card-inner group-hover:[transform:rotateY(180deg)] relative w-full h-full">
{/* FRONT FACE */}
<div className="flip-card-face absolute inset-0 w-full h-full rounded-2xl overflow-hidden shadow-lg">
{/* Background Image */}
<ImageWithFallback
src={attraction.image}
alt={attraction.name}
className="w-full h-full object-cover"
/>
{/* Rating Badge */}
<div className="absolute top-4 right-4 bg-white/95 backdrop-blur-sm rounded-full px-3 py-1.5 flex items-center gap-1 shadow-lg z-10">
<div className="w-4 h-4 bg-gradient-to-r from-yellow-400 to-yellow-500 rounded-full flex items-center justify-center">
<span className="text-white text-xs"></span>
</div>
<span className="text-sm font-medium text-gray-900">{attraction.rating}</span>
</div>
{/* Front Content - Clean Title & Location */}
<div className="absolute bottom-0 left-0 right-0">
<div className="bg-gradient-to-t from-black/80 via-black/50 to-transparent p-6">
<h3 className="font-bold text-xl text-white mb-1">{attraction.name}</h3>
<p className="text-white/90 text-sm">
{attraction.city}, {attraction.country}
</p>
</div>
</div>
</div>
{/* BACK FACE */}
<div className="flip-card-face flip-card-back absolute inset-0 w-full h-full rounded-2xl overflow-hidden shadow-lg bg-gradient-to-br from-gray-900 to-black">
{/* Back Content Container */}
<div className="relative w-full h-full p-6 flex flex-col justify-center text-white">
{/* Included Value Section */}
<div className="mb-4">
<div className="inline-flex items-center gap-2 bg-gradient-to-r from-green-500 to-emerald-600 text-white px-3 py-1.5 rounded-full text-sm font-medium mb-3">
<CheckCircle className="w-4 h-4" />
<span>Included Value</span>
</div>
<div className="text-2xl font-bold mb-1">{attraction.includedValue}</div>
<p className="text-white/80 text-sm">
{attraction.originalPrice === "Free"
? "Premium access included"
: "Save money with CityCard"}
</p>
</div>
{/* What's Included List */}
<div className="mb-4">
<h4 className="font-semibold text-sm mb-3">What's Included:</h4>
<div className="space-y-2">
{attraction.perks.slice(0, 3).map((perk, perkIndex) => (
<div key={perkIndex} className="flex items-center gap-3 text-white/90">
<div className="w-6 h-6 rounded-full bg-white/20 backdrop-blur-sm flex items-center justify-center">
<perk.icon className="w-3 h-3 text-white" />
</div>
<span className="text-sm">{perk.label}</span>
</div>
))}
</div>
</div>
{/* Duration & Meta Info */}
<div className="mb-4">
<div className="flex items-center gap-4 text-white/80 text-sm">
<div className="flex items-center gap-1">
<Clock className="w-4 h-4" />
<span>2-3 hours</span>
</div>
<div className="flex items-center gap-1">
<Users className="w-4 h-4" />
<span>All ages</span>
</div>
</div>
</div>
{/* Footer Features */}
<div className="border-t border-white/20 pt-4">
<div className="flex items-center justify-between text-white/80 text-xs">
<div className="flex items-center gap-2">
<MapPin className="w-3 h-3" />
<span>Mobile ticket</span>
</div>
<div className="flex items-center gap-2">
<CheckCircle className="w-3 h-3" />
<span>Instant confirmation</span>
</div>
</div>
</div>
{/* Decorative Elements */}
<div className="absolute top-4 right-4 w-16 h-16 bg-gradient-to-br from-primary/20 to-secondary/20 rounded-full blur-xl"></div>
<div className="absolute bottom-4 left-4 w-12 h-12 bg-gradient-to-tr from-secondary/15 to-primary/15 rounded-full blur-lg"></div>
</div>
</div>
</div>
</motion.div>
);
return (
<section className="py-20 bg-gradient-to-br from-gray-50 to-white relative overflow-hidden">
<div className="container mx-auto px-4">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<div className="inline-flex items-center gap-2 bg-gradient-to-r from-primary/10 to-secondary/10 px-4 py-2 rounded-full mb-6">
<div className="w-2 h-2 bg-gradient-to-r from-primary to-secondary rounded-full"></div>
<span className="text-sm font-medium bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
Melbourne Must-Sees
</span>
</div>
<h2 className="heading-dynamic text-4xl md:text-5xl lg:text-6xl text-gray-900 mb-4">
<span className="font-light">Discover</span>{' '}
<span className="font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent italic">
Melbourne's
</span>{' '}
<span className="font-normal">Best</span>{' '}
<span className="font-semibold text-emphasis">Experiences</span>
</h2>
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
Discover Melbourne's iconic landmarks, vibrant culture, world-class dining, and hidden gems - all included with your Melbourne CityCard
</p>
</motion.div>
{/* Category Tabs */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
viewport={{ once: true }}
className="flex flex-wrap justify-center gap-3 mb-12"
>
{categories.map((category, index) => (
<motion.button
key={category}
initial={{ opacity: 0, scale: 0.8 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3, delay: index * 0.05 }}
viewport={{ once: true }}
onClick={() => setActiveCategory(category)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className={`px-6 py-4 h-14 rounded-2xl font-medium transition-all duration-300 ${
activeCategory === category
? 'bg-gradient-to-r from-primary to-secondary text-white shadow-xl shadow-primary/25 ring-2 ring-primary/20'
: 'bg-white/80 backdrop-blur-sm text-gray-700 hover:text-gray-900 hover:shadow-lg border border-gray-200/50 hover:border-primary/20 hover:bg-white'
}`}
>
{category}
</motion.button>
))}
</motion.div>
{/* Mobile Horizontal Carousel */}
<div className="block md:hidden mb-8">
<div className="relative">
{/* Scroll Container */}
<div className="flex gap-6 overflow-x-auto scrollbar-hide pb-4 px-4 -mx-4">
{filteredAttractions.map((attraction, index) => (
<AttractionCard key={attraction.id} attraction={attraction} index={index} />
))}
</div>
{/* Scroll Indicators */}
<div className="flex justify-center mt-6 gap-2">
{Array.from({ length: Math.ceil(filteredAttractions.length / 2) }).map((_, index) => (
<div
key={index}
className="w-2 h-2 rounded-full bg-gray-300"
/>
))}
</div>
{/* Mobile Hint Text */}
<div className="text-center mt-4">
<p className="text-sm text-gray-500">
Swipe to explore more Melbourne attractions
</p>
</div>
</div>
</div>
{/* Desktop Bento Grid */}
<div className="hidden md:block w-full">
{/* Top Row - 3 equal cards */}
<div className="grid grid-cols-3 gap-6">
{filteredAttractions.slice(0, 3).map((attraction, index) => (
<AttractionCard key={attraction.id} attraction={attraction} index={index} />
))}
</div>
{/* Consistent Vertical Spacing */}
<div className="h-6"></div>
{/* Bottom Row - 2 larger cards */}
<div className="grid grid-cols-2 gap-6">
{filteredAttractions.slice(3, 5).map((attraction, index) => (
<AttractionCard key={attraction.id} attraction={attraction} index={index + 3} />
))}
</div>
</div>
{/* Call to Action */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
viewport={{ once: true }}
className="text-center mt-12"
>
<motion.button
whileHover={{ scale: 1.05, boxShadow: "0 20px 40px rgba(99,102,241,0.3)" }}
whileTap={{ scale: 0.95 }}
className="relative bg-gradient-to-r from-primary to-secondary text-white py-4 px-12 rounded-lg text-lg shadow-xl transition-all duration-300 overflow-hidden group"
>
<span className="relative z-10">Get Your Melbourne Card</span>
{/* Shine animation */}
<div className="absolute inset-0 opacity-30">
<div className="h-full bg-gradient-to-r from-transparent via-white to-transparent animate-shine"></div>
</div>
</motion.button>
</motion.div>
</div>
</section>
);
}

View File

@@ -0,0 +1,291 @@
import { motion } from 'motion/react';
import { ImageWithFallback } from './figma/ImageWithFallback';
import { Calendar, Clock, User, ArrowRight, Coffee, Camera, MapPin, Star } from 'lucide-react';
import { Button } from './ui/button';
const blogPosts = [
{
id: 1,
title: "Hidden Laneways: Melbourne's Street Art Revolution",
excerpt: "Discover the vibrant street art scene that has transformed Melbourne's narrow alleyways into outdoor galleries, making the city a global street art capital.",
image: "https://images.unsplash.com/photo-1705120624704-0970afc29fea?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBzdHJlZXQlMjBhcnQlMjBsYW5ld2F5c3xlbnwxfHx8fDE3NTczMzkyNjZ8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
author: "Melbourne Explorer",
date: "Dec 15, 2024",
readTime: "5 min read",
category: "Culture",
featured: true,
tags: ["Street Art", "Laneways", "Culture", "Photography"]
},
{
id: 2,
title: "Coffee Capital: Melbourne's World-Famous Cafe Culture",
excerpt: "From hole-in-the-wall espresso bars to artisanal third-wave coffee shops, explore why Melbourne is considered the world's coffee capital.",
image: "https://images.unsplash.com/photo-1681745623555-efc392301d6d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBjb2ZmZWUlMjBjdWx0dXJlJTIwY2FmZXxlbnwxfHx8fDE3NTczMzkyNzJ8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
author: "Coffee Connoisseur",
date: "Dec 12, 2024",
readTime: "7 min read",
category: "Food & Drink",
featured: false,
tags: ["Coffee", "Food", "Local Culture", "Cafes"]
},
{
id: 3,
title: "Royal Botanic Gardens: Melbourne's Green Oasis",
excerpt: "Escape the urban hustle in Melbourne's 38-hectare botanical paradise, home to over 8,500 plant species and stunning city views.",
image: "https://images.unsplash.com/photo-1721272962395-a848331ce92d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjByb3lhbCUyMGJvdGFuaWMlMjBnYXJkZW5zfGVufDF8fHx8MTc1NzMzNzc4OXww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
author: "Nature Guide",
date: "Dec 10, 2024",
readTime: "4 min read",
category: "Nature",
featured: false,
tags: ["Gardens", "Nature", "Photography", "Relaxation"]
},
{
id: 4,
title: "Sports Capital: Melbourne's Sporting Heritage",
excerpt: "From the iconic MCG to Formula 1 racing, discover why Melbourne holds the title of Australia's sporting capital and home to major international events.",
image: "https://images.unsplash.com/photo-1720347247737-9252d85d3027?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBjaXR5JTIwc2t5bGluZSUyMGZsaW5kZXJzJTIwc3RyZWV0fGVufDF8fHx8MTc1NzMzOTAyNHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
author: "Sports Fan",
date: "Dec 8, 2024",
readTime: "6 min read",
category: "Sports",
featured: false,
tags: ["Sports", "MCG", "Events", "Culture"]
},
{
id: 5,
title: "Foodie Paradise: Melbourne's Multicultural Dining Scene",
excerpt: "Experience Melbourne's incredible culinary diversity, from authentic Greek tavernas to innovative modern Australian cuisine in award-winning restaurants.",
image: "https://images.unsplash.com/photo-1681745623555-efc392301d6d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBjb2ZmZWUlMjBjdWx0dXJlJTIwY2FmZXxlbnwxfHx8fDE3NTczMzkyNzJ8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
author: "Food Critic",
date: "Dec 5, 2024",
readTime: "8 min read",
category: "Food & Drink",
featured: false,
tags: ["Food", "Restaurants", "Multicultural", "Dining"]
},
{
id: 6,
title: "Melbourne After Dark: Rooftop Bars & Nightlife",
excerpt: "Explore Melbourne's sophisticated nightlife scene, from hidden speakeasies in historic buildings to stunning rooftop bars with panoramic city views.",
image: "https://images.unsplash.com/photo-1720347247737-9252d85d3027?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBjaXR5JTIwc2t5bGluZSUyMGZsaW5kZXJzJTIwc3RyZWV0fGVufDF8fHx8MTc1NzMzOTAyNHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral",
author: "Nightlife Expert",
date: "Dec 2, 2024",
readTime: "5 min read",
category: "Nightlife",
featured: false,
tags: ["Nightlife", "Rooftop Bars", "Entertainment", "City Views"]
}
];
const categories = [
{ name: "All", count: 6, color: "from-gray-500 to-gray-600" },
{ name: "Culture", count: 2, color: "from-purple-500 to-pink-500" },
{ name: "Food & Drink", count: 2, color: "from-orange-500 to-red-500" },
{ name: "Nature", count: 1, color: "from-green-500 to-emerald-500" },
{ name: "Sports", count: 1, color: "from-blue-500 to-cyan-500" },
{ name: "Nightlife", count: 1, color: "from-indigo-500 to-purple-500" }
];
export function MelbourneBlogs() {
const featuredPost = blogPosts.find(post => post.featured);
const regularPosts = blogPosts.filter(post => !post.featured);
return (
<section className="py-20 bg-gradient-to-br from-gray-50 via-white to-gray-50 relative overflow-hidden">
{/* Background Pattern */}
<div className="absolute inset-0 opacity-[0.02]">
<div className="absolute top-0 left-0 w-full h-full bg-gradient-to-br from-primary/20 via-secondary/20 to-primary/20"></div>
</div>
<div className="container mx-auto px-4 relative z-10">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<div className="inline-flex items-center gap-2 bg-gradient-to-r from-primary/10 to-secondary/10 px-4 py-2 rounded-full mb-6">
<div className="w-2 h-2 bg-gradient-to-r from-primary to-secondary rounded-full"></div>
<span className="text-sm font-medium bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
Melbourne Stories
</span>
</div>
<h2 className="font-merchant text-4xl md:text-5xl lg:text-6xl text-gray-900 mb-6">
<span className="font-normal">Melbourne</span>{' '}
<span className="font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent italic">
Blogs
</span>
</h2>
<p className="text-xl text-gray-600 max-w-4xl mx-auto leading-relaxed">
Dive deep into Melbourne's rich cultural tapestry, from hidden laneway treasures to world-renowned
coffee culture. Discover insider stories, local secrets, and expert guides to Australia's cultural capital
that will transform your Melbourne experience into an unforgettable journey.
</p>
</motion.div>
{/* Categories Filter */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.1 }}
viewport={{ once: true }}
className="flex flex-wrap justify-center gap-3 mb-16"
>
{categories.map((category, index) => (
<motion.button
key={category.name}
initial={{ opacity: 0, scale: 0.9 }}
whileInView={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.4, delay: 0.2 + index * 0.05 }}
viewport={{ once: true }}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className={`px-6 py-3 rounded-full bg-gradient-to-r ${category.color} text-white font-medium shadow-lg hover:shadow-xl transition-all duration-300 group`}
>
<span className="flex items-center gap-2">
{category.name}
<span className="text-xs bg-white/20 px-2 py-1 rounded-full group-hover:bg-white/30 transition-colors duration-200">
{category.count}
</span>
</span>
</motion.button>
))}
</motion.div>
{/* Featured Post */}
{featuredPost && (
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
viewport={{ once: true }}
className="mb-16"
>
</motion.div>
)}
{/* Regular Blog Posts Grid */}
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{regularPosts.map((post, index) => (
<motion.article
key={post.id}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.3 + index * 0.1 }}
viewport={{ once: true }}
className="bg-white rounded-3xl shadow-lg overflow-hidden border border-gray-100 group hover:shadow-2xl hover:border-gray-200 transition-all duration-500 cursor-pointer h-[480px] flex flex-col"
>
{/* Post Image */}
<div className="relative overflow-hidden h-48">
<ImageWithFallback
src={post.image}
alt={post.title}
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/20 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
{/* Category Badge */}
<div className="absolute top-4 left-4 bg-white/95 backdrop-blur-sm text-gray-900 px-3 py-1 rounded-full text-xs font-medium">
{post.category}
</div>
</div>
{/* Post Content */}
<div className="p-6 flex-1 flex flex-col justify-between">
<div className="flex items-center gap-3 text-xs text-gray-500 mb-3">
<div className="flex items-center gap-1">
<User className="w-3 h-3" />
{post.author}
</div>
<div className="flex items-center gap-1">
<Calendar className="w-3 h-3" />
{post.date}
</div>
<div className="flex items-center gap-1">
<Clock className="w-3 h-3" />
{post.readTime}
</div>
</div>
<div className="flex-1 flex flex-col">
<h3 className="font-merchant text-xl font-semibold text-gray-900 mb-3 leading-tight group-hover:text-primary transition-colors duration-200 line-clamp-2">
{post.title}
</h3>
<p className="text-gray-600 leading-relaxed mb-4 text-sm flex-1 line-clamp-3">
{post.excerpt}
</p>
{/* Tags */}
<div className="flex flex-wrap gap-1 mb-4">
{post.tags.slice(0, 2).map((tag, tagIndex) => (
<span
key={tagIndex}
className="px-2 py-1 bg-gray-100 text-gray-600 rounded-full text-xs font-medium"
>
{tag}
</span>
))}
{post.tags.length > 2 && (
<span className="px-2 py-1 bg-gray-100 text-gray-600 rounded-full text-xs">
+{post.tags.length - 2}
</span>
)}
</div>
</div>
<div className="flex items-center justify-between mt-auto">
<span className="text-primary font-medium text-sm group-hover:text-secondary transition-colors duration-200">
Read More
</span>
<ArrowRight className="w-4 h-4 text-primary group-hover:text-secondary group-hover:translate-x-1 transition-all duration-200" />
</div>
</div>
</motion.article>
))}
</div>
{/* Call to Action */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.5 }}
viewport={{ once: true }}
className="text-center mt-16"
>
<div className="bg-gradient-to-br from-primary/5 via-secondary/5 to-primary/5 rounded-3xl p-8 md:p-12 border border-gray-100">
<h3 className="font-merchant text-2xl md:text-3xl font-semibold text-gray-900 mb-4">
Want to explore Melbourne yourself?
</h3>
<p className="text-gray-600 text-lg mb-8 max-w-2xl mx-auto">
Get your Melbourne CityCard and unlock access to all these incredible experiences and more.
Start your adventure today with exclusive deals and insider access.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button
className="bg-gradient-to-r from-primary to-secondary text-white font-semibold px-8 py-4 rounded-2xl hover:scale-105 transition-all duration-300 shadow-lg hover:shadow-xl"
>
<MapPin className="w-5 h-5 mr-2" />
Explore Melbourne
</Button>
<Button
variant="outline"
className="border-2 border-gray-300 text-gray-700 font-semibold px-8 py-4 rounded-2xl hover:border-primary hover:text-primary hover:scale-105 transition-all duration-300"
>
<Coffee className="w-5 h-5 mr-2" />
View All Blogs
</Button>
</div>
</div>
</motion.div>
</div>
</section>
);
}

View File

@@ -0,0 +1,278 @@
import { useState } from 'react';
import { Check, X, Star, Users, MapPin, Calendar, Clock, Zap, Eye } from 'lucide-react';
import { Button } from './ui/button';
import { motion } from 'motion/react';
const cardOptions = [
{
id: 'selective',
name: 'Selective Card',
subtitle: 'Pick 5-10 things to do from a choice of 102 attractions tours and activities',
priceRange: '$89-159',
duration: '3-7 days',
popular: false,
color: 'from-blue-500 to-cyan-500',
features: {
passCategory: 'Selective Card',
accessToAttractions: true,
entryToAttractions: true,
accessToExperiences: true,
entryToSites: true,
accessToVenues: false,
entryToEvents: 'Pass Category',
accessToLocations: 'Pass Category',
entryToActivities: true,
accessToExhibits: true,
entryToActivitiesSecond: true
}
},
{
id: 'unlimited',
name: 'Melbourne Unlimited Card',
subtitle: 'Pick 5-30 things to do from a choice of 102 attractions tours and activities',
priceRange: '$159-299',
duration: '3-7 days',
popular: true,
color: 'from-purple-500 to-pink-500',
features: {
passCategory: 'Pass Category',
accessToAttractions: true,
entryToAttractions: true,
accessToExperiences: true,
entryToSites: true,
accessToVenues: true,
entryToEvents: 'Pass Category',
accessToLocations: 'Pass Category',
entryToActivities: true,
accessToExhibits: true,
entryToActivitiesSecond: true
}
}
];
const features = [
{ key: 'passCategory', label: 'Pass Category', icon: Star },
{ key: 'accessToAttractions', label: 'Access to Attractions', icon: MapPin },
{ key: 'entryToAttractions', label: 'Entry to Attractions', icon: Zap },
{ key: 'accessToExperiences', label: 'Access to Experiences', icon: Users },
{ key: 'entryToSites', label: 'Entry to Sites', icon: MapPin },
{ key: 'accessToVenues', label: 'Access to Venues', icon: MapPin },
{ key: 'entryToEvents', label: 'Entry to Events', icon: Calendar },
{ key: 'accessToLocations', label: 'Access to Locations', icon: MapPin },
{ key: 'entryToActivities', label: 'Entry to Activities', icon: Users },
{ key: 'accessToExhibits', label: 'Access to Exhibits', icon: Eye },
{ key: 'entryToActivitiesSecond', label: 'Entry to Activities', icon: Users }
];
const FeatureIcon = ({ feature }: { feature: typeof features[0] }) => {
const Icon = feature.icon;
return <Icon className="w-4 h-4 text-gray-500" />;
};
interface MelbourneCardComparisonProps {
onCheckoutClick?: () => void;
}
export function MelbourneCardComparison({ onCheckoutClick }: MelbourneCardComparisonProps) {
const [selectedCard, setSelectedCard] = useState<string>('unlimited');
const renderFeatureValue = (value: boolean | string, cardId: string) => {
if (typeof value === 'boolean') {
return value ? (
<div className="flex justify-center">
<div className="w-6 h-6 bg-green-500 rounded-full flex items-center justify-center">
<Check className="w-4 h-4 text-white" />
</div>
</div>
) : (
<div className="flex justify-center">
<div className="w-6 h-6 bg-gray-200 rounded-full flex items-center justify-center">
<X className="w-3 h-3 text-gray-400" />
</div>
</div>
);
}
return (
<div className="text-center text-sm text-gray-600 px-2">
{value}
</div>
);
};
return (
<section className="py-20 bg-gradient-to-br from-gray-50 via-white to-gray-50 relative overflow-hidden">
{/* Background Pattern */}
<div className="absolute inset-0 opacity-5">
<div className="absolute top-0 left-0 w-full h-full bg-gradient-to-br from-primary/10 to-secondary/10"></div>
</div>
<div className="container mx-auto px-4 relative z-10">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<div className="inline-flex items-center gap-2 bg-gradient-to-r from-primary/10 to-secondary/10 px-4 py-2 rounded-full mb-6">
<div className="w-2 h-2 bg-gradient-to-r from-primary to-secondary rounded-full"></div>
<span className="text-sm font-medium bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
Choose Your Adventure
</span>
</div>
<h2 className="font-merchant text-4xl md:text-5xl lg:text-6xl text-gray-900 mb-6">
<span className="font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent italic">
Buy
</span>{' '}
<span className="font-normal">Now</span>
</h2>
<p className="text-xl text-gray-600 max-w-4xl mx-auto leading-relaxed">
Melbourne is a must-visit cultural epicenter, and this spectacular trip unlocks
your access around the city in one easy. Save over the cost of visiting Melbourne's
landmarks, have lunch at Phi Phi Leh, snorkel at Bamboo Island, and visit Monkey Beach.
</p>
</motion.div>
{/* Card Options Header */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
viewport={{ once: true }}
className="flex justify-center mb-12"
>
<div className="flex bg-white rounded-2xl p-2 shadow-lg border border-gray-100 gap-2">
{cardOptions.map((card) => (
<motion.button
key={card.id}
onClick={() => setSelectedCard(card.id)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className={`px-8 py-4 rounded-xl font-medium transition-all duration-300 relative overflow-hidden ${
selectedCard === card.id
? `bg-gradient-to-r ${card.color} text-white shadow-lg`
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-50'
}`}
>
{card.popular && (
<div className="absolute -top-1 -right-1 w-3 h-3 bg-yellow-400 rounded-full"></div>
)}
<div className="text-center">
<div className="font-semibold text-lg">{card.name}</div>
<div className="text-sm opacity-90 mt-1">{card.priceRange}</div>
</div>
</motion.button>
))}
</div>
</motion.div>
{/* Comparison Table */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
viewport={{ once: true }}
className="bg-white rounded-3xl shadow-xl border border-gray-100 overflow-hidden"
>
{/* Table Header */}
<div className="bg-gradient-to-r from-gray-50 to-gray-100 px-8 py-6">
<div className="grid grid-cols-3 gap-8 items-center">
<div className="font-merchant text-xl font-semibold text-gray-900">
Features
</div>
{cardOptions.map((card) => (
<div key={card.id} className="text-center">
<div className={`inline-block px-6 py-3 rounded-2xl bg-gradient-to-r ${card.color} text-white mb-2`}>
<div className="font-semibold text-lg">{card.name}</div>
</div>
<div className="text-sm text-gray-600 max-w-xs mx-auto leading-relaxed">
{card.subtitle}
</div>
</div>
))}
</div>
</div>
{/* Table Body */}
<div className="divide-y divide-gray-100">
{features.map((feature, index) => (
<motion.div
key={feature.key}
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.4, delay: index * 0.05 }}
viewport={{ once: true }}
className="grid grid-cols-3 gap-8 items-center px-8 py-6 hover:bg-gray-50/50 transition-colors duration-200"
>
<div className="flex items-center gap-3">
<FeatureIcon feature={feature} />
<span className="font-medium text-gray-900">{feature.label}</span>
</div>
{cardOptions.map((card) => (
<div key={card.id} className="text-center">
{renderFeatureValue(card.features[feature.key as keyof typeof card.features], card.id)}
</div>
))}
</motion.div>
))}
</div>
{/* Call to Action Footer */}
<div className="bg-gradient-to-r from-primary/5 to-secondary/5 px-8 py-8">
<div className="grid grid-cols-3 gap-8 items-center">
<div className="text-center">
<div className="font-medium text-gray-600 text-sm mb-2">Ready to explore?</div>
<div className="text-xs text-gray-500">Compare features above</div>
</div>
{cardOptions.map((card) => (
<motion.div key={card.id} className="text-center">
<div className="mb-4">
<div className="text-3xl font-bold text-gray-900">{card.priceRange}</div>
<div className="text-sm text-gray-600">{card.duration}</div>
</div>
<Button
withShine={true}
className={`w-full h-14 rounded-2xl text-white font-semibold text-lg bg-gradient-to-r ${card.color} hover:scale-105 transition-all duration-300 shadow-lg hover:shadow-xl`}
onClick={onCheckoutClick}
>
Buy {card.name}
</Button>
</motion.div>
))}
</div>
</div>
</motion.div>
{/* Additional Info */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.4 }}
viewport={{ once: true }}
className="text-center mt-12"
>
<div className="flex flex-wrap justify-center gap-8 text-sm text-gray-600">
<div className="flex items-center gap-2">
<Clock className="w-4 h-4 text-primary" />
<span>Instant digital delivery</span>
</div>
<div className="flex items-center gap-2">
<Users className="w-4 h-4 text-primary" />
<span>24/7 customer support</span>
</div>
<div className="flex items-center gap-2">
<Star className="w-4 h-4 text-primary" />
<span>90-day money back guarantee</span>
</div>
</div>
</motion.div>
</div>
</section>
);
}

View File

@@ -0,0 +1,212 @@
import { motion } from 'motion/react';
import {
HelpCircle,
Clock,
MapPin,
CreditCard,
Calendar,
Users,
Coffee,
Camera,
Train,
Smartphone
} from 'lucide-react';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "./ui/accordion";
const faqData = [
{
id: "refund",
question: "Can I get a refund on my Melbourne CityCard?",
answer: "Yes, you can cancel your Melbourne CityCard and receive a full refund if you cancel at least 24 hours in advance of your selected start date. For cancellations within 24 hours, refunds are subject to our cancellation policy. Digital cards can be refunded instantly through your account.",
icon: CreditCard
},
{
id: "duration",
question: "How long is my Melbourne CityCard valid?",
answer: "Melbourne CityCards are available in 1, 2, 3, and 5-day options. Your card activates on the first attraction you visit and is valid for consecutive days only. The 5-day Melbourne Unlimited Card provides the best value for extended stays with access to over 40 premium attractions.",
icon: Calendar
},
{
id: "transportation",
question: "Does the Melbourne CityCard include public transport?",
answer: "The Melbourne Unlimited Card includes a complimentary Myki card loaded with travel credit for trams, trains, and buses within Melbourne's CBD and inner suburbs. The Selective Card focuses on attractions only, but we provide detailed transport guides for each venue.",
icon: Train
},
{
id: "attractions",
question: "What are the must-visit attractions included with my card?",
answer: "Your Melbourne CityCard includes iconic experiences like Eureka Tower SkyDeck, Royal Botanic Gardens tours, Melbourne Zoo, SEA LIFE Melbourne Aquarium, and Melbourne Star observation wheel. Plus unique local experiences like laneways art tours, coffee culture walks, and rooftop dining discounts.",
icon: Camera
},
{
id: "best-time",
question: "When is the best time to visit Melbourne?",
answer: "Melbourne is fantastic year-round! Spring (Sep-Nov) offers perfect weather and blooming gardens. Summer (Dec-Feb) brings outdoor festivals and rooftop season. Autumn (Mar-May) showcases beautiful foliage and harvest events. Winter (Jun-Aug) is ideal for cozy cafes, indoor attractions, and cultural experiences.",
icon: Clock
},
{
id: "coffee-culture",
question: "How can I experience Melbourne's famous coffee culture?",
answer: "Your Melbourne CityCard includes guided coffee tours through famous laneways, visits to historic coffee roasters, and discounts at award-winning cafes. We've partnered with local baristas to offer exclusive tastings and behind-the-scenes experiences at Melbourne's most beloved coffee institutions.",
icon: Coffee
},
{
id: "group-bookings",
question: "Do you offer group discounts for families or friends?",
answer: "Yes! Groups of 4+ receive automatic discounts, and families with children under 16 get special pricing. School groups and corporate bookings receive additional benefits. Contact our Melbourne team for custom packages that can include private tours and exclusive venue access.",
icon: Users
},
{
id: "mobile-app",
question: "Do I need the mobile app to use my Melbourne CityCard?",
answer: "While not required, our mobile app enhances your Melbourne experience with interactive maps, real-time attraction wait times, insider tips from locals, and the ability to skip lines at participating venues. Download it for offline access to your itinerary and exclusive app-only deals.",
icon: Smartphone
},
{
id: "neighborhoods",
question: "Which Melbourne neighborhoods should I explore?",
answer: "Your CityCard provides access to experiences across Melbourne's diverse neighborhoods: Fitzroy for street art and vintage shopping, St. Kilda for beaches and nightlife, Southbank for dining and culture, CBD for iconic attractions, and Richmond for authentic Vietnamese food and shopping.",
icon: MapPin
}
];
export function MelbourneFAQ() {
return (
<section className="py-16 md:py-24 bg-gradient-to-br from-gray-50 via-white to-gray-50 relative overflow-hidden">
{/* Background Elements */}
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-secondary/5"></div>
<div className="absolute top-0 right-0 w-96 h-96 bg-gradient-to-br from-primary/10 to-transparent rounded-full -translate-y-48 translate-x-48 blur-2xl"></div>
<div className="absolute bottom-0 left-0 w-96 h-96 bg-gradient-to-tr from-secondary/10 to-transparent rounded-full translate-y-48 -translate-x-48 blur-2xl"></div>
<div className="container mx-auto px-4 relative z-10">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<div className="inline-flex items-center gap-2 bg-gradient-to-r from-primary/10 to-secondary/10 px-4 py-2 rounded-full mb-6">
<HelpCircle className="w-4 h-4 text-primary" />
<span className="text-sm font-medium bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
Melbourne Guide
</span>
</div>
<h2 className="font-merchant text-4xl md:text-5xl lg:text-6xl text-gray-900 mb-6">
<span className="font-normal">Frequently Asked</span>{' '}
<span className="font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent italic">
Questions
</span>
</h2>
<p className="text-xl text-gray-600 max-w-4xl mx-auto leading-relaxed">
Everything you need to know about exploring Melbourne with your CityCard. From iconic attractions
to hidden local gems, we've got your Melbourne adventure covered.
</p>
</motion.div>
{/* FAQ Grid */}
<div className="max-w-4xl mx-auto">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
viewport={{ once: true }}
>
<Accordion type="single" collapsible className="space-y-4">
{faqData.map((faq, index) => {
const IconComponent = faq.icon;
return (
<motion.div
key={faq.id}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.3 + index * 0.05 }}
viewport={{ once: true }}
>
<AccordionItem
value={faq.id}
className="bg-white rounded-2xl shadow-sm border border-gray-100 hover:shadow-md transition-all duration-300 overflow-hidden group"
>
<AccordionTrigger className="px-6 py-5 hover:no-underline group-hover:bg-gray-50/50 transition-colors duration-200">
<div className="flex items-center gap-4 text-left">
<div className="flex-shrink-0 w-10 h-10 bg-gradient-to-br from-primary/10 to-secondary/10 rounded-xl flex items-center justify-center group-hover:from-primary/20 group-hover:to-secondary/20 transition-all duration-200">
<IconComponent className="w-5 h-5 text-primary" />
</div>
<span className="font-merchant font-semibold text-gray-900 group-hover:text-primary transition-colors duration-200">
{faq.question}
</span>
</div>
</AccordionTrigger>
<AccordionContent className="px-6 pb-6">
<div className="pl-14">
<p className="text-gray-600 leading-relaxed">
{faq.answer}
</p>
</div>
</AccordionContent>
</AccordionItem>
</motion.div>
);
})}
</Accordion>
</motion.div>
</div>
{/* Call to Action */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.5 }}
viewport={{ once: true }}
className="text-center mt-16"
>
<div className="bg-gradient-to-br from-primary/5 via-secondary/5 to-primary/5 rounded-3xl p-8 md:p-12 border border-gray-100 max-w-3xl mx-auto">
<div className="mb-6">
<div className="w-16 h-16 bg-gradient-to-br from-primary to-secondary rounded-2xl mx-auto flex items-center justify-center mb-4">
<HelpCircle className="w-8 h-8 text-white" />
</div>
<h3 className="font-merchant text-2xl md:text-3xl font-semibold text-gray-900 mb-4">
Still have questions?
</h3>
<p className="text-gray-600 text-lg mb-8 max-w-2xl mx-auto">
Our Melbourne experts are here to help you make the most of your visit.
Get personalized recommendations and insider tips.
</p>
</div>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="bg-gradient-to-r from-primary to-secondary text-white font-semibold px-8 py-4 rounded-2xl shadow-lg hover:shadow-xl transition-all duration-300 group"
>
<span className="flex items-center gap-2">
Contact Melbourne Support
<HelpCircle className="w-5 h-5 group-hover:rotate-12 transition-transform duration-200" />
</span>
</motion.button>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="border-2 border-gray-300 text-gray-700 font-semibold px-8 py-4 rounded-2xl hover:border-primary hover:text-primary transition-all duration-300 group"
>
<span className="flex items-center gap-2">
Browse Melbourne Guide
<MapPin className="w-5 h-5 group-hover:bounce transition-all duration-200" />
</span>
</motion.button>
</div>
</div>
</motion.div>
</div>
</section>
);
}

View File

@@ -0,0 +1,256 @@
import { motion } from 'motion/react';
import { Button } from './ui/button';
import Navbar from './Navbar';
import { CitySubmenu } from './CitySubmenu';
import { MelbourneAttractions } from './MelbourneAttractions';
import { MelbourneCardComparison } from './MelbourneCardComparison';
import { MelbourneTourOverview } from './MelbourneTourOverview';
import { MelbourneBlogs } from './MelbourneBlogs';
import { EnhancedTestimonials } from './EnhancedTestimonials';
import { MobileAppPromotion } from './MobileAppPromotion';
import { MelbourneFAQ } from './MelbourneFAQ';
import { Footer } from './Footer';
interface User {
email: string;
name: string;
}
interface MelbournePageProps {
onBackClick: () => void;
onHomeClick: () => void;
onAttractionsClick: () => void;
onPassesClick: () => void;
onCheckoutClick?: () => void;
onSignInClick: () => void;
onSignOutClick?: () => void;
onBlogsClick: () => void;
onHowItWorksClick: () => void;
onFAQClick: () => void;
onPrivacyPolicyClick: () => void;
onAboutUsClick: () => void;
onProfileClick?: () => void;
onCityCardsClick?: () => void;
onMagicItineraryClick?: () => void;
onPostCardsClick?: () => void;
onOffersClick?: () => void;
onEsimsClick?: () => void;
onHotelDiscountsClick?: () => void;
onContactUsClick?: () => void;
currentPage?: string;
user?: User | null;
}
export function MelbournePage({
onBackClick,
onHomeClick,
onAttractionsClick,
onPassesClick,
onCheckoutClick,
onSignInClick,
onSignOutClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onProfileClick,
onCityCardsClick,
onMagicItineraryClick,
onPostCardsClick,
onOffersClick,
onEsimsClick,
onHotelDiscountsClick,
onContactUsClick,
currentPage,
user
}: MelbournePageProps) {
return (
<div className="min-h-screen bg-background">
{/* Navbar */}
<Navbar
activeCity="Melbourne"
onCityChange={(city) => {
if (city === 'Melbourne') {
// Already on Melbourne page
}
}}
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
onPassesClick={onPassesClick}
onCheckoutClick={onCheckoutClick}
onHomeClick={onHomeClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick}
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
onEsimsClick={onEsimsClick}
onHotelDiscountsClick={onHotelDiscountsClick}
currentPage={currentPage || "melbourne"}
isUserSignedIn={!!user}
user={user}
/>
{/* City Submenu */}
<CitySubmenu
onClose={() => {}} // Empty function since it's always shown
currentPage="melbourne"
onHomeClick={onHomeClick}
onMelbourneClick={() => {}} // Already on Melbourne page
onAttractionsClick={onAttractionsClick}
onPassesClick={onPassesClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
/>
{/* Hero Section - Full Width */}
<div
className="relative w-full h-[85vh] bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `linear-gradient(rgba(0, 0, 0, 0.42), rgba(0, 0, 0, 0.42)), url('https://images.unsplash.com/photo-1573639571368-065819727a52?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx8fDE3NTczOTY3MDd8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral')`
}}
>
<div className="absolute inset-x-0 top-0 bottom-0 flex items-center justify-start pl-8 md:pl-16 lg:pl-24">
<div className="text-left text-white px-4">
<motion.h1
className="font-poppins text-5xl md:text-6xl lg:text-7xl leading-tight mb-6"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 1, delay: 0.3 }}
>
<span className="font-light">Discover</span>{' '}
<span className="font-bold italic bg-gradient-to-r from-yellow-300 to-orange-400 bg-clip-text text-transparent">
Melbourne
</span>
</motion.h1>
<motion.p
className="font-poppins text-xl leading-relaxed font-light mb-4 max-w-3xl"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.5 }}
>
Explore Melbourne's vibrant culture, world-class dining, and iconic attractions
</motion.p>
<motion.p
className="font-poppins text-xl leading-relaxed font-normal mb-8 text-yellow-300"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.8, delay: 0.7 }}
>
Save up to 50% on all attractions
</motion.p>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.9 }}
>
<Button
withShine={true}
className="font-poppins font-semibold h-[70px] min-w-[280px] px-12 py-6 rounded-full text-white text-xl hover:scale-105 transition-transform duration-300"
style={{ backgroundColor: '#F95F62' }}
onClick={onCheckoutClick}
>
BUY A CARD
</Button>
</motion.div>
</div>
</div>
</div>
{/* 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 Card Comparison Section */}
<MelbourneCardComparison onCheckoutClick={onCheckoutClick} />
{/* Melbourne Tour Overview Section */}
<MelbourneTourOverview />
{/* Melbourne Blogs Section */}
<MelbourneBlogs />
{/* Enhanced Testimonials Section */}
<EnhancedTestimonials />
{/* Mobile App Promotion Section */}
<MobileAppPromotion />
{/* Melbourne FAQ Section */}
<MelbourneFAQ />
{/* Footer */}
<Footer
onHomeClick={onHomeClick}
onMelbourneClick={onHomeClick}
onPassesClick={onPassesClick}
onSignInClick={onSignInClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onContactUsClick={onContactUsClick}
currentPage="melbourne"
/>
</div>
);
}

View File

@@ -0,0 +1,277 @@
import { motion } from 'motion/react';
import { ImageWithFallback } from './figma/ImageWithFallback';
import { CheckCircle, Users, MapPin, Calendar, Coffee, Camera, Clock, Zap, Volume2, Star } from 'lucide-react';
export function MelbourneTourOverview() {
const whatsIncluded = [
{
icon: Coffee,
text: "Food and coffee tours in famous laneways",
color: "text-amber-600"
},
{
icon: MapPin,
text: "Transport via trams and walking tours",
color: "text-blue-600"
},
{
icon: Users,
text: "Local guides and cultural experiences",
color: "text-green-600"
},
{
icon: Zap,
text: "Skip-the-line access to major attractions",
color: "text-purple-600"
},
{
icon: Volume2,
text: "Audio guides and mobile app",
color: "text-indigo-600"
},
{
icon: Clock,
text: "Flexible timing and self-paced tours",
color: "text-rose-600"
}
];
const tourHighlights = [
{
icon: Star,
text: "Experience the cultural capital's vibrant arts scene and hidden laneways",
color: "text-yellow-600"
},
{
icon: Coffee,
text: "Discover Melbourne's world-famous coffee culture and rooftop bars",
color: "text-amber-600"
},
{
icon: Camera,
text: "Capture stunning views from Eureka Skydeck and Royal Botanic Gardens",
color: "text-purple-600"
},
{
icon: Users,
text: "Enjoy food tours through Queen Victoria Market and ethnic quarters",
color: "text-green-600"
},
{
icon: MapPin,
text: "Explore sports culture at MCG and cultural sites in Federation Square",
color: "text-blue-600"
},
{
icon: Calendar,
text: "Access year-round events, festivals, and seasonal attractions",
color: "text-rose-600"
}
];
return (
<section className="py-20 bg-gradient-to-br from-white via-gray-50/30 to-white relative overflow-hidden">
{/* Background Pattern */}
<div className="absolute inset-0 opacity-[0.02]">
<div className="absolute top-0 left-0 w-full h-full">
<div className="w-full h-full bg-gradient-to-br from-primary/20 via-secondary/20 to-primary/20 opacity-30"></div>
</div>
</div>
<div className="container mx-auto px-4 relative z-10">
{/* Header */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="mb-16"
>
<h2 className="heading-dynamic font-merchant text-4xl md:text-5xl lg:text-6xl text-gray-900 mb-8">
<span className="font-light">Melbourne</span>{' '}
<span className="font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent italic">
Tour
</span>{' '}
<span className="font-normal">Overview</span>
</h2>
</motion.div>
{/* Main Content Grid */}
<div className="grid lg:grid-cols-2 gap-16 items-start">
{/* Left Content */}
<div className="space-y-12">
{/* Description */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
viewport={{ once: true }}
>
<p className="text-lg md:text-xl text-gray-700 leading-relaxed">
Melbourne is a must-visit cultural epicenter, and this spectacular experience unlocks
your access around the city in one easy pass. Save over the cost of visiting Melbourne's
landmarks, explore famous laneways and street art, enjoy world-class dining in hidden bars,
and immerse yourself in the sports capital's vibrant atmosphere. From Royal Botanic Gardens
to Federation Square, hotel pickup and drop-off all included.
</p>
</motion.div>
{/* What's Included */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
viewport={{ once: true }}
className="bg-white rounded-3xl p-8 shadow-lg border border-gray-100"
>
<h3 className="font-merchant text-2xl md:text-3xl font-semibold text-gray-900 mb-6">
What's included
</h3>
<div className="grid md:grid-cols-2 gap-4">
{whatsIncluded.map((item, index) => (
<motion.div
key={index}
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.4, delay: 0.3 + index * 0.1 }}
viewport={{ once: true }}
className="flex items-start gap-3 group"
>
<div className={`w-6 h-6 rounded-full bg-gray-100 flex items-center justify-center flex-shrink-0 mt-0.5 group-hover:bg-gray-200 transition-colors duration-200`}>
<item.icon className={`w-4 h-4 ${item.color}`} />
</div>
<span className="text-gray-700 font-medium leading-relaxed group-hover:text-gray-900 transition-colors duration-200">
{item.text}
</span>
</motion.div>
))}
</div>
</motion.div>
</div>
{/* Right Image */}
<motion.div
initial={{ opacity: 0, x: 20 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
viewport={{ once: true }}
className="relative"
>
<div className="relative rounded-3xl overflow-hidden shadow-2xl">
<ImageWithFallback
src="https://images.unsplash.com/photo-1720347247737-9252d85d3027?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBjaXR5JTIwc2t5bGluZSUyMGZsaW5kZXJzJTIwc3RyZWV0fGVufDF8fHx8MTc1NzMzOTAyNHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral"
alt="Melbourne City Skyline"
className="w-full h-[400px] md:h-[500px] object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/20 via-transparent to-transparent"></div>
{/* Floating Badge */}
<div className="absolute top-6 left-6 bg-white/95 backdrop-blur-sm rounded-full px-4 py-2 shadow-lg">
<div className="flex items-center gap-2">
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
<span className="text-sm font-medium text-gray-900">Live Experience</span>
</div>
</div>
</div>
</motion.div>
</div>
{/* Tour Highlights - Full Width Below */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.4 }}
viewport={{ once: true }}
className="mt-20"
>
<div className="grid lg:grid-cols-2 gap-16 items-center">
{/* Left Image for Tour Highlights */}
<motion.div
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: 0.5 }}
viewport={{ once: true }}
className="order-2 lg:order-1"
>
<div className="relative rounded-3xl overflow-hidden shadow-2xl">
<ImageWithFallback
src="https://images.unsplash.com/photo-1720347247737-9252d85d3027?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBjaXR5JTIwc2t5bGluZSUyMGZsaW5kZXJzJTIwc3RyZWV0fGVufDF8fHx8MTc1NzMzOTAyNHww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral"
alt="Melbourne Tour Highlights"
className="w-full h-[400px] md:h-[500px] object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-br from-primary/10 via-transparent to-secondary/10"></div>
{/* Floating Stats */}
<div className="absolute bottom-6 right-6 bg-white/95 backdrop-blur-sm rounded-2xl p-4 shadow-lg">
<div className="text-center">
<div className="text-2xl font-bold text-gray-900">50+</div>
<div className="text-xs text-gray-600">Attractions</div>
</div>
</div>
</div>
</motion.div>
{/* Right Content - Tour Highlights */}
<motion.div
initial={{ opacity: 0, x: 20 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: 0.6 }}
viewport={{ once: true }}
className="order-1 lg:order-2"
>
<div className="bg-white rounded-3xl p-8 shadow-lg border border-gray-100">
<h3 className="font-merchant text-2xl md:text-3xl font-semibold text-gray-900 mb-8">
Tour Highlights
</h3>
<div className="space-y-6">
{tourHighlights.map((highlight, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.7 + index * 0.1 }}
viewport={{ once: true }}
className="flex items-start gap-4 group"
>
<div className={`w-8 h-8 rounded-full bg-gray-100 flex items-center justify-center flex-shrink-0 mt-1 group-hover:bg-gray-200 transition-all duration-200 group-hover:scale-110`}>
<highlight.icon className={`w-4 h-4 ${highlight.color}`} />
</div>
<p className="text-gray-700 font-medium leading-relaxed group-hover:text-gray-900 transition-colors duration-200">
{highlight.text}
</p>
</motion.div>
))}
</div>
{/* Call to Action in Tour Highlights */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 1.3 }}
viewport={{ once: true }}
className="mt-8 pt-6 border-t border-gray-100"
>
<div className="flex items-center justify-between">
<div>
<div className="text-sm text-gray-600 mb-1">Starting from</div>
<div className="text-2xl font-bold text-gray-900">$89</div>
</div>
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
className="bg-gradient-to-r from-primary to-secondary text-white px-6 py-3 rounded-xl font-medium shadow-lg hover:shadow-xl transition-all duration-300"
>
View All Tours
</motion.button>
</div>
</motion.div>
</div>
</motion.div>
</div>
</motion.div>
</div>
</section>
);
}

View File

@@ -0,0 +1,381 @@
import { motion } from 'motion/react';
import { ImageWithFallback } from './figma/ImageWithFallback';
import { Download, Apple, Play } from 'lucide-react';
export function MobileAppPromotion() {
return (
<section className="py-20 bg-gradient-to-br from-gray-50 to-white relative overflow-hidden">
<div className="container mx-auto px-4">
<div className="grid lg:grid-cols-2 gap-8 items-center">
{/* Left Content */}
<motion.div
initial={{ opacity: 0, x: -30 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="space-y-8"
>
{/* Badge */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.1 }}
viewport={{ once: true }}
className="inline-flex items-center gap-2 bg-gradient-to-r from-primary/10 to-secondary/10 px-4 py-2 rounded-full"
>
<div className="w-2 h-2 bg-gradient-to-r from-primary to-secondary rounded-full"></div>
<span className="text-sm font-medium bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
Mobile App
</span>
</motion.div>
{/* Heading */}
<motion.h2
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
viewport={{ once: true }}
className="font-merchant text-4xl md:text-5xl lg:text-6xl text-gray-900 leading-tight"
>
<span className="font-light">Access all your</span>{' '}
<span className="font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
city cards
</span>{' '}
<span className="font-light">on your phone.</span>
</motion.h2>
{/* Description */}
<motion.p
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
viewport={{ once: true }}
className="text-xl text-gray-600 leading-relaxed"
>
Download the app to manage your city cards, keep track of attractions visited, and complete your urban adventures without missing a beat. Stay on track and make the most of every destination!
</motion.p>
{/* Get the App Section */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.4 }}
viewport={{ once: true }}
className="space-y-4"
>
<h3 className="font-merchant text-xl font-semibold text-gray-900">
Get the App
</h3>
{/* Download Buttons */}
<div className="flex flex-col sm:flex-row gap-4">
{/* Google Play Button */}
<motion.a
whileHover={{ scale: 1.02, y: -2 }}
whileTap={{ scale: 0.98 }}
href="#"
className="group relative bg-black text-white rounded-lg px-6 py-3 flex items-center gap-3 transition-all duration-300 hover:shadow-xl overflow-hidden"
>
<div className="relative z-10 flex items-center gap-3">
<Play className="w-8 h-8 text-white fill-current" />
<div className="text-left">
<div className="text-xs opacity-80">GET IT ON</div>
<div className="text-lg font-semibold">Google Play</div>
</div>
</div>
{/* Shine effect */}
<div className="absolute inset-0 opacity-0 group-hover:opacity-30 transition-opacity duration-300">
<div className="h-full bg-gradient-to-r from-transparent via-white to-transparent animate-shine"></div>
</div>
</motion.a>
{/* App Store Button */}
<motion.a
whileHover={{ scale: 1.02, y: -2 }}
whileTap={{ scale: 0.98 }}
href="#"
className="group relative bg-black text-white rounded-lg px-6 py-3 flex items-center gap-3 transition-all duration-300 hover:shadow-xl overflow-hidden"
>
<div className="relative z-10 flex items-center gap-3">
<Apple className="w-8 h-8 text-white" />
<div className="text-left">
<div className="text-xs opacity-80">Download on the</div>
<div className="text-lg font-semibold">App Store</div>
</div>
</div>
{/* Shine effect */}
<div className="absolute inset-0 opacity-0 group-hover:opacity-30 transition-opacity duration-300">
<div className="h-full bg-gradient-to-r from-transparent via-white to-transparent animate-shine"></div>
</div>
</motion.a>
</div>
</motion.div>
{/* Features List */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.5 }}
viewport={{ once: true }}
className="grid sm:grid-cols-2 gap-4 pt-4"
>
{[
{ icon: '📱', text: 'Offline access to all cards' },
{ icon: '📍', text: 'GPS-enabled attraction finder' },
{ icon: '🎫', text: 'Digital ticket storage' },
{ icon: '📊', text: 'Track your adventures' }
].map((feature, index) => (
<motion.div
key={index}
initial={{ opacity: 0, x: -20 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: 0.6 + (index * 0.1) }}
viewport={{ once: true }}
className="flex items-center gap-3 p-2"
>
<span className="text-2xl">{feature.icon}</span>
<span className="text-sm font-medium text-gray-700">{feature.text}</span>
</motion.div>
))}
</motion.div>
</motion.div>
{/* Right Content - Phone Mockup */}
<motion.div
initial={{ opacity: 0, x: 30 }}
whileInView={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
viewport={{ once: true }}
className="relative flex justify-center lg:justify-end"
>
{/* Phone Frame */}
<div className="relative">
{/* Phone Shadow */}
<div className="absolute inset-0 bg-black/20 blur-3xl transform translate-y-8 scale-95 rounded-3xl"></div>
{/* Phone Container */}
<motion.div
initial={{ y: 20, scale: 0.9 }}
whileInView={{ y: 0, scale: 1 }}
transition={{ duration: 0.8, delay: 0.4 }}
viewport={{ once: true }}
className="relative bg-black rounded-[3rem] p-3 shadow-2xl"
>
{/* Screen */}
<div className="bg-white rounded-[2.5rem] overflow-hidden w-72 h-[600px] relative">
{/* Status Bar */}
<div className="bg-black h-8 rounded-t-[2.5rem] relative">
<div className="absolute inset-x-0 top-0 h-6 bg-black rounded-t-[2.5rem]"></div>
<div className="absolute left-1/2 transform -translate-x-1/2 top-2 w-20 h-2 bg-black rounded-full"></div>
</div>
{/* App Content */}
<div className="h-full bg-gray-50 relative overflow-hidden">
{/* App Header */}
<div className="bg-gradient-to-r from-primary to-secondary px-4 py-3 text-white">
<div className="flex items-center justify-between">
<div>
<h3 className="font-merchant font-semibold text-sm">Melbourne CityCards</h3>
<p className="text-xs opacity-90">5 attractions unlocked</p>
</div>
<div className="w-8 h-8 bg-white/20 rounded-full flex items-center justify-center">
<span className="text-xs">👤</span>
</div>
</div>
</div>
{/* Card Balance Banner */}
<div className="bg-yellow-100 border-l-4 border-yellow-400 px-4 py-2">
<p className="text-xs text-yellow-800">
<span className="font-semibold">$89 saved</span> 3 more attractions available
</p>
</div>
{/* Attractions List */}
<div className="flex-1 p-3 space-y-2 overflow-y-auto">
{/* Attraction 1 */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
className="bg-white rounded-lg p-3 shadow-sm border border-gray-100"
>
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-lg overflow-hidden flex-shrink-0">
<ImageWithFallback
src="https://images.unsplash.com/photo-1512220567447-9ff58da12d87?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBtdXNldW0lMjBhdHRyYWN0aW9ufGVufDF8fHx8MTc1NzM5OTMyMnww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral"
alt="Melbourne Museum"
className="w-full h-full object-cover"
/>
</div>
<div className="flex-1 min-w-0">
<h4 className="font-medium text-xs text-gray-900 truncate">Melbourne Museum</h4>
<p className="text-xs text-gray-500">Natural History</p>
<div className="flex items-center gap-2 mt-1">
<span className="text-xs bg-green-100 text-green-700 px-2 py-0.5 rounded">Save $8</span>
<span className="text-xs text-gray-400"> Added</span>
</div>
</div>
</div>
</motion.div>
{/* Attraction 2 */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: 0.3 }}
className="bg-white rounded-lg p-3 shadow-sm border border-gray-100"
>
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-lg overflow-hidden flex-shrink-0">
<ImageWithFallback
src="https://images.unsplash.com/photo-1610186356191-880ceaa884f3?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjB6b28lMjB3aWxkbGlmZXxlbnwxfHx8fDE3NTczOTkzMjV8MA&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral"
alt="Melbourne Zoo"
className="w-full h-full object-cover"
/>
</div>
<div className="flex-1 min-w-0">
<h4 className="font-medium text-xs text-gray-900 truncate">Melbourne Zoo</h4>
<p className="text-xs text-gray-500">Wildlife & Animals</p>
<div className="flex items-center gap-2 mt-1">
<span className="text-xs bg-green-100 text-green-700 px-2 py-0.5 rounded">Save $12</span>
<span className="text-xs text-gray-400"> Added</span>
</div>
</div>
</div>
</motion.div>
{/* Attraction 3 */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: 0.4 }}
className="bg-white rounded-lg p-3 shadow-sm border border-gray-100"
>
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-lg overflow-hidden flex-shrink-0">
<ImageWithFallback
src="https://images.unsplash.com/photo-1735605918618-0193db0a30af?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBnYXJkZW5zJTIwYm90YW5pY2FsfGVufDF8fHx8MTc1NzM5OTMyOXww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral"
alt="Botanic Gardens"
className="w-full h-full object-cover"
/>
</div>
<div className="flex-1 min-w-0">
<h4 className="font-medium text-xs text-gray-900 truncate">Royal Botanic Gardens</h4>
<p className="text-xs text-gray-500">Gardens & Nature</p>
<div className="flex items-center gap-2 mt-1">
<span className="text-xs bg-green-100 text-green-700 px-2 py-0.5 rounded">Save $5</span>
<span className="text-xs text-gray-400"> Added</span>
</div>
</div>
</div>
</motion.div>
{/* Attraction 4 - Available to Add */}
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5, delay: 0.5 }}
className="bg-white rounded-lg p-3 shadow-sm border border-gray-100"
>
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-lg overflow-hidden flex-shrink-0">
<ImageWithFallback
src="https://images.unsplash.com/photo-1554050640-3f0b16d9c7bd?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBhcXVhcml1bSUyMG9jZWFufGVufDF8fHx8MTc1NzM5OTMzMXww&ixlib=rb-4.1.0&q=80&w=1080&utm_source=figma&utm_medium=referral"
alt="SEA LIFE Aquarium"
className="w-full h-full object-cover"
/>
</div>
<div className="flex-1 min-w-0">
<h4 className="font-medium text-xs text-gray-900 truncate">SEA LIFE Aquarium</h4>
<p className="text-xs text-gray-500">Marine Life</p>
<div className="flex items-center gap-2 mt-1">
<span className="text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded">Save $15</span>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
className="text-xs bg-gradient-to-r from-primary to-secondary text-white px-2 py-0.5 rounded font-medium"
>
+ Add
</motion.button>
</div>
</div>
</div>
</motion.div>
{/* Progress Section */}
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.6 }}
className="bg-gradient-to-r from-primary/10 to-secondary/10 rounded-lg p-3 mt-4"
>
<div className="flex items-center justify-between mb-2">
<span className="text-xs font-medium text-gray-700">Progress</span>
<span className="text-xs text-gray-500">3 of 8 visited</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1.5">
<motion.div
initial={{ width: 0 }}
animate={{ width: '37.5%' }}
transition={{ duration: 1, delay: 0.8 }}
className="bg-gradient-to-r from-primary to-secondary h-1.5 rounded-full"
></motion.div>
</div>
</motion.div>
</div>
{/* Bottom Navigation */}
<div className="bg-white border-t border-gray-100 px-4 py-2">
<div className="flex justify-around">
<button className="flex flex-col items-center gap-1">
<span className="text-primary">🏛</span>
<span className="text-xs text-primary font-medium">Cards</span>
</button>
<button className="flex flex-col items-center gap-1">
<span className="text-gray-400">📍</span>
<span className="text-xs text-gray-400">Map</span>
</button>
<button className="flex flex-col items-center gap-1">
<span className="text-gray-400"></span>
<span className="text-xs text-gray-400">Saved</span>
</button>
<button className="flex flex-col items-center gap-1">
<span className="text-gray-400">👤</span>
<span className="text-xs text-gray-400">Profile</span>
</button>
</div>
</div>
</div>
</div>
</motion.div>
</div>
{/* Floating Elements */}
<motion.div
animate={{
y: [20, -20, 20],
rotate: [0, -5, 0, 5, 0]
}}
transition={{
duration: 5,
repeat: Infinity,
ease: "easeInOut",
delay: 2
}}
className="absolute -bottom-8 -right-8 w-14 h-14 bg-gradient-to-br from-green-400 to-blue-500 rounded-xl shadow-lg flex items-center justify-center text-white text-xl"
>
📍
</motion.div>
</motion.div>
</div>
</div>
{/* Background Decorations */}
<div className="absolute top-0 right-0 w-96 h-96 bg-gradient-to-br from-primary/5 to-secondary/5 rounded-full blur-3xl transform translate-x-1/2 -translate-y-1/2"></div>
<div className="absolute bottom-0 left-0 w-64 h-64 bg-gradient-to-tr from-secondary/5 to-primary/5 rounded-full blur-3xl transform -translate-x-1/2 translate-y-1/2"></div>
</section>
);
}

View File

@@ -0,0 +1,642 @@
import { ArrowRight, Smartphone, MapPin, Star, Clock, Users, Heart, Share2, Filter, Search } from 'lucide-react';
import { motion } from 'motion/react';
import imgFrame1597884939 from "figma:asset/5da1b0444c0d21bc7ee776c49e36e2a8ea4d3e12.png";
export function MobileAppSection() {
// Generate a realistic QR code pattern
const generateQRPattern = () => {
const size = 21; // Standard QR code size
const pattern = [];
for (let i = 0; i < size * size; i++) {
// Create corner detection patterns
const row = Math.floor(i / size);
const col = i % size;
// Corner squares (7x7)
const isCornerSquare =
(row < 7 && col < 7) || // Top-left
(row < 7 && col >= 14) || // Top-right
(row >= 14 && col < 7); // Bottom-left
// Finder patterns within corner squares
const isFinderPattern = isCornerSquare && (
(row === 0 || row === 6 || col === 0 || col === 6) ||
(row >= 2 && row <= 4 && col >= 2 && col <= 4)
);
// Timing patterns
const isTimingPattern = (row === 6 && col >= 8 && col <= 12) || (col === 6 && row >= 8 && row <= 12);
// Random data pattern for other areas
const isDataPattern = !isCornerSquare && !isTimingPattern && Math.random() > 0.45;
pattern.push(isFinderPattern || isTimingPattern || isDataPattern);
}
return pattern;
};
const qrPattern = generateQRPattern();
return (
<section className="bg-muted/30 relative overflow-hidden">
{/* Subtle Background Elements */}
<div className="absolute inset-0">
<div className="absolute top-1/3 left-1/6 w-64 h-64 bg-primary/3 rounded-full blur-3xl"></div>
<div className="absolute bottom-1/2 right-1/6 w-48 h-48 bg-secondary/3 rounded-full blur-3xl"></div>
</div>
<div className="container mx-auto px-4 pt-4 relative z-10">
{/* Figma Layout Implementation */}
<motion.div
className="flex flex-col gap-16 lg:gap-20"
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8, ease: "easeOut" }}
>
{/* Header Section - Following Figma Layout */}
<div className="flex flex-col lg:flex-row items-start justify-between gap-12 lg:gap-16">
{/* Left Side - Main Heading */}
<motion.div
className="flex-1 max-w-2xl"
initial={{ opacity: 0, x: -40 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.7, ease: "easeOut" }}
>
<h1 className="font-merchant text-4xl lg:text-5xl xl:text-6xl leading-tight text-foreground">
<span className="font-normal">Access all your</span>{' '}
<span className="bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent font-bold italic">
city cards
</span>
<br />
<span className="font-normal">on your</span>{' '}
<span className="font-semibold">phone.</span>
</h1>
</motion.div>
{/* Right Side - Description and Buttons */}
<motion.div
className="flex flex-col gap-8 w-full lg:w-[400px] xl:w-[450px]"
initial={{ opacity: 0, x: 40 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.7, ease: "easeOut", delay: 0.1 }}
>
{/* Description Text */}
<p className="font-poppins text-lg text-muted-foreground leading-relaxed font-normal">
Download our mobile app and unlock instant access to premium city experiences across Australia.
</p>
{/* Download Buttons - Following Figma Layout */}
<div className="flex flex-col sm:flex-row gap-6">
{/* Android Download Button */}
<motion.button
className="interactive-button relative flex items-center gap-3 bg-foreground text-background px-6 py-4 rounded-xl font-medium text-base hover:bg-foreground/90 transition-all duration-300 shadow-lg hover:shadow-xl flex-1 overflow-hidden group"
whileHover={{ scale: 1.02, y: -2 }}
whileTap={{ scale: 0.98 }}
transition={{ duration: 0.2 }}
>
{/* Continuous Shine Effect */}
<div className="absolute inset-0 -top-px opacity-100">
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/25 to-transparent transform -skew-x-12 animate-shine"></div>
</div>
{/* Google Play Logo */}
<svg className="w-6 h-6 relative z-10" viewBox="0 0 24 24" fill="currentColor">
<path d="M3,20.5V3.5C3,2.91 3.34,2.39 3.84,2.15L13.69,12L3.84,21.85C3.34,21.6 3,21.09 3,20.5M16.81,15.12L6.05,21.34L14.54,12.85L16.81,15.12M20.16,10.81C20.5,11.08 20.75,11.5 20.75,12C20.75,12.5 20.53,12.9 20.18,13.18L17.89,14.5L15.39,12L17.89,9.5L20.16,10.81M6.05,2.66L16.81,8.88L14.54,11.15L6.05,2.66Z"/>
</svg>
<div className="text-left relative z-10">
<div className="font-poppins text-xs opacity-80">Get it on</div>
<div className="font-poppins font-semibold">Google Play</div>
</div>
</motion.button>
{/* iOS Download Button */}
<motion.button
className="interactive-button relative flex items-center gap-3 bg-foreground text-background px-6 py-4 rounded-xl font-medium text-base hover:bg-foreground/90 transition-all duration-300 shadow-lg hover:shadow-xl flex-1 overflow-hidden group"
whileHover={{ scale: 1.02, y: -2 }}
whileTap={{ scale: 0.98 }}
transition={{ duration: 0.2 }}
>
{/* Continuous Shine Effect */}
<div className="absolute inset-0 -top-px opacity-100">
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/25 to-transparent transform -skew-x-12 animate-shine"></div>
</div>
{/* Apple Logo */}
<svg className="w-6 h-6 relative z-10" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/>
</svg>
<div className="text-left relative z-10">
<div className="font-poppins text-xs opacity-80">Download on the</div>
<div className="font-poppins font-semibold">App Store</div>
</div>
</motion.button>
</div>
</motion.div>
</div>
{/* Mobile Mockups Section - Ultra-Realistic and Larger */}
<motion.div
className="relative w-full h-[400px] lg:h-[500px] rounded-3xl overflow-hidden shadow-2xl"
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8, ease: "easeOut", delay: 0.2 }}
style={{
background: `url(${imgFrame1597884939}) center/cover`
}}
>
{/* Mobile Mockup 1 - Enhanced Main App Screen */}
<div className="absolute top-[-6rem] lg:top-[-7rem] right-[calc(50%-100px-8.25rem)] lg:right-[calc(50%-100px-10.25rem)]">
<motion.div
className="w-60 lg:w-64 xl:w-72 h-[420px] lg:h-[480px] xl:h-[540px] bg-white rounded-[2.5rem] shadow-2xl border-4 lg:border-8 border-gray-900 relative overflow-hidden"
initial={{ opacity: 0, y: -30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.4 }}
whileHover={{ scale: 1.02, rotateY: 2 }}
>
{/* iPhone Status Bar */}
<div className="flex justify-between items-center px-6 py-3 bg-white">
<div className="flex items-center gap-1">
<span className="text-sm font-semibold text-black">9:41</span>
</div>
<div className="flex items-center gap-1">
{/* Signal bars */}
<div className="flex gap-0.5">
{[1, 2, 3, 4].map((bar) => (
<div key={bar} className={`w-1 bg-black rounded-full ${bar === 1 ? 'h-1' : bar === 2 ? 'h-2' : bar === 3 ? 'h-3' : 'h-4'}`}></div>
))}
</div>
{/* WiFi */}
<svg className="w-4 h-4 ml-1" viewBox="0 0 24 24" fill="currentColor">
<path d="M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.07 2.93 1 9zm8 8l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.87 9.14 5 13z"/>
</svg>
{/* Battery */}
<div className="w-6 h-3 border border-black rounded-sm ml-1">
<div className="w-4 h-1.5 bg-green-500 rounded-sm m-0.5"></div>
</div>
<div className="w-0.5 h-2 bg-black rounded-r-sm"></div>
</div>
</div>
{/* App Header */}
<div className="px-5 py-4 bg-gradient-to-r from-primary to-secondary">
<div className="flex items-center justify-between text-white mb-3">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-white/20 rounded-xl flex items-center justify-center">
<svg className="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
</svg>
</div>
<div>
<h3 className="text-lg font-bold">CityCards</h3>
<div className="text-xs text-white/70">Premium Active</div>
</div>
</div>
<div className="flex items-center gap-3">
<div className="relative">
<Search className="w-5 h-5 text-white/80" />
<div className="absolute -top-1 -right-1 w-2 h-2 bg-orange-400 rounded-full"></div>
</div>
<div className="w-8 h-8 bg-white/20 rounded-full flex items-center justify-center">
<div className="w-4 h-4 bg-white rounded-full"></div>
</div>
</div>
</div>
{/* Location */}
<div className="flex items-center justify-between text-white/90">
<div className="flex items-center gap-2">
<MapPin className="w-4 h-4" />
<span className="text-sm font-medium">Sydney, NSW</span>
<div className="w-1 h-1 bg-white/60 rounded-full"></div>
<span className="text-xs">22°C </span>
</div>
<div className="flex items-center gap-2 text-xs bg-white/20 px-3 py-1.5 rounded-full">
<span className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></span>
<span>3 active</span>
</div>
</div>
</div>
{/* App Content */}
<div className="flex-1 overflow-hidden">
{/* Today's Deal Banner */}
<div className="mx-5 my-4 bg-gradient-to-r from-purple-100 via-blue-100 to-indigo-100 rounded-xl p-4 border border-purple-200">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="text-xs font-bold text-purple-700 bg-purple-200 px-2 py-1 rounded-full">TODAY ONLY</span>
<div className="flex items-center gap-1 text-xs text-purple-600">
<Clock className="w-3 h-3" />
<span>8h left</span>
</div>
</div>
<Heart className="w-4 h-4 text-red-400" />
</div>
<h4 className="font-bold text-gray-900 text-sm mb-1">Sydney Explorer Pass</h4>
<p className="text-xs text-gray-700 mb-3">Visit 3+ attractions & get 40% off</p>
<div className="flex gap-2">
<button className="bg-purple-600 text-white px-3 py-1.5 rounded-lg text-xs font-semibold">
Activate
</button>
<button className="border border-purple-300 text-purple-700 px-3 py-1.5 rounded-lg text-xs font-medium">
Details
</button>
</div>
</div>
{/* Stats Bar */}
<div className="mx-5 mb-4">
<div className="bg-gray-50 rounded-xl p-3 border border-gray-100">
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2">
<div className="w-5 h-5 bg-green-100 rounded-full flex items-center justify-center">
<span className="text-xs font-bold text-green-700">3</span>
</div>
<span className="text-gray-700 text-xs">Passes used today</span>
</div>
<div className="text-right">
<div className="font-bold text-green-600 text-sm">$127</div>
<div className="text-xs text-gray-500">saved</div>
</div>
</div>
</div>
</div>
{/* Attraction Cards */}
<div className="mx-5 space-y-3">
{/* Opera House */}
<div className="bg-white border border-gray-200 rounded-xl p-3 shadow-sm relative">
<div className="absolute top-2 right-2 bg-yellow-100 text-yellow-600 text-xs font-bold px-2 py-1 rounded-full">
PREMIUM
</div>
<div className="flex gap-3 mb-2">
<div className="w-12 h-12 bg-gradient-to-br from-blue-500 to-purple-600 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.94-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>
</svg>
</div>
<div className="flex-1">
<div className="flex items-start justify-between">
<div>
<h4 className="font-bold text-gray-900 text-sm">Sydney Opera House</h4>
<div className="flex items-center gap-1 text-xs text-gray-600 mt-0.5">
<Star className="w-3 h-3 text-yellow-400 fill-current" />
<span className="font-medium">4.8</span>
<span className="text-gray-400">(2.4k)</span>
<span className="mx-1"></span>
<MapPin className="w-3 h-3 text-blue-500" />
<span>2.3km</span>
</div>
</div>
<div className="flex gap-1">
<Heart className="w-3 h-3 text-gray-400" />
<Share2 className="w-3 h-3 text-gray-400" />
</div>
</div>
<div className="flex items-center gap-1.5 mt-2 text-xs">
<div className="bg-orange-100 text-orange-700 px-2 py-0.5 rounded-full font-medium">
Architecture
</div>
<div className="bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full font-medium">
Cultural
</div>
</div>
</div>
</div>
<div className="flex items-center justify-between pt-2 border-t border-gray-100">
<div className="flex items-center gap-2">
<div>
<div className="flex items-center gap-1">
<span className="text-base font-bold text-gray-900">$45</span>
<span className="text-xs text-gray-500 line-through">$75</span>
</div>
<div className="text-xs text-green-600 font-medium">Save $30</div>
</div>
<div className="text-xs bg-green-100 text-green-700 px-2 py-1 rounded-full border border-green-200">
40% OFF
</div>
</div>
<button className="bg-primary text-white px-3 py-1.5 rounded-lg font-semibold text-xs">
Use Pass
</button>
</div>
</div>
{/* Harbour Bridge */}
<div className="bg-white border border-gray-200 rounded-xl p-3 shadow-sm relative">
<div className="absolute top-2 right-2 bg-blue-100 text-blue-600 text-xs font-bold px-2 py-1 rounded-full">
POPULAR
</div>
<div className="flex gap-3 mb-2">
<div className="w-12 h-12 bg-gradient-to-br from-emerald-500 to-green-600 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
</div>
<div className="flex-1">
<div className="flex items-start justify-between">
<div>
<h4 className="font-bold text-gray-900 text-sm">Harbour Bridge Climb</h4>
<div className="flex items-center gap-1 text-xs text-gray-600 mt-0.5">
<Star className="w-3 h-3 text-yellow-400 fill-current" />
<span className="font-medium">4.9</span>
<span className="text-gray-400">(1.8k)</span>
<span className="mx-1"></span>
<MapPin className="w-3 h-3 text-blue-500" />
<span>1.8km</span>
</div>
</div>
<div className="flex gap-1">
<Heart className="w-3 h-3 text-red-400 fill-current" />
<Share2 className="w-3 h-3 text-gray-400" />
</div>
</div>
<div className="flex items-center gap-1.5 mt-2 text-xs">
<div className="bg-green-100 text-green-700 px-2 py-0.5 rounded-full font-medium">
Adventure
</div>
<div className="bg-red-100 text-red-700 px-2 py-0.5 rounded-full font-medium">
Iconic
</div>
</div>
</div>
</div>
<div className="flex items-center justify-between pt-2 border-t border-gray-100">
<div className="flex items-center gap-2">
<div>
<div className="flex items-center gap-1">
<span className="text-base font-bold text-gray-900">$159</span>
<span className="text-xs text-gray-500 line-through">$289</span>
</div>
<div className="text-xs text-green-600 font-medium">Save $130</div>
</div>
<div className="text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded-full border border-blue-200">
45% OFF
</div>
</div>
<button className="bg-primary text-white px-3 py-1.5 rounded-lg font-semibold text-xs">
Use Pass
</button>
</div>
</div>
{/* Botanic Gardens */}
<div className="bg-white border border-gray-200 rounded-xl p-3 shadow-sm relative">
<div className="absolute top-2 right-2 bg-emerald-100 text-emerald-600 text-xs font-bold px-2 py-1 rounded-full">
FREE
</div>
<div className="flex gap-3 mb-2">
<div className="w-12 h-12 bg-gradient-to-br from-green-400 to-emerald-500 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M17,8C8,10 5.9,16.17 3.82,21.34L5.71,22L6.66,19.7C7.14,19.87 7.64,20 8,20C19,20 22,3 22,3C21,5 14,5.25 9,6.25C4,7.25 2,11.5 2,13.5C2,15.5 3.75,17.25 3.75,17.25C7,8 17,8 17,8Z"/>
</svg>
</div>
<div className="flex-1">
<div className="flex items-start justify-between">
<div>
<h4 className="font-bold text-gray-900 text-sm">Royal Botanic Gardens</h4>
<div className="flex items-center gap-1 text-xs text-gray-600 mt-0.5">
<Star className="w-3 h-3 text-yellow-400 fill-current" />
<span className="font-medium">4.7</span>
<span className="text-gray-400">(956)</span>
<span className="mx-1"></span>
<MapPin className="w-3 h-3 text-blue-500" />
<span>1.2km</span>
</div>
</div>
<div className="flex gap-1">
<Heart className="w-3 h-3 text-gray-400" />
<Share2 className="w-3 h-3 text-gray-400" />
</div>
</div>
<div className="flex items-center gap-1.5 mt-2 text-xs">
<div className="bg-green-100 text-green-700 px-2 py-0.5 rounded-full font-medium">
Nature
</div>
<div className="bg-purple-100 text-purple-700 px-2 py-0.5 rounded-full font-medium">
Family
</div>
</div>
</div>
</div>
<div className="flex items-center justify-between pt-2 border-t border-gray-100">
<div className="flex items-center gap-2">
<div>
<span className="text-base font-bold text-green-600">FREE</span>
<div className="text-xs text-green-600 font-medium">Included</div>
</div>
</div>
<button className="bg-green-600 text-white px-3 py-1.5 rounded-lg font-semibold text-xs">
Visit Now
</button>
</div>
</div>
</div>
</div>
{/* Bottom Navigation */}
<div className="border-t border-gray-100 bg-white/95 backdrop-blur-sm">
<div className="flex items-center justify-around py-3">
{[
{
icon: (
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
</svg>
),
label: "Home",
active: true,
badge: null
},
{
icon: (
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/>
</svg>
),
label: "Map",
active: false,
badge: "3"
},
{
icon: (
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M22 10v6c0 1.11-.89 2-2 2H4c-1.11 0-2-.89-2-2v-8c0-1.11.89-2 2-2h14l2-2v6zm-9 4.5c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5z"/>
</svg>
),
label: "Passes",
active: false,
badge: null
},
{
icon: (
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/>
</svg>
),
label: "Activity",
active: false,
badge: null
},
{
icon: (
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
</svg>
),
label: "Profile",
active: false,
badge: null
}
].map((item, index) => (
<div key={index} className={`flex flex-col items-center justify-center gap-1 py-1 relative ${item.active ? 'text-primary' : 'text-gray-500'}`}>
<div className="relative">
{item.icon}
{item.badge && (
<div className="absolute -top-2 -right-2 bg-red-500 text-white text-xs rounded-full min-w-[16px] h-4 flex items-center justify-center px-1">
{item.badge}
</div>
)}
</div>
<span className={`text-xs ${item.active ? 'font-semibold' : 'font-medium'}`}>
{item.label}
</span>
{item.active && (
<div className="absolute bottom-0 left-1/2 transform -translate-x-1/2 w-4 h-0.5 bg-primary rounded-full"></div>
)}
</div>
))}
</div>
</div>
</motion.div>
</div>
{/* Mobile Mockup 2 - Map View */}
<div className="absolute bottom-[-6rem] lg:bottom-[-7rem] left-[calc(50%-100px-8.25rem)] lg:left-[calc(50%-100px-10.25rem)]">
<motion.div
className="w-56 lg:w-60 xl:w-64 h-[380px] lg:h-[440px] xl:h-[480px] bg-white rounded-[2.5rem] shadow-2xl border-4 lg:border-8 border-gray-900 relative overflow-hidden"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.6 }}
whileHover={{ scale: 1.02, rotateY: -2 }}
>
{/* Status Bar */}
<div className="flex justify-between items-center px-6 py-3 bg-white">
<div className="flex items-center gap-1">
<span className="text-sm font-semibold text-black">9:41</span>
</div>
<div className="flex items-center gap-1">
<div className="flex gap-0.5">
{[1, 2, 3, 4].map((bar) => (
<div key={bar} className={`w-1 bg-black rounded-full ${bar === 1 ? 'h-1' : bar === 2 ? 'h-2' : bar === 3 ? 'h-3' : 'h-4'}`}></div>
))}
</div>
<svg className="w-4 h-4 ml-1" viewBox="0 0 24 24" fill="currentColor">
<path d="M1 9l2 2c4.97-4.97 13.03-4.97 18 0l2-2C16.93 2.93 7.07 2.93 1 9zm8 8l3 3 3-3c-1.65-1.66-4.34-1.66-6 0zm-4-4l2 2c2.76-2.76 7.24-2.76 10 0l2-2C15.14 9.14 8.87 9.14 5 13z"/>
</svg>
<div className="w-6 h-3 border border-black rounded-sm ml-1">
<div className="w-4 h-1.5 bg-green-500 rounded-sm m-0.5"></div>
</div>
<div className="w-0.5 h-2 bg-black rounded-r-sm"></div>
</div>
</div>
{/* Map Header */}
<div className="px-5 py-3 bg-white border-b border-gray-100">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<button className="w-8 h-8 bg-gray-100 rounded-lg flex items-center justify-center">
<svg className="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</button>
<div>
<h3 className="font-bold text-gray-900">Explore Sydney</h3>
<div className="text-xs text-gray-500">3 nearby attractions</div>
</div>
</div>
<button className="w-8 h-8 bg-primary/10 rounded-lg flex items-center justify-center">
<Filter className="w-4 h-4 text-primary" />
</button>
</div>
</div>
{/* Map Content */}
<div className="flex-1 bg-gradient-to-br from-green-100 to-blue-100 relative overflow-hidden">
{/* Simplified Map Pattern */}
<div className="absolute inset-0 opacity-20">
<div className="w-full h-full bg-gradient-to-br from-blue-200 via-green-200 to-yellow-200"></div>
{/* Street lines */}
<div className="absolute top-1/4 left-0 right-0 h-px bg-gray-400 opacity-30"></div>
<div className="absolute top-1/2 left-0 right-0 h-px bg-gray-400 opacity-40"></div>
<div className="absolute top-3/4 left-0 right-0 h-px bg-gray-400 opacity-30"></div>
<div className="absolute left-1/4 top-0 bottom-0 w-px bg-gray-400 opacity-30"></div>
<div className="absolute left-1/2 top-0 bottom-0 w-px bg-gray-400 opacity-40"></div>
<div className="absolute left-3/4 top-0 bottom-0 w-px bg-gray-400 opacity-30"></div>
</div>
{/* Location Pins */}
<div className="absolute top-8 left-8 w-3 h-3 bg-red-500 rounded-full animate-pulse"></div>
<div className="absolute top-16 right-12 w-3 h-3 bg-blue-500 rounded-full animate-pulse"></div>
<div className="absolute bottom-20 left-12 w-3 h-3 bg-green-500 rounded-full animate-pulse"></div>
<div className="absolute bottom-12 right-8 w-3 h-3 bg-purple-500 rounded-full animate-pulse"></div>
{/* Current Location */}
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
<div className="relative">
<div className="w-6 h-6 bg-blue-600 rounded-full border-4 border-white shadow-lg animate-pulse"></div>
<div className="absolute inset-0 w-6 h-6 bg-blue-600 rounded-full animate-ping opacity-20"></div>
</div>
</div>
{/* Route Line */}
<svg className="absolute inset-0 w-full h-full" viewBox="0 0 100 100" preserveAspectRatio="none">
<path
d="M20,30 Q40,20 60,40 T80,70"
stroke="#6366f1"
strokeWidth="2"
fill="none"
strokeDasharray="5,5"
className="animate-pulse"
/>
</svg>
</div>
{/* Bottom Card */}
<div className="absolute bottom-4 left-4 right-4 bg-white rounded-xl p-3 shadow-lg border border-gray-100">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-gradient-to-br from-blue-500 to-purple-600 rounded-lg flex items-center justify-center">
<svg className="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.94-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>
</svg>
</div>
<div className="flex-1">
<h4 className="font-bold text-gray-900 text-sm">Sydney Opera House</h4>
<div className="flex items-center gap-1 text-xs text-gray-600">
<MapPin className="w-3 h-3 text-blue-500" />
<span>2.3km away 5 min walk</span>
</div>
</div>
<button className="bg-primary text-white px-3 py-1.5 rounded-lg font-semibold text-xs">
Go
</button>
</div>
</div>
</motion.div>
</div>
</motion.div>
</motion.div>
</div>
</section>
);
}

666
src/components/Navbar.tsx Normal file
View File

@@ -0,0 +1,666 @@
import { useState, useEffect, useRef, forwardRef } from 'react';
import { Menu, X, ShoppingBag, ChevronDown, Globe, User, Settings, LogOut } from 'lucide-react';
import { motion, AnimatePresence } from 'motion/react';
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';
interface NavbarProps {
activeCity: string;
onCityChange: (city: string) => void;
onSignInClick: () => void;
onSignOutClick?: () => void;
onPassesClick: () => void;
onCheckoutClick?: () => void;
onHomeClick?: () => void;
onAttractionsClick?: () => void;
onBlogsClick?: () => void;
onHowItWorksClick?: () => void;
onFAQClick?: () => void;
onPrivacyPolicyClick?: () => void;
onAboutUsClick?: () => void;
onProfileClick?: () => void;
onCityCardsClick?: () => void;
onMagicItineraryClick?: () => void;
onPostCardsClick?: () => void;
onOffersClick?: () => void;
onEsimsClick?: () => void;
onHotelDiscountsClick?: () => void;
currentPage?: 'home' | 'signin' | 'passes' | 'attractions' | 'checkout' | 'blogs' | 'how-it-works' | 'faq' | 'privacy-policy' | 'about-us' | 'melbourne' | 'profile' | 'citycards' | 'magic-itinerary' | 'postcards' | 'offers' | 'esims' | 'hotel-discounts';
isUserSignedIn?: boolean;
user?: { email: string; name: string } | null;
}
interface DropdownItem {
id: string;
label: string;
icon?: React.ReactNode;
action?: () => void;
badge?: string | number;
}
interface CartItem {
id: string;
name: string;
price: string;
image?: string;
quantity: number;
}
interface DropdownProps {
isOpen: boolean;
onToggle: () => void;
items: DropdownItem[];
trigger: React.ReactNode;
title?: string;
className?: string;
}
export default function Navbar({
activeCity,
onCityChange,
onSignInClick,
onSignOutClick,
onPassesClick,
onCheckoutClick,
onHomeClick,
onAttractionsClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onProfileClick,
onCityCardsClick,
onMagicItineraryClick,
onPostCardsClick,
onOffersClick,
onEsimsClick,
onHotelDiscountsClick,
currentPage = 'home',
isUserSignedIn = false,
user
}: NavbarProps) {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [isScrolled, setIsScrolled] = useState(false);
const [activeLanguageDropdown, setActiveLanguageDropdown] = useState(false);
const [activeCartDropdown, setActiveCartDropdown] = useState(false);
const [activeUserDropdown, setActiveUserDropdown] = useState(false);
const [activeProductsDropdown, setActiveProductsDropdown] = useState(false);
const languageRef = useRef<HTMLDivElement>(null);
const cartRef = useRef<HTMLDivElement>(null);
const userRef = useRef<HTMLDivElement>(null);
const productsRef = useRef<HTMLDivElement>(null);
// Languages available
const languages: DropdownItem[] = [
{ id: 'en', label: 'English', icon: <span className="text-base">🇺🇸</span> },
{ id: 'es', label: 'Español', icon: <span className="text-base">🇪🇸</span> },
{ id: 'fr', label: 'Français', icon: <span className="text-base">🇫🇷</span> },
{ id: 'it', label: 'Italiano', icon: <span className="text-base">🇮🇹</span> },
];
// Products dropdown items
const productsItems: DropdownItem[] = [
{
id: 'citycards',
label: 'CityCards',
action: onCityCardsClick
},
{
id: 'magic-itinerary',
label: 'Magic Itinerary',
action: onMagicItineraryClick
},
{
id: 'postcards',
label: 'Post Cards',
action: onPostCardsClick
},
{
id: 'offers',
label: 'Offers',
action: onOffersClick
},
{
id: 'esims',
label: 'eSIMs',
action: onEsimsClick
}
];
// Mock cart items
const cartItems: CartItem[] = [
{ id: '1', name: 'Sydney 2-Day Pass', price: '$89', quantity: 1 },
{ id: '2', name: 'Melbourne Premium Pass', price: '$129', quantity: 1 },
];
// Section IDs for navigation
const sectionIds = [
'hero-section',
'why-choose-section',
'variety-adventures-section',
'how-it-works-section',
'magic-itinerary-section',
'book-attraction-section',
'custom-postcards-section',
'upcoming-cities-section',
'trust-section',
'mobile-app-section'
];
const scrollToSection = (index: number) => {
const sectionId = sectionIds[index];
const element = document.getElementById(sectionId);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
setIsMobileMenuOpen(false);
};
const closeMobileMenu = () => {
setIsMobileMenuOpen(false);
};
// Detect scroll for navbar styling
useEffect(() => {
const handleScroll = () => {
const scrolled = window.scrollY > 20;
setIsScrolled(scrolled);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
// Close dropdowns when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
// Add a small delay to prevent immediate closure when toggle is clicked
setTimeout(() => {
if (languageRef.current && !languageRef.current.contains(event.target as Node)) {
setActiveLanguageDropdown(false);
}
if (cartRef.current && !cartRef.current.contains(event.target as Node)) {
setActiveCartDropdown(false);
}
if (userRef.current && !userRef.current.contains(event.target as Node)) {
setActiveUserDropdown(false);
}
if (productsRef.current && !productsRef.current.contains(event.target as Node)) {
setActiveProductsDropdown(false);
}
}, 10);
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const handleNavClick = (action: string) => {
switch (action) {
case 'about':
onAboutUsClick?.();
break;
case 'attractions':
onAttractionsClick?.();
break;
case 'card':
onPassesClick?.();
break;
default:
break;
}
closeMobileMenu();
};
const isNavItemActive = (action: string) => {
if (action === 'about') {
return currentPage === 'about-us';
}
return currentPage === action;
};
// Calculate cart total
const cartTotal = cartItems.reduce((total, item) => {
const price = parseFloat(item.price.replace('$', ''));
return total + (price * item.quantity);
}, 0);
// Dropdown component with proper ref forwarding and glassmorphism
const Dropdown = forwardRef<HTMLDivElement, DropdownProps>(({
isOpen,
onToggle,
items,
trigger,
title,
className = ""
}, ref) => (
<div ref={ref} className={`relative ${className}`} style={{ height: 'auto', minHeight: 'auto' }}>
<motion.button
onClick={onToggle}
className="relative"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
{trigger}
</motion.button>
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, y: 10, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 10, scale: 0.95 }}
transition={{ duration: 0.2, ease: [0.25, 0.1, 0.25, 1] }}
className="absolute top-full right-0 mt-2 bg-white/95 backdrop-blur-xl rounded-2xl shadow-xl border border-white/20 min-w-[280px] max-w-[320px] overflow-hidden z-50"
style={{
position: 'absolute',
top: '100%',
right: '0',
marginTop: '0.5rem'
}}
>
{title && (
<div className="px-5 py-4 border-b border-gray-100/50">
<h3 className="font-merchant font-semibold text-gray-900 text-base">{title}</h3>
</div>
)}
<div className="py-3 px-2">
{items.map((item, index) => (
<motion.button
key={item.id}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
// Handle action first
if (item.action && item.id !== 'total') {
item.action();
}
// Only close dropdown for actionable items (checkout and regular cart items)
if (item.id === 'checkout' || (item.id !== 'total' && !item.action)) {
setTimeout(() => onToggle(), 100);
}
}}
className={`w-full flex items-center justify-between text-left transition-colors duration-200 ${
item.id === 'checkout'
? 'bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-medium rounded-lg px-4 py-2.5 mb-1 mt-2'
: item.id === 'total'
? 'cursor-default font-semibold border-t border-gray-100/50 pt-4 px-3 py-2'
: 'hover:bg-gray-50/80 px-3 py-2.5 rounded-md'
}`}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.05 }}
disabled={item.id === 'total'}
>
<div className="flex items-center space-x-3">
{item.icon}
<span className={`text-sm font-medium ${
item.id === 'checkout' ? 'text-white' :
item.id === 'total' ? 'text-gray-900' : 'text-gray-700'
}`}>{item.label}</span>
</div>
{item.badge && (
<span className="bg-primary text-primary-foreground text-xs px-2 py-1 rounded-full">
{item.badge}
</span>
)}
</motion.button>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
));
// Set display name for debugging
Dropdown.displayName = 'Dropdown';
return (
<>
{/* Desktop Navbar - Enhanced Glassmorphism */}
<motion.nav
className="fixed top-6 left-0 right-0 z-50 hidden lg:block"
initial={{ y: -100, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.6, ease: [0.25, 0.1, 0.25, 1], delay: 0.2 }}
>
<div className="container mx-auto px-4">
<motion.div
className={`w-full transition-all duration-500 ease-out rounded-full px-8 py-4 bg-white backdrop-blur-[20px] border border-white/20 ${
isScrolled
? 'shadow-[0_10px_15px_-3px_rgba(0,0,0,0.08),0_4px_6px_-2px_rgba(0,0,0,0.05)]'
: 'shadow-lg shadow-black/5'
}`}
initial={{ scale: 0.95, opacity: 0, y: 0 }}
animate={{
scale: isScrolled ? 0.98 : 1,
opacity: 1,
y: isScrolled ? 2 : 0
}}
transition={{ duration: 0.3, ease: "easeOut" }}
>
<div className="flex items-center justify-between">
{/* Logo */}
<motion.div
className="flex items-center cursor-pointer flex-shrink-0"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => onHomeClick?.()}
>
<ImageWithFallback
src={logoImage}
alt="CityCards Logo"
className="h-10 w-auto"
/>
</motion.div>
{/* Navigation Links - Centered */}
<div className="absolute left-1/2 -translate-x-1/2 flex items-center gap-[51px]">
{[
{ label: 'About Us', action: 'about' },
{ label: 'Your Card', action: 'card' }
].map((item) => (
<motion.button
key={item.action}
onClick={() => handleNavClick(item.action)}
className={`relative px-0 py-2 text-base font-medium transition-all duration-200 whitespace-nowrap group capitalize ${
isNavItemActive(item.action)
? 'text-primary'
: 'text-gray-700 hover:text-gray-900'
}`}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
{item.label}
{/* Active indicator */}
<motion.div
className="absolute bottom-0 left-0 h-0.5 bg-gradient-to-r from-primary to-secondary rounded-full"
initial={{ width: 0 }}
animate={{
width: isNavItemActive(item.action) ? "100%" : 0,
opacity: isNavItemActive(item.action) ? 1 : 0
}}
whileHover={{
width: "100%",
opacity: 1
}}
transition={{ duration: 0.2 }}
/>
{/* Hover background */}
<motion.div
className="absolute inset-0 bg-gray-100/50 backdrop-blur-sm rounded-lg -z-10"
initial={{ scale: 0, opacity: 0 }}
whileHover={{ scale: 1, opacity: 0.5 }}
transition={{ duration: 0.2 }}
/>
</motion.button>
))}
{/* Our Products Dropdown */}
<button
onClick={onCityCardsClick}
className="flex items-center text-gray-700 hover:text-gray-900 text-base font-medium transition-colors duration-200 cursor-pointer rounded-lg hover:bg-gray-50/50 px-2 py-1"
>
<span>Our Products</span>
</button>
</div>
{/* Right Section */}
<div className="flex items-center gap-5">
{/* Language Dropdown */}
<Dropdown
ref={languageRef}
isOpen={activeLanguageDropdown}
onToggle={() => setActiveLanguageDropdown(!activeLanguageDropdown)}
items={languages}
title="Select Language"
trigger={
<div className="flex items-center space-x-1.5 text-gray-700 hover:text-gray-900 text-base font-medium transition-colors duration-200 cursor-pointer rounded-lg hover:bg-gray-50/50 uppercase px-2 py-1">
<Globe className="w-4 h-4" />
<span>ENG</span>
<ChevronDown className={`w-3.5 h-3.5 transition-transform duration-200 ${activeLanguageDropdown ? 'rotate-180' : ''}`} />
</div>
}
/>
{/* Shopping Cart */}
<Dropdown
ref={cartRef}
isOpen={activeCartDropdown}
onToggle={() => setActiveCartDropdown(prev => !prev)}
items={[
...cartItems.map(item => ({
id: item.id,
label: `${item.name} - ${item.price}`,
badge: `${item.quantity}x`
})),
{
id: 'total',
label: `Total: ${cartTotal.toFixed(2)}`,
icon: <ShoppingBag className="w-4 h-4" />
},
{
id: 'checkout',
label: 'Proceed to Checkout',
action: onCheckoutClick
}
]}
title="Shopping Cart"
trigger={
<div className="relative text-gray-700 hover:text-gray-900 transition-colors duration-200 rounded-lg hover:bg-gray-50/50 cursor-pointer p-2">
<ShoppingBag className="w-6 h-6" />
<motion.div
className="absolute -top-1 -right-1 w-5 h-5 bg-primary rounded-full flex items-center justify-center"
animate={{ scale: [1, 1.1, 1] }}
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
>
<span className="text-xs text-primary-foreground font-bold">{cartItems.length}</span>
</motion.div>
</div>
}
/>
{/* City Card Button */}
<div className="flex items-center gap-3">
<div className="relative">
<CTAButton
user={user}
onClick={user ? () => setActiveUserDropdown(prev => !prev) : (onSignInClick || (() => {}))}
className="hover:scale-105 transition-transform duration-200"
/>
{/* User Profile Dropdown attached to CTA Button */}
{isUserSignedIn && user && (
<Dropdown
ref={userRef}
isOpen={activeUserDropdown}
onToggle={() => setActiveUserDropdown(prev => !prev)}
items={[
{
id: 'profile',
label: 'My Profile',
icon: <User className="w-4 h-4" />,
action: onProfileClick
},
{
id: 'settings',
label: 'Settings',
icon: <Settings className="w-4 h-4" />
},
{
id: 'logout',
label: 'Sign Out',
icon: <LogOut className="w-4 h-4" />,
action: onSignOutClick
}
]}
title="Account"
trigger={null}
/>
)}
</div>
</div>
</div>
</div>
</motion.div>
</div>
</motion.nav>
{/* Mobile Navbar - Enhanced Glassmorphism */}
<nav className="fixed top-0 w-full z-50 lg:hidden">
<div className={`transition-all duration-500 ease-out border-b shadow-sm ${
isScrolled
? 'bg-white/85 backdrop-blur-2xl border-white/40 shadow-black/5'
: 'bg-white/70 backdrop-blur-3xl border-white/50 shadow-black/10'
}`}>
<div className="container mx-auto px-4">
<div className="flex justify-between items-center h-16">
{/* Mobile Logo */}
<motion.div
className="flex items-center cursor-pointer"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => onHomeClick?.()}
>
<ImageWithFallback
src={logoImage}
alt="CityCards Logo"
className="h-10 w-auto"
/>
</motion.div>
{/* Mobile Actions */}
<div className="flex items-center space-x-2">
{/* Mobile Cart */}
<motion.button
className="relative text-gray-700 hover:text-gray-900 p-2 transition-colors duration-200 rounded-lg hover:bg-gray-50/50"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={() => onCheckoutClick?.()}
>
<ShoppingBag className="w-6 h-6" />
<div className="absolute -top-1 -right-1 w-5 h-5 bg-primary rounded-full flex items-center justify-center">
<span className="text-xs text-primary-foreground font-bold">{cartItems.length}</span>
</div>
</motion.button>
{/* Mobile menu button */}
<motion.button
onClick={() => setIsMobileMenuOpen(true)}
className="inline-flex items-center justify-center p-2 rounded-lg text-gray-700 hover:text-gray-900 hover:bg-gray-100/50 transition-colors duration-200"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Menu className="h-6 w-6" />
</motion.button>
</div>
</div>
</div>
</div>
</nav>
{/* Mobile Menu Overlay - Enhanced Glassmorphism */}
<AnimatePresence>
{isMobileMenuOpen && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
className="fixed inset-0 bg-black/70 backdrop-blur-lg z-50 lg:hidden"
onClick={closeMobileMenu}
/>
<motion.div
initial={{ x: '100%' }}
animate={{ x: 0 }}
exit={{ x: '100%' }}
transition={{ duration: 0.3, ease: [0.25, 0.1, 0.25, 1] }}
className="fixed top-0 right-0 h-full w-80 bg-white/95 backdrop-blur-2xl shadow-2xl z-50 lg:hidden overflow-y-auto"
>
<div className="flex items-center justify-between p-6 border-b border-gray-200/50">
<h2 className="text-xl font-semibold text-gray-900">Menu</h2>
<motion.button
onClick={closeMobileMenu}
className="p-2 rounded-lg text-gray-500 hover:text-gray-700 hover:bg-gray-100/50 transition-colors duration-200"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<X className="h-6 w-6" />
</motion.button>
</div>
<div className="p-6 space-y-6">
{/* Mobile Navigation Links */}
<div className="space-y-4">
{[
{ label: 'About Us', action: 'about' },
{ label: 'Cities', action: 'cities' },
{ label: 'Attractions', action: 'attractions' },
{ label: 'Your Card', action: 'card' },
{ label: 'Deals', action: 'offer' }
].map((item) => (
<motion.button
key={item.action}
onClick={() => handleNavClick(item.action)}
className={`w-full flex items-center justify-between py-3 px-4 rounded-lg text-left transition-colors duration-200 ${
isNavItemActive(item.action)
? 'bg-primary/10 text-primary font-medium'
: 'text-gray-700 hover:bg-gray-100/70 hover:text-gray-900'
}`}
whileHover={{ x: 4 }}
whileTap={{ scale: 0.98 }}
>
<span className="text-base">{item.label}</span>
</motion.button>
))}
</div>
{/* Mobile Our Products Section */}
<div className="space-y-4">
<h3 className="text-lg font-semibold text-gray-900 px-4">Our Products</h3>
{productsItems.map((item) => (
<motion.button
key={item.id}
onClick={() => {
item.action?.();
closeMobileMenu();
}}
className="w-full flex items-center justify-between py-3 px-4 rounded-lg text-left transition-colors duration-200 text-gray-700 hover:bg-gray-100/70 hover:text-gray-900"
whileHover={{ x: 4 }}
whileTap={{ scale: 0.98 }}
>
<span className="text-base">{item.label}</span>
</motion.button>
))}
</div>
{/* Mobile CTA Button */}
<Button
onClick={() => {
onSignInClick();
closeMobileMenu();
}}
className="w-full bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-medium py-3"
>
GET A CITY CARD
</Button>
</div>
</motion.div>
</>
)}
</AnimatePresence>
</>
);
}

View File

@@ -0,0 +1,181 @@
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Mail } from 'lucide-react';
import { motion } from 'motion/react';
import { ImageWithFallback } from './figma/ImageWithFallback';
export function NewsletterSection() {
return (
<section className="relative min-h-screen overflow-hidden bg-gradient-to-br from-primary/5 via-white to-secondary/5">
{/* Background Pattern */}
<div className="absolute inset-0 opacity-30">
<div className="absolute top-20 left-20 w-32 h-32 bg-primary/10 rounded-full blur-3xl animate-float"></div>
<div className="absolute bottom-32 right-16 w-40 h-40 bg-secondary/10 rounded-full blur-3xl animate-float animate-delay-1000"></div>
<div className="absolute top-1/2 left-1/4 w-24 h-24 bg-primary/5 rounded-full blur-2xl"></div>
<div className="absolute bottom-1/4 right-1/3 w-36 h-36 bg-secondary/5 rounded-full blur-3xl"></div>
</div>
{/* Subtle Grid Pattern */}
<div className="absolute inset-0 opacity-5">
<div className="w-full h-1/2" style={{
backgroundImage: `
linear-gradient(rgba(99, 102, 241, 0.1) 1px, transparent 1px),
linear-gradient(90deg, rgba(99, 102, 241, 0.1) 1px, transparent 1px)
`,
backgroundSize: '50px 50px'
}}></div>
</div>
{/* Floating Email Icons */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<motion.div
className="absolute top-1/4 left-10 text-primary/20"
animate={{
y: [0, -20, 0],
rotate: [0, 5, 0]
}}
transition={{
duration: 6,
repeat: Infinity,
ease: "easeInOut"
}}
>
<Mail className="w-8 h-8" />
</motion.div>
<motion.div
className="absolute top-1/3 right-16 text-secondary/20"
animate={{
y: [0, 15, 0],
rotate: [0, -5, 0]
}}
transition={{
duration: 8,
repeat: Infinity,
ease: "easeInOut",
delay: 2
}}
>
<Mail className="w-6 h-6" />
</motion.div>
<motion.div
className="absolute bottom-1/3 left-1/4 text-primary/15"
animate={{
y: [0, -10, 0],
rotate: [0, 3, 0]
}}
transition={{
duration: 7,
repeat: Infinity,
ease: "easeInOut",
delay: 4
}}
>
<Mail className="w-5 h-5" />
</motion.div>
</div>
{/* Main Content */}
<div className="relative z-10 py-20 lg:py-28 text-center space-y-8 px-4">
{/* Main Heading with Typography Guidelines */}
<motion.div
className="space-y-4"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, ease: [0.25, 0.1, 0.25, 1] }}
>
<h2 className="font-merchant text-5xl md:text-6xl lg:text-7xl tracking-tight leading-tight">
<div className="font-light">Get</div>
<div>
<span className="font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent italic">
travel tips
</span>
<span className="font-light"> &</span>
</div>
<div className="font-semibold">exclusive offers.</div>
</h2>
<motion.p
className="font-poppins text-lg lg:text-xl text-gray-600 max-w-2xl mx-auto leading-relaxed font-normal"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.2 }}
>
We recommend you to subscribe, drop your email below to get daily update about us
</motion.p>
</motion.div>
{/* Newsletter Subscription Interface */}
<motion.div
className="max-w-2xl mx-auto space-y-6"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.3 }}
>
{/* Email Subscription Bar */}
<div className="relative">
<div className="flex items-center space-x-3 bg-white rounded-full p-2 shadow-xl border border-gray-100/50">
<div className="flex-1 relative">
<Mail className="absolute left-4 top-1/2 transform -translate-y-1/2 text-gray-400 w-5 h-5" />
<Input
type="email"
placeholder="Enter your email address"
className="pl-12 pr-4 h-14 text-base border-0 focus:ring-0 focus:border-0 bg-transparent"
/>
</div>
<motion.div
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<Button className="bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 py-6 px-12 rounded-full text-lg shadow-lg">
Subscribe Now
</Button>
</motion.div>
</div>
</div>
{/* Trust Indicators */}
<motion.div
className="flex flex-wrap justify-center items-center gap-4 text-sm text-gray-500"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.5, delay: 0.5 }}
>
<div className="flex items-center space-x-2">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span>No spam, unsubscribe anytime</span>
</div>
<div className="hidden md:block"></div>
<div className="flex items-center space-x-2">
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
<span>Read our Privacy Policy</span>
</div>
<div className="hidden md:block"></div>
<div className="flex items-center space-x-2">
<div className="w-2 h-2 bg-purple-500 rounded-full"></div>
<span>Join 10,000+ subscribers</span>
</div>
</motion.div>
{/* Additional Info */}
<motion.p
className="text-sm text-gray-500 mt-4"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.4, delay: 0.8 }}
>
Get exclusive travel tips, destination guides, and special offers delivered to your inbox
</motion.p>
</motion.div>
</div>
{/* Bottom Decorative Elements */}
<div className="absolute bottom-0 left-0 right-0 h-32 bg-gradient-to-t from-white/50 to-transparent pointer-events-none"></div>
</section>
);
}

View File

@@ -0,0 +1,853 @@
import { useState } from 'react';
import { motion } from 'motion/react';
import { ArrowLeft, Search, Filter, Star, MapPin, Clock, Tag, Heart, Share2, Copy, ChevronDown, ChevronRight, Check, Hotel, Plane, Building2, MapPinned, Home } from 'lucide-react';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
import { Badge } from './ui/badge';
import { Separator } from './ui/separator';
import { Checkbox } from './ui/checkbox';
import Navbar from './Navbar';
import SubNavbar from './SubNavbar';
import { Footer } from './Footer';
import { ImageWithFallback } from './figma/ImageWithFallback';
import { TrustSection } from './TrustSection';
import { MobileAppSection } from './MobileAppSection';
import { ReviewsSection } from './ReviewsSection';
import { TrustedCompanies } from './TrustedCompanies';
interface OffersPageProps {
onBackClick: () => void;
onHomeClick: () => void;
onMelbourneClick: () => void;
onPassesClick: () => void;
onCheckoutClick: () => void;
onSignInClick: () => void;
onSignOutClick: () => void;
onAttractionsClick: () => void;
onBlogsClick: () => void;
onHowItWorksClick: () => void;
onFAQClick: () => void;
onPrivacyPolicyClick: () => void;
onAboutUsClick: () => void;
onProfileClick: () => void;
onCityCardsClick: () => void;
onMagicItineraryClick: () => void;
onPostCardsClick: () => void;
onOffersClick: () => void;
onContactUsClick?: () => void;
onEsimsClick?: () => void;
onHotelDiscountsClick?: () => void;
fromSource: 'products' | 'passes';
currentPage: string;
user?: { email: string; name: string; } | null;
}
// Mock offers data
const offersData = [
{
id: '1',
business: 'Aster Hotels',
title: '20% Off on dining and drinks on purchase upto $500 T&Cs* apply',
discountCode: 'AFJIJFJS500',
rating: 4.5,
image: 'https://images.unsplash.com/photo-1566073771259-6a8506099945?w=400',
category: 'dining'
},
{
id: '2',
business: 'Melbourne Sports Club',
title: '30% Off on sports activities and equipment rental',
discountCode: 'SPORT300',
rating: 4.8,
image: 'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?w=400',
category: 'sports'
},
{
id: '3',
business: 'Luxury Stays Melbourne',
title: '25% Off on weekend stays at premium hotels',
discountCode: 'STAY250',
rating: 4.7,
image: 'https://images.unsplash.com/photo-1551882547-ff40c63fe5fa?w=400',
category: 'stays'
},
{
id: '4',
business: 'Gourmet Food Tours',
title: '15% Off on food walking tours and tastings',
discountCode: 'FOOD150',
rating: 4.6,
image: 'https://images.unsplash.com/photo-1414235077428-338989a2e8c0?w=400',
category: 'food'
},
{
id: '5',
business: 'Craft Drinks Co',
title: '20% Off on craft beer and cocktail experiences',
discountCode: 'DRINK200',
rating: 4.4,
image: 'https://images.unsplash.com/photo-1436114775196-43267ba1fbb8?w=400',
category: 'drinks'
},
{
id: '6',
business: 'Melbourne Museum',
title: '35% Off on museum entries and special exhibitions',
discountCode: 'MUSEUM350',
rating: 4.9,
image: 'https://images.unsplash.com/photo-1566127992631-137a642a90f4?w=400',
category: 'museum'
},
{
id: '7',
business: 'Fine Dining Melbourne',
title: '40% Off on fine dining experiences at top restaurants',
discountCode: 'FINE400',
rating: 4.8,
image: 'https://images.unsplash.com/photo-1414235077428-338989a2e8c0?w=400',
category: 'dining'
},
{
id: '8',
business: 'Adventure Sports',
title: '30% Off on extreme sports and adventure activities',
discountCode: 'ADVENTURE',
rating: 4.7,
image: 'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?w=400',
category: 'sports'
},
{
id: '9',
business: 'Boutique Hotels',
title: '20% Off on boutique accommodation packages',
discountCode: 'BOUTIQUE20',
rating: 4.6,
image: 'https://images.unsplash.com/photo-1551882547-ff40c63fe5fa?w=400',
category: 'stays'
},
{
id: '10',
business: 'Local Food Markets',
title: '25% Off on local market tours and food tastings',
discountCode: 'MARKET250',
rating: 4.5,
image: 'https://images.unsplash.com/photo-1414235077428-338989a2e8c0?w=400',
category: 'food'
},
{
id: '11',
business: 'Wine Bar Melbourne',
title: '30% Off on wine tastings and sommelier experiences',
discountCode: 'WINE300',
rating: 4.7,
image: 'https://images.unsplash.com/photo-1436114775196-43267ba1fbb8?w=400',
category: 'drinks'
},
{
id: '12',
business: 'Art Gallery Melbourne',
title: '25% Off on art exhibitions and cultural events',
discountCode: 'ART250',
rating: 4.8,
image: 'https://images.unsplash.com/photo-1566127992631-137a642a90f4?w=400',
category: 'museum'
},
{
id: '13',
business: 'Rooftop Dining',
title: '35% Off on rooftop dining and sunset experiences',
discountCode: 'ROOFTOP35',
rating: 4.9,
image: 'https://images.unsplash.com/photo-1414235077428-338989a2e8c0?w=400',
category: 'dining'
}
];
const filterCategories = [
{ value: 'dining', label: 'Dining', count: 3 },
{ value: 'sports', label: 'Sports', count: 3 },
{ value: 'stays', label: 'Stays', count: 3 },
{ value: 'food', label: 'Food', count: 3 },
{ value: 'drinks', label: 'Drinks', count: 3 },
{ value: 'museum', label: 'Museum', count: 3 }
];
// Categories data for the 20+ Categories section
const categoriesData = [
{
icon: Hotel,
title: 'Hotels',
description: 'Exclusive deals on luxury and boutique hotels',
offers: '50+ offers',
color: 'from-blue-500 to-cyan-500'
},
{
icon: Plane,
title: 'Flights',
description: 'Save on domestic and international flights',
offers: '30+ offers',
color: 'from-purple-500 to-pink-500'
},
{
icon: MapPinned,
title: 'Destinations',
description: 'Discover amazing destination packages',
offers: '40+ offers',
color: 'from-orange-500 to-red-500'
},
{
icon: Building2,
title: 'Places',
description: 'Visit top attractions with special discounts',
offers: '60+ offers',
color: 'from-green-500 to-emerald-500'
},
{
icon: Home,
title: 'Accommodations',
description: 'From hostels to resorts at best prices',
offers: '45+ offers',
color: 'from-indigo-500 to-blue-500'
}
];
export function OffersPage({
onBackClick,
onHomeClick,
onMelbourneClick,
onPassesClick,
onCheckoutClick,
onSignInClick,
onSignOutClick,
onAttractionsClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onProfileClick,
onCityCardsClick,
onMagicItineraryClick,
onPostCardsClick,
onOffersClick,
onContactUsClick,
onEsimsClick,
onHotelDiscountsClick,
fromSource,
currentPage,
user
}: OffersPageProps) {
const [searchQuery, setSearchQuery] = useState('');
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
const [currentPage_, setCurrentPage_] = useState(1);
const [showLoadMore, setShowLoadMore] = useState(true);
const [copiedCode, setCopiedCode] = useState<string | null>(null);
const toggleCategory = (category: string) => {
setSelectedCategories(prev =>
prev.includes(category)
? prev.filter(c => c !== category)
: [...prev, category]
);
};
const filteredOffers = offersData.filter(offer => {
const matchesSearch = offer.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
offer.business.toLowerCase().includes(searchQuery.toLowerCase());
const matchesCategory = selectedCategories.length === 0 || selectedCategories.includes(offer.category);
return matchesSearch && matchesCategory;
});
const itemsPerPage = 12;
const displayedOffers = filteredOffers.slice(0, currentPage_ * itemsPerPage);
const hasMoreItems = filteredOffers.length > displayedOffers.length;
const copyDiscountCode = (code: string) => {
navigator.clipboard.writeText(code);
setCopiedCode(code);
setTimeout(() => setCopiedCode(null), 2000);
};
const handleLoadMore = () => {
setCurrentPage_(prev => prev + 1);
if (!hasMoreItems) setShowLoadMore(false);
};
// Show different layouts based on login state
if (!user) {
// Not logged in - show marketing/landing page
return (
<div className="min-h-screen bg-background">
<Navbar
activeCity="Paris"
onCityChange={() => {}}
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onPassesClick={onPassesClick}
onCheckoutClick={onCheckoutClick}
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick}
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
onEsimsClick={onEsimsClick}
onHotelDiscountsClick={onHotelDiscountsClick}
currentPage={currentPage}
isUserSignedIn={!!user}
user={user}
/>
{/* Sub Navbar */}
<SubNavbar
activeTab="offers"
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
onEsimsClick={onEsimsClick}
onHotelDiscountsClick={onHotelDiscountsClick}
/>
{/* Hero Section */}
<section className="relative pt-52 pb-20 overflow-hidden">
{/* Background gradient */}
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-secondary/5 to-background"></div>
<div className="container mx-auto px-4 relative z-10">
<motion.div
className="max-w-4xl mx-auto text-center"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<h1 className="font-merchant text-4xl md:text-5xl lg:text-6xl leading-tight mb-6">
<span className="font-light">Save money on</span>{' '}
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
various categories
</span>
</h1>
<p className="font-poppins text-xl leading-relaxed font-normal text-gray-600 mb-8 max-w-2xl mx-auto">
Unlock exclusive deals and discounts across hotels, flights, dining, and more. Start saving on your next adventure today.
</p>
<Button
onClick={onSignInClick}
className="bg-primary hover:bg-primary/90 text-white px-8 py-6 font-poppins font-semibold text-lg"
>
Get Started Now
</Button>
</motion.div>
</div>
{/* Decorative elements */}
<div className="absolute top-20 left-10 w-20 h-20 bg-primary/10 rounded-full blur-xl"></div>
<div className="absolute bottom-20 right-10 w-32 h-32 bg-secondary/10 rounded-full blur-xl"></div>
</section>
{/* Trusted By Companies Section */}
<section className="py-12 bg-background">
<div className="container mx-auto px-4">
<div className="max-w-6xl mx-auto text-center">
<div className="mb-10">
<h2 className="font-merchant text-2xl md:text-3xl lg:text-4xl leading-tight mb-4">
<span className="font-normal">Trusted by the </span>
<span className="font-semibold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">world's best</span>
</h2>
<p className="font-poppins text-base md:text-lg leading-relaxed text-muted-foreground max-w-2xl mx-auto">
Join thousands of travelers who trust our partners for unforgettable experiences
</p>
</div>
<TrustedCompanies />
</div>
</div>
</section>
{/* Featured Offers Section */}
<section className="py-20">
<div className="container mx-auto px-4">
<motion.div
className="text-center mb-12"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<h2 className="font-merchant text-3xl md:text-4xl lg:text-5xl leading-tight mb-4">
<span className="font-light">Featured</span>{' '}
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
Offers
</span>
</h2>
<p className="font-poppins text-lg leading-relaxed font-normal text-gray-600 max-w-2xl mx-auto">
Check out our top deals and start saving on your favorite experiences
</p>
</motion.div>
{/* Featured offers grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-12">
{offersData.slice(0, 6).map((offer, index) => (
<motion.div
key={offer.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
<Card className="bg-white border border-gray-200 rounded-xl overflow-hidden h-full hover:shadow-lg transition-shadow duration-300">
<div className="relative h-48 bg-gray-200">
<ImageWithFallback
src={offer.image}
alt={offer.title}
className="w-full h-full object-cover"
/>
<Badge className="absolute top-4 left-4 bg-primary text-white font-normal">
Featured
</Badge>
</div>
<CardContent className="p-6">
<div className="flex items-center gap-2 mb-3">
<div className="w-4 h-4 bg-gray-300 rounded"></div>
<span className="font-poppins text-sm text-gray-500 font-normal">{offer.business}</span>
</div>
<h3 className="font-merchant text-lg font-medium text-gray-900 mb-3 line-clamp-2">
{offer.title}
</h3>
<div className="flex items-center gap-1 mb-4">
{[...Array(5)].map((_, i) => (
<Star
key={i}
className={`w-4 h-4 ${
i < Math.floor(offer.rating)
? 'fill-yellow-400 text-yellow-400'
: 'fill-gray-300 text-gray-300'
}`}
/>
))}
</div>
<Button
onClick={onSignInClick}
className="w-full bg-gray-900 hover:bg-gray-800 text-white font-poppins font-medium"
>
Sign in to View Code
</Button>
</CardContent>
</Card>
</motion.div>
))}
</div>
<div className="text-center">
<Button
onClick={onSignInClick}
variant="outline"
className="font-poppins font-medium border-primary text-primary hover:bg-primary hover:text-white"
>
View All Offers
</Button>
</div>
</div>
</section>
{/* How It Works Section */}
<section className="py-20 bg-muted/30">
<div className="container mx-auto px-4">
<motion.div
className="text-center mb-16"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<h2 className="font-merchant text-3xl md:text-4xl lg:text-5xl leading-tight mb-4">
<span className="font-light">How</span>{' '}
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
It Works
</span>
</h2>
<p className="font-poppins text-lg leading-relaxed font-normal text-gray-600 max-w-2xl mx-auto">
Get access to exclusive offers in three simple steps
</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{[
{
step: '01',
title: 'Sign Up',
description: 'Create your free CityCards account in seconds',
icon: '👤'
},
{
step: '02',
title: 'Browse Offers',
description: 'Explore thousands of exclusive deals across categories',
icon: '🔍'
},
{
step: '03',
title: 'Save Money',
description: 'Redeem discount codes and start saving instantly',
icon: '💰'
}
].map((item, index) => (
<motion.div
key={item.step}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.2 }}
>
<Card className="bg-white p-8 rounded-xl hover:shadow-lg transition-shadow duration-300">
<div className="text-6xl mb-4">{item.icon}</div>
<div className="font-merchant text-5xl font-bold text-primary/20 mb-4">
{item.step}
</div>
<h3 className="font-merchant text-2xl font-semibold text-gray-900 mb-3">
{item.title}
</h3>
<p className="font-poppins text-base leading-relaxed font-normal text-gray-600">
{item.description}
</p>
</Card>
</motion.div>
))}
</div>
</div>
</section>
{/* 20+ Categories Section */}
<section className="py-20">
<div className="container mx-auto px-4">
<motion.div
className="text-center mb-16"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<h2 className="font-merchant text-3xl md:text-4xl lg:text-5xl leading-tight mb-4">
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
20+ Categories
</span>{' '}
<span className="font-light">to Explore</span>
</h2>
<p className="font-poppins text-lg leading-relaxed font-normal text-gray-600 max-w-2xl mx-auto">
From accommodations to flights, discover deals across all your travel needs
</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{categoriesData.map((category, index) => (
<motion.div
key={category.title}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
<Card className="bg-white p-8 rounded-xl hover:shadow-lg transition-all duration-300 group cursor-pointer">
<div className={`w-16 h-16 rounded-full bg-gradient-to-br ${category.color} flex items-center justify-center mb-6 group-hover:scale-110 transition-transform duration-300`}>
<category.icon className="w-8 h-8 text-white" />
</div>
<h3 className="font-merchant text-2xl font-semibold text-gray-900 mb-3">
{category.title}
</h3>
<p className="font-poppins text-base leading-relaxed font-normal text-gray-600 mb-4">
{category.description}
</p>
<div className="flex items-center justify-between">
<span className="font-poppins text-sm font-medium text-primary">
{category.offers}
</span>
<Button
onClick={onSignInClick}
variant="ghost"
size="sm"
className="text-primary hover:text-primary/80 font-poppins font-medium"
>
Explore →
</Button>
</div>
</Card>
</motion.div>
))}
</div>
<div className="text-center mt-12">
<Button
onClick={onSignInClick}
className="bg-primary hover:bg-primary/90 text-white px-8 py-6 font-poppins font-semibold text-lg"
>
Browse All Categories
</Button>
</div>
</div>
</section>
{/* Access Your CityCards Section */}
<section className="py-20 bg-muted/30">
<MobileAppSection />
</section>
<Footer
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onPassesClick={onPassesClick}
onCheckoutClick={onCheckoutClick}
onSignInClick={onSignInClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick}
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
/>
</div>
);
}
// Logged in - show offers grid with filters
return (
<div className="min-h-screen bg-[#f8f8f8]">
<Navbar
activeCity="Paris"
onCityChange={() => {}}
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onPassesClick={onPassesClick}
onCheckoutClick={onCheckoutClick}
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick}
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
onEsimsClick={onEsimsClick}
onHotelDiscountsClick={onHotelDiscountsClick}
currentPage={currentPage}
isUserSignedIn={!!user}
user={user}
/>
{/* Sub Navbar */}
<SubNavbar
activeTab="offers"
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
onEsimsClick={onEsimsClick}
onHotelDiscountsClick={onHotelDiscountsClick}
/>
<div className="container mx-auto px-4 pt-51 pb-16">
<div className="flex gap-8">
{/* Left Sidebar - Filters */}
<div className="w-64 flex-shrink-0">
<Card className="p-8 sticky top-48">
<div className="space-y-6">
{/* Search by header */}
<div className="flex items-center gap-4">
<div className="h-0 w-6 border-t-[3px] border-[#2d2d2d] rotate-90"></div>
<h3 className="font-merchant text-[22px] text-[#2d2d2d] font-normal">Search by</h3>
</div>
{/* Filter categories */}
<div className="space-y-4">
{filterCategories.map(category => (
<div key={category.value} className="flex items-center gap-3">
<Checkbox
id={category.value}
checked={selectedCategories.includes(category.value)}
onCheckedChange={() => toggleCategory(category.value)}
className="border-[#bebebe]"
/>
<label
htmlFor={category.value}
className="font-poppins text-sm text-[#414141] cursor-pointer flex-1 font-normal"
>
{category.label} ({category.count})
</label>
</div>
))}
</div>
</div>
</Card>
</div>
{/* Main Content */}
<div className="flex-1">
{/* Breadcrumb */}
<div className="mb-8">
<p className="text-[14.297px] text-[#05073c]">
{fromSource === 'passes' ? (
<>
<span>My Profile{'>'}My passes{'>'}</span>
<span className="font-bold">Offers</span>
</>
) : (
<>
<span>Our Products{'>'}</span>
<span className="font-bold">Offers</span>
</>
)}
</p>
</div>
{/* Header Section */}
<div className="mb-8">
<h1 className="text-[48px] font-medium text-[#2d3134] tracking-[0.48px] leading-[54px]">
Offers for you
</h1>
</div>
{/* Offers Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 mb-16">
{displayedOffers.map((offer, index) => (
<motion.div
key={offer.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: index * 0.1 }}
>
<Card className="bg-white border border-[#e7e6e6] rounded-[12px] overflow-hidden h-full">
{/* Image */}
<div className="relative h-[214px] bg-[#c4c4c4]">
<ImageWithFallback
src={offer.image}
alt={offer.title}
className="w-full h-full object-cover"
/>
<Button className="absolute bottom-4 right-3 bg-white rounded-[35px] shadow-lg w-[35px] h-[35px] p-0 hover:bg-gray-100">
<Heart className="w-4 h-4 text-black" />
</Button>
</div>
<CardContent className="space-y-4 px-[17px] py-[0px]">
{/* Business Name */}
<div className="flex items-center gap-2">
<div className="w-4 h-4 bg-gray-300 rounded"></div>
<span className="text-[12.289px] text-[#717171]">{offer.business}</span>
</div>
{/* Title */}
<h3 className="text-[15.875px] font-medium text-[#05073c] leading-[24px] min-h-[48px] font-bold font-normal">
{offer.title}
</h3>
{/* Rating Stars */}
<div className="flex items-center gap-1">
{[...Array(5)].map((_, i) => (
<Star
key={i}
className={`w-3 h-3 ${
i < Math.floor(offer.rating)
? 'fill-[#e2ad64] text-[#e2ad64]'
: 'fill-[#bebebe] text-[#bebebe]'
}`}
/>
))}
</div>
{/* Discount Code */}
<div className="bg-[#f4f4f4] h-[41px] flex items-center justify-between pl-4 -mx-5">
<span className="text-[15px] font-semibold text-[#2d3134]">
{offer.discountCode}
</span>
<Button
onClick={() => copyDiscountCode(offer.discountCode)}
className="bg-[#2c2c2c] rounded-br-[5px] rounded-tr-[5px] h-[40px] w-[38px] p-0 hover:bg-[#1c1c1c]"
>
<Copy className="w-4 h-4 text-white" />
</Button>
</div>
</CardContent>
{copiedCode === offer.discountCode && (
<div className="absolute top-4 left-4 bg-green-500 text-white px-2 py-1 rounded text-xs">
Copied!
</div>
)}
</Card>
</motion.div>
))}
</div>
{/* Minimal Pagination */}
<div className="flex justify-center py-8">
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
className="w-8 h-8 p-0"
disabled={currentPage_ === 1}
>
<ChevronRight className="w-4 h-4 rotate-180" />
</Button>
<div className="flex items-center gap-1">
{[1, 2, 3].map((page) => (
<Button
key={page}
variant={currentPage_ === page ? "default" : "ghost"}
size="sm"
className="w-8 h-8 p-0"
onClick={() => setCurrentPage_(page)}
>
{page}
</Button>
))}
</div>
<Button
variant="outline"
size="sm"
className="w-8 h-8 p-0"
disabled={currentPage_ === 3}
>
<ChevronRight className="w-4 h-4" />
</Button>
</div>
</div>
</div>
</div>
</div>
<Footer
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onPassesClick={onPassesClick}
onCheckoutClick={onCheckoutClick}
onSignInClick={onSignInClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick}
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
onContactUsClick={onContactUsClick}
currentPage={currentPage}
/>
</div>
);
}

View File

@@ -0,0 +1,195 @@
import { ArrowRight, Star, Clock, MapPin } from 'lucide-react';
import { ImageWithFallback } from './figma/ImageWithFallback';
import { Button } from './ui/button';
import { motion } from 'motion/react';
const otherCities = [
{
id: 1,
name: 'London',
country: 'United Kingdom',
rating: 4.8,
experiences: 180,
duration: '2-3 days',
highlight: 'Historic Landmarks',
image: 'https://images.unsplash.com/photo-1559788591-f5ea2371b915?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxsb25kb24lMjBicmlkZ2UlMjBjaXR5c2NhcGV8ZW58MXx8fHwxNzU2MTIzNTYyfDA&ixlib=rb-4.1.0&q=80&w=1080',
description: 'Discover centuries of history, royal palaces, and world-class museums in England\'s capital city.'
},
{
id: 2,
name: 'Hong Kong',
country: 'China',
rating: 4.7,
experiences: 125,
duration: '3-4 days',
highlight: 'Urban Adventure',
image: 'https://images.unsplash.com/photo-1698416286339-7edbf0922953?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxob25nJTIwa29uZyUyMHNreWxpbmUlMjBuaWdodHxlbnwxfHx8fDE3NTYxMjM1NjZ8MA&ixlib=rb-4.1.0&q=80&w=1080',
description: 'Experience the perfect blend of East meets West with stunning skylines and incredible cuisine.'
},
{
id: 3,
name: 'Istanbul',
country: 'Turkey',
rating: 4.6,
experiences: 95,
duration: '3-5 days',
highlight: 'Cultural Heritage',
image: 'https://images.unsplash.com/photo-1669117403979-be8e9448d9b3?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxpc3RhbmJ1bCUyMGJvc3Bob3J1cyUyMG1vc3F1ZXxlbnwxfHx8fDE3NTYxMjM1NzB8MA&ixlib=rb-4.1.0&q=80&w=1080',
description: 'Immerse yourself in the crossroads of Europe and Asia with stunning architecture and rich history.'
},
{
id: 4,
name: 'Cairo',
country: 'Egypt',
rating: 4.5,
experiences: 78,
duration: '4-5 days',
highlight: 'Ancient Wonders',
image: 'https://images.unsplash.com/photo-1705874930271-88eeb8f533dc?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxjYWlybyUyMHB5cmFtaWRzJTIwYW5jaWVudHxlbnwxfHx8fDE3NTYxMjM1NzR8MA&ixlib=rb-4.1.0&q=80&w=1080',
description: 'Journey through millennia of civilization with the Great Pyramids and the treasures of the Nile.'
},
{
id: 5,
name: 'Vancouver',
country: 'Canada',
rating: 4.8,
experiences: 110,
duration: '2-3 days',
highlight: 'Nature & City',
image: 'https://images.unsplash.com/photo-1730661906876-18bfc6e95f2f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx2YW5jb3V2ZXIlMjBtb3VudGFpbnMlMjBjaXR5c2NhcGV8ZW58MXx8fHwxNzU2MTIzNTc4fDA&ixlib=rb-4.1.0&q=80&w=1080',
description: 'Where urban sophistication meets breathtaking natural beauty, from mountains to ocean.'
},
{
id: 6,
name: 'Miami',
country: 'United States',
rating: 4.6,
experiences: 135,
duration: '3-4 days',
highlight: 'Beach Culture',
image: 'https://images.unsplash.com/photo-1735825713164-192ff57874bf?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtaWFtaSUyMGJlYWNoJTIwYXJ0JTIwZGVjb3xlbnwxfHx8fDE3NTYxMjM1ODR8MA&ixlib=rb-4.1.0&q=80&w=1080',
description: 'Dive into vibrant Art Deco architecture, world-famous beaches, and electric nightlife.'
}
];
export function OtherCities() {
return (
<section className="py-20 bg-white">
<div className="container mx-auto px-4">
{/* Header */}
<motion.div
className="text-center mb-16"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
>
<h2 className="heading-dynamic text-4xl md:text-5xl text-gray-900 mb-6">
<span className="font-light">Explore</span>{' '}
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
Amazing
</span>{' '}
<span className="font-normal">Cities</span>{' '}
<span className="font-semibold text-emphasis">Worldwide</span>
</h2>
<p className="font-poppins text-xl text-gray-600 max-w-3xl mx-auto leading-relaxed font-normal">
Discover incredible destinations around the world with our comprehensive city guides and curated experiences.
</p>
</motion.div>
{/* Cities Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{otherCities.map((city, index) => (
<motion.div
key={city.id}
className="group relative bg-white rounded-3xl overflow-hidden shadow-lg hover:shadow-2xl transition-all duration-500"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: index * 0.1 }}
viewport={{ once: true }}
whileHover={{ y: -8 }}
>
{/* Image */}
<div className="relative h-64 overflow-hidden">
<ImageWithFallback
src={city.image}
alt={city.name}
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-700"
/>
{/* Gradient Overlay */}
<div className="absolute inset-0 bg-gradient-to-t from-black/50 via-transparent to-transparent" />
{/* Rating Badge */}
<div className="absolute top-4 left-4 bg-white/95 backdrop-blur-sm px-3 py-1 rounded-full flex items-center gap-1">
<Star className="w-4 h-4 text-yellow-500 fill-current" />
<span className="text-sm font-semibold text-gray-900">{city.rating}</span>
</div>
{/* Highlight Badge */}
<div className="absolute top-4 right-4 bg-primary/90 backdrop-blur-sm px-3 py-1 rounded-full">
<span className="text-sm font-medium text-white">{city.highlight}</span>
</div>
</div>
{/* Content */}
<div className="p-6">
{/* City Info */}
<div className="mb-4">
<h3 className="text-2xl font-bold text-gray-900 mb-1">{city.name}</h3>
<div className="flex items-center text-gray-600 mb-3">
<MapPin className="w-4 h-4 mr-1" />
<span className="text-sm">{city.country}</span>
</div>
</div>
{/* Stats */}
<div className="flex items-center justify-between mb-4 text-sm text-gray-600">
<div className="flex items-center gap-1">
<span className="font-semibold text-primary">{city.experiences}</span>
<span>experiences</span>
</div>
<div className="flex items-center gap-1">
<Clock className="w-4 h-4" />
<span>{city.duration}</span>
</div>
</div>
{/* Description */}
<p className="text-gray-600 mb-6 line-clamp-3">
{city.description}
</p>
{/* CTA Button */}
<Button
className="w-full bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-medium py-3 rounded-2xl transition-all duration-300 group/btn"
>
<span>Explore {city.name}</span>
<ArrowRight className="w-4 h-4 ml-2 group-hover/btn:translate-x-1 transition-transform duration-300" />
</Button>
</div>
</motion.div>
))}
</div>
{/* Bottom CTA */}
<motion.div
className="text-center mt-16"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
>
<Button
size="lg"
variant="outline"
className="px-8 py-4 text-lg border-2 border-primary text-primary hover:bg-primary hover:text-white transition-all duration-300"
>
<span>View All Cities</span>
<ArrowRight className="w-5 h-5 ml-2" />
</Button>
</motion.div>
</div>
</section>
);
}

View File

@@ -0,0 +1,142 @@
import { Button } from './ui/button';
import { Check } from 'lucide-react';
interface Pass {
id: string;
name: string;
description: string;
price: string;
period: string;
originalPrice?: string;
popular?: boolean;
features: string[];
forText: string;
}
const passes: Pass[] = [
{
id: 'selective',
name: 'Selective Card',
description: 'Ideal for first-time visitors. Enjoy access to a curated selection of attractions and basic customization options to get your journey started.',
price: '$39',
period: '/24 hours',
originalPrice: '$65',
forText: 'For first-time visitors',
features: [
'Access to 5+ top attractions',
'Skip-the-line at selected venues',
'Valid for 24 consecutive hours',
'Mobile ticket delivery',
'Free cancellation up to 24h',
'Basic audio guide access'
]
},
{
id: 'unlimited',
name: 'Unlimited Card',
description: 'Perfect for adventure seekers, providing unlimited access to all attractions, an extensive library of experiences and integration with popular travel tools.',
price: '$79',
period: '/48 hours',
originalPrice: '$150',
popular: true,
forText: 'For adventure seekers',
features: [
'Unlimited access to all attractions',
'Skip-the-line at every venue',
'Valid for 48 consecutive hours',
'Free public transport included',
'Priority customer support',
'Enhanced regular updates',
'Perfect for exploring teams',
'Integration with 25+ travel tool including Maps, Reviews, and Local Guides'
]
}
];
export function PassComparison() {
return (
<section className="py-16 md:py-24 bg-gray-50">
<div className="container mx-auto px-4">
<div className="text-center space-y-4 mb-12">
<p className="text-purple-600 font-medium">Pricing Plans</p>
<h2 className="text-3xl md:text-4xl font-bold text-gray-900">
Choose the perfect plan for your needs
</h2>
</div>
<div className="grid md:grid-cols-2 gap-6 max-w-5xl mx-auto">
{passes.map((pass) => (
<div
key={pass.id}
className={`relative bg-white rounded-3xl p-8 transition-all duration-300 ${
pass.popular
? 'border-2 border-purple-500 shadow-xl transform scale-105'
: 'border border-gray-200 shadow-lg hover:shadow-xl'
}`}
>
{pass.popular && (
<div className="absolute -top-3 left-1/2 transform -translate-x-1/2">
<span className="bg-yellow-400 text-black px-4 py-1 rounded-full text-sm font-medium">
Most Popular
</span>
</div>
)}
<div className="text-center mb-8">
<h3 className="text-2xl font-bold text-gray-900 mb-3">{pass.name}</h3>
<p className="text-gray-600 text-sm mb-6 leading-relaxed">
{pass.description}
</p>
<div className="mb-2">
<span className="text-4xl font-bold text-gray-900">{pass.price}</span>
<span className="text-lg text-gray-500">{pass.period}</span>
</div>
{pass.originalPrice && (
<div className="text-sm text-gray-500">
<span className="line-through">{pass.originalPrice}</span> billed annually
</div>
)}
<Button
className={`w-full mt-6 h-12 rounded-xl font-medium ${
pass.popular
? 'bg-primary hover:bg-primary/90 text-white'
: 'bg-primary hover:bg-primary/90 text-white'
}`}
>
Choose plan
</Button>
</div>
<div className="space-y-6">
<div>
<h4 className="font-semibold text-gray-900 mb-4">{pass.forText}</h4>
<ul className="space-y-3">
{pass.features.map((feature, index) => (
<li key={index} className="flex items-start space-x-3">
<div className="mt-0.5">
<Check className="w-5 h-5 text-gray-400" />
</div>
<span className="text-sm text-gray-700 leading-relaxed">{feature}</span>
</li>
))}
</ul>
</div>
</div>
</div>
))}
</div>
<div className="text-center mt-12">
<p className="text-gray-600 mb-4">
Not sure which pass is right for you?
</p>
<Button variant="outline" size="lg" className="h-12 px-8">
Compare All Features
</Button>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,657 @@
import { useState } from 'react';
import { Check, X, Star, Shield, Clock, Smartphone, Download, QrCode, Heart, Users, Award, Headphones } from 'lucide-react';
import { Button } from './ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card';
import { RadioGroup, RadioGroupItem } from './ui/radio-group';
import { Badge } from './ui/badge';
import Navbar from './Navbar';
import { CitySubmenu } from './CitySubmenu';
import { EnhancedTestimonials } from './EnhancedTestimonials';
import { Footer } from './Footer';
import { ReviewsSection } from './ReviewsSection';
interface PassesPageProps {
onBackClick: () => void;
onHomeClick: () => void;
onMelbourneClick: () => void;
onPassesClick: () => void;
onCheckoutClick?: () => void;
onSignInClick: () => void;
onSignOutClick?: () => void;
onAttractionsClick: () => void;
onBlogsClick: () => void;
onHowItWorksClick: () => void;
onFAQClick: () => void;
onPrivacyPolicyClick: () => void;
onAboutUsClick?: () => void;
onProfileClick?: () => void;
onCityCardsClick: () => void;
onMagicItineraryClick: () => void;
onPostCardsClick: () => void;
onOffersClick: () => void;
onEsimsClick?: () => void;
onHotelDiscountsClick?: () => void;
onContactUsClick?: () => void;
currentPage: string;
user?: { email: string; name: string; } | null;
}
interface PassType {
id: string;
name: string;
title: string;
description: string;
price: string;
originalPrice?: string;
period: string;
popular?: boolean;
features: string[];
tableFeatures: {
freeDelivery: boolean | string;
attractionsIncluded: string;
validityPeriod: string;
audioGuide: boolean;
skipTheLine: boolean | string;
mobileApp: boolean;
cancellationPolicy: string;
customerSupport: string;
additionalPerks: string;
};
}
const passTypes: PassType[] = [
{
id: 'selective',
name: 'Selective Pass',
title: 'SELECTIVE PASS',
description: 'Perfect for travelers who want to explore selected attractions at their own pace with essential features.',
price: '$59.99',
originalPrice: '$89.99',
period: 'per person',
features: [
'Access to selected attractions',
'Limited number of attractions per pass',
'Flexible validity period',
'Priority entry where available',
'Mobile ticket delivery'
],
tableFeatures: {
freeDelivery: true,
attractionsIncluded: 'Up to 5 attractions',
validityPeriod: '30 days',
audioGuide: false,
skipTheLine: 'Selected venues',
mobileApp: true,
cancellationPolicy: '24 hours free cancellation',
customerSupport: 'Email support',
additionalPerks: 'Basic travel tips'
}
},
{
id: 'unlimited',
name: 'Unlimited Pass',
title: 'UNLIMITED CARD',
description: 'The ultimate experience for adventure seekers who want unlimited access to all attractions with premium features.',
price: '$89.99',
originalPrice: '$149.99',
period: 'per person',
popular: true,
features: [
'Unlimited access to all attractions',
'Time-limited validity (7 days)',
'Skip-the-line access',
'Expert guide inclusion',
'Mobile app access',
'Premium customer support'
],
tableFeatures: {
freeDelivery: true,
attractionsIncluded: 'All attractions (50+)',
validityPeriod: '7 consecutive days',
audioGuide: true,
skipTheLine: 'All venues',
mobileApp: true,
cancellationPolicy: '24 hours free cancellation',
customerSupport: '24/7 Phone & Email',
additionalPerks: 'Exclusive discounts & priority booking'
}
}
];
const featureComparison = [
{ key: 'freeDelivery', label: 'Free Delivery' },
{ key: 'attractionsIncluded', label: 'Attractions Included' },
{ key: 'validityPeriod', label: 'Validity Period' },
{ key: 'audioGuide', label: 'Audio Guide' },
{ key: 'skipTheLine', label: 'Skip-the-Line Access' },
{ key: 'mobileApp', label: 'Mobile App' },
{ key: 'cancellationPolicy', label: 'Cancellation Policy' },
{ key: 'customerSupport', label: 'Customer Support' },
{ key: 'additionalPerks', label: 'Additional Perks' }
];
const trustFeatures = [
{
icon: Star,
title: 'Unique Benefits',
description: 'Exclusive access to premium attractions and experiences',
stat: '50+ Partner Venues',
color: 'from-yellow-400 to-orange-500'
},
{
icon: Users,
title: 'Proven Performance',
description: 'Trusted by thousands of satisfied travelers worldwide',
stat: '4.8/5 Rating',
color: 'from-blue-400 to-cyan-500'
},
{
icon: Shield,
title: 'Quality Assurance',
description: 'Safety-first approach with verified partners and secure booking',
stat: '100% Secure',
color: 'from-green-400 to-emerald-500'
},
{
icon: Headphones,
title: 'Award-winning Support',
description: 'Round-the-clock assistance for seamless travel experience',
stat: '24/7 Available',
color: 'from-purple-400 to-pink-500'
}
];
export function PassesPage({
onHomeClick,
onMelbourneClick,
onPassesClick,
onCheckoutClick,
onSignInClick,
onSignOutClick,
onAttractionsClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onProfileClick,
onCityCardsClick,
onMagicItineraryClick,
onPostCardsClick,
onOffersClick,
onEsimsClick,
onHotelDiscountsClick,
onContactUsClick,
currentPage,
user
}: PassesPageProps) {
const [selectedPass, setSelectedPass] = useState<string>('unlimited');
const renderFeatureValue = (value: boolean | string) => {
if (typeof value === 'boolean') {
return value ? (
<Check className="w-5 h-5 text-green-500 mx-auto" />
) : (
<X className="w-5 h-5 text-red-400 mx-auto" />
);
}
return <span className="text-gray-700">{value}</span>;
};
return (
<div className="min-h-screen bg-background">
{/* Navigation */}
<Navbar
activeCity="Melbourne"
onCityChange={() => {}}
onHomeClick={onHomeClick}
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
onPassesClick={onPassesClick}
onCheckoutClick={onCheckoutClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick}
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
onEsimsClick={onEsimsClick}
onHotelDiscountsClick={onHotelDiscountsClick}
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 */}
<div className="text-center mb-16">
<div className="mb-6">
<h1 className="font-merchant font-light text-4xl md:text-5xl lg:text-6xl mb-4">
Buy <span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">Passes</span>
</h1>
<p className="font-poppins text-xl leading-relaxed font-normal text-gray-600 max-w-3xl mx-auto">
Skip the lines, save money, and explore more with our flexible city cards designed for modern travelers
</p>
</div>
</div>
{/* Pass Comparison Section */}
<div className="mb-20">
<RadioGroup
value={selectedPass}
onValueChange={setSelectedPass}
className="grid md:grid-cols-2 gap-8 max-w-6xl mx-auto"
>
{passTypes.map((pass) => (
<div key={pass.id} className="relative h-full">
<Card className={`relative h-full flex flex-col transition-all duration-300 cursor-pointer ${
pass.popular
? 'ring-2 ring-primary shadow-xl'
: selectedPass === pass.id
? 'ring-2 ring-primary/50 shadow-lg'
: 'border-gray-200 shadow-md hover:shadow-lg hover:border-primary/30'
}`}>
{/* Popular Badge */}
{pass.popular && (
<div className="absolute -top-3 left-1/2 transform -translate-x-1/2 z-10">
<Badge className="bg-gradient-to-r from-yellow-400 to-orange-500 text-black px-6 py-1.5 font-semibold shadow-lg font-poppins">
Most Popular
</Badge>
</div>
)}
{/* Radio Button */}
<div className="absolute top-5 right-5 z-10">
<RadioGroupItem value={pass.id} id={pass.id} className="w-5 h-5" />
</div>
{/* Header - Fixed Height */}
<CardHeader className="text-center pb-4 pt-8 flex-shrink-0">
<CardTitle className="font-merchant text-2xl leading-tight mb-3 text-gray-900">
{pass.title}
</CardTitle>
<CardDescription className="font-poppins text-sm text-gray-600 leading-relaxed font-normal min-h-[48px] flex items-center justify-center px-4">
{pass.description}
</CardDescription>
</CardHeader>
{/* Pricing Section - Fixed Height */}
<div className="px-6 pb-6 flex-shrink-0">
<div className="flex items-baseline justify-center gap-2 mb-2">
<span className="text-5xl font-bold text-gray-900 font-poppins">{pass.price}</span>
<span className="text-gray-500 font-poppins text-base">/ {pass.period}</span>
</div>
<div className="h-5 flex items-center justify-center">
{pass.originalPrice && (
<div className="text-sm text-gray-500 font-poppins">
<span className="line-through mr-2">{pass.originalPrice}</span>
<span className="text-green-600 font-medium">Save {Math.round(((parseFloat(pass.originalPrice.slice(1)) - parseFloat(pass.price.slice(1))) / parseFloat(pass.originalPrice.slice(1))) * 100)}%</span>
</div>
)}
</div>
</div>
{/* Content - Flexible Height with Fixed Features Area */}
<CardContent className="pt-0 pb-6 px-6 flex-grow flex flex-col">
{/* Features List - Fixed height */}
<div className="flex-grow mb-6">
<div className="space-y-3">
{pass.features.slice(0, 6).map((feature, index) => (
<div key={index} className="flex items-start gap-3">
<Check className="w-4 h-4 text-green-500 mt-1 flex-shrink-0" />
<span className="text-sm text-gray-700 font-poppins leading-relaxed font-normal">{feature}</span>
</div>
))}
</div>
</div>
{/* CTA Button - Pushed to bottom */}
<div className="flex-shrink-0 space-y-3">
<Button
className={`w-full h-12 rounded-lg font-semibold transition-all duration-300 font-poppins ${
pass.popular
? 'bg-primary hover:bg-primary/90 text-white shadow-md hover:shadow-lg'
: 'bg-gray-900 hover:bg-gray-800 text-white hover:shadow-md'
}`}
onClick={onCheckoutClick}
>
PURCHASE NOW
</Button>
<p className="text-xs text-gray-500 text-center font-poppins font-normal leading-tight">
Free cancellation up to 24 hours Instant delivery
</p>
</div>
</CardContent>
</Card>
</div>
))}
</RadioGroup>
</div>
{/* Detailed Features Comparison Table */}
<Card className="overflow-hidden mb-20 bg-white shadow-lg">
<CardHeader className="text-center">
<CardTitle className="font-merchant text-3xl font-bold text-gray-900 mb-2">
Detailed Feature Comparison
</CardTitle>
<CardDescription className="text-lg text-gray-600 font-light">
See exactly what's included with each pass type
</CardDescription>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-gray-200 bg-gray-50">
<th className="text-left p-6 font-semibold text-gray-900 min-w-[200px]">Features</th>
<th className="text-center p-6 font-semibold text-gray-900 min-w-[200px]">Selective Pass</th>
<th className="text-center p-6 font-semibold text-gray-900 min-w-[200px] bg-primary/5">Premium Unlimited</th>
</tr>
</thead>
<tbody>
{featureComparison.map((feature, index) => (
<tr key={feature.key} className={`border-b border-gray-100 ${index % 2 === 0 ? 'bg-white' : 'bg-gray-50/50'}`}>
<td className="p-6 font-medium text-gray-900">
<div className="flex items-center">
<span className="w-2 h-2 bg-primary rounded-full mr-3"></span>
{feature.label}
</div>
</td>
<td className="p-6 text-center">
{renderFeatureValue(passTypes[0].tableFeatures[feature.key as keyof typeof passTypes[0]['tableFeatures']])}
</td>
<td className="p-6 text-center bg-primary/5">
{renderFeatureValue(passTypes[1].tableFeatures[feature.key as keyof typeof passTypes[1]['tableFeatures']])}
</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
{/* Mobile App Promotion Banner */}
<div className="mb-20">
<Card className="bg-gray-50 overflow-hidden shadow-lg">
<CardContent className="p-0">
<div className="grid lg:grid-cols-2 gap-12 items-center">
{/* Smartphone Mockup */}
<div className="relative flex justify-center p-8 lg:p-16">
<div className="relative">
{/* Phone Frame */}
<div className="w-64 h-[520px] bg-black rounded-[2.5rem] p-2 shadow-2xl">
<div className="w-full h-full bg-white rounded-[2rem] relative overflow-hidden">
{/* Screen Content */}
<div className="absolute inset-0 bg-gradient-to-b from-primary/90 to-secondary/90">
{/* Status Bar */}
<div className="flex justify-between items-center px-6 pt-4 pb-2 text-white text-sm">
<span>9:41</span>
<div className="flex gap-1">
<div className="w-4 h-2 bg-white rounded-sm opacity-80"></div>
<div className="w-4 h-2 bg-white rounded-sm opacity-60"></div>
<div className="w-4 h-2 bg-white rounded-sm opacity-40"></div>
</div>
</div>
{/* App Header */}
<div className="px-6 py-4">
<div className="flex items-center gap-3 mb-6">
<div className="w-12 h-12 bg-white/20 rounded-xl flex items-center justify-center backdrop-blur-sm">
<span className="text-xl">🏙️</span>
</div>
<div>
<h3 className="text-white font-semibold text-lg">CityCards</h3>
<p className="text-white/70 text-sm">Melbourne Explorer</p>
</div>
</div>
</div>
{/* Active Pass Card */}
<div className="mx-6 mb-6">
<div className="bg-white/15 backdrop-blur-md rounded-2xl p-4 border border-white/20">
<div className="flex justify-between items-center mb-3">
<span className="text-white font-semibold">Premium Pass</span>
<Badge className="bg-green-400 text-green-900 text-xs">Active</Badge>
</div>
<div className="h-2 bg-white/20 rounded-full mb-3">
<div className="h-2 bg-gradient-to-r from-yellow-400 to-orange-500 rounded-full w-3/4"></div>
</div>
<p className="text-white/80 text-xs">18 of 24 attractions visited</p>
</div>
</div>
{/* Quick Actions */}
<div className="px-6 mb-6">
<div className="grid grid-cols-3 gap-3">
{[
{ icon: '🗺', label: 'Map' },
{ icon: '📍', label: 'Near Me' },
{ icon: '', label: 'Favorites' }
].map((action, index) => (
<div key={index} className="bg-white/10 backdrop-blur-sm rounded-xl p-3 text-center border border-white/20">
<span className="text-lg mb-1 block">{action.icon}</span>
<span className="text-white text-xs">{action.label}</span>
</div>
))}
</div>
</div>
{/* Featured Attraction */}
<div className="mx-6">
<div className="bg-white/10 backdrop-blur-sm rounded-xl p-4 border border-white/20">
<h4 className="text-white font-medium mb-2">Today's Highlight</h4>
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-white/20 rounded-lg"></div>
<div>
<p className="text-white text-sm font-medium">Royal Botanic Gardens</p>
<p className="text-white/70 text-xs">2.5km away Open now</p>
</div>
</div>
</div>
</div>
{/* Bottom Tab Bar */}
<div className="absolute bottom-6 left-4 right-4">
<div className="bg-white/10 backdrop-blur-md rounded-2xl p-2 border border-white/20">
<div className="grid grid-cols-4 gap-2 text-center">
{['Home', 'Passes', 'Map', 'Profile'].map((tab, index) => (
<div key={index} className="py-2">
<span className="text-white text-xs">{tab}</span>
</div>
))}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Content */}
<div className="p-8 lg:p-16">
<h2 className="heading-dynamic text-4xl text-gray-900 mb-6">
<span className="font-light">Access all your</span>{' '}
<span className="font-bold italic text-gradient-primary">city cards</span>{' '}
<span className="font-normal">on your</span>{' '}
<span className="font-semibold text-emphasis">phone</span>
</h2>
<p className="text-xl text-gray-600 mb-8 leading-relaxed font-light">
Download our mobile app for easy access to your passes, maps, and exclusive offers.
Never worry about losing your tickets again.
</p>
{/* App Features */}
<div className="space-y-4 mb-8">
{[
'Instant pass activation and QR code access',
'Offline maps and attraction information',
'Real-time updates and exclusive app-only offers',
'Track your progress and plan your journey'
].map((feature, index) => (
<div key={index} className="flex items-center gap-3">
<Check className="w-5 h-5 text-green-500" />
<span className="text-gray-700 font-light">{feature}</span>
</div>
))}
</div>
{/* Download Buttons */}
<div className="flex flex-col sm:flex-row gap-4 mb-8">
<Button className="bg-black text-white hover:bg-gray-800 flex items-center justify-center gap-3 px-6 py-4 rounded-xl font-semibold">
<Download className="w-5 h-5" />
Download for iOS
</Button>
<Button className="bg-black text-white hover:bg-gray-800 flex items-center justify-center gap-3 px-6 py-4 rounded-xl font-semibold">
<Download className="w-5 h-5" />
Get it on Google Play
</Button>
</div>
{/* QR Code */}
<div className="flex items-center gap-4">
<div className="w-16 h-16 bg-black rounded-xl p-2">
<div className="w-full h-full bg-white rounded-lg flex items-center justify-center">
<QrCode className="w-8 h-8 text-black" />
</div>
</div>
<div>
<p className="font-medium text-gray-900">Scan to download</p>
<p className="text-sm text-gray-600 font-light">Available on iOS and Android</p>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Why Choose Us Section */}
<div className="mb-20">
<div className="text-center mb-12">
<h2 className="heading-dynamic text-4xl text-gray-900 mb-4">
<span className="font-light">Why Choose</span>{' '}
<span className="font-bold italic text-gradient-primary">CityCards</span><span className="font-normal">?</span>
</h2>
<p className="text-xl text-gray-600 max-w-3xl mx-auto font-light">
We're committed to providing the best city exploration experience with unmatched value and service
</p>
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-8">
{trustFeatures.map((feature, index) => {
const IconComponent = feature.icon;
return (
<Card key={index} className="text-center p-8 border-gray-200 hover:shadow-lg transition-shadow duration-300">
<CardContent className="p-0">
<div className={`w-16 h-16 bg-gradient-to-r ${feature.color} rounded-2xl flex items-center justify-center mx-auto mb-6`}>
<IconComponent className="w-8 h-8 text-white" />
</div>
<h4 className="font-semibold text-gray-900 mb-3">{feature.title}</h4>
<p className="text-gray-600 mb-4 leading-relaxed font-light">{feature.description}</p>
<div className="text-2xl font-bold text-primary mb-1">{feature.stat}</div>
</CardContent>
</Card>
);
})}
</div>
</div>
{/* Enhanced Testimonials Section */}
<EnhancedTestimonials />
{/* Reviews Section */}
<ReviewsSection />
{/* Final CTA Section */}
<div className="text-center bg-gradient-to-r from-primary to-secondary rounded-3xl p-12 text-white relative overflow-hidden">
{/* Background Pattern */}
<div className="absolute inset-0 bg-gradient-to-br from-white/10 to-transparent"></div>
<div className="absolute top-10 right-10 w-32 h-32 bg-white/5 rounded-full"></div>
<div className="absolute bottom-10 left-10 w-24 h-24 bg-white/5 rounded-full"></div>
<div className="relative z-10">
<h3 className="heading-dynamic text-4xl mb-4">
<span className="font-light">Ready to</span>{' '}
<span className="font-bold italic text-emphasis">explore</span>{' '}
<span className="font-semibold">Melbourne?</span>
</h3>
<p className="text-xl mb-8 max-w-2xl mx-auto opacity-90 font-light">
Choose your pass and start discovering amazing attractions with skip-the-line access.
Money-back guarantee included.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center mb-8">
<Button
size="lg"
className="bg-white text-primary hover:bg-gray-100 py-4 px-8 rounded-2xl font-semibold text-lg shadow-lg hover:shadow-xl transition-all duration-200"
>
Choose Your Pass
</Button>
<div className="flex items-center gap-2 text-white/80">
<Check className="w-5 h-5" />
<span className="font-light">No hidden fees Instant confirmation</span>
</div>
</div>
<div className="flex justify-center items-center gap-8 text-sm text-white/70">
<span className="flex items-center gap-2 font-light">
<Shield className="w-4 h-4" />
SSL Secured
</span>
<span className="flex items-center gap-2 font-light">
<Heart className="w-4 h-4" />
Money Back Guarantee
</span>
<span className="flex items-center gap-2 font-light">
<Clock className="w-4 h-4" />
24/7 Support
</span>
</div>
</div>
</div>
</div>
<Footer
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onPassesClick={onPassesClick}
onSignInClick={onSignInClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onContactUsClick={onContactUsClick}
currentPage={currentPage}
/>
</div>
);
}
export default PassesPage;

View File

@@ -0,0 +1,268 @@
import React from 'react';
import { motion } from 'motion/react';
import { ArrowLeft, Camera, Edit3, Upload, Heart, Star, Download, Share2 } from 'lucide-react';
import { Button } from './ui/button';
import { Card, CardContent } from './ui/card';
import { Badge } from './ui/badge';
import Navbar from './Navbar';
import SubNavbar from './SubNavbar';
import { Footer } from './Footer';
import { MobileAppSection } from './MobileAppSection';
import { EnhancedTestimonials } from './EnhancedTestimonials';
import { CustomPostcards } from './CustomPostcards';
import { HowItWorks } from './HowItWorks';
import { ImageWithFallback } from './figma/ImageWithFallback';
interface User {
email: string;
name: string;
}
interface PostCardsPageProps {
onBackClick: () => void;
onHomeClick: () => void;
onMelbourneClick: () => void;
onPassesClick: () => void;
onCheckoutClick: () => void;
onSignInClick: () => void;
onSignOutClick: () => void;
onAttractionsClick: () => void;
onBlogsClick: () => void;
onHowItWorksClick: () => void;
onFAQClick: () => void;
onPrivacyPolicyClick: () => void;
onAboutUsClick: () => void;
onProfileClick: () => void;
onCityCardsClick: () => void;
onMagicItineraryClick: () => void;
onPostCardsClick: () => void;
onOffersClick: () => void;
onContactUsClick?: () => void;
onEsimsClick?: () => void;
onHotelDiscountsClick?: () => void;
currentPage: string;
user: User | null;
}
export function PostCardsPage({
onBackClick,
onHomeClick,
onMelbourneClick,
onPassesClick,
onCheckoutClick,
onSignInClick,
onSignOutClick,
onAttractionsClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onProfileClick,
onCityCardsClick,
onMagicItineraryClick,
onPostCardsClick,
onOffersClick,
onContactUsClick,
onEsimsClick,
onHotelDiscountsClick,
currentPage,
user
}: PostCardsPageProps) {
return (
<div className="min-h-screen bg-background">
{/* Navbar */}
<Navbar
activeCity=""
onCityChange={() => {}}
onSignInClick={onSignInClick}
onPassesClick={onPassesClick}
onCheckoutClick={onCheckoutClick}
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick}
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
onEsimsClick={onEsimsClick}
onHotelDiscountsClick={onHotelDiscountsClick}
currentPage="postcards"
isUserSignedIn={!!user}
user={user}
/>
{/* Sub Navbar */}
<SubNavbar
activeTab="postcards"
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
onEsimsClick={onEsimsClick}
onHotelDiscountsClick={onHotelDiscountsClick}
/>
{/* Hero Section */}
<section className="relative pt-52 pb-20 overflow-hidden">
{/* Background gradient */}
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-secondary/5 to-background"></div>
<div className="container mx-auto px-4 relative z-10">
<motion.div
className="max-w-4xl mx-auto text-center"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<h1 className="font-merchant text-4xl md:text-5xl lg:text-6xl leading-tight mb-6">
<span className="font-light">Create Beautiful</span>{' '}
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
Custom Postcards
</span>
</h1>
<p className="font-poppins text-xl leading-relaxed font-normal text-gray-600 mb-8 max-w-2xl mx-auto">
Transform your travel memories into stunning, personalized postcards with authentic handwritten messages.
Share your journey in a way that feels truly personal and meaningful.
</p>
<Button
onClick={onCheckoutClick}
className="bg-primary hover:bg-primary/90 text-white px-8 py-6 font-poppins font-semibold text-lg"
>
Start Creating Postcards
</Button>
</motion.div>
</div>
{/* Decorative elements */}
<div className="absolute top-20 left-10 w-20 h-20 bg-primary/10 rounded-full blur-xl"></div>
<div className="absolute bottom-20 right-10 w-32 h-32 bg-secondary/10 rounded-full blur-xl"></div>
</section>
{/* Reuse CustomPostcards Component */}
<CustomPostcards />
{/* How It Works Section */}
<HowItWorks />
{/* Features Section */}
{/* Gallery Section */}
<section className="py-16 bg-gradient-to-br from-amber-50 to-orange-50">
<div className="container mx-auto px-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
className="text-center mb-12"
>
<h2 className="font-merchant text-3xl mb-4">Postcard Gallery</h2>
<p className="text-gray-600 font-poppins max-w-2xl mx-auto">
Get inspired by beautiful postcards created by our community of travelers
</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{[
{
image: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400&h=600&fit=crop',
title: 'Tropical Paradise',
message: 'Greetings from paradise! The beaches here are absolutely breathtaking...',
location: 'Maldives'
},
{
image: 'https://images.unsplash.com/photo-1499856871958-5b9627545d1a?w=400&h=600&fit=crop',
title: 'City Adventures',
message: 'Having the most amazing time exploring this incredible city...',
location: 'Paris, France'
},
{
image: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400&h=600&fit=crop',
title: 'Mountain Views',
message: 'The views from up here are simply unbelievable. Wish you were here...',
location: 'Swiss Alps'
},
{
image: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400&h=600&fit=crop',
title: 'Cultural Journey',
message: 'Immersing myself in the rich culture and history of this amazing place...',
location: 'Kyoto, Japan'
},
{
image: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400&h=600&fit=crop',
title: 'Safari Adventure',
message: 'Just saw the most incredible wildlife! This experience is unforgettable...',
location: 'Kenya, Africa'
},
{
image: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400&h=600&fit=crop',
title: 'Island Escape',
message: 'Living the island life and loving every moment of this peaceful retreat...',
location: 'Santorini, Greece'
}
].map((postcard, index) => (
<motion.div
key={index}
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.6, delay: 0.3 + index * 0.1 }}
>
<Card className="hover:shadow-xl transition-all duration-300 transform hover:-translate-y-2">
<CardContent className="p-0">
<div className="relative">
<ImageWithFallback
src={postcard.image}
alt={postcard.title}
className="w-full h-48 object-cover rounded-t-lg"
/>
<Badge className="absolute top-3 left-3 bg-white/90 text-gray-700">
{postcard.location}
</Badge>
</div>
<div className="p-6">
<h3 className="font-merchant text-lg mb-2">{postcard.title}</h3>
<p className="text-gray-600 font-poppins text-sm italic">"{postcard.message}"</p>
</div>
</CardContent>
</Card>
</motion.div>
))}
</div>
</div>
</section>
{/* Ready to Explore Melbourne Section */}
{/* Mobile App Section */}
<MobileAppSection />
{/* Customer Reviews */}
<EnhancedTestimonials />
{/* Footer */}
<Footer
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onPassesClick={onPassesClick}
onSignInClick={onSignInClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onContactUsClick={onContactUsClick}
currentPage={currentPage}
/>
</div>
);
}

View File

@@ -0,0 +1,420 @@
import { useState, useEffect } from 'react';
import { motion, useScroll, useTransform } from 'motion/react';
import { ArrowLeft } from 'lucide-react';
import Navbar from './Navbar';
import { CitySubmenu } from './CitySubmenu';
import { Footer } from './Footer';
import { MobileAppSection } from './MobileAppSection';
import { WhyChooseCityCards } from './WhyChooseCityCards';
import { EnhancedTestimonials } from './EnhancedTestimonials';
import { ReviewsSection } from './ReviewsSection';
interface User {
email: string;
name: string;
}
interface PrivacyPolicyPageProps {
onBackClick: () => void;
onHomeClick: () => void;
onMelbourneClick: () => void;
onPassesClick: () => void;
onCheckoutClick: () => void;
onSignInClick: () => void;
onSignOutClick: () => void;
onAttractionsClick: () => void;
onBlogsClick: () => void;
onHowItWorksClick: () => void;
onFAQClick: () => void;
onPrivacyPolicyClick: () => void;
onAboutUsClick: () => void;
onProfileClick: () => void;
onCityCardsClick: () => void;
onMagicItineraryClick: () => void;
onPostCardsClick: () => void;
onOffersClick: () => void;
onContactUsClick?: () => void;
onEsimsClick?: () => void;
onHotelDiscountsClick?: () => void;
currentPage: string;
user: User | null;
}
export function PrivacyPolicyPage({
onBackClick,
onHomeClick,
onMelbourneClick,
onPassesClick,
onCheckoutClick,
onSignInClick,
onSignOutClick,
onAttractionsClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onProfileClick,
onCityCardsClick,
onMagicItineraryClick,
onPostCardsClick,
onOffersClick,
onContactUsClick,
onEsimsClick,
onHotelDiscountsClick,
currentPage,
user
}: PrivacyPolicyPageProps) {
const { scrollY } = useScroll();
const headerOpacity = useTransform(scrollY, [0, 200], [1, 0.3]);
const headerY = useTransform(scrollY, [0, 200], [0, -50]);
return (
<div className="min-h-screen bg-background">
{/* Navigation */}
<Navbar
activeCity="Melbourne"
onCityChange={() => {}}
onHomeClick={onHomeClick}
onSignInClick={onSignInClick}
onPassesClick={onPassesClick}
onCheckoutClick={onCheckoutClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick}
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
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-merchant font-light text-4xl md:text-5xl lg:text-6xl mb-4">
Privacy{' '}
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
Policy
</span>
</h1>
<p className="font-poppins text-xl leading-relaxed font-normal text-gray-600 max-w-3xl mx-auto">
Learn how we collect, use, and protect your personal information
</p>
</motion.div>
{/* Privacy Policy Content */}
<div className="max-w-6xl mx-auto space-y-16">
{/* Section 1: Data Protection */}
<motion.section
className="space-y-8"
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<h3 className="text-2xl md:text-3xl font-semibold text-foreground">
1. DATA PROTECTION, CONTROLLER AND DATA PRIVACY OFFICER
</h3>
<div className="max-w-3xl space-y-6 text-left">
<p className="text-muted-foreground leading-relaxed">
We would like to inform you about how we collect personal data when you use our website.
</p>
<p className="text-muted-foreground leading-relaxed">
Personal data is any data which relates to you personally, such as your name, address, e-mail
addresses, and user behavior.
</p>
<p className="text-muted-foreground leading-relaxed">
The controller responsible for processing your personal data through this website is
</p>
<div className="text-muted-foreground space-y-1">
<p>CityCards Australia Pty Ltd</p>
<p>Level 15, 333 Collins Street</p>
<p>Melbourne VIC 3000</p>
<p>Australia</p>
<p className="text-primary">hello@citycards.com.au</p>
<p>e-mail: <span className="text-primary">privacy@citycards.com.au</span></p>
</div>
<p className="text-muted-foreground leading-relaxed">
You can contact our data privacy officer using the email address{' '}
<span className="text-primary">privacy@citycards.com.au</span> or our postal address,
with the addition of "The Data Privacy Officer".
</p>
</div>
</motion.section>
{/* Section 2: Cookies */}
<motion.section
className="space-y-8"
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.1 }}
>
<h3 className="text-2xl md:text-3xl font-semibold text-foreground">
2. COOKIES
</h3>
<div className="max-w-3xl space-y-6 text-left">
<div className="bg-primary/5 rounded-xl p-6 border border-primary/10">
<h4 className="text-lg font-medium text-foreground mb-4">Change cookie settings</h4>
<p className="text-muted-foreground text-sm">
You can manage your cookie preferences at any time through your browser settings.
</p>
</div>
<p className="text-muted-foreground leading-relaxed">
To make our website easier to use, we use functional cookies, in which we e.g. store your language
settings. Using functional cookies represents a legitimate interest on our part. The legal basis of this
is also Art. 6, Par. 1 lit. f GDPR.
</p>
<p className="text-muted-foreground leading-relaxed">
Cookies are small text files which are copied onto your hard drive and deleted again automatically
according to the settings in your browser or after a defined period. Cookies cannot execute any
programs or place viruses on your computer. They serve to make the website more user-friendly and
effective as a whole.
</p>
<p className="text-muted-foreground leading-relaxed">
You can set your browser at any time to permit or exclude the use of cookies, or to ask for your
confirmation before using them, and to delete them automatically after each session. If you do
not allow cookies, it may be that not all the functions of our website are available to you.
</p>
</div>
</motion.section>
{/* Section 3: Data Transfer */}
<motion.section
className="space-y-8"
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.2 }}
>
<h3 className="text-2xl md:text-3xl font-semibold text-foreground">
3. INFORMATION ON DATA TRANSFER TO THE USA AND OTHER NON-EU COUNTRIES
</h3>
<div className="max-w-3xl space-y-6 text-left">
<p className="text-muted-foreground leading-relaxed">
Among other things, we use tools of companies domiciled in the United States or other from a data
protection perspective non-secure non-EU countries.
</p>
<p className="text-muted-foreground leading-relaxed">
If these tools are active, your personal data may potentially be transferred to these non-EU countries
and may be processed there. We must point out that in these countries, a data protection level that is
comparable to that in the EU cannot be guaranteed. For instance, U.S. enterprises are under a mandate
to release personal data to the security agencies and you as the data subject do not have any litigation
options to defend yourself in court. Hence, it cannot be ruled out that U.S. agencies (e.g., the Secret
Service) may process, analyze, and permanently archive your personal data for surveillance purposes.
We have no control over these processing activities.
</p>
</div>
</motion.section>
{/* Section 4: Data Collection */}
<motion.section
className="space-y-8"
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.3 }}
>
<h3 className="text-2xl md:text-3xl font-semibold text-foreground">
4. DATA COLLECTION ON OUR WEBSITE
</h3>
<div className="max-w-3xl space-y-6 text-left">
<p className="text-muted-foreground leading-relaxed">
When you visit our website, we collect certain information automatically as part of the website's
basic functionality. This includes your IP address, browser type, operating system, referring website,
and the date and time of your visit.
</p>
<p className="text-muted-foreground leading-relaxed">
We also collect information that you provide to us directly, such as when you create an account,
make a purchase, or contact us with questions or feedback. This may include your name, email
address, phone number, payment information, and any other information you choose to provide.
</p>
</div>
</motion.section>
{/* Section 5: Your Rights */}
<motion.section
className="space-y-8"
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.4 }}
>
<h3 className="text-2xl md:text-3xl font-semibold text-foreground">
5. YOUR RIGHTS
</h3>
<div className="max-w-3xl space-y-6 text-left">
<p className="text-muted-foreground leading-relaxed">
Under data protection law, you have the following rights in relation to your personal data:
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-left">
<div className="bg-muted/30 rounded-lg p-4">
<h4 className="font-medium text-foreground mb-2">Right of Access</h4>
<p className="text-sm text-muted-foreground">
You can request information about the personal data we hold about you.
</p>
</div>
<div className="bg-muted/30 rounded-lg p-4">
<h4 className="font-medium text-foreground mb-2">Right to Rectification</h4>
<p className="text-sm text-muted-foreground">
You can request that we correct any inaccurate personal data.
</p>
</div>
<div className="bg-muted/30 rounded-lg p-4">
<h4 className="font-medium text-foreground mb-2">Right to Erasure</h4>
<p className="text-sm text-muted-foreground">
You can request that we delete your personal data in certain circumstances.
</p>
</div>
<div className="bg-muted/30 rounded-lg p-4">
<h4 className="font-medium text-foreground mb-2">Right to Portability</h4>
<p className="text-sm text-muted-foreground">
You can request a copy of your personal data in a structured format.
</p>
</div>
</div>
</div>
</motion.section>
{/* Section 6: Updates */}
<motion.section
className="space-y-8"
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.5 }}
>
<h3 className="text-2xl md:text-3xl font-semibold text-foreground">
6. UPDATES TO THIS PRIVACY POLICY
</h3>
<div className="max-w-3xl space-y-6 text-left">
<p className="text-muted-foreground leading-relaxed">
We may update this privacy policy from time to time to reflect changes in our practices or
applicable laws. When we make changes, we will update the "Last Updated" date at the bottom
of this policy and notify you through our website or other appropriate means.
</p>
<p className="text-muted-foreground leading-relaxed">
We encourage you to review this privacy policy periodically to stay informed about how we
collect, use, and protect your information.
</p>
</div>
</motion.section>
{/* Last Updated */}
<motion.div
className="text-center pt-8 border-t border-border/20"
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.6 }}
>
<p className="text-sm text-muted-foreground">
Last updated: December 2024
</p>
</motion.div>
</div>
</div>
{/* Additional Sections from Homepage */}
{/* Mobile App Section - "Access all your city cards" */}
<motion.section
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8, ease: "easeOut" }}
>
<MobileAppSection />
</motion.section>
{/* Why Choose Us Section */}
<motion.section
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8, ease: "easeOut", delay: 0.2 }}
>
<WhyChooseCityCards />
</motion.section>
{/* Enhanced Testimonials Section */}
<motion.section
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8, ease: "easeOut", delay: 0.3 }}
>
<EnhancedTestimonials />
</motion.section>
{/* Customer Reviews Section */}
<motion.section
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.8, ease: "easeOut", delay: 0.4 }}
>
<ReviewsSection />
</motion.section>
{/* Footer */}
<Footer
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onPassesClick={onPassesClick}
onSignInClick={onSignInClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onContactUsClick={onContactUsClick}
currentPage={currentPage}
/>
</div>
);
}

View File

@@ -0,0 +1,786 @@
import { useState } from 'react';
import { motion } from 'motion/react';
import {
ArrowLeft,
User,
CreditCard,
Calendar,
MapPin,
Settings,
Download,
QrCode,
Plus,
Clock,
Star,
Badge as BadgeIcon,
Camera
} from 'lucide-react';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Label } from './ui/label';
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
import { Separator } from './ui/separator';
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
import { Badge } from './ui/badge';
import Navbar from './Navbar';
import { Footer } from './Footer';
import { ImageWithFallback } from './figma/ImageWithFallback';
interface ProfilePageProps {
onBackClick: () => void;
onHomeClick: () => void;
onMelbourneClick: () => void;
onPassesClick: () => void;
onCheckoutClick: () => void;
onSignInClick: () => void;
onSignOutClick?: () => void;
onAttractionsClick: () => void;
onBlogsClick: () => void;
onHowItWorksClick: () => void;
onFAQClick: () => void;
onPrivacyPolicyClick: () => void;
onAboutUsClick: () => void;
onProfileClick: () => void;
onCityCardsClick: () => void;
onMagicItineraryClick: () => void;
onPostCardsClick: () => void;
onCreateItineraryClick: () => void;
onViewItineraryClick?: () => void;
onOffersClick: () => void;
onDownloadAppClick?: () => void;
onContactUsClick?: () => void;
onEsimsClick?: () => void;
onHotelDiscountsClick?: () => void;
currentPage: string;
}
// Mock user data
const mockUserData = {
firstName: 'John',
lastName: 'Doe',
email: 'john.doe@example.com',
phone: '+1 (555) 123-4567',
country: 'us',
address: '123 Main Street',
city: 'New York',
postalCode: '10001'
};
// Mock passes data
const mockPasses = [
{
id: '1',
name: 'Melbourne Unlimited Card',
city: 'Melbourne',
type: 'Unlimited Pass',
status: 'active',
price: 149.00,
originalPrice: 249.00,
discount: 40,
attractions: 25,
validFrom: '2024-01-15',
validUntil: '2024-01-22',
daysRemaining: 3,
image: 'https://images.unsplash.com/photo-1514395462725-fb4566210144?w=400',
usedAttractions: 8
},
{
id: '2',
name: 'Melbourne Selective Card',
city: 'Melbourne',
type: 'Selective Pass',
status: 'active',
price: 89.00,
originalPrice: 149.00,
discount: 40,
attractions: 12,
validFrom: '2024-02-01',
validUntil: '2024-02-08',
daysRemaining: 12,
image: 'https://images.unsplash.com/photo-1502602898536-47ad22581b52?w=400',
usedAttractions: 3
},
{
id: '3',
name: 'Sydney Explorer Pass',
city: 'Sydney',
type: 'Standard Pass',
status: 'expired',
price: 89.00,
originalPrice: 149.00,
discount: 40,
attractions: 15,
validFrom: '2023-12-01',
validUntil: '2023-12-08',
daysRemaining: 0,
image: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=400',
usedAttractions: 12
}
];
// Mock itineraries data
const mockItineraries = [
{
id: '1',
name: 'Melbourne Unlimited Card',
city: 'Melbourne',
duration: '7 days',
attractions: 25,
createdDate: '2024-01-15',
status: 'active'
}
];
export function ProfilePage({
onBackClick,
onHomeClick,
onMelbourneClick,
onPassesClick,
onCheckoutClick,
onSignInClick,
onSignOutClick,
onAttractionsClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onProfileClick,
onCityCardsClick,
onMagicItineraryClick,
onPostCardsClick,
onCreateItineraryClick,
onViewItineraryClick,
onOffersClick,
onDownloadAppClick,
onContactUsClick,
onEsimsClick,
onHotelDiscountsClick,
currentPage
}: ProfilePageProps) {
const [activeTab, setActiveTab] = useState('profile');
const [formData, setFormData] = useState(mockUserData);
const handleInputChange = (field: string, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const handleSaveProfile = () => {
console.log('Saving profile...', formData);
// Handle profile save
};
const activePasses = mockPasses.filter(pass => pass.status === 'active');
const expiredPasses = mockPasses.filter(pass => pass.status === 'expired');
return (
<div className="min-h-screen bg-background">
{/* Navbar */}
<Navbar
activeCity=""
onCityChange={() => {}}
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onPassesClick={onPassesClick}
onCheckoutClick={onCheckoutClick}
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onAboutUsClick={onAboutUsClick}
onProfileClick={onProfileClick}
onCityCardsClick={onCityCardsClick}
onMagicItineraryClick={onMagicItineraryClick}
onPostCardsClick={onPostCardsClick}
onOffersClick={onOffersClick}
currentPage={currentPage}
isUserSignedIn={true}
user={{ email: "user@example.com", name: "John Doe" }}
/>
{/* Header Section */}
<section className="pt-40 pb-8 bg-gradient-to-br from-muted/30 to-background">
<div className="container mx-auto px-4">
{/* Back Button */}
<motion.button
onClick={onBackClick}
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-6 transition-colors duration-200"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.5 }}
>
<ArrowLeft className="w-5 h-5" />
<span className="font-normal">Back</span>
</motion.button>
{/* Page Title */}
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
>
<h1 className="font-poppins text-3xl md:text-4xl lg:text-5xl mb-4">
<span className="font-light">My</span>{' '}
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">Profile</span>
</h1>
<p className="font-poppins text-xl leading-relaxed font-normal text-gray-600">
Manage your account, passes, and travel itineraries
</p>
</motion.div>
</div>
</section>
{/* Main Content */}
<section className="py-12">
<div className="container mx-auto px-4">
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-8">
{/* Tab Navigation */}
<TabsList className="grid w-full grid-cols-3 lg:w-[400px]">
<TabsTrigger value="profile" className="font-poppins font-light">My Profile</TabsTrigger>
<TabsTrigger value="passes" className="font-poppins font-light">My Passes</TabsTrigger>
<TabsTrigger value="itineraries" className="font-poppins font-light">My Itineraries</TabsTrigger>
</TabsList>
{/* My Profile Tab */}
<TabsContent value="profile" className="space-y-8">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Profile Form */}
<div className="lg:col-span-2">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 font-poppins font-normal">
<User className="w-5 h-5" />
Personal Information
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor="firstName" className="font-poppins font-light">First Name</Label>
<Input
id="firstName"
value={formData.firstName}
onChange={(e) => handleInputChange('firstName', e.target.value)}
className="mt-1 font-poppins font-light"
/>
</div>
<div>
<Label htmlFor="lastName" className="font-poppins font-light">Last Name</Label>
<Input
id="lastName"
value={formData.lastName}
onChange={(e) => handleInputChange('lastName', e.target.value)}
className="mt-1 font-poppins font-light"
/>
</div>
</div>
<div>
<Label htmlFor="email" className="font-poppins font-light">Email Address</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleInputChange('email', e.target.value)}
className="mt-1 font-poppins font-light"
/>
</div>
<div>
<Label htmlFor="phone" className="font-poppins font-light">Phone Number</Label>
<Input
id="phone"
type="tel"
value={formData.phone}
onChange={(e) => handleInputChange('phone', e.target.value)}
className="mt-1 font-poppins font-light"
/>
</div>
<Separator />
<h3 className="font-poppins font-normal">Billing Address</h3>
<div>
<Label htmlFor="country" className="font-poppins font-light">Country</Label>
<Select value={formData.country} onValueChange={(value) => handleInputChange('country', value)}>
<SelectTrigger className="mt-1">
<SelectValue placeholder="Select country" />
</SelectTrigger>
<SelectContent>
<SelectItem value="us">United States</SelectItem>
<SelectItem value="au">Australia</SelectItem>
<SelectItem value="uk">United Kingdom</SelectItem>
<SelectItem value="ca">Canada</SelectItem>
<SelectItem value="de">Germany</SelectItem>
<SelectItem value="fr">France</SelectItem>
<SelectItem value="in">India</SelectItem>
<SelectItem value="jp">Japan</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="address" className="font-poppins font-light">Street Address</Label>
<Input
id="address"
value={formData.address}
onChange={(e) => handleInputChange('address', e.target.value)}
className="mt-1 font-poppins font-light"
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor="city" className="font-poppins font-light">City</Label>
<Input
id="city"
value={formData.city}
onChange={(e) => handleInputChange('city', e.target.value)}
className="mt-1 font-poppins font-light"
/>
</div>
<div>
<Label htmlFor="postalCode" className="font-poppins font-light">Postal Code</Label>
<Input
id="postalCode"
value={formData.postalCode}
onChange={(e) => handleInputChange('postalCode', e.target.value)}
className="mt-1 font-poppins font-light"
/>
</div>
</div>
<Button
onClick={handleSaveProfile}
className="w-full bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-normal py-3 font-poppins"
>
Save Changes
</Button>
</CardContent>
</Card>
</motion.div>
</div>
{/* App Download Section */}
<div className="lg:col-span-1">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.4 }}
>
<Card className="border border-gray-200 shadow-sm">
<CardContent className="p-8 space-y-6">
{(() => {
// Determine which pass type to show
const hasUnlimitedPass = activePasses.some(pass => pass.type === 'Unlimited Pass');
const hasSelectivePass = activePasses.some(pass => pass.type === 'Selective Pass');
if (hasUnlimitedPass) {
return (
<>
<div className="space-y-3">
<h3 className="font-poppins text-xl font-normal">
Get{' '}
<span className="text-primary">Melbourne Unlimited Card</span>
</h3>
<p className="text-sm text-gray-600 font-poppins leading-relaxed font-light">
Unlimited access to 25+ attractions. Visit as many places as you want with one simple card.
Save up to 40% compared to individual tickets.
</p>
</div>
{/* Card Benefits */}
<div className="bg-gradient-to-br from-primary/5 to-secondary/5 rounded-lg p-4 space-y-2">
<div className="flex items-center gap-2 text-sm font-poppins font-normal">
<div className="w-5 h-5 rounded-full bg-primary flex items-center justify-center flex-shrink-0">
<CreditCard className="w-3 h-3 text-white" />
</div>
<span>Unlimited entries to all attractions</span>
</div>
<div className="flex items-center gap-2 text-sm font-poppins font-normal">
<div className="w-5 h-5 rounded-full bg-primary flex items-center justify-center flex-shrink-0">
<Calendar className="w-3 h-3 text-white" />
</div>
<span>Valid for 7 consecutive days</span>
</div>
<div className="flex items-center gap-2 text-sm font-poppins font-normal">
<div className="w-5 h-5 rounded-full bg-primary flex items-center justify-center flex-shrink-0">
<MapPin className="w-3 h-3 text-white" />
</div>
<span>Skip the queue at major venues</span>
</div>
</div>
{/* Purchase CTA */}
<div className="space-y-3">
<Button
onClick={onPassesClick}
className="w-full bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-poppins font-medium h-12"
>
Purchase Unlimited Card
</Button>
<Button
variant="outline"
onClick={onCityCardsClick}
className="w-full font-poppins font-normal"
>
Learn More
</Button>
</div>
</>
);
} else if (hasSelectivePass) {
return (
<>
<div className="space-y-3">
<h3 className="font-poppins text-xl font-normal">
Get{' '}
<span className="text-primary">Selective Card</span>
{' '}now
</h3>
<p className="text-sm text-gray-600 font-poppins leading-relaxed font-light">
Choose your own adventure with 12 hand-picked attractions. Perfect for visitors
who want flexibility and value.
</p>
</div>
{/* Card Benefits */}
<div className="bg-gradient-to-br from-primary/5 to-secondary/5 rounded-lg p-4 space-y-2">
<div className="flex items-center gap-2 text-sm font-poppins font-normal">
<div className="w-5 h-5 rounded-full bg-primary flex items-center justify-center flex-shrink-0">
<CreditCard className="w-3 h-3 text-white" />
</div>
<span>Choose from 12 curated attractions</span>
</div>
<div className="flex items-center gap-2 text-sm font-poppins font-normal">
<div className="w-5 h-5 rounded-full bg-primary flex items-center justify-center flex-shrink-0">
<Calendar className="w-3 h-3 text-white" />
</div>
<span>Flexible 7-day validity period</span>
</div>
<div className="flex items-center gap-2 text-sm font-poppins font-normal">
<div className="w-5 h-5 rounded-full bg-primary flex items-center justify-center flex-shrink-0">
<Star className="w-3 h-3 text-white" />
</div>
<span>Save 40% on combined ticket price</span>
</div>
</div>
{/* Purchase CTA */}
<div className="space-y-3">
<Button
onClick={onPassesClick}
className="w-full bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-poppins font-medium h-12"
>
Purchase Selective Card
</Button>
<Button
variant="outline"
onClick={onCityCardsClick}
className="w-full font-poppins font-normal"
>
View All Attractions
</Button>
</div>
</>
);
} else {
return (
<>
<div className="space-y-3">
<h3 className="font-poppins text-xl font-normal">
Get{' '}
<span className="text-primary">CityCards</span>
{' '}now
</h3>
<p className="text-sm text-gray-600 font-poppins leading-relaxed font-light">
Explore Melbourne's best attractions with our flexible card options.
Choose unlimited access or select your favorites.
</p>
</div>
{/* Card Options */}
<div className="space-y-3">
<div className="bg-gradient-to-br from-primary/5 to-secondary/5 rounded-lg p-4">
<div className="flex items-start justify-between mb-2">
<div>
<h4 className="font-poppins text-base font-medium mb-1">Unlimited Card</h4>
<p className="text-xs text-gray-600 font-poppins font-light">25+ attractions, unlimited visits</p>
</div>
<Badge className="bg-primary text-white">Popular</Badge>
</div>
<div className="text-2xl font-poppins font-semibold text-primary">$149</div>
</div>
<div className="bg-gray-50 rounded-lg p-4">
<div className="flex items-start justify-between mb-2">
<div>
<h4 className="font-poppins text-base font-medium mb-1">Selective Card</h4>
<p className="text-xs text-gray-600 font-poppins font-light">12 attractions of your choice</p>
</div>
</div>
<div className="text-2xl font-poppins font-semibold text-primary">$89</div>
</div>
</div>
{/* Purchase CTA */}
<div className="space-y-3">
<Button
onClick={onPassesClick}
className="w-full bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-poppins font-medium h-12"
>
Explore All Cards
</Button>
<Button
variant="outline"
onClick={onCityCardsClick}
className="w-full font-poppins font-normal"
>
Learn More
</Button>
</div>
</>
);
}
})()}
</CardContent>
</Card>
</motion.div>
</div>
</div>
</TabsContent>
{/* My Passes Tab */}
<TabsContent value="passes" className="space-y-8">
{/* Active Passes */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<h2 className="font-poppins text-2xl mb-6 font-normal">Active Passes</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{activePasses.map((pass) => (
<Card key={pass.id} className="overflow-hidden">
<div
className="flex cursor-pointer hover:bg-gray-50 transition-colors duration-200 rounded-lg p-2 -m-2"
onClick={() => onDownloadAppClick?.()}
>
<div className="w-32 h-32 flex-shrink-0">
<ImageWithFallback
src={pass.image}
alt={pass.city}
className="w-full h-full object-cover"
/>
</div>
<div className="flex-1 p-6">
<div className="flex items-start justify-between mb-2">
<div>
<h3 className="font-normal font-poppins">{pass.name}</h3>
<Badge variant={pass.status === 'active' ? 'default' : 'secondary'} className="mt-1">
{pass.status}
</Badge>
</div>
<div className="text-right">
<div className="font-semibold text-lg font-poppins">${pass.price}</div>
<div className="text-sm text-gray-500 line-through font-poppins font-light">${pass.originalPrice}</div>
</div>
</div>
<div className="space-y-2 text-sm font-poppins font-light">
<div className="flex justify-between">
<span>Attractions:</span>
<span>{pass.usedAttractions}/{pass.attractions}</span>
</div>
<div className="flex justify-between">
<span>Valid until:</span>
<span>{new Date(pass.validUntil).toLocaleDateString()}</span>
</div>
<div className="flex justify-between">
<span>Days remaining:</span>
<span className="text-primary font-normal">{pass.daysRemaining} days</span>
</div>
</div>
</div>
</div>
</Card>
))}
</div>
{/* Offers Button */}
<div className="mt-8 text-center">
<Button
onClick={onOffersClick}
className="bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-poppins px-8 py-3 font-normal"
>
<Star className="w-4 h-4 mr-2" />
View Special Offers
</Button>
</div>
</motion.div>
{/* Expired Passes */}
{expiredPasses.length > 0 && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.4 }}
>
<h2 className="font-poppins text-2xl mb-6 font-normal">Expired Passes</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{expiredPasses.map((pass) => (
<Card key={pass.id} className="overflow-hidden opacity-60">
<div className="flex">
<div className="w-32 h-32 flex-shrink-0">
<ImageWithFallback
src={pass.image}
alt={pass.city}
className="w-full h-full object-cover grayscale"
/>
</div>
<div className="flex-1 p-6">
<div className="flex items-start justify-between mb-2">
<div>
<h3 className="font-normal font-poppins">{pass.name}</h3>
<Badge variant="secondary" className="mt-1">
{pass.status}
</Badge>
</div>
<div className="text-right">
<div className="font-semibold text-lg font-poppins">${pass.price}</div>
</div>
</div>
<div className="space-y-2 text-sm font-poppins font-light">
<div className="flex justify-between">
<span>Attractions visited:</span>
<span>{pass.usedAttractions}/{pass.attractions}</span>
</div>
<div className="flex justify-between">
<span>Expired on:</span>
<span>{new Date(pass.validUntil).toLocaleDateString()}</span>
</div>
</div>
</div>
</div>
</Card>
))}
</div>
</motion.div>
)}
</TabsContent>
{/* My Itineraries Tab */}
<TabsContent value="itineraries" className="space-y-8">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<div className="flex items-center justify-between mb-6">
<h2 className="font-poppins text-2xl font-normal">My Itineraries</h2>
<Button
className="bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-poppins font-normal"
onClick={onCreateItineraryClick}
>
<Plus className="w-4 h-4 mr-2" />
Create Itinerary
</Button>
</div>
{mockItineraries.length > 0 ? (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{mockItineraries.map((itinerary) => (
<Card key={itinerary.id}>
<CardContent className="p-6">
<div className="flex items-start justify-between mb-4">
<div>
<h3 className="font-normal font-poppins">{itinerary.name}</h3>
<p className="text-sm text-gray-600 font-poppins font-light">{itinerary.city}</p>
</div>
<Badge variant={itinerary.status === 'active' ? 'default' : 'secondary'}>
{itinerary.status}
</Badge>
</div>
<div className="space-y-2 text-sm font-poppins font-light">
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4 text-gray-500" />
<span>{itinerary.duration}</span>
</div>
<div className="flex items-center gap-2">
<MapPin className="w-4 h-4 text-gray-500" />
<span>{itinerary.attractions} attractions</span>
</div>
<div className="flex items-center gap-2">
<Clock className="w-4 h-4 text-gray-500" />
<span>Created {new Date(itinerary.createdDate).toLocaleDateString()}</span>
</div>
</div>
<Button
variant="outline"
className="w-full mt-4 font-poppins font-normal"
onClick={onViewItineraryClick}
>
View Itinerary
</Button>
</CardContent>
</Card>
))}
</div>
) : (
<div className="text-center py-16">
<div className="max-w-md mx-auto">
<div className="w-24 h-24 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-6">
<Calendar className="w-12 h-12 text-gray-400" />
</div>
<h3 className="font-poppins text-xl mb-4 font-normal">You don't have an itinerary yet</h3>
<p className="text-gray-600 mb-6 font-poppins font-light">
Create your first itinerary to plan your perfect trip
</p>
<Button
className="bg-gradient-to-r from-primary to-secondary hover:from-primary/90 hover:to-secondary/90 text-white font-poppins font-normal"
onClick={onCreateItineraryClick}
>
<Plus className="w-4 h-4 mr-2" />
Create Itinerary
</Button>
</div>
</div>
)}
</motion.div>
</TabsContent>
</Tabs>
</div>
</section>
{/* Footer */}
<Footer
onHomeClick={onHomeClick}
onMelbourneClick={onMelbourneClick}
onPassesClick={onPassesClick}
onSignInClick={onSignInClick}
onAttractionsClick={onAttractionsClick}
onBlogsClick={onBlogsClick}
onHowItWorksClick={onHowItWorksClick}
onFAQClick={onFAQClick}
onPrivacyPolicyClick={onPrivacyPolicyClick}
onContactUsClick={onContactUsClick}
currentPage={currentPage}
/>
</div>
);
}
export default ProfilePage;

View File

@@ -0,0 +1,57 @@
import { useState } from 'react';
import { motion } from 'motion/react';
import { Star, ChevronLeft, ChevronRight } from 'lucide-react';
import { ImageWithFallback } from './figma/ImageWithFallback';
// Customer review data
const reviews = [
{
id: 1,
name: 'Andrew Sarma',
role: 'Entrepreneur',
review: 'Salty helped me a lot in finding the best place for our first outdoor adventure trip. They responded very quickly and gave me a detailed account of the place— its history, as well as its best features.',
rating: 5,
image: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtYW4lMjBwb3J0cmFpdCUyMGJ1c2luZXNzfGVufDF8fHx8MTc1NjEyNDA4M3ww&ixlib=rb-4.1.0&q=80&w=1080'
},
{
id: 2,
name: 'Sarah Chen',
role: 'Travel Blogger',
review: 'The Melbourne City Card exceeded all my expectations! The instant QR access made exploring so convenient, and I saved nearly 40% on attraction tickets. The exclusive perks were incredible.',
rating: 5,
image: 'https://images.unsplash.com/photo-1494790108755-2616b612b786?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx3b21hbiUyMHBvcnRyYWl0JTIwc21pbGluZ3xlbnwxfHx8fDE3NTYxMjQwODd8MA&ixlib=rb-4.1.0&q=80&w=1080'
},
{
id: 3,
name: 'Marcus Rodriguez',
role: 'Family Traveler',
review: 'Traveling with kids can be challenging, but CityCards made it seamless. The skip-the-line access and family-friendly recommendations were game-changers for our Melbourne trip.',
rating: 5,
image: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtYW4lMjBwb3J0cmFpdCUyMHNtaWxpbmd8ZW58MXx8fHwxNzU2MTI0MDkxfDA&ixlib=rb-4.1.0&q=80&w=1080'
}
];
export function ReviewsSection() {
const [currentReview, setCurrentReview] = useState(0);
const nextReview = () => {
setCurrentReview((prev) => (prev + 1) % reviews.length);
};
const prevReview = () => {
setCurrentReview((prev) => (prev - 1 + reviews.length) % reviews.length);
};
const renderStars = (rating: number) => {
return Array.from({ length: 5 }, (_, index) => (
<Star
key={index}
className={`w-4 h-4 ${
index < rating ? 'text-yellow-400 fill-current' : 'text-gray-300'
}`}
/>
));
};
return null;
}

Some files were not shown because too many files have changed in this diff Show More