new fixes
This commit is contained in:
@@ -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>
|
||||
|
||||
BIN
src/assets/melbourne-logo.png
Normal file
BIN
src/assets/melbourne-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
126
src/components/CitySelectionDialog.tsx
Normal file
126
src/components/CitySelectionDialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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 */}
|
||||
<ArrowRight className="w-5 h-5 rotate-180" />
|
||||
</motion.button>
|
||||
|
||||
{/* 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]"
|
||||
ref={postcardRef}
|
||||
className="relative w-full rounded-xl cursor-pointer [transform-style:preserve-3d]"
|
||||
style={{
|
||||
backfaceVisibility: "hidden",
|
||||
WebkitBackfaceVisibility: "hidden"
|
||||
aspectRatio: '720 / 470',
|
||||
maxWidth: '720px',
|
||||
maxHeight: '470px',
|
||||
minWidth: '280px',
|
||||
minHeight: '183px',
|
||||
rotateX,
|
||||
rotateY,
|
||||
transformStyle: "preserve-3d"
|
||||
}}
|
||||
>
|
||||
<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 - Full Image */}
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-xl overflow-hidden [backface-visibility:hidden]"
|
||||
style={{
|
||||
backfaceVisibility: "hidden",
|
||||
WebkitBackfaceVisibility: "hidden",
|
||||
transform: "rotateY(180deg)"
|
||||
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)"
|
||||
}}
|
||||
>
|
||||
<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,
|
||||
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>
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
672
src/components/SuperSavingsPage.tsx
Normal file
672
src/components/SuperSavingsPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user