new src added
32
.gitignore
vendored
Normal 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
@@ -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
@@ -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
64
package.json
Normal 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
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
}
|
||||
}
|
||||
1209
src/App.tsx
Normal file
3
src/Attributions.md
Normal 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).
|
||||
BIN
src/assets/01db7df74b38a6ea709f973ff1a3ce03c9a5a75d.png
Normal file
|
After Width: | Height: | Size: 249 KiB |
BIN
src/assets/0920e544195a4f6cffeb43277fdbb28bbf0e88b6.png
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
src/assets/129015504c88093e4b54dc6e8c91b3997d9ccc5b.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
src/assets/1512fbc49cf628796adeccca6752a29d1e61ecf4.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src/assets/183ba346669a0a079762ab3f9535b7d1469ed4d9.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
src/assets/1d756a3ad6ee9098afb373e07e18ddd36571954c.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
src/assets/26f0daececd90e6473f4b370af7b26b7fb2a67ad.png
Normal file
|
After Width: | Height: | Size: 219 KiB |
BIN
src/assets/2a32b13e0ed94d79c4867062010cb1a848d72dc1.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src/assets/3c5d5a12a7b44ab1b1ac2db7faa54270de100d86.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/assets/43f3bc1f9c8cc5b8f60f3f6be0bc1ad29eded0d7.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
src/assets/4af0d5b295cf8650b4caa23233d3dcf539032a73.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
src/assets/5da1b0444c0d21bc7ee776c49e36e2a8ea4d3e12.png
Normal file
|
After Width: | Height: | Size: 401 KiB |
BIN
src/assets/66c6d4d2fa6c02d9f60e12fcde70c13d4a78d0b7.png
Normal file
|
After Width: | Height: | Size: 6.3 KiB |
BIN
src/assets/6c1d7aa4854f71023510e35ac1f4eb623441cb4a.png
Normal file
|
After Width: | Height: | Size: 3.3 MiB |
BIN
src/assets/84c8cfb942f3c96e9f5fbb29459a9861138f61d8.png
Normal file
|
After Width: | Height: | Size: 936 KiB |
BIN
src/assets/84f4f124e1476f3bb1eee24bc4c0fa75f1fed2a8.png
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
src/assets/916a2ac02159f19d4e05b6fbbad76f315c8dfb66.png
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
src/assets/a384e2e7712b48e54fadc7594244c40162ad4152.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/assets/a3956d2491d1e44ca71fc856b229d3e22523f828.png
Normal file
|
After Width: | Height: | Size: 350 B |
BIN
src/assets/a812c79b0d125e4946b33446eae0353f518627e2.png
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
src/assets/ad526a204d3f66901399ea0452aaf33e105b0f32.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
src/assets/bc70aef6686e5f4d059b5ef3380fd4f44bb9f4c6.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
src/assets/cc387e616b2e6552715d051e5c9a41e1a1637857.png
Normal file
|
After Width: | Height: | Size: 272 KiB |
BIN
src/assets/cit-logo.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
src/assets/d37e3e3f9d3f32fcf86dddd187aa6fd48832406a.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
src/assets/dda914a03c57961a1affac724d29681004341456.png
Normal file
|
After Width: | Height: | Size: 991 B |
BIN
src/assets/e8091276ed2c976b5c975f21687b0d1702bd9c90.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
src/assets/e96a0ba8c1e8ee053e3eb462a3b4552a8657e7b6.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
src/assets/eaf15191e9a315d2d4b384ffcb22910687c3d328.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src/assets/eb1420e4d74097842eb197fc60970d29fa0afd1f.png
Normal file
|
After Width: | Height: | Size: 234 KiB |
BIN
src/assets/ef6f48b2b60d04fc8723d03a71f9dca2b6067704.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src/assets/f32abfc6ad0bc41084ce6dd6d98890502970dda7.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
src/assets/f6c7124d0a9ca2463b53b6a370a48c4c0bbba57d.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src/assets/f9d75a20370022ef4bcdb5f3d08841527703cee6.png
Normal file
|
After Width: | Height: | Size: 197 KiB |
93
src/batch_replace_fonts.py
Normal 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()
|
||||
539
src/components/AboutUsPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
79
src/components/ArticlesSection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
474
src/components/AttractionDetailsPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
225
src/components/AttractionHassleFreeSection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
604
src/components/AttractionsPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
510
src/components/BlogDetailsPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
529
src/components/BlogsPage.tsx
Normal 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;
|
||||
410
src/components/BookAttractionSection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
93
src/components/CTAButton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
767
src/components/CheckoutPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
685
src/components/CitiesPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
747
src/components/CityAttractionsPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
391
src/components/CityCardsPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
285
src/components/CitySubmenu.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
490
src/components/ContactUsPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
1450
src/components/CreateMagicItineraryPage.tsx
Normal file
842
src/components/CustomPostcards.tsx
Normal 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
768
src/components/DownloadAppPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
323
src/components/EnhancedTestimonials.tsx
Normal 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
511
src/components/FAQPage.tsx
Normal 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;
|
||||
246
src/components/FeaturedCities.tsx
Normal 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
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
72
src/components/FooterBottom.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
68
src/components/FooterBrand.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
97
src/components/FooterNavigation.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
227
src/components/HandwrittenText.tsx
Normal 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
@@ -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>
|
||||
);
|
||||
}
|
||||
713
src/components/HeroSection.tsx
Normal 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
@@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
1
src/components/HorizontalHowItWorksPage.tsx
Normal file
@@ -0,0 +1 @@
|
||||
// This file has been removed - consolidated into HowItWorksPage.tsx
|
||||
875
src/components/HotelDiscountsPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
2224
src/components/HowItWorks.tsx
Normal file
406
src/components/HowItWorksPage.tsx
Normal 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;
|
||||
732
src/components/ItineraryViewPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
285
src/components/LoginModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
565
src/components/MagicItinerary.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
398
src/components/MagicItineraryPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
397
src/components/MelbourneAttractions.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
291
src/components/MelbourneBlogs.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
278
src/components/MelbourneCardComparison.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
212
src/components/MelbourneFAQ.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
256
src/components/MelbournePage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
277
src/components/MelbourneTourOverview.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
381
src/components/MobileAppPromotion.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
642
src/components/MobileAppSection.tsx
Normal 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
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
181
src/components/NewsletterSection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
853
src/components/OffersPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
195
src/components/OtherCities.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
142
src/components/PassComparison.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
657
src/components/PassesPage.tsx
Normal 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;
|
||||
268
src/components/PostCardsPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
420
src/components/PrivacyPolicyPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
786
src/components/ProfilePage.tsx
Normal 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;
|
||||
57
src/components/ReviewsSection.tsx
Normal 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;
|
||||
}
|
||||