new fixes

This commit is contained in:
priyanshuvish
2025-11-03 19:57:32 +05:30
parent 90f0cb953f
commit a55e473e3c
9 changed files with 1139 additions and 194 deletions

View File

@@ -30,6 +30,7 @@ import { ContactUsPage } from './components/ContactUsPage';
import { pageTransition } from './utils/animations';
import { LandingPage } from './pages/landingPage';
import ComingSoonPage from './pages/ComingSoonPage';
import { SuperSavingsPage } from './components/SuperSavingsPage';
// User type definition
interface User {
@@ -252,6 +253,11 @@ export function AppRouter({
<FAQPage {...commonNavHandlers} />
</motion.div>
} />
<Route path="/super-savings" element={
<motion.div key="super-savings" {...pageTransition}>
<SuperSavingsPage {...commonNavHandlers} />
</motion.div>
} />
</Routes>

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -0,0 +1,126 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom'; // ✅ import navigation hook
import { Dialog, DialogContent, DialogTitle, DialogDescription } from './ui/dialog';
import { ArrowLeft, Search } from 'lucide-react';
import { Input } from './ui/input';
import { motion, AnimatePresence } from 'motion/react';
import { ImageWithFallback } from './figma/ImageWithFallback';
interface City {
id: string;
name: string;
imageUrl: string;
}
interface CitySelectionDialogProps {
isOpen: boolean;
onClose: () => void;
}
const cities: City[] = [
{ id: 'melbourne', name: 'Melbourne', imageUrl: 'https://images.unsplash.com/photo-1624341373902-70e3a8dc9acc?...' },
{ id: 'new-york', name: 'New York', imageUrl: 'https://images.unsplash.com/photo-1514565131-fce0801e5785?...' },
{ id: 'abu-dhabi', name: 'Abu Dhabi', imageUrl: 'https://images.unsplash.com/photo-1584551246679-0daf3d275d0f?...' },
{ id: 'dubai', name: 'Dubai', imageUrl: 'https://images.unsplash.com/photo-1518684079-3c830dcef090?...' },
{ id: 'tokyo', name: 'Tokyo', imageUrl: 'https://images.unsplash.com/photo-1613487897980-50cc440ce118?...' },
{ id: 'ontario', name: 'Ontario', imageUrl: 'https://images.unsplash.com/photo-1542704792-e30dac463c90?...' },
{ id: 'mumbai', name: 'Mumbai', imageUrl: 'https://images.unsplash.com/photo-1600867161422-79f8f6e08c84?...' },
{ id: 'louisiana', name: 'Louisiana', imageUrl: 'https://images.unsplash.com/photo-1646508262200-455d62c22182?...' },
];
export function CitySelectionDialog({ isOpen, onClose }: CitySelectionDialogProps) {
const [searchQuery, setSearchQuery] = useState('');
const navigate = useNavigate(); // ✅ navigation hook
const filteredCities = cities.filter(city =>
city.name.toLowerCase().includes(searchQuery.toLowerCase())
);
const handleCityClick = (city: City) => {
console.log('Selected city:', city.name);
onClose();
// ✅ navigate to /passes with selected city info (optional query param)
navigate(`/passes?city=${encodeURIComponent(city.name)}`);
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-md w-full p-0 gap-0 font-poppins">
{/* Accessible Title */}
<DialogTitle className="sr-only">Select a City</DialogTitle>
<DialogDescription className="sr-only">
Choose from our available cities to explore attractions and experiences
</DialogDescription>
{/* Header */}
<div className="flex items-center gap-4 px-6 py-5 border-b border-gray-100">
<button
onClick={onClose}
className="flex items-center justify-center text-foreground hover:text-primary transition-colors duration-200"
aria-label="Close dialog"
>
<ArrowLeft className="w-5 h-5" />
</button>
<h2 className="font-poppins font-semibold" aria-hidden="true">Select a City</h2>
</div>
{/* Search Bar */}
<div className="px-6 py-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<Input
type="text"
placeholder="Search Cities"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 bg-input border-0 rounded-lg h-11 font-poppins placeholder:text-gray-400"
/>
</div>
</div>
{/* City Grid */}
<div className="px-6 pb-6 max-h-[60vh] overflow-y-auto">
<AnimatePresence>
<div className="grid grid-cols-2 gap-3">
{filteredCities.map((city, index) => (
<motion.button
key={city.id}
onClick={() => handleCityClick(city)} // ✅ navigate on click
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
transition={{ delay: index * 0.05 }}
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.98 }}
className="relative h-28 rounded-2xl overflow-hidden group cursor-pointer"
>
<ImageWithFallback
src={city.imageUrl}
alt={city.name}
className="w-full h-full object-cover transition-transform duration-300 group-hover:scale-110"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent" />
<div className="absolute bottom-3 left-3 right-3">
<h3 className="font-poppins font-semibold text-white text-left">
{city.name}
</h3>
</div>
</motion.button>
))}
</div>
</AnimatePresence>
{/* No Results */}
{filteredCities.length === 0 && (
<div className="text-center py-8">
<p className="text-gray-500 font-poppins">
No cities found matching "{searchQuery}"
</p>
</div>
)}
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -4,7 +4,7 @@ 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 postcardImage from '../assets/eaf15191e9a315d2d4b384ffcb22910687c3d328.png';
import postcardImage from 'figma:asset/d3a880cf8b7f1bec6da9b3f2ce4a76e822e483cf.png';
interface EditableCardProps {
isEditing: boolean;
@@ -17,8 +17,11 @@ interface EditableCardProps {
export function CustomPostcards() {
const [editingCard, setEditingCard] = useState<string | null>(null);
const [isFlipped, setIsFlipped] = useState(false);
const [uploadedImage, setUploadedImage] = useState<string | null>(null);
const postcardRef = useRef<HTMLDivElement>(null);
const sectionRef = useRef<HTMLDivElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
// 3D tilt effect using mouse position
const mouseX = useMotionValue(0);
@@ -44,9 +47,9 @@ export function CustomPostcards() {
// Handwritten text control
const handwrittenControl = useHandwrittenText(false);
// Handle mouse movement for 3D effect
// Handle mouse movement for 3D effect (disabled when flipped)
const handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
if (!postcardRef.current) return;
if (!postcardRef.current || isFlipped) return;
const rect = postcardRef.current.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
@@ -60,6 +63,7 @@ export function CustomPostcards() {
};
const handleMouseLeave = () => {
if (isFlipped) return;
mouseX.set(0);
mouseY.set(0);
};
@@ -75,6 +79,25 @@ export function CustomPostcards() {
console.log('Navigate to postcard creation page...');
};
// Handle image upload
const handleImageUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setUploadedImage(reader.result as string);
// Automatically flip to show the uploaded image
setIsFlipped(true);
};
reader.readAsDataURL(file);
}
};
// Trigger file input
const triggerFileInput = () => {
fileInputRef.current?.click();
};
// Start handwriting animation when section comes into view
useEffect(() => {
if (isInView && !editingCard) {
@@ -93,6 +116,14 @@ export function CustomPostcards() {
}
}, [editingCard, handwrittenControl]);
// Reset 3D tilt when card is flipped
useEffect(() => {
if (isFlipped) {
mouseX.set(0);
mouseY.set(0);
}
}, [isFlipped, mouseX, mouseY]);
const EditableCard = ({ isEditing, onEdit, children, className = "", style = {}, editIcon }: EditableCardProps) => {
return (
<motion.div
@@ -684,7 +715,7 @@ export function CustomPostcards() {
{/* Centered Postcard Preview - Enhanced with Animations */}
<div className="flex justify-center mb-12 px-4">
<motion.div
className="relative group w-full max-w-4xl [perspective:2000px]"
className="relative group w-full max-w-4xl [perspective:2000px] flex items-center gap-6"
initial={{ opacity: 0, y: 30, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{
@@ -693,104 +724,194 @@ export function CustomPostcards() {
delay: 0.2
}}
>
{/* Interactive 3D Container with Flip */}
<motion.div
ref={postcardRef}
className="relative w-full rounded-xl cursor-pointer [transform-style:preserve-3d] transition-transform duration-700"
style={{
aspectRatio: '720 / 470',
maxWidth: '720px',
maxHeight: '470px',
minWidth: '280px',
minHeight: '183px',
rotateX,
rotateY,
transformStyle: "preserve-3d"
}}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
whileHover={{
scale: 1.02,
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(99, 102, 241, 0.1)",
rotateY: 180,
transition: { duration: 0.6 }
}}
animate={{
y: [0, -8, 0],
}}
transition={{
y: {
duration: 4,
repeat: Infinity,
ease: "easeInOut"
}
}}
{/* Left Flip Button - Show when viewing back (flipped) */}
<motion.button
onClick={() => setIsFlipped(false)}
className={`hidden md:flex items-center justify-center w-12 h-12 rounded-full bg-white shadow-lg border-2 border-primary hover:bg-primary hover:text-white transition-all duration-300 ${isFlipped ? 'opacity-100' : 'opacity-0 pointer-events-none'}`}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: isFlipped ? 1 : 0, x: isFlipped ? 0 : -20 }}
transition={{ duration: 0.3 }}
>
{/* Front Side - Postcard Design */}
<motion.div
className="absolute inset-0 rounded-xl overflow-hidden [backface-visibility:hidden]"
style={{
backfaceVisibility: "hidden",
WebkitBackfaceVisibility: "hidden"
}}
>
<PostcardFrame />
{/* Subtle glow effect */}
<motion.div
className="absolute inset-0 rounded-xl opacity-0 pointer-events-none"
style={{
background: "radial-gradient(circle at center, rgba(99, 102, 241, 0.1) 0%, transparent 70%)",
filter: "blur(20px)"
}}
whileHover={{ opacity: 0.5 }}
transition={{ duration: 0.3 }}
/>
</motion.div>
<ArrowRight className="w-5 h-5 rotate-180" />
</motion.button>
{/* Back Side - Full Image */}
{/* Postcard Container */}
<div className="relative flex-1">
{/* Interactive 3D Container with Flip */}
<motion.div
className="absolute inset-0 rounded-xl overflow-hidden [backface-visibility:hidden]"
style={{
backfaceVisibility: "hidden",
WebkitBackfaceVisibility: "hidden",
transform: "rotateY(180deg)"
ref={postcardRef}
className="relative w-full rounded-xl cursor-pointer [transform-style:preserve-3d]"
style={{
aspectRatio: '720 / 470',
maxWidth: '720px',
maxHeight: '470px',
minWidth: '280px',
minHeight: '183px',
rotateX,
rotateY,
transformStyle: "preserve-3d"
}}
>
<ImageWithFallback
src={postcardImage}
alt="Postcard travel destination"
className="w-full h-full object-cover"
/>
{/* Subtle gradient overlay */}
<div className="absolute inset-0 bg-gradient-to-br from-black/5 via-transparent to-black/10" />
</motion.div>
</motion.div>
{/* Animated Edit Instructions */}
{editingCard && (
<motion.div
className="absolute -top-12 left-1/2 transform -translate-x-1/2 bg-primary text-white px-3 py-2 md:px-4 md:py-2 rounded-lg shadow-lg text-xs md:text-sm font-medium whitespace-nowrap z-20"
initial={{ opacity: 0, y: 10, scale: 0.9 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -10, scale: 0.9 }}
transition={{ duration: 0.3, ease: "easeOut" }}
>
<motion.span
animate={{
color: ["#ffffff", "#e0e7ff", "#ffffff"]
}}
transition={{
duration: 2,
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
whileHover={{
scale: 1.02,
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(99, 102, 241, 0.1)"
}}
animate={{
y: [0, -8, 0],
rotateY: isFlipped ? 180 : 0
}}
transition={{
y: {
duration: 4,
repeat: Infinity,
ease: "easeInOut"
},
rotateY: {
duration: 0.4,
ease: [0.25, 1, 0.5, 1]
}
}}
>
{/* Front Side - Postcard Design */}
<motion.div
className="absolute inset-0 rounded-xl overflow-hidden [backface-visibility:hidden]"
style={{
backfaceVisibility: "hidden",
WebkitBackfaceVisibility: "hidden"
}}
>
Click on any element to edit it
</motion.span>
<div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-primary"></div>
<PostcardFrame />
{/* Subtle glow effect */}
<motion.div
className="absolute inset-0 rounded-xl opacity-0 pointer-events-none"
style={{
background: "radial-gradient(circle at center, rgba(99, 102, 241, 0.1) 0%, transparent 70%)",
filter: "blur(20px)"
}}
whileHover={{ opacity: 0.5 }}
transition={{ duration: 0.3 }}
/>
</motion.div>
{/* Back Side - Postcard Frame with Upload */}
<motion.div
className="absolute inset-0 rounded-xl overflow-hidden [backface-visibility:hidden] group/image"
style={{
backfaceVisibility: "hidden",
WebkitBackfaceVisibility: "hidden",
transform: "rotateY(180deg)"
}}
>
{/* Postcard frame with gradient background - matching Figma import */}
<div className="bg-gradient-to-r from-[#e2d6c2] to-[#ffffff] via-50% via-[#fff5e6] relative rounded-xl size-full">
<div className="flex flex-col items-center size-full">
<div className="box-border content-stretch flex flex-col gap-[4px] items-center overflow-clip px-[12px] py-[8px] relative size-full">
<div className="basis-0 grow min-h-px min-w-px relative rounded-[2px] shrink-0 w-full">
<div aria-hidden="true" className="absolute inset-0 pointer-events-none rounded-[2px]">
<div className="absolute bg-[#d9d9d9] inset-0 rounded-[2px]" />
<div className="absolute inset-0 overflow-hidden rounded-[2px]">
{uploadedImage ? (
<img
alt="Your uploaded postcard"
className="absolute h-full left-0 max-w-none top-0 w-full object-cover"
src={uploadedImage}
/>
) : (
<img
src="https://images.unsplash.com/photo-1631786517313-52f119448c17?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtZWxib3VybmUlMjBjaXR5JTIwc2t5bGluZXxlbnwxfHx8fDE3NjIxNzExNzF8MA&ixlib=rb-4.1.0&q=80&w=1080"
alt="Melbourne skyline"
className="absolute h-full left-0 max-w-none top-0 w-full object-cover"
/>
)}
</div>
</div>
</div>
</div>
</div>
<div aria-hidden="true" className="absolute border border-[rgba(0,0,0,0.12)] border-solid inset-0 pointer-events-none rounded-xl" />
{/* Upload/Edit Button - appears on hover */}
<motion.div
className="absolute inset-0 bg-black/40 flex items-center justify-center opacity-0 group-hover/image:opacity-100 transition-opacity duration-300 rounded-xl"
onClick={(e) => {
e.stopPropagation();
triggerFileInput();
}}
>
<motion.button
className="bg-white hover:bg-primary text-gray-800 hover:text-white px-6 py-3 rounded-full shadow-lg transition-all duration-300 flex items-center gap-2 font-poppins font-semibold"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Upload className="w-5 h-5" />
<span>{uploadedImage ? 'Change Image' : 'Upload Image'}</span>
</motion.button>
</motion.div>
</div>
{/* Hidden file input */}
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleImageUpload}
className="hidden"
/>
</motion.div>
</motion.div>
)}
{/* Animated Edit Instructions */}
{editingCard && (
<motion.div
className="absolute -top-12 left-1/2 transform -translate-x-1/2 bg-primary text-white px-3 py-2 md:px-4 md:py-2 rounded-lg shadow-lg text-xs md:text-sm font-medium whitespace-nowrap z-20"
initial={{ opacity: 0, y: 10, scale: 0.9 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -10, scale: 0.9 }}
transition={{ duration: 0.3, ease: "easeOut" }}
>
<motion.span
animate={{
color: ["#ffffff", "#e0e7ff", "#ffffff"]
}}
transition={{
duration: 2,
repeat: Infinity,
ease: "easeInOut"
}}
>
Click on any element to edit it
</motion.span>
<div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-primary"></div>
</motion.div>
)}
{/* Mobile Flip Button - Below postcard on mobile */}
<motion.button
onClick={() => setIsFlipped(!isFlipped)}
className="md:hidden mt-4 mx-auto flex items-center justify-center gap-2 px-6 py-3 rounded-full bg-primary text-white font-poppins font-semibold shadow-lg hover:bg-primary/90 transition-all duration-300"
whileTap={{ scale: 0.95 }}
>
<Camera className="w-4 h-4" />
<span>{isFlipped ? 'View Postcard' : 'View Image'}</span>
</motion.button>
</div>
{/* Right Flip Button - Show when viewing front (not flipped) */}
<motion.button
onClick={() => setIsFlipped(true)}
className={`hidden md:flex items-center justify-center w-12 h-12 rounded-full bg-white shadow-lg border-2 border-primary hover:bg-primary hover:text-white transition-all duration-300 ${!isFlipped ? 'opacity-100' : 'opacity-0 pointer-events-none'}`}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95 }}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: !isFlipped ? 1 : 0, x: !isFlipped ? 0 : 20 }}
transition={{ duration: 0.3 }}
>
<ArrowRight className="w-5 h-5" />
</motion.button>
</motion.div>
</div>

View File

@@ -2,7 +2,7 @@ import image_bc70aef6686e5f4d059b5ef3380fd4f44bb9f4c6 from '../assets/bc70aef668
import { motion } from 'motion/react';
import { Apple, Play } from 'lucide-react';
import { ImageWithFallback } from './figma/ImageWithFallback';
import cityCardsLogo from '../assets/cityLogo.png';
import cityCardsLogo from '../assets/cit-logo.png';
export function FooterBrand() {
return (

View File

@@ -4,7 +4,6 @@ import { ArrowRight, Calendar, Thermometer, Eye } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import Navbar from './Navbar';
import { ImageWithFallback } from './figma/ImageWithFallback';
import { CitySubmenu } from './CitySubmenu';
import { MelbourneAttractions } from './MelbourneAttractions';
import { MelbourneCardComparison } from './MelbourneCardComparison';
import { MelbourneTourOverview } from './MelbourneTourOverview';
@@ -294,7 +293,6 @@ export function MelbournePage({
return (
<Layout
activeCity="Melbourne"
showCitySubmenu={true}
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
user={user}
@@ -302,13 +300,13 @@ export function MelbournePage({
<div className="min-h-screen bg-background">
{/* Hero Banner Carousel */}
{/* <HeroBannerCarousel
<HeroBannerCarousel
onCheckoutClick={onCheckoutClick}
onPassesClick={onPassesClick}
onEsimsClick={onEsimsClick}
onHotelDiscountsClick={onHotelDiscountsClick}
/> */}
<PersonalizedTourHero />
/>
{/* <PersonalizedTourHero /> */}
{/* Main Content */}
<main>
<div className="container mx-auto px-4 py-16">

View File

@@ -6,7 +6,9 @@ import Frame1597884853 from '../imports/Frame1597884853';
import { Button } from './ui/button';
import { ImageWithFallback } from './figma/ImageWithFallback';
import { CTAButton } from './CTAButton';
import logoImage from '../assets/cityLogo.png';
import logoImage from '../assets/cit-logo.png';
import melbourneLogo from '../assets/melbourne-logo.png';
import { CitySelectionDialog } from './CitySelectionDialog';
interface NavbarProps {
activeCity: string;
@@ -57,6 +59,7 @@ export default function Navbar({
const [activeCartDropdown, setActiveCartDropdown] = useState(false);
const [activeUserDropdown, setActiveUserDropdown] = useState(false);
const [activeCityDropdown, setActiveCityDropdown] = useState(false);
const [isCityDialogOpen, setIsCityDialogOpen] = useState(false);
const languageRef = useRef<HTMLDivElement>(null);
const cartRef = useRef<HTMLDivElement>(null);
@@ -66,6 +69,14 @@ export default function Navbar({
const location = useLocation();
const navigate = useNavigate();
const handleOpenCityDialog = () => {
setIsCityDialogOpen(true);
};
const handleCloseCityDialog = () => {
setIsCityDialogOpen(false);
};
// Available cities
const cities = [
{ id: 'melbourne', label: 'Melbourne' },
@@ -90,7 +101,7 @@ export default function Navbar({
const melbourneNavigationItems = [
{ label: 'Attractions', path: '/attractions' },
{ label: 'Magic Itinerary', path: '/magic-itinerary' },
{ label: 'Super Savings', path: '/comming-soon' },
{ label: 'Super Savings', path: '/super-savings' },
{ label: 'How It Works', path: '/how-it-works' },
{ label: 'Your Card', path: '/passes' }
];
@@ -114,6 +125,34 @@ export default function Navbar({
{ id: '2', name: 'Melbourne Premium Pass', price: '$129', quantity: 1 },
];
// Calculate cart total
const cartTotal = cartItems.reduce((total, item) => {
const price = parseFloat(item.price.replace('$', ''));
return total + (price * item.quantity);
}, 0);
// Cart dropdown items with proper navigation for checkout
const cartDropdownItems: DropdownItem[] = [
...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: () => {
navigate('/checkout');
setActiveCartDropdown(false); // Close dropdown after navigation
}
}
];
const scrollToSection = (index: number) => {
const sectionIds = [
'hero-section',
@@ -151,29 +190,6 @@ export default function Navbar({
return () => window.removeEventListener('scroll', handleScroll);
}, []);
// Close dropdowns when clicking outside
// useEffect(() => {
// const handleClickOutside = (event: MouseEvent) => {
// 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 (cityRef.current && !cityRef.current.contains(event.target as Node)) {
// setActiveCityDropdown(false);
// }
// }, 10);
// };
// document.addEventListener('mousedown', handleClickOutside);
// return () => document.removeEventListener('mousedown', handleClickOutside);
// }, []);
const handleNavClick = (path: string) => {
navigate(path);
closeMobileMenu();
@@ -183,7 +199,6 @@ export default function Navbar({
return location.pathname === path;
};
// Handle city change
// Handle city change
const handleCityChange = (city: string) => {
console.log('City selected:', city); // Debug log
@@ -200,12 +215,6 @@ export default function Navbar({
}
};
// Calculate cart total
const cartTotal = cartItems.reduce((total, item) => {
const price = parseFloat(item.price.replace('$', ''));
return total + (price * item.quantity);
}, 0);
// Simple Dropdown component without blinking
const Dropdown = forwardRef<HTMLDivElement, DropdownProps>(({
isOpen,
@@ -262,7 +271,7 @@ export default function Navbar({
key={item.id}
onClick={(e) => {
e.stopPropagation();
console.log('City dropdown item clicked:', item.label);
console.log('Dropdown item clicked:', item.label);
if (item.action) {
item.action();
@@ -273,6 +282,11 @@ export default function Navbar({
>
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-700">{item.label}</span>
{item.badge && (
<span className="text-xs bg-gray-100 text-gray-600 px-2 py-1 rounded-full">
{item.badge}
</span>
)}
</div>
</div>
))}
@@ -319,10 +333,15 @@ export default function Navbar({
>
<Link to="/">
<ImageWithFallback
src={logoImage}
alt="CityCards Logo"
src={activeCity?.toLowerCase() === 'melbourne' ? melbourneLogo : logoImage}
alt={
activeCity?.toLowerCase() === 'melbourne'
? 'Melbourne CityCards Logo'
: 'CityCards Logo'
}
className="h-10 w-auto"
/>
</Link>
</motion.div>
@@ -418,28 +437,12 @@ export default function Navbar({
}
/>
{/* Shopping Cart */}
{/* Shopping Cart - UPDATED: using cartDropdownItems */}
<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',
path: '/checkout'
}
]}
items={cartDropdownItems} // Using the updated array with navigation
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">
@@ -457,43 +460,61 @@ export default function Navbar({
{/* City Card Button */}
<div className="flex items-center gap-3 pl-2">
<div className="relative">
<CTAButton
user={user ?? null}
onClick={user ? () => setActiveUserDropdown(prev => !prev) : onSignInClick}
className="hover:scale-105 transition-transform duration-200"
/>
<div className="relative">
<CTAButton
user={user ?? null}
// 👇 open the city selection dialog on click
onClick={handleOpenCityDialog}
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" />,
path: '/profile'
},
{
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>
{/* ✅ City Selection Dialog */}
<CitySelectionDialog
isOpen={isCityDialogOpen}
onClose={handleCloseCityDialog}
/>
{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: () => {
navigate('/profile');
setActiveUserDropdown(false);
},
},
{
id: 'settings',
label: 'Settings',
icon: <Settings className="w-4 h-4" />,
action: () => {
navigate('/settings');
setActiveUserDropdown(false);
},
},
{
id: 'logout',
label: 'Sign Out',
icon: <LogOut className="w-4 h-4" />,
action: () => {
if (onSignOutClick) {
onSignOutClick();
}
setActiveUserDropdown(false);
},
},
]}
title="Account"
trigger={null}
/>
)}
</div>
</div>
</div>
</div>
@@ -650,11 +671,10 @@ export default function Navbar({
{/* 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"
onClick={() => setIsCityDialogOpen(true)}
withShine={true}
size="lg"
className="min-w-[180px] font-poppins font-semibold rounded-full"
>
GET A CITY CARD
</Button>

View File

@@ -0,0 +1,672 @@
import { useState } from 'react';
import { motion } from 'motion/react';
import { ArrowLeft, Search, Filter, Star, MapPin, Clock, Tag, Heart, Share2, ChevronDown, ChevronRight, Check, Hotel, Plane, Building2, MapPinned, Home, Gift, Percent } 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 { Footer } from './Footer';
import { ImageWithFallback } from './figma/ImageWithFallback';
import { TrustSection } from './TrustSection';
import { MobileAppSection } from './MobileAppSection';
import { ReviewsSection } from './ReviewsSection';
import { TrustedCompanies } from './TrustedCompanies';
import { Layout } from '../Layout';
interface SuperSavingsPageProps {
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;
onSuperSavingsClick: () => void;
onContactUsClick?: () => void;
onEsimsClick?: () => void;
onHotelDiscountsClick?: () => void;
fromSource?: 'products' | 'passes';
currentPage: string;
user?: { email: string; name: string; } | null;
}
// Mock super savings data
const savingsData = [
{
id: '1',
business: 'Grand Hotels Melbourne',
title: 'Up to 50% Off on luxury hotel stays across Melbourne',
discount: '50% OFF',
savedAmount: 'Save up to $300',
image: 'https://images.unsplash.com/photo-1566073771259-6a8506099945?w=400',
category: 'hotels'
},
{
id: '2',
business: 'Adventure Tours',
title: '40% Off on guided adventure tours and experiences',
discount: '40% OFF',
savedAmount: 'Save up to $150',
image: 'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?w=400',
category: 'tours'
},
{
id: '3',
business: 'Premium Spa & Wellness',
title: '45% Off on spa packages and wellness treatments',
discount: '45% OFF',
savedAmount: 'Save up to $200',
image: 'https://images.unsplash.com/photo-1544161515-4ab6ce6db874?w=400',
category: 'wellness'
},
{
id: '4',
business: 'Culinary Delights',
title: '35% Off on fine dining at Michelin-starred restaurants',
discount: '35% OFF',
savedAmount: 'Save up to $120',
image: 'https://images.unsplash.com/photo-1414235077428-338989a2e8c0?w=400',
category: 'dining'
},
{
id: '5',
business: 'Entertainment Pass',
title: '60% Off on theater shows and concert tickets',
discount: '60% OFF',
savedAmount: 'Save up to $250',
image: 'https://images.unsplash.com/photo-1514306191717-452ec28c7814?w=400',
category: 'entertainment'
},
{
id: '6',
business: 'Museum Pass',
title: '55% Off on museum entries and special exhibitions',
discount: '55% OFF',
savedAmount: 'Save up to $180',
image: 'https://images.unsplash.com/photo-1566127992631-137a642a90f4?w=400',
category: 'museums'
},
{
id: '7',
business: 'Luxury Shopping',
title: '30% Off on designer boutiques and luxury shopping',
discount: '30% OFF',
savedAmount: 'Save up to $500',
image: 'https://images.unsplash.com/photo-1441986300917-64674bd600d8?w=400',
category: 'shopping'
},
{
id: '8',
business: 'Water Sports',
title: '45% Off on water sports and beach activities',
discount: '45% OFF',
savedAmount: 'Save up to $175',
image: 'https://images.unsplash.com/photo-1476514525535-07fb3b4ae5f1?w=400',
category: 'sports'
},
{
id: '9',
business: 'Wine Tasting Tours',
title: '40% Off on wine country tours and tastings',
discount: '40% OFF',
savedAmount: 'Save up to $160',
image: 'https://images.unsplash.com/photo-1506377247377-2a5b3b417ebb?w=400',
category: 'tours'
},
{
id: '10',
business: 'Family Fun Parks',
title: '50% Off on family entertainment and theme parks',
discount: '50% OFF',
savedAmount: 'Save up to $220',
image: 'https://images.unsplash.com/photo-1524850011238-e3d235c7d4c9?w=400',
category: 'entertainment'
},
{
id: '11',
business: 'Boutique Stays',
title: '55% Off on boutique hotels and bed & breakfasts',
discount: '55% OFF',
savedAmount: 'Save up to $280',
image: 'https://images.unsplash.com/photo-1551882547-ff40c63fe5fa?w=400',
category: 'hotels'
},
{
id: '12',
business: 'Art Galleries',
title: '35% Off on contemporary art galleries and workshops',
discount: '35% OFF',
savedAmount: 'Save up to $140',
image: 'https://images.unsplash.com/photo-1561214115-f2f134cc4912?w=400',
category: 'museums'
},
{
id: '13',
business: 'Luxury Cruises',
title: '65% Off on harbor cruises and yacht experiences',
discount: '65% OFF',
savedAmount: 'Save up to $400',
image: 'https://images.unsplash.com/photo-1544551763-46a013bb70d5?w=400',
category: 'tours'
}
];
const filterCategories = [
{ value: 'hotels', label: 'Hotels', count: 2 },
{ value: 'tours', label: 'Tours', count: 3 },
{ value: 'wellness', label: 'Wellness', count: 1 },
{ value: 'dining', label: 'Dining', count: 1 },
{ value: 'entertainment', label: 'Entertainment', count: 2 },
{ value: 'museums', label: 'Museums', count: 2 },
{ value: 'shopping', label: 'Shopping', count: 1 },
{ value: 'sports', label: 'Sports', count: 1 }
];
// Categories data for the Super Savings Categories section
const categoriesData = [
{
icon: Hotel,
title: 'Luxury Hotels',
description: 'Premium stays at unbeatable prices',
savings: 'Up to 50% off',
color: 'from-primary to-primary/80'
},
{
icon: Plane,
title: 'Travel Tours',
description: 'Guided experiences worth your time',
savings: 'Up to 45% off',
color: 'from-primary to-primary/80'
},
{
icon: MapPinned,
title: 'Attractions',
description: 'Must-see landmarks and experiences',
savings: 'Up to 60% off',
color: 'from-primary to-primary/80'
},
{
icon: Building2,
title: 'Shopping',
description: 'Designer brands and local boutiques',
savings: 'Up to 35% off',
color: 'from-primary to-primary/80'
},
{
icon: Gift,
title: 'Wellness',
description: 'Spa treatments and relaxation',
savings: 'Up to 45% off',
color: 'from-primary to-primary/80'
}
];
export function SuperSavingsPage({
onBackClick,
onHomeClick,
onMelbourneClick,
onPassesClick,
onCheckoutClick,
onSignInClick,
onSignOutClick,
onAttractionsClick,
onBlogsClick,
onHowItWorksClick,
onFAQClick,
onPrivacyPolicyClick,
onAboutUsClick,
onProfileClick,
onCityCardsClick,
onMagicItineraryClick,
onPostCardsClick,
onOffersClick,
onSuperSavingsClick,
onContactUsClick,
onEsimsClick,
onHotelDiscountsClick,
fromSource = 'products',
currentPage,
user
}: SuperSavingsPageProps) {
const [searchQuery, setSearchQuery] = useState('');
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
const [currentPage_, setCurrentPage_] = useState(1);
const [showLoadMore, setShowLoadMore] = useState(true);
const toggleCategory = (category: string) => {
setSelectedCategories(prev =>
prev.includes(category)
? prev.filter(c => c !== category)
: [...prev, category]
);
};
const filteredSavings = savingsData.filter(saving => {
const matchesSearch = saving.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
saving.business.toLowerCase().includes(searchQuery.toLowerCase());
const matchesCategory = selectedCategories.length === 0 || selectedCategories.includes(saving.category);
return matchesSearch && matchesCategory;
});
const itemsPerPage = 12;
const displayedSavings = filteredSavings.slice(0, currentPage_ * itemsPerPage);
const hasMoreItems = filteredSavings.length > displayedSavings.length;
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 (
<Layout
activeCity="Melbourne"
onSignInClick={onSignInClick}
onSignOutClick={onSignOutClick}
user={user}
>
<div className="min-h-screen bg-background">
{/* 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-poppins text-4xl md:text-5xl lg:text-6xl leading-tight mb-6">
<span className="font-light">Unlock</span>{' '}
<span className="font-bold italic bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
Super Savings
</span>
</h1>
<p className="font-poppins text-lg md:text-xl leading-relaxed text-gray-600 mb-8 max-w-2xl mx-auto">
Experience incredible discounts up to 65% off on premium experiences, luxury stays, and unforgettable attractions.
</p>
<Button
onClick={onSignInClick}
className="bg-primary hover:bg-primary/90 text-white px-8 py-6 font-poppins font-semibold"
>
Start Saving 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-poppins text-2xl md:text-3xl lg:text-4xl leading-tight mb-4">
<span>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 leading-relaxed text-muted-foreground max-w-2xl mx-auto">
Join thousands of savvy travelers enjoying massive savings on premium experiences
</p>
</div>
<TrustedCompanies />
</div>
</div>
</section>
{/* Featured Super Savings 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-poppins 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">
Super Savings
</span>
</h2>
<p className="font-poppins leading-relaxed text-gray-600 max-w-2xl mx-auto">
Check out our biggest discounts and start saving on premium experiences
</p>
</motion.div>
<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-gray-800 rotate-90"></div>
<h3 className="font-poppins font-medium text-gray-800">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-gray-400"
/>
<label
htmlFor={category.value}
className="font-poppins text-sm text-gray-700 cursor-pointer flex-1"
>
{category.label} ({category.count})
</label>
</div>
))}
</div>
</div>
</Card>
</div>
{/* Main Content */}
<div className="flex-1">
{/* Breadcrumb */}
<div className="mb-8">
<p className="font-poppins text-sm text-gray-800">
{fromSource === 'passes' ? (
<>
<span>My Profile{'>'}My passes{'>'}</span>
<span className="font-semibold">Super Savings</span>
</>
) : (
<>
<span>Our Products{'>'}</span>
<span className="font-semibold">Super Savings</span>
</>
)}
</p>
</div>
{/* Header Section */}
<div className="mb-8">
<h1 className="font-poppins md:text-5xl font-medium text-gray-800 leading-tight text-[24px]">
Super Savings
</h1>
<p className="font-poppins text-gray-600 mt-2">
Exclusive discounts up to 65% off on premium experiences
</p>
</div>
{/* Savings Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 gap-6 mb-16">
{displayedSavings.map((saving, index) => (
<motion.div
key={saving.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 relative">
{/* Image */}
<div className="relative h-52 bg-gray-300">
<ImageWithFallback
src={saving.image}
alt={saving.title}
className="w-full h-full object-cover"
/>
<Button className="absolute bottom-4 right-3 bg-white rounded-full shadow-lg w-9 h-9 p-0 hover:bg-gray-100 transition-colors">
<Heart className="w-4 h-4 text-gray-800" />
</Button>
{/* Discount Badge */}
<div className="absolute top-4 left-4 bg-primary text-white px-3 py-1.5 rounded-lg">
<span className="font-poppins font-semibold text-sm">{saving.discount}</span>
</div>
</div>
<CardContent className="space-y-4 px-4 py-4">
{/* Business Name */}
<div className="flex items-center gap-2">
<div className="w-4 h-4 bg-gray-300 rounded"></div>
<span className="font-poppins text-sm text-gray-500">{saving.business}</span>
</div>
{/* Title */}
<h3 className="font-poppins font-medium text-gray-900 leading-relaxed min-h-[48px]">
{saving.title}
</h3>
{/* Saved Amount Display */}
<div className="bg-gradient-to-r from-primary/10 to-secondary/10 h-12 flex items-center justify-center rounded-lg">
<div className="flex items-center gap-2">
<Percent className="w-4 h-4 text-primary" />
<span className="font-poppins font-semibold text-primary">
{saving.savedAmount}
</span>
</div>
</div>
</CardContent>
</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 font-poppins"
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 font-poppins ${currentPage_ === page ? 'bg-primary hover:bg-primary/90' : ''}`}
onClick={() => setCurrentPage_(page)}
>
{page}
</Button>
))}
</div>
<Button
variant="outline"
size="sm"
className="w-8 h-8 p-0 font-poppins"
disabled={currentPage_ === 3}
>
<ChevronRight className="w-4 h-4" />
</Button>
</div>
</div>
</div>
</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 Super Savings
</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-poppins 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 leading-relaxed text-gray-600 max-w-2xl mx-auto">
Access massive discounts in three simple steps
</p>
</motion.div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{[
{
step: '01',
title: 'Get Your Pass',
description: 'Purchase a CityCards pass and unlock instant access',
icon: '🎫'
},
{
step: '02',
title: 'Browse Deals',
description: 'Explore hundreds of exclusive super savings across categories',
icon: '💎'
},
{
step: '03',
title: 'Save Big',
description: 'Enjoy discounts up to 65% on premium experiences',
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-poppins text-5xl font-bold text-primary/20 mb-4">
{item.step}
</div>
<h3 className="font-poppins font-semibold text-gray-900 mb-3">
{item.title}
</h3>
<p className="font-poppins leading-relaxed text-gray-600">
{item.description}
</p>
</Card>
</motion.div>
))}
</div>
</div>
</section>
{/* 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-poppins 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">
Top Categories
</span>{' '}
<span className="font-light">to Save</span>
</h2>
<p className="font-poppins leading-relaxed text-gray-600 max-w-2xl mx-auto">
From luxury hotels to exciting tours, find massive savings on everything you love
</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-poppins font-semibold text-gray-900 mb-3">
{category.title}
</h3>
<p className="font-poppins leading-relaxed 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.savings}
</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"
>
Browse All Categories
</Button>
</div>
</div>
</section>
{/* Access Your CityCards Section */}
<section className="py-20 bg-muted/30">
<MobileAppSection />
</section>
</div>
</Layout>
);
}
}

View File

@@ -16,6 +16,7 @@ import { LandingUpcomingCities } from '../components/LandingUpcomingCities';
import { LandingTrustSection } from '../components/LandingTrustSection';
import { LandingMobileAppSection } from '../components/LandingMobileAppSection';
import { LandingNewsletterSection } from '../components/LandingNewsletterSection';
import { CustomPostcards } from '../components/CustomPostcards';
@@ -193,7 +194,8 @@ export function LandingPage({ onSignInClick,
<LandingBookAttractionSection />
{/* CustomPostcards Section */}
<LandingCustomPostcards />
{/* <LandingCustomPostcards /> */}
<CustomPostcards/>
{/* UpcomingCities Section */}
<LandingUpcomingCities />