import cart page and payment page code from figma
This commit is contained in:
@@ -32,6 +32,8 @@ import { SuperSavingsPage } from './pages/SuperSavingsPage';
|
||||
import { WhatsIncluded } from './pages/WhatsIncluded';
|
||||
import { LandingMagicItineraryPage } from './pages/LandingMagicItineraryPage';
|
||||
import { DiscoverPage } from './pages/DiscoverPage';
|
||||
import { CartPage } from './pages/CartPage';
|
||||
import { PaymentDetailsPage } from './pages/PaymentDetailsPage';
|
||||
|
||||
// User type definition
|
||||
interface User {
|
||||
@@ -270,6 +272,17 @@ export function AppRouter({
|
||||
<SuperSavingsPage {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
|
||||
<Route path="/cart" element={
|
||||
<motion.div key="super-savings" {...pageTransition}>
|
||||
<CartPage {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
<Route path="/payment" element={
|
||||
<motion.div key="super-savings" {...pageTransition}>
|
||||
<PaymentDetailsPage {...commonNavHandlers} />
|
||||
</motion.div>
|
||||
} />
|
||||
</Routes>
|
||||
</AnimatePresence>
|
||||
</>
|
||||
|
||||
@@ -656,7 +656,7 @@ export default function Navbar({
|
||||
/>
|
||||
|
||||
{/* Shopping Cart */}
|
||||
<Dropdown
|
||||
{/* <Dropdown
|
||||
ref={cartRef}
|
||||
isOpen={activeCartDropdown}
|
||||
onToggle={() => setActiveCartDropdown(prev => !prev)}
|
||||
@@ -674,7 +674,8 @@ export default function Navbar({
|
||||
</motion.div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
/> */}
|
||||
<ShoppingBag className="w-6 h-6" onClick={() => navigate("/cart-page")} />
|
||||
|
||||
{/* Enhanced City Card Button with Source Tracking */}
|
||||
<div className="flex items-center gap-3 pl-2">
|
||||
@@ -690,7 +691,7 @@ export default function Navbar({
|
||||
label: 'My Profile',
|
||||
icon: <User className="w-4 h-4" />,
|
||||
action: () => {
|
||||
navigate(citySelected?`/${slugify(cityName)}/profile`:'/profile');
|
||||
navigate(citySelected ? `/${slugify(cityName)}/profile` : '/profile');
|
||||
setActiveUserDropdown(false);
|
||||
}
|
||||
},
|
||||
|
||||
877
src/pages/CartPage.tsx
Normal file
877
src/pages/CartPage.tsx
Normal file
@@ -0,0 +1,877 @@
|
||||
import React, { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
import {
|
||||
Users, Baby, ShoppingBag, Trash2, Check, CreditCard, Mail,
|
||||
ChevronRight, ChevronDown, Minus, Plus, Calendar, ArrowLeft, MapPin,
|
||||
Zap, Shield, Clock, Percent, Sparkles
|
||||
} from 'lucide-react';
|
||||
import Navbar from './Navbar';
|
||||
import { Footer } from './Footer';
|
||||
import { ImageWithFallback } from './figma/ImageWithFallback';
|
||||
import { CheckoutStepper } from './CheckoutStepper';
|
||||
import imgRectangle26 from "figma:asset/2496f45326066d3adf0d5494c1dc1595575894ff.png";
|
||||
|
||||
/* ─── Types ─── */
|
||||
export interface CartItem {
|
||||
id: string;
|
||||
city: string;
|
||||
cardType: 'Flexi' | 'Unlimited';
|
||||
days: number;
|
||||
adults: number;
|
||||
children: number;
|
||||
quantity: number;
|
||||
pricePerUnit: number;
|
||||
image: string;
|
||||
}
|
||||
|
||||
interface Attraction {
|
||||
id: string;
|
||||
name: string;
|
||||
image: string;
|
||||
category: string;
|
||||
included: boolean;
|
||||
}
|
||||
|
||||
interface CartPageProps {
|
||||
onBackClick: () => void;
|
||||
onHomeClick: () => void;
|
||||
onPassesClick: () => void;
|
||||
onCheckoutClick?: () => void;
|
||||
onSecureCheckoutClick?: (item: CartItem) => 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;
|
||||
onEsimsClick?: () => void;
|
||||
onHotelDiscountsClick?: () => void;
|
||||
onContactUsClick?: () => void;
|
||||
onCartClick?: () => void;
|
||||
currentPage?: string;
|
||||
user?: { email: string; name: string } | null;
|
||||
}
|
||||
|
||||
/* ─── Data ─── */
|
||||
const initialCartItems: CartItem[] = [
|
||||
{
|
||||
id: '1', city: 'Melbourne', cardType: 'Flexi', days: 3, adults: 3, children: 3, quantity: 2, pricePerUnit: 49.50,
|
||||
image: 'https://images.unsplash.com/photo-1655963754904-2cf2b562a681?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxNZWxib3VybmUlMjBmbGluZGVycyUyMHN0YXRpb24lMjBzdW5zZXR8ZW58MXx8fHwxNzc2MzE5NDgzfDA&ixlib=rb-4.1.0&q=80&w=1080',
|
||||
},
|
||||
{
|
||||
id: '2', city: 'Sydney', cardType: 'Flexi', days: 3, adults: 3, children: 3, quantity: 2, pricePerUnit: 49.50,
|
||||
image: 'https://images.unsplash.com/photo-1695018228065-2e0026c654af?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxTeWRuZXklMjBvcGVyYSUyMGhvdXNlJTIwaGFyYm91ciUyMGJyaWRnZXxlbnwxfHx8fDE3NzYzMTk0ODN8MA&ixlib=rb-4.1.0&q=80&w=1080',
|
||||
},
|
||||
{
|
||||
id: '3', city: 'Melbourne', cardType: 'Unlimited', days: 6, adults: 2, children: 1, quantity: 1, pricePerUnit: 79.00,
|
||||
image: 'https://images.unsplash.com/photo-1705120624704-0970afc29fea?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxNZWxib3VybmUlMjBzdHJlZXQlMjBhcnQlMjBsYW5ld2F5c3xlbnwxfHx8fDE3NzYzMTk0ODR8MA&ixlib=rb-4.1.0&q=80&w=1080',
|
||||
},
|
||||
];
|
||||
|
||||
const dayOptions = [3, 6, 12, 18, 24];
|
||||
|
||||
const attractionsData: Record<string, Record<string, Attraction[]>> = {
|
||||
Melbourne: {
|
||||
Flexi: [
|
||||
{ id: 'mel-1', name: 'SEA LIFE Aquarium', image: 'https://images.unsplash.com/photo-1536845111858-bb269af65cb6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxNZWxib3VybmUlMjBhcXVhcml1bSUyMHVuZGVyd2F0ZXJ8ZW58MXx8fHwxNzc2MzE5OTcwfDA&ixlib=rb-4.1.0&q=80&w=1080', category: 'Wildlife', included: true },
|
||||
{ id: 'mel-2', name: 'Melbourne Zoo', image: 'https://images.unsplash.com/photo-1730074888490-31239540bacf?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxNZWxib3VybmUlMjB6b28lMjB3aWxkbGlmZXxlbnwxfHx8fDE3NzYzMTk5NzB8MA&ixlib=rb-4.1.0&q=80&w=1080', category: 'Wildlife', included: true },
|
||||
{ id: 'mel-3', name: 'Royal Botanic Gardens', image: 'https://images.unsplash.com/photo-1585894507208-eeead8cb9a56?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxNZWxib3VybmUlMjBib3RhbmljYWwlMjBnYXJkZW4lMjBncmVlbnxlbnwxfHx8fDE3NzYzMTk5NzF8MA&ixlib=rb-4.1.0&q=80&w=1080', category: 'Nature', included: true },
|
||||
{ id: 'mel-4', name: 'NGV Art Gallery', image: 'https://images.unsplash.com/photo-1752429242469-55ba7ec210d2?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxhcnQlMjBnYWxsZXJ5JTIwbXVzZXVtJTIwaW50ZXJpb3J8ZW58MXx8fHwxNzc2MzE5OTczfDA&ixlib=rb-4.1.0&q=80&w=1080', category: 'Culture', included: true },
|
||||
],
|
||||
Unlimited: [
|
||||
{ id: 'mel-1', name: 'SEA LIFE Aquarium', image: 'https://images.unsplash.com/photo-1536845111858-bb269af65cb6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxNZWxib3VybmUlMjBhcXVhcml1bSUyMHVuZGVyd2F0ZXJ8ZW58MXx8fHwxNzc2MzE5OTcwfDA&ixlib=rb-4.1.0&q=80&w=1080', category: 'Wildlife', included: true },
|
||||
{ id: 'mel-2', name: 'Melbourne Zoo', image: 'https://images.unsplash.com/photo-1730074888490-31239540bacf?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxNZWxib3VybmUlMjB6b28lMjB3aWxkbGlmZXxlbnwxfHx8fDE3NzYzMTk5NzB8MA&ixlib=rb-4.1.0&q=80&w=1080', category: 'Wildlife', included: true },
|
||||
{ id: 'mel-3', name: 'Royal Botanic Gardens', image: 'https://images.unsplash.com/photo-1585894507208-eeead8cb9a56?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxNZWxib3VybmUlMjBib3RhbmljYWwlMjBnYXJkZW4lMjBncmVlbnxlbnwxfHx8fDE3NzYzMTk5NzF8MA&ixlib=rb-4.1.0&q=80&w=1080', category: 'Nature', included: true },
|
||||
{ id: 'mel-4', name: 'NGV Art Gallery', image: 'https://images.unsplash.com/photo-1752429242469-55ba7ec210d2?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxhcnQlMjBnYWxsZXJ5JTIwbXVzZXVtJTIwaW50ZXJpb3J8ZW58MXx8fHwxNzc2MzE5OTczfDA&ixlib=rb-4.1.0&q=80&w=1080', category: 'Culture', included: true },
|
||||
{ id: 'mel-5', name: 'Melbourne Star Wheel', image: 'https://images.unsplash.com/photo-1769880659692-fa77e04c5ffa?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxvYnNlcnZhdGlvbiUyMHdoZWVsJTIwYW11c2VtZW50JTIwbmlnaHR8ZW58MXx8fHwxNzc2MzE5OTc2fDA&ixlib=rb-4.1.0&q=80&w=1080', category: 'Experience', included: true },
|
||||
{ id: 'mel-6', name: 'Penguin Parade', image: 'https://images.unsplash.com/photo-1670391050251-d1cfbc3891c4?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxwZW5ndWlucyUyMHdpbGRsaWZlJTIwbmF0dXJlfGVufDF8fHx8MTc3NjMxOTk3Nnww&ixlib=rb-4.1.0&q=80&w=1080', category: 'Wildlife', included: true },
|
||||
{ id: 'mel-7', name: 'Yarra River Cruise', image: 'https://images.unsplash.com/photo-1562003914-018a4a6c2171?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxyaXZlciUyMGNydWlzZSUyMGJvYXQlMjBjaXR5fGVufDF8fHx8MTc3NjMxOTk3M3ww&ixlib=rb-4.1.0&q=80&w=1080', category: 'Experience', included: true },
|
||||
],
|
||||
},
|
||||
Sydney: {
|
||||
Flexi: [
|
||||
{ id: 'syd-1', name: 'Harbour Bridge Climb', image: 'https://images.unsplash.com/photo-1767974062666-2685a670e353?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxTeWRuZXklMjBoYXJib3VyJTIwYnJpZGdlJTIwY2xpbWJ8ZW58MXx8fHwxNzc2MzE5OTcxfDA&ixlib=rb-4.1.0&q=80&w=1080', category: 'Adventure', included: true },
|
||||
{ id: 'syd-2', name: 'Taronga Zoo', image: 'https://images.unsplash.com/photo-1704852168456-b70e08441917?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxTeWRuZXklMjB0YXJvbmdhJTIwem9vJTIwYW5pbWFsc3xlbnwxfHx8fDE3NzYzMTk5NzJ8MA&ixlib=rb-4.1.0&q=80&w=1080', category: 'Wildlife', included: true },
|
||||
{ id: 'syd-3', name: 'Art Gallery NSW', image: 'https://images.unsplash.com/photo-1752429242469-55ba7ec210d2?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxhcnQlMjBnYWxsZXJ5JTIwbXVzZXVtJTIwaW50ZXJpb3J8ZW58MXx8fHwxNzc2MzE5OTczfDA&ixlib=rb-4.1.0&q=80&w=1080', category: 'Culture', included: true },
|
||||
],
|
||||
Unlimited: [
|
||||
{ id: 'syd-1', name: 'Harbour Bridge Climb', image: 'https://images.unsplash.com/photo-1767974062666-2685a670e353?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxTeWRuZXklMjBoYXJib3VyJTIwYnJpZGdlJTIwY2xpbWJ8ZW58MXx8fHwxNzc2MzE5OTcxfDA&ixlib=rb-4.1.0&q=80&w=1080', category: 'Adventure', included: true },
|
||||
{ id: 'syd-2', name: 'Taronga Zoo', image: 'https://images.unsplash.com/photo-1704852168456-b70e08441917?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxTeWRuZXklMjB0YXJvbmdhJTIwem9vJTIwYW5pbWFsc3xlbnwxfHx8fDE3NzYzMTk5NzJ8MA&ixlib=rb-4.1.0&q=80&w=1080', category: 'Wildlife', included: true },
|
||||
{ id: 'syd-3', name: 'Art Gallery NSW', image: 'https://images.unsplash.com/photo-1752429242469-55ba7ec210d2?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxhcnQlMjBnYWxsZXJ5JTIwbXVzZXVtJTIwaW50ZXJpb3J8ZW58MXx8fHwxNzc2MzE5OTczfDA&ixlib=rb-4.1.0&q=80&w=1080', category: 'Culture', included: true },
|
||||
{ id: 'syd-4', name: 'Sydney Harbour Cruise', image: 'https://images.unsplash.com/photo-1562003914-018a4a6c2171?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxyaXZlciUyMGNydWlzZSUyMGJvYXQlMjBjaXR5fGVufDF8fHx8MTc3NjMxOTk3M3ww&ixlib=rb-4.1.0&q=80&w=1080', category: 'Experience', included: true },
|
||||
{ id: 'syd-5', name: 'SEA LIFE Aquarium', image: 'https://images.unsplash.com/photo-1536845111858-bb269af65cb6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxNZWxib3VybmUlMjBhcXVhcml1bSUyMHVuZGVyd2F0ZXJ8ZW58MXx8fHwxNzc2MzE5OTcwfDA&ixlib=rb-4.1.0&q=80&w=1080', category: 'Wildlife', included: true },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const offersData: Record<string, { title: string; description: string; image: string }[]> = {
|
||||
Flexi: [
|
||||
{ title: 'Astor Hotels Ultra Deluxe', description: '15% Discount on all treatments for first-time clients', image: 'https://images.unsplash.com/photo-1715191904112-4a5d9c3089fa?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxsdXh1cnklMjBob3RlbCUyMHJlc29ydCUyMGV4dGVyaW9yfGVufDF8fHx8MTc3NjMyMTM2MXww&ixlib=rb-4.1.0&q=80&w=1080' },
|
||||
{ title: 'Green Valley Spa Lux', description: '20% Off on membership plans for new members', image: 'https://images.unsplash.com/photo-1759216853079-831ef8c8b327?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzcGElMjB3ZWxsbmVzcyUyMHRyZWF0bWVudCUyMGludGVyaW9yfGVufDF8fHx8MTc3NjMyMTM2M3ww&ixlib=rb-4.1.0&q=80&w=1080' },
|
||||
{ title: 'Harbour Dining Co.', description: '10% Off your first dining experience at waterfront', image: 'https://images.unsplash.com/photo-1676471932681-45fa972d848a?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxyZXN0YXVyYW50JTIwZmluZSUyMGRpbmluZ3xlbnwxfHx8fDE3NzYzMTkxNDl8MA&ixlib=rb-4.1.0&q=80&w=1080' },
|
||||
{ title: 'National Gallery Exhibition', description: 'Free audio guide with every gallery visit', image: 'https://images.unsplash.com/photo-1569342380852-035f42d9ca41?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxtdXNldW0lMjBnYWxsZXJ5JTIwZXhoaWJpdGlvbnxlbnwxfHx8fDE3NzYyNDYwMjh8MA&ixlib=rb-4.1.0&q=80&w=1080' },
|
||||
{ title: 'Sunset Harbour Cruise', description: 'Complimentary drink on every sunset cruise booking', image: 'https://images.unsplash.com/photo-1765783800962-83d99ff7b158?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxjcnVpc2UlMjBib2F0JTIwaGFyYm9yJTIwdG91cnxlbnwxfHx8fDE3NzYzMjE2MDd8MA&ixlib=rb-4.1.0&q=80&w=1080' },
|
||||
],
|
||||
Unlimited: [
|
||||
{ title: 'SkyView Ferris Wheel', description: 'Complimentary second ride for all pass holders', image: 'https://images.unsplash.com/photo-1626209025747-b41ee6ec191f?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxmZXJyaXMlMjB3aGVlbCUyMGFtdXNlbWVudCUyMHBhcmt8ZW58MXx8fHwxNzc2MzE3NDI2fDA&ixlib=rb-4.1.0&q=80&w=1080' },
|
||||
{ title: 'City Mall Boutique', description: '15% Off at select boutique stores with your pass', image: 'https://images.unsplash.com/photo-1567966689299-819568579d36?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxzaG9wcGluZyUyMG1hbGwlMjBib3V0aXF1ZSUyMHJldGFpbHxlbnwxfHx8fDE3NzYzMjEzNjN8MA&ixlib=rb-4.1.0&q=80&w=1080' },
|
||||
{ title: 'Adventure Outfitters', description: 'Free gear rental on outdoor adventure bookings', image: 'https://images.unsplash.com/photo-1761131221577-0716baffc6ef?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxhZHZlbnR1cmUlMjBzcG9ydHMlMjBvdXRkb29yJTIwYWN0aXZpdHl8ZW58MXx8fHwxNzc2MzIxMzYzfDA&ixlib=rb-4.1.0&q=80&w=1080' },
|
||||
{ title: 'Skyline Rooftop Lounge', description: 'Buy one get one free on signature cocktails', image: 'https://images.unsplash.com/photo-1642114955097-8f3d0e141641?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHxyb29mdG9wJTIwYmFyJTIwY2l0eSUyMHNreWxpbmUlMjBuaWdodHxlbnwxfHx8fDE3NzYyNDU2NTl8MA&ixlib=rb-4.1.0&q=80&w=1080' },
|
||||
{ title: 'Yarra Valley Wines', description: 'Exclusive wine tasting tour with pass holders discount', image: 'https://images.unsplash.com/photo-1764649841527-c8852b63cc53?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w3Nzg4Nzd8MHwxfHNlYXJjaHwxfHx3aW5lJTIwdGFzdGluZyUyMHZpbmV5YXJkJTIwY2VsbGFyfGVufDF8fHx8MTc3NjMyMTYwOHww&ixlib=rb-4.1.0&q=80&w=1080' },
|
||||
],
|
||||
};
|
||||
|
||||
const priceTable: Record<string, Record<number, number>> = {
|
||||
Flexi: { 3: 49.5, 6: 69, 12: 99, 18: 129, 24: 159 },
|
||||
Unlimited: { 3: 79, 6: 109, 12: 149, 18: 189, 24: 229 },
|
||||
};
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
FIGMA CARD TYPE COMPONENTS
|
||||
═══════════════════════════════════════════ */
|
||||
|
||||
function FlexiCardPreview({ city, adultPrice, childPrice, isSelected }: { city: string; adultPrice: number; childPrice: number; isSelected: boolean }) {
|
||||
return (
|
||||
<div className={`relative h-[160px] w-full rounded-lg transition-all duration-200 ${
|
||||
isSelected ? 'ring-2 ring-[#F95F62] shadow-md shadow-[#F95F62]/10' : 'hover:shadow-md'
|
||||
}`}>
|
||||
{/* Card bg */}
|
||||
<div className="absolute inset-0 bg-white border border-[rgba(249,95,175,0.2)] rounded-lg shadow-[0px_4px_20px_0px_rgba(0,0,0,0.06)]" />
|
||||
{/* City image */}
|
||||
<div className="absolute h-[158px] left-[1px] top-[1px] w-[103px] rounded-bl-[7px] rounded-tl-[7px] overflow-hidden">
|
||||
<img alt="" className="absolute inset-0 w-full h-full object-cover" src={imgRectangle26} />
|
||||
</div>
|
||||
{/* City name - left aligned */}
|
||||
<div className="absolute left-[112px] top-[12px]">
|
||||
<p className="font-['Poppins',sans-serif] font-medium text-[16px] text-[#2a2a2a] leading-[22px] whitespace-nowrap">{city}</p>
|
||||
</div>
|
||||
{/* Pricing */}
|
||||
<div className="absolute left-[112px] top-[40px] flex flex-col gap-[6px]">
|
||||
<div className="flex gap-[2px] items-center">
|
||||
<span className="font-['Poppins',sans-serif] text-[11px] text-[rgba(0,0,0,0.6)] tracking-[0.06px]">From</span>
|
||||
<span className="font-['Poppins',sans-serif] font-medium text-[24px] text-[#f95f62] tracking-[-0.96px] leading-[1.3]">${adultPrice}</span>
|
||||
<span className="font-['Poppins',sans-serif] text-[11px] text-[rgba(0,0,0,0.8)] tracking-[0.06px]">/Adult</span>
|
||||
</div>
|
||||
<div className="flex gap-[2px] items-center">
|
||||
<span className="font-['Poppins',sans-serif] text-[11px] text-[rgba(0,0,0,0.6)] tracking-[0.06px]">and</span>
|
||||
<span className="font-['Poppins',sans-serif] font-medium text-[24px] text-[#f95f62] tracking-[-0.96px] leading-[1.3]">${childPrice}</span>
|
||||
<span className="font-['Poppins',sans-serif] text-[11px] text-[rgba(0,0,0,0.8)] tracking-[0.06px]">/Child</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* Description */}
|
||||
<div className="absolute left-[112px] top-[112px] right-[44px]">
|
||||
<p className="font-['Poppins',sans-serif] text-[11px] text-left text-[rgba(0,0,0,0.4)] tracking-[0.06px] leading-[14px]">
|
||||
Dive into an extensive selection of thrilling destinations!
|
||||
</p>
|
||||
</div>
|
||||
{/* Side tab - Flexi (pink) */}
|
||||
<div className="absolute bg-[#f95faf] h-full right-0 top-0 w-[35px] rounded-br-lg rounded-tr-lg flex flex-col items-center justify-center gap-[2px]">
|
||||
<span className="font-['Poppins',sans-serif] text-[12px] text-white/70 [writing-mode:vertical-rl] rotate-180">Card</span>
|
||||
<span className="font-['Poppins',sans-serif] text-[16px] text-white [writing-mode:vertical-rl] rotate-180">Flexi</span>
|
||||
</div>
|
||||
{/* Selected checkmark */}
|
||||
{isSelected && (
|
||||
<div className="absolute top-2 right-[44px] w-6 h-6 rounded-full bg-[#F95F62] flex items-center justify-center z-10">
|
||||
<Check className="w-3.5 h-3.5 text-white" strokeWidth={3} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UnlimitedCardPreview({ city, adultPrice, childPrice, isSelected }: { city: string; adultPrice: number; childPrice: number; isSelected: boolean }) {
|
||||
return (
|
||||
<div className={`relative h-[160px] w-full rounded-lg transition-all duration-200 ${
|
||||
isSelected ? 'ring-2 ring-[#F95F62] shadow-md shadow-[#F95F62]/10' : 'hover:shadow-md'
|
||||
}`}>
|
||||
{/* Card bg */}
|
||||
<div className="absolute inset-0 bg-white border border-[rgba(0,0,0,0.2)] rounded-lg" />
|
||||
{/* City image */}
|
||||
<div className="absolute h-[158px] left-[1px] top-[1px] w-[103px] rounded-bl-[7px] rounded-tl-[7px] overflow-hidden">
|
||||
<img alt="" className="absolute inset-0 w-full h-full object-cover" src={imgRectangle26} />
|
||||
</div>
|
||||
{/* City name - left aligned */}
|
||||
<div className="absolute left-[112px] top-[12px]">
|
||||
<p className="font-['Poppins',sans-serif] font-medium text-[16px] text-[#2a2a2a] leading-[20px] whitespace-nowrap">{city}</p>
|
||||
</div>
|
||||
{/* Pricing */}
|
||||
<div className="absolute left-[112px] top-[40px] flex flex-col gap-[6px]">
|
||||
<div className="flex gap-[2px] items-center">
|
||||
<span className="font-['Poppins',sans-serif] text-[11px] text-[rgba(0,0,0,0.6)] tracking-[0.06px]">From</span>
|
||||
<span className="font-['Poppins',sans-serif] font-medium text-[24px] text-[#f95f62] tracking-[-0.96px] leading-[1.3]">${adultPrice}</span>
|
||||
<span className="font-['Poppins',sans-serif] text-[11px] text-[rgba(0,0,0,0.8)] tracking-[0.06px]">/Adult</span>
|
||||
</div>
|
||||
<div className="flex gap-[2px] items-center">
|
||||
<span className="font-['Poppins',sans-serif] text-[11px] text-[rgba(0,0,0,0.6)] tracking-[0.06px]">and</span>
|
||||
<span className="font-['Poppins',sans-serif] font-medium text-[24px] text-[#f95f62] tracking-[-0.96px] leading-[1.3]">${childPrice}</span>
|
||||
<span className="font-['Poppins',sans-serif] text-[11px] text-[rgba(0,0,0,0.8)] tracking-[0.06px]">/Child</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* Description */}
|
||||
<div className="absolute left-[112px] top-[112px] right-[44px]">
|
||||
<p className="font-['Poppins',sans-serif] text-[11px] text-left text-[rgba(0,0,0,0.4)] tracking-[0.06px] leading-[14px]">
|
||||
Dive into an extensive selection of thrilling destinations!
|
||||
</p>
|
||||
</div>
|
||||
{/* Side tab - Unlimited (coral) */}
|
||||
<div className="absolute bg-[#f95f62] h-full right-0 top-0 w-[35px] rounded-br-lg rounded-tr-lg flex flex-col items-center justify-center gap-[2px]">
|
||||
<span className="font-['Poppins',sans-serif] text-[12px] text-white/70 [writing-mode:vertical-rl] rotate-180">Card</span>
|
||||
<span className="font-['Poppins',sans-serif] text-[16px] text-white [writing-mode:vertical-rl] rotate-180">Unlimited</span>
|
||||
</div>
|
||||
{/* Selected checkmark */}
|
||||
{isSelected && (
|
||||
<div className="absolute top-2 right-[44px] w-6 h-6 rounded-full bg-[#F95F62] flex items-center justify-center z-10">
|
||||
<Check className="w-3.5 h-3.5 text-white" strokeWidth={3} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
CHECKOUT CONFIGURATION CARD (Mobile-first)
|
||||
═══════════════════════════════════════════ */
|
||||
|
||||
function CheckoutConfigCard({
|
||||
item,
|
||||
onChange,
|
||||
onProceed,
|
||||
}: {
|
||||
item: CartItem;
|
||||
onChange: (updates: Partial<CartItem>) => void;
|
||||
onProceed: () => void;
|
||||
}) {
|
||||
const [daysOpen, setDaysOpen] = useState(false);
|
||||
const originalPrice = (item.pricePerUnit * item.quantity * 1.35);
|
||||
const totalPrice = item.pricePerUnit * item.quantity;
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-2xl shadow-[0px_4px_24px_0px_rgba(0,0,0,0.06)] overflow-hidden w-full max-w-[400px]">
|
||||
{/* City header */}
|
||||
<div className="pt-6 pb-2 text-center">
|
||||
<h4 className="font-poppins text-lg leading-snug font-medium text-[#2a2a2a]">{item.city}</h4>
|
||||
<div className="mt-2 flex justify-center">
|
||||
<span className={`inline-flex items-center px-4 py-1 rounded-full font-poppins text-xs font-medium ${
|
||||
item.cardType === 'Flexi'
|
||||
? 'bg-[#f95faf]/10 text-[#f95faf]'
|
||||
: 'bg-[#f95f62]/10 text-[#f95f62]'
|
||||
}`}>
|
||||
{item.cardType} Card
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Configuration rows */}
|
||||
<div className="px-6 py-4 space-y-0">
|
||||
{/* No. of Adults */}
|
||||
<div className="flex items-center justify-between py-4 border-b border-gray-100">
|
||||
<span className="font-poppins text-sm font-normal text-[#2a2a2a]">No. of Adults</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => item.adults > 1 && onChange({ adults: item.adults - 1 })}
|
||||
disabled={item.adults <= 1}
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center transition-colors ${
|
||||
item.adults <= 1 ? 'bg-gray-100 text-gray-300 cursor-not-allowed' : 'bg-[#f95f62]/10 text-[#f95f62] hover:bg-[#f95f62]/20'
|
||||
}`}
|
||||
>
|
||||
<Minus className="w-4 h-4" />
|
||||
</button>
|
||||
<span className="font-poppins text-base font-medium text-[#2a2a2a] w-5 text-center tabular-nums">{item.adults}</span>
|
||||
<button
|
||||
onClick={() => onChange({ adults: item.adults + 1 })}
|
||||
className="w-8 h-8 rounded-full bg-[#f95f62]/10 text-[#f95f62] hover:bg-[#f95f62]/20 flex items-center justify-center transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* No. of Children */}
|
||||
<div className="flex items-center justify-between py-4 border-b border-gray-100">
|
||||
<span className="font-poppins text-sm font-normal text-[#2a2a2a]">No. of Children</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => item.children > 0 && onChange({ children: item.children - 1 })}
|
||||
disabled={item.children <= 0}
|
||||
className={`w-8 h-8 rounded-full flex items-center justify-center transition-colors ${
|
||||
item.children <= 0 ? 'bg-gray-100 text-gray-300 cursor-not-allowed' : 'bg-[#f95f62]/10 text-[#f95f62] hover:bg-[#f95f62]/20'
|
||||
}`}
|
||||
>
|
||||
<Minus className="w-4 h-4" />
|
||||
</button>
|
||||
<span className="font-poppins text-base font-medium text-[#2a2a2a] w-5 text-center tabular-nums">{item.children}</span>
|
||||
<button
|
||||
onClick={() => onChange({ children: item.children + 1 })}
|
||||
className="w-8 h-8 rounded-full bg-[#f95f62]/10 text-[#f95f62] hover:bg-[#f95f62]/20 flex items-center justify-center transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* No. of Days (dropdown) */}
|
||||
<div className="flex items-center justify-between py-4 border-b border-gray-100">
|
||||
<span className="font-poppins text-sm font-normal text-[#2a2a2a]">
|
||||
{item.cardType === 'Flexi' ? 'No. of Attractions' : 'No. of Days'}
|
||||
</span>
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setDaysOpen(!daysOpen)}
|
||||
className="flex items-center gap-2 border border-[#f95f62]/30 rounded-lg px-3 py-1.5 min-w-[72px] justify-between hover:border-[#f95f62] transition-colors"
|
||||
>
|
||||
<span className="font-poppins text-base font-medium text-[#f95f62] tabular-nums">{item.days}</span>
|
||||
<ChevronDown className={`w-4 h-4 text-[#f95f62] transition-transform ${daysOpen ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
<AnimatePresence>
|
||||
{daysOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -4, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: -4, scale: 0.95 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className="absolute right-0 top-full mt-1 bg-white rounded-lg shadow-lg border border-gray-100 z-30 min-w-[72px] overflow-hidden"
|
||||
>
|
||||
{dayOptions.map((d) => (
|
||||
<button
|
||||
key={d}
|
||||
onClick={() => { onChange({ days: d }); setDaysOpen(false); }}
|
||||
className={`w-full px-3 py-2 text-left font-poppins text-sm transition-colors ${
|
||||
item.days === d
|
||||
? 'bg-[#f95f62]/10 text-[#f95f62] font-medium'
|
||||
: 'text-[#2a2a2a] hover:bg-gray-50 font-normal'
|
||||
}`}
|
||||
>
|
||||
{d}
|
||||
</button>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* You Pay */}
|
||||
<div className="flex items-center justify-between py-5">
|
||||
<span className="font-poppins text-sm font-normal text-[#2a2a2a]">You Pay</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-poppins text-sm font-normal text-[#aaa] line-through">
|
||||
${originalPrice.toFixed(0)}
|
||||
</span>
|
||||
<span className="font-poppins text-2xl font-medium text-[#f95f62] tracking-tight">
|
||||
${totalPrice.toFixed(0)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Proceed button */}
|
||||
<div className="px-6 pb-6">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.01 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={onProceed}
|
||||
className="w-full py-4 rounded-full bg-[#f95f62] text-white font-poppins text-base font-medium hover:bg-[#e8545a] transition-colors shadow-lg shadow-[#f95f62]/20"
|
||||
>
|
||||
Proceed to Pay
|
||||
</motion.button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
MAIN CART PAGE
|
||||
═══════════════════════════════════════════ */
|
||||
|
||||
export function CartPage({
|
||||
onBackClick,
|
||||
onHomeClick,
|
||||
onPassesClick,
|
||||
onCheckoutClick,
|
||||
onSecureCheckoutClick,
|
||||
onSignInClick,
|
||||
onSignOutClick,
|
||||
onAttractionsClick,
|
||||
onBlogsClick,
|
||||
onHowItWorksClick,
|
||||
onFAQClick,
|
||||
onPrivacyPolicyClick,
|
||||
onAboutUsClick,
|
||||
onProfileClick,
|
||||
onCityCardsClick,
|
||||
onMagicItineraryClick,
|
||||
onPostCardsClick,
|
||||
onOffersClick,
|
||||
onSuperSavingsClick,
|
||||
onEsimsClick,
|
||||
onHotelDiscountsClick,
|
||||
onContactUsClick,
|
||||
onCartClick,
|
||||
currentPage,
|
||||
user,
|
||||
}: CartPageProps) {
|
||||
const [activeTab, setActiveTab] = useState<'cards' | 'postcards'>('cards');
|
||||
const [cartItems, setCartItems] = useState<CartItem[]>(initialCartItems);
|
||||
const [selectedCardId, setSelectedCardId] = useState<string | null>(null);
|
||||
const [view, setView] = useState<'cart' | 'checkout'>('cart');
|
||||
const [checkoutItem, setCheckoutItem] = useState<CartItem | null>(null);
|
||||
|
||||
const handleRemoveItem = (id: string) => {
|
||||
setCartItems(prev => prev.filter(item => item.id !== id));
|
||||
if (selectedCardId === id) setSelectedCardId(null);
|
||||
};
|
||||
|
||||
const handleSelectCard = (id: string) => {
|
||||
setSelectedCardId(prev => (prev === id ? null : id));
|
||||
};
|
||||
|
||||
const handleGoToCheckout = () => {
|
||||
const item = cartItems.find(i => i.id === selectedCardId);
|
||||
if (item) {
|
||||
setCheckoutItem({ ...item });
|
||||
setView('checkout');
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
|
||||
const handleBackToCart = () => {
|
||||
setView('cart');
|
||||
setCheckoutItem(null);
|
||||
};
|
||||
|
||||
const handleCheckoutItemChange = (updates: Partial<CartItem>) => {
|
||||
if (!checkoutItem) return;
|
||||
const updated = { ...checkoutItem, ...updates };
|
||||
const prices = priceTable[updated.cardType];
|
||||
if (prices && prices[updated.days] !== undefined) {
|
||||
updated.pricePerUnit = prices[updated.days];
|
||||
}
|
||||
setCheckoutItem(updated);
|
||||
};
|
||||
|
||||
const isEmpty = cartItems.length === 0;
|
||||
const selectedItem = cartItems.find(i => i.id === selectedCardId);
|
||||
const attractions = checkoutItem ? (attractionsData[checkoutItem.city]?.[checkoutItem.cardType] || []) : [];
|
||||
const offers = checkoutItem ? (offersData[checkoutItem.cardType] || []) : [];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#fafafa] font-poppins">
|
||||
<Navbar
|
||||
activeCity="Melbourne" onCityChange={() => {}} onSignInClick={onSignInClick} onSignOutClick={onSignOutClick}
|
||||
onPassesClick={onPassesClick} onCheckoutClick={onCheckoutClick} onHomeClick={onHomeClick}
|
||||
onAttractionsClick={onAttractionsClick} onBlogsClick={onBlogsClick} onHowItWorksClick={onHowItWorksClick}
|
||||
onFAQClick={onFAQClick} onPrivacyPolicyClick={onPrivacyPolicyClick} onAboutUsClick={onAboutUsClick}
|
||||
onProfileClick={onProfileClick} onCityCardsClick={onCityCardsClick} onMagicItineraryClick={onMagicItineraryClick}
|
||||
onPostCardsClick={onPostCardsClick} onOffersClick={onOffersClick} onSuperSavingsClick={onSuperSavingsClick}
|
||||
onEsimsClick={onEsimsClick} onHotelDiscountsClick={onHotelDiscountsClick} onCartClick={onCartClick}
|
||||
currentPage={currentPage as any} user={user}
|
||||
/>
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
{view === 'cart' ? (
|
||||
/* ─── CART VIEW ─── */
|
||||
<motion.div
|
||||
key="cart-view"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0, x: -30 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="w-full px-4 sm:px-6 lg:px-10 xl:px-16 pt-32 pb-24 max-w-[1440px] mx-auto"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h2 className="font-poppins text-2xl md:text-3xl lg:text-4xl leading-tight">
|
||||
<span className="font-light">Your</span>{' '}
|
||||
<span className="font-bold italic bg-gradient-to-r from-[#F95F62] to-[#F95FAF] bg-clip-text text-transparent">Cart</span>
|
||||
</h2>
|
||||
<p className="font-poppins text-sm leading-relaxed font-normal text-[#8e8e8e] mt-1">
|
||||
{isEmpty ? 'Your cart is empty' : `${cartItems.length} ${cartItems.length === 1 ? 'item' : 'items'} in your cart`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tab switcher */}
|
||||
{/* Cards listed directly below */}
|
||||
|
||||
{/* Content */}
|
||||
<AnimatePresence mode="wait">
|
||||
{activeTab === 'cards' ? (
|
||||
<motion.div key="cards-content" initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -12 }} transition={{ duration: 0.2 }}>
|
||||
{isEmpty ? (
|
||||
<EmptyState icon={<CreditCard className="w-16 h-16 text-[#F95F62]/20" strokeWidth={1.2} />} title="No cards in your cart" description="Browse our city passes to unlock amazing experiences and savings on your next adventure" actionLabel="Explore Passes" onAction={onPassesClick} />
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{/* Table header (desktop) */}
|
||||
<div className="hidden md:grid md:grid-cols-12 gap-4 px-5 pb-2">
|
||||
<div className="col-span-5 font-poppins text-xs font-medium text-[#8e8e8e] uppercase tracking-wider">City Cards</div>
|
||||
<div className="col-span-2 font-poppins text-xs font-medium text-[#8e8e8e] uppercase tracking-wider text-center">Travellers</div>
|
||||
<div className="col-span-1 font-poppins text-xs font-medium text-[#8e8e8e] uppercase tracking-wider text-center">Qty</div>
|
||||
<div className="col-span-3 font-poppins text-xs font-medium text-[#8e8e8e] uppercase tracking-wider text-right">Price</div>
|
||||
<div className="col-span-1" />
|
||||
</div>
|
||||
|
||||
<AnimatePresence>
|
||||
{cartItems.map((item) => {
|
||||
const isSelected = selectedCardId === item.id;
|
||||
const totalPrice = item.pricePerUnit * item.quantity;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={item.id}
|
||||
layout
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, x: -60, transition: { duration: 0.25 } }}
|
||||
onClick={() => handleSelectCard(item.id)}
|
||||
className={`relative bg-white rounded-2xl overflow-hidden cursor-pointer transition-all duration-300 ${
|
||||
isSelected ? 'ring-2 ring-[#F95F62] shadow-lg shadow-[#F95F62]/8' : 'ring-1 ring-gray-100 hover:ring-gray-200 hover:shadow-md'
|
||||
}`}
|
||||
>
|
||||
{/* Selected badge */}
|
||||
<AnimatePresence>
|
||||
{isSelected && (
|
||||
<motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }} className="absolute top-3 left-3 z-20 w-6 h-6 rounded-full bg-[#F95F62] flex items-center justify-center shadow-md">
|
||||
<Check className="w-3.5 h-3.5 text-white" strokeWidth={3} />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Mobile layout */}
|
||||
<div className="md:hidden flex gap-4 p-4">
|
||||
<div className="w-20 h-20 rounded-xl overflow-hidden flex-shrink-0 relative">
|
||||
<ImageWithFallback src={item.image} alt={item.city} className="absolute inset-0 w-full h-full object-cover" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h5 className="font-poppins text-base leading-snug font-medium text-[#2a2a2a]">{item.city}</h5>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
<span className={`inline-flex px-2 py-0.5 rounded-full text-[10px] font-medium ${item.cardType === 'Flexi' ? 'bg-[#F95FAF]/10 text-[#F95FAF]' : 'bg-[#F95F62]/10 text-[#F95F62]'}`}>{item.cardType}</span>
|
||||
<span className="font-poppins text-xs font-normal text-[#8e8e8e]">{item.days}d</span>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={(e) => { e.stopPropagation(); handleRemoveItem(item.id); }} className="p-1.5 rounded-lg text-gray-300 hover:text-[#F95F62] hover:bg-red-50 transition-colors">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
<span className="font-poppins text-xs font-normal text-[#8e8e8e]">{item.adults}A · {item.children}C · Qty {item.quantity}</span>
|
||||
<div className="text-right">
|
||||
<span className="font-poppins text-base font-medium text-[#F95F62] tracking-tight">${totalPrice.toFixed(2)}</span>
|
||||
{item.quantity > 1 && <span className="block font-poppins text-[10px] font-normal text-[#aaa]">${item.pricePerUnit.toFixed(2)}/ea</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop layout */}
|
||||
<div className="hidden md:grid md:grid-cols-12 gap-4 items-center p-5">
|
||||
<div className="col-span-5 flex items-center gap-4">
|
||||
<div className="w-16 h-16 rounded-xl overflow-hidden flex-shrink-0 relative">
|
||||
<ImageWithFallback src={item.image} alt={item.city} className="absolute inset-0 w-full h-full object-cover" />
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="font-poppins text-base leading-snug font-medium text-[#2a2a2a]">{item.city}</h5>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
<span className={`inline-flex px-2.5 py-0.5 rounded-full text-xs font-medium ${item.cardType === 'Flexi' ? 'bg-[#F95FAF]/10 text-[#F95FAF]' : 'bg-[#F95F62]/10 text-[#F95F62]'}`}>{item.cardType} Card</span>
|
||||
<span className="flex items-center gap-1 font-poppins text-xs font-normal text-[#8e8e8e]"><Calendar className="w-3 h-3" />{item.days} days</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2 text-center">
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<span className="flex items-center gap-1 font-poppins text-sm font-normal text-[#555]"><Users className="w-3.5 h-3.5 text-[#8e8e8e]" />{item.adults}</span>
|
||||
<span className="flex items-center gap-1 font-poppins text-sm font-normal text-[#555]"><Baby className="w-3.5 h-3.5 text-[#8e8e8e]" />{item.children}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-1 flex justify-center">
|
||||
<span className="font-poppins text-sm font-medium text-[#2a2a2a] bg-gray-50 px-4 py-1.5 rounded-full">{item.quantity}</span>
|
||||
</div>
|
||||
<div className="col-span-3 text-right">
|
||||
<span className="font-poppins text-lg font-medium text-[#F95F62] tracking-tight">${totalPrice.toFixed(2)}</span>
|
||||
{item.quantity > 1 && <span className="block font-poppins text-xs font-normal text-[#aaa] mt-0.5">${item.pricePerUnit.toFixed(2)} per unit</span>}
|
||||
</div>
|
||||
<div className="col-span-1 flex justify-end">
|
||||
<button onClick={(e) => { e.stopPropagation(); handleRemoveItem(item.id); }} className="p-2 rounded-lg text-gray-300 hover:text-[#F95F62] hover:bg-red-50 transition-all" title="Remove from cart">
|
||||
<Trash2 className="w-4.5 h-4.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Bottom checkout bar */}
|
||||
<motion.div layout className="mt-6 bg-white rounded-2xl ring-1 ring-gray-100 p-5 flex flex-col sm:flex-row items-center justify-between gap-4">
|
||||
<div className="text-center sm:text-left">
|
||||
{selectedItem ? (
|
||||
<>
|
||||
<p className="font-poppins text-xs font-normal text-[#8e8e8e]">
|
||||
Selected: {selectedItem.city} {selectedItem.cardType} · {selectedItem.days}d · Qty {selectedItem.quantity}
|
||||
</p>
|
||||
<p className="font-poppins text-2xl font-medium text-[#F95F62] tracking-tight mt-0.5">
|
||||
${(selectedItem.pricePerUnit * selectedItem.quantity).toFixed(2)}
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="font-poppins text-sm font-normal text-[#8e8e8e]">Tap a card above to select it for checkout</p>
|
||||
)}
|
||||
</div>
|
||||
<motion.button
|
||||
whileHover={selectedItem ? { scale: 1.02 } : {}}
|
||||
whileTap={selectedItem ? { scale: 0.98 } : {}}
|
||||
onClick={handleGoToCheckout}
|
||||
disabled={!selectedItem}
|
||||
className={`w-full sm:w-auto px-8 py-3.5 rounded-xl font-poppins text-base font-medium flex items-center justify-center gap-2 transition-all duration-200 ${
|
||||
selectedItem ? 'bg-[#F95F62] text-white hover:bg-[#e8545a] shadow-lg shadow-[#F95F62]/20' : 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
Secure Checkout <ChevronRight className="w-4 h-4" />
|
||||
</motion.button>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div key="postcards-content" initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -12 }} transition={{ duration: 0.2 }}>
|
||||
<EmptyState icon={<Mail className="w-16 h-16 text-[#F95F62]/20" strokeWidth={1.2} />} title="No post cards yet" description="Send beautiful digital post cards to friends and family from your favourite destinations around the world" actionLabel="Browse Post Cards" onAction={onPostCardsClick} />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
) : (
|
||||
/* ─── CHECKOUT VIEW ─── */
|
||||
<motion.div
|
||||
key="checkout-view"
|
||||
initial={{ opacity: 0, x: 40 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: 40 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="w-full px-4 sm:px-6 lg:px-10 xl:px-16 pt-32 pb-24 max-w-[1440px] mx-auto"
|
||||
>
|
||||
{checkoutItem && (
|
||||
<>
|
||||
{/* Back */}
|
||||
<button onClick={handleBackToCart} className="flex items-center gap-2 text-[#8e8e8e] hover:text-[#2a2a2a] transition-colors font-poppins text-sm font-normal mb-8">
|
||||
<ArrowLeft className="w-4 h-4" />Back to Cart
|
||||
</button>
|
||||
|
||||
{/* Stepper */}
|
||||
<CheckoutStepper currentStep={2} />
|
||||
|
||||
{/* Checkout heading */}
|
||||
<div className="mb-10">
|
||||
<h2 className="font-poppins text-2xl md:text-3xl lg:text-4xl leading-tight">
|
||||
<span className="font-light">Checkout</span>{' '}
|
||||
<span className="font-bold italic bg-gradient-to-r from-[#F95F62] to-[#F95FAF] bg-clip-text text-transparent">{checkoutItem.city}</span>
|
||||
</h2>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-10">
|
||||
{/* Left column */}
|
||||
<div className="flex-1 space-y-8">
|
||||
|
||||
{/* ── Card Type Selection (Figma cards) ── */}
|
||||
<div>
|
||||
<h3 className="font-poppins text-xl md:text-2xl leading-snug font-medium text-[#2a2a2a]">
|
||||
Choose Your Card
|
||||
</h3>
|
||||
<p className="font-poppins text-sm leading-relaxed font-normal text-[#8e8e8e] mt-1 mb-4">
|
||||
Select the card type that best suits your travel style
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-[16px]">
|
||||
{/* Flexi */}
|
||||
<button
|
||||
onClick={() => handleCheckoutItemChange({ cardType: 'Flexi' })}
|
||||
className="relative transition-all duration-200"
|
||||
>
|
||||
<FlexiCardPreview
|
||||
city={checkoutItem.city}
|
||||
adultPrice={priceTable.Flexi[checkoutItem.days] || 80}
|
||||
childPrice={10}
|
||||
isSelected={checkoutItem.cardType === 'Flexi'}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{/* Unlimited */}
|
||||
<button
|
||||
onClick={() => handleCheckoutItemChange({ cardType: 'Unlimited' })}
|
||||
className="relative transition-all duration-200"
|
||||
>
|
||||
<UnlimitedCardPreview
|
||||
city={checkoutItem.city}
|
||||
adultPrice={priceTable.Unlimited[checkoutItem.days] || 120}
|
||||
childPrice={20}
|
||||
isSelected={checkoutItem.cardType === 'Unlimited'}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* ── Config Card (mobile only) — right after card selection ── */}
|
||||
<div className="lg:hidden mt-6">
|
||||
<CheckoutConfigCard
|
||||
item={checkoutItem}
|
||||
onChange={handleCheckoutItemChange}
|
||||
onProceed={() => checkoutItem && onSecureCheckoutClick?.(checkoutItem)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Features Comparison */}
|
||||
<div className="mt-6 bg-[#f5f5f5] rounded-xl p-4">
|
||||
<div className="grid grid-cols-[1fr_70px_70px] gap-y-0 items-center">
|
||||
{/* Header */}
|
||||
<p className="font-poppins font-medium text-sm text-[#2a2a2a] py-3">Features</p>
|
||||
<p className="font-poppins font-medium text-sm text-[#2a2a2a] text-center py-3">Flexi</p>
|
||||
<p className="font-poppins font-medium text-sm text-[#2a2a2a] text-center py-3">Unlimited</p>
|
||||
{[
|
||||
{ feature: 'Access to attractions', flexi: true, unlimited: true },
|
||||
{ feature: 'Entry to attractions', flexi: true, unlimited: true },
|
||||
{ feature: 'Access to experiences', flexi: true, unlimited: true },
|
||||
{ feature: 'Entry to sites', flexi: false, unlimited: true },
|
||||
{ feature: 'Access to venues', flexi: true, unlimited: true },
|
||||
{ feature: 'Entry to events', flexi: true, unlimited: true },
|
||||
{ feature: 'Access to experiences', flexi: false, unlimited: true },
|
||||
{ feature: 'Access to Itinerary creation', flexi: false, unlimited: true },
|
||||
{ feature: 'Access to postcard creation', flexi: false, unlimited: true },
|
||||
].map((row, i) => (
|
||||
<React.Fragment key={i}>
|
||||
<p className="font-poppins font-normal text-[13px] text-[#2a2a2a] py-2.5 border-t border-[rgba(0,0,0,0.08)] flex items-center gap-1.5">
|
||||
<span className="text-[#2a2a2a]">•</span> {row.feature}
|
||||
</p>
|
||||
<div className="flex justify-center py-2.5 border-t border-[rgba(0,0,0,0.08)]">
|
||||
{row.flexi ? (
|
||||
<div className="w-5 h-5 rounded-full bg-[#F95F62] flex items-center justify-center">
|
||||
<Check className="w-3 h-3 text-white" strokeWidth={3} />
|
||||
</div>
|
||||
) : (
|
||||
<span className="font-poppins text-[13px] text-[rgba(0,0,0,0.3)]">–</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-center py-2.5 border-t border-[rgba(0,0,0,0.08)]">
|
||||
{row.unlimited ? (
|
||||
<div className="w-5 h-5 rounded-full bg-[#F95F62] flex items-center justify-center">
|
||||
<Check className="w-3 h-3 text-white" strokeWidth={3} />
|
||||
</div>
|
||||
) : (
|
||||
<span className="font-poppins text-[13px] text-[rgba(0,0,0,0.3)]">–</span>
|
||||
)}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Offers ── */}
|
||||
<div>
|
||||
<h3 className="font-poppins text-xl md:text-2xl leading-snug font-medium text-[#2a2a2a]">
|
||||
{checkoutItem.cardType} Card Offers
|
||||
</h3>
|
||||
<p className="font-poppins text-sm leading-relaxed font-normal text-[#8e8e8e] mt-1 mb-4">
|
||||
Exclusive deals and discounts included with your {checkoutItem.cardType} pass
|
||||
</p>
|
||||
<div className="flex gap-3 overflow-x-auto pb-2 -mx-4 px-4 snap-x snap-mandatory scrollbar-hide">
|
||||
{offers.map((offer, idx) => (
|
||||
<div key={idx} className="relative bg-white rounded-xl shrink-0 w-[180px] h-[260px] snap-start">
|
||||
<div className="flex flex-col gap-2 items-start overflow-hidden p-3 rounded-xl h-full">
|
||||
<div className="h-[120px] w-full rounded-lg overflow-hidden shrink-0 relative">
|
||||
<ImageWithFallback
|
||||
src={offer.image}
|
||||
alt={offer.title}
|
||||
className="absolute inset-0 w-full h-full object-cover rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full h-[44px] overflow-hidden">
|
||||
<p className="font-['Poppins',sans-serif] font-normal text-[18px] text-black tracking-[-0.72px] leading-[22px] line-clamp-2">
|
||||
{offer.title}
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-full flex-1">
|
||||
<p className="font-['Poppins',sans-serif] font-normal text-[12px] text-[rgba(0,0,0,0.6)] leading-[16px] line-clamp-3">
|
||||
{offer.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute inset-0 border border-[rgba(249,95,98,0.24)] rounded-xl pointer-events-none" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Available Attractions ── */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-poppins text-xl md:text-2xl leading-snug font-medium text-[#2a2a2a]">Available Attractions</h3>
|
||||
<span className="font-poppins text-xs font-medium text-[#F95F62] bg-[#F95F62]/10 px-3 py-1 rounded-full">{attractions.length} included</span>
|
||||
</div>
|
||||
<p className="font-poppins text-sm leading-relaxed font-normal text-[#8e8e8e] mt-1 mb-4">
|
||||
Explore all the experiences you can enjoy with your pass
|
||||
</p>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3">
|
||||
{attractions.map((a) => (
|
||||
<div key={a.id} className="group relative rounded-xl overflow-hidden">
|
||||
<div className="aspect-[4/3] relative">
|
||||
<ImageWithFallback src={a.image} alt={a.name} className="absolute inset-0 w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-black/10 to-transparent" />
|
||||
<div className="absolute top-2 right-2">
|
||||
<span className="inline-flex px-2 py-0.5 rounded-full bg-white/90 backdrop-blur-sm text-[10px] font-poppins font-medium text-[#555]">{a.category}</span>
|
||||
</div>
|
||||
<div className="absolute bottom-2 left-2 right-2">
|
||||
<h6 className="font-poppins text-sm leading-snug font-medium text-white drop-shadow-sm">{a.name}</h6>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right column: Config card (desktop only, sticky) */}
|
||||
<div className="hidden lg:block lg:w-[420px] flex-shrink-0">
|
||||
<div className="lg:sticky lg:top-28">
|
||||
<CheckoutConfigCard
|
||||
item={checkoutItem}
|
||||
onChange={handleCheckoutItemChange}
|
||||
onProceed={() => checkoutItem && onSecureCheckoutClick?.(checkoutItem)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<Footer
|
||||
onHomeClick={onHomeClick} onPassesClick={onPassesClick} onAttractionsClick={onAttractionsClick}
|
||||
onBlogsClick={onBlogsClick} onHowItWorksClick={onHowItWorksClick} onFAQClick={onFAQClick}
|
||||
onPrivacyPolicyClick={onPrivacyPolicyClick} onAboutUsClick={onAboutUsClick} onContactUsClick={onContactUsClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── Empty state ─── */
|
||||
function EmptyState({ icon, title, description, actionLabel, onAction }: {
|
||||
icon: React.ReactNode; title: string; description: string; actionLabel: string; onAction?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<motion.div initial={{ opacity: 0, scale: 0.98 }} animate={{ opacity: 1, scale: 1 }} transition={{ duration: 0.4 }} className="flex flex-col items-center justify-center py-20 max-w-sm mx-auto text-center">
|
||||
<motion.div className="w-28 h-28 rounded-3xl bg-[#fee7e7]/50 flex items-center justify-center mb-6" animate={{ y: [0, -6, 0] }} transition={{ duration: 3, repeat: Infinity, ease: 'easeInOut' }}>{icon}</motion.div>
|
||||
<h3 className="font-poppins text-xl md:text-2xl leading-snug font-medium text-[#2a2a2a] mb-2">{title}</h3>
|
||||
<p className="font-poppins text-sm leading-relaxed font-normal text-[#8e8e8e] mb-8">{description}</p>
|
||||
<motion.button whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} onClick={onAction} className="bg-[#F95F62] text-white font-poppins text-base font-medium px-8 py-3.5 rounded-xl hover:bg-[#e8545a] transition-colors shadow-lg shadow-[#F95F62]/15 w-full">{actionLabel}</motion.button>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
494
src/pages/PaymentDetailsPage.tsx
Normal file
494
src/pages/PaymentDetailsPage.tsx
Normal file
@@ -0,0 +1,494 @@
|
||||
import React, { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'motion/react';
|
||||
import {
|
||||
ArrowLeft, User, MapPin, Lock, Shield, ChevronDown,
|
||||
Check, AlertCircle, Pencil, UserCheck, Gift
|
||||
} from 'lucide-react';
|
||||
import Navbar from '../components/Navbar';
|
||||
import { Footer } from '../components/Footer';
|
||||
import { Card, CardContent, CardHeader } from '../components/ui/card';
|
||||
import { Separator } from '../components/ui/separator';
|
||||
|
||||
export interface CheckoutOrderItem {
|
||||
city: string;
|
||||
cardType: 'Flexi' | 'Unlimited';
|
||||
days: number;
|
||||
adults: number;
|
||||
children: number;
|
||||
quantity: number;
|
||||
pricePerUnit: number;
|
||||
}
|
||||
|
||||
interface PaymentDetailsPageProps {
|
||||
checkoutOrder?: CheckoutOrderItem | null;
|
||||
onBackClick: () => void;
|
||||
onPaymentComplete: () => void;
|
||||
onHomeClick: () => void;
|
||||
onPassesClick: () => 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;
|
||||
onEsimsClick?: () => void;
|
||||
onHotelDiscountsClick?: () => void;
|
||||
onContactUsClick?: () => void;
|
||||
onCartClick?: () => void;
|
||||
onCheckoutClick?: () => void;
|
||||
onSignInClick: () => void;
|
||||
onSignOutClick?: () => void;
|
||||
currentPage?: string;
|
||||
user?: { email: string; name: string } | null;
|
||||
}
|
||||
|
||||
/* ─── Profile data ─── */
|
||||
const profileData = {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
email: 'john.doe@example.com',
|
||||
phone: '+1 (555) 123-4567',
|
||||
address: '123 Main Street',
|
||||
city: 'New York',
|
||||
state: 'NY',
|
||||
postcode: '10001',
|
||||
country: 'United States',
|
||||
};
|
||||
|
||||
/* ─── Editable field ─── */
|
||||
function Field({
|
||||
label, value, onChange, placeholder, type = 'text',
|
||||
error, maxLength, inputMode, prefilled,
|
||||
}: {
|
||||
label: string; value: string; onChange: (v: string) => void; placeholder?: string;
|
||||
type?: string; error?: string; maxLength?: number;
|
||||
inputMode?: React.HTMLAttributes<HTMLInputElement>['inputMode']; prefilled?: boolean;
|
||||
}) {
|
||||
const [focused, setFocused] = useState(false);
|
||||
return (
|
||||
<div className="flex flex-col gap-1 w-full">
|
||||
<label className="font-poppins text-sm font-normal text-[#555] leading-relaxed">{label}</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onFocus={() => setFocused(true)}
|
||||
onBlur={() => setFocused(false)}
|
||||
placeholder={placeholder}
|
||||
maxLength={maxLength}
|
||||
inputMode={inputMode}
|
||||
className={`w-full border rounded-xl px-4 py-3 pr-10 font-poppins text-base font-normal text-[#2a2a2a] outline-none transition-all duration-200 placeholder:text-[#ccc] ${
|
||||
error
|
||||
? 'border-red-300 focus:border-red-400 bg-red-50/30'
|
||||
: focused
|
||||
? 'border-[#F95F62] ring-2 ring-[#F95F62]/10'
|
||||
: prefilled
|
||||
? 'border-[#F95F62]/25 bg-[#F95F62]/[0.02]'
|
||||
: 'border-gray-200'
|
||||
}`}
|
||||
/>
|
||||
{prefilled && !focused && (
|
||||
<Pencil className="absolute right-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-[#F95F62]/40" />
|
||||
)}
|
||||
</div>
|
||||
{error && (
|
||||
<span className="font-poppins text-xs font-normal text-red-500 flex items-center gap-1">
|
||||
<AlertCircle className="w-3 h-3" />{error}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── Card type badge ─── */
|
||||
function CardTypeBadge({ cardType }: { cardType: 'Flexi' | 'Unlimited' }) {
|
||||
return (
|
||||
<span className={`inline-flex items-center px-3 py-1 rounded-full font-poppins text-xs font-medium ${
|
||||
cardType === 'Flexi'
|
||||
? 'bg-[#f95faf]/10 text-[#f95faf]'
|
||||
: 'bg-[#F95F62]/10 text-[#F95F62]'
|
||||
}`}>
|
||||
{cardType} Card
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── Main component ─── */
|
||||
export function PaymentDetailsPage({
|
||||
checkoutOrder,
|
||||
onBackClick,
|
||||
onPaymentComplete,
|
||||
onHomeClick,
|
||||
onPassesClick,
|
||||
onAttractionsClick,
|
||||
onBlogsClick,
|
||||
onHowItWorksClick,
|
||||
onFAQClick,
|
||||
onPrivacyPolicyClick,
|
||||
onAboutUsClick,
|
||||
onProfileClick,
|
||||
onCityCardsClick,
|
||||
onMagicItineraryClick,
|
||||
onPostCardsClick,
|
||||
onOffersClick,
|
||||
onSuperSavingsClick,
|
||||
onEsimsClick,
|
||||
onHotelDiscountsClick,
|
||||
onContactUsClick,
|
||||
onCartClick,
|
||||
onCheckoutClick,
|
||||
onSignInClick,
|
||||
onSignOutClick,
|
||||
currentPage,
|
||||
user,
|
||||
}: PaymentDetailsPageProps) {
|
||||
|
||||
/* ── Purchase type ── */
|
||||
const [selectedTab, setSelectedTab] = useState<'myself' | 'gift'>('myself');
|
||||
const [giftName, setGiftName] = useState('');
|
||||
const [giftEmail, setGiftEmail] = useState('');
|
||||
|
||||
/* ── Personal Info ── */
|
||||
const [firstName, setFirstName] = useState(profileData.firstName);
|
||||
const [lastName, setLastName] = useState(profileData.lastName);
|
||||
const [email, setEmail] = useState(user?.email || profileData.email);
|
||||
const [phone, setPhone] = useState(profileData.phone);
|
||||
|
||||
/* ── Billing Address ── */
|
||||
const [address, setAddress] = useState(profileData.address);
|
||||
const [billingCity, setBillingCity] = useState(profileData.city);
|
||||
const [state, setState] = useState(profileData.state);
|
||||
const [postcode, setPostcode] = useState(profileData.postcode);
|
||||
const [country, setCountry] = useState(profileData.country);
|
||||
|
||||
/* ── Validation ── */
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const order = checkoutOrder || {
|
||||
city: 'Melbourne', cardType: 'Flexi' as const,
|
||||
days: 3, adults: 2, children: 0, quantity: 1, pricePerUnit: 49.50,
|
||||
};
|
||||
|
||||
const subtotal = order.pricePerUnit * order.quantity;
|
||||
const tax = subtotal * 0.1;
|
||||
const total = subtotal + tax;
|
||||
|
||||
const validate = () => {
|
||||
const e: Record<string, string> = {};
|
||||
if (!firstName.trim()) e.firstName = 'Required';
|
||||
if (!lastName.trim()) e.lastName = 'Required';
|
||||
if (!email.trim() || !/\S+@\S+\.\S+/.test(email)) e.email = 'Valid email required';
|
||||
if (!phone.trim()) e.phone = 'Required';
|
||||
if (!address.trim()) e.address = 'Required';
|
||||
if (!billingCity.trim()) e.billingCity = 'Required';
|
||||
if (!postcode.trim()) e.postcode = 'Required';
|
||||
return e;
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
const e = validate();
|
||||
setErrors(e);
|
||||
if (Object.keys(e).length > 0) return;
|
||||
setSubmitting(true);
|
||||
setTimeout(() => {
|
||||
setSubmitting(false);
|
||||
onPaymentComplete();
|
||||
}, 1800);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#fafafa] font-poppins">
|
||||
<Navbar
|
||||
activeCity="Melbourne" onCityChange={() => {}} onSignInClick={onSignInClick} onSignOutClick={onSignOutClick}
|
||||
onPassesClick={onPassesClick} onCheckoutClick={onCheckoutClick} onHomeClick={onHomeClick}
|
||||
onAttractionsClick={onAttractionsClick} onBlogsClick={onBlogsClick} onHowItWorksClick={onHowItWorksClick}
|
||||
onFAQClick={onFAQClick} onPrivacyPolicyClick={onPrivacyPolicyClick} onAboutUsClick={onAboutUsClick}
|
||||
onProfileClick={onProfileClick} onCityCardsClick={onCityCardsClick} onMagicItineraryClick={onMagicItineraryClick}
|
||||
onPostCardsClick={onPostCardsClick} onOffersClick={onOffersClick} onSuperSavingsClick={onSuperSavingsClick}
|
||||
onEsimsClick={onEsimsClick} onHotelDiscountsClick={onHotelDiscountsClick} onCartClick={onCartClick}
|
||||
currentPage={currentPage as any} user={user}
|
||||
/>
|
||||
|
||||
<div className="w-full px-4 sm:px-6 lg:px-10 xl:px-16 pt-32 pb-24 max-w-[1440px] mx-auto">
|
||||
|
||||
{/* Back */}
|
||||
<button
|
||||
onClick={onBackClick}
|
||||
className="flex items-center gap-2 text-[#8e8e8e] hover:text-[#2a2a2a] transition-colors font-poppins text-sm font-normal mb-8"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" /> Back
|
||||
</button>
|
||||
|
||||
{/* Page heading */}
|
||||
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="mb-8">
|
||||
<div className="flex items-center gap-4 mb-2">
|
||||
<h1 className="font-poppins text-2xl md:text-3xl lg:text-4xl leading-tight">
|
||||
<span className="font-light">Secure</span>{' '}
|
||||
<span className="font-bold italic bg-gradient-to-r from-[#F95F62] to-[#F95FAF] bg-clip-text text-transparent">Checkout</span>
|
||||
</h1>
|
||||
<div className="flex items-center gap-2 text-[#22a86b]">
|
||||
<Shield className="w-4 h-4" />
|
||||
<span className="font-poppins text-sm font-medium">SSL Secured</span>
|
||||
</div>
|
||||
</div>
|
||||
<p className="font-poppins text-sm leading-relaxed font-normal text-[#8e8e8e]">
|
||||
Complete your purchase securely. Your payment information is protected.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid lg:grid-cols-3 gap-8">
|
||||
|
||||
{/* ── LEFT: Forms ── */}
|
||||
<motion.div initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }} className="lg:col-span-2">
|
||||
<Card className="shadow-lg border-0 overflow-hidden">
|
||||
|
||||
{/* Purchase type tabs */}
|
||||
<CardHeader className="pb-0 pt-6 px-6 border-b border-gray-100">
|
||||
<div className="flex gap-2 mb-6">
|
||||
<button
|
||||
onClick={() => setSelectedTab('myself')}
|
||||
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl font-poppins text-sm font-medium transition-all duration-200 ${
|
||||
selectedTab === 'myself'
|
||||
? 'bg-[#F95F62] text-white shadow-md shadow-[#F95F62]/20'
|
||||
: 'bg-gray-100 text-[#555] hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
<User className="w-4 h-4" />
|
||||
<span>For myself</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedTab('gift')}
|
||||
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-xl font-poppins text-sm font-medium transition-all duration-200 ${
|
||||
selectedTab === 'gift'
|
||||
? 'bg-[#F95F62] text-white shadow-md shadow-[#F95F62]/20'
|
||||
: 'bg-gray-100 text-[#555] hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
<Gift className="w-4 h-4" />
|
||||
<span>To gift Someone</span>
|
||||
</button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="px-6 py-6 space-y-6">
|
||||
|
||||
{/* Pre-filled notice banner */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="flex items-center gap-3 bg-[#F95F62]/6 border border-[#F95F62]/20 rounded-xl px-4 py-3"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-lg bg-[#F95F62]/10 flex items-center justify-center flex-shrink-0">
|
||||
<UserCheck className="w-4 h-4 text-[#F95F62]" />
|
||||
</div>
|
||||
<p className="font-poppins text-sm font-normal text-[#555] leading-relaxed">
|
||||
<span className="font-medium text-[#2a2a2a]">Details pre-filled from your profile.</span>{' '}
|
||||
All fields are editable — just tap to make changes.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* 1. Personal Information */}
|
||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.1 }} className="space-y-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-[#F95F62] text-white rounded-full flex items-center justify-center font-poppins text-sm font-semibold flex-shrink-0">
|
||||
1
|
||||
</div>
|
||||
<h2 className="font-poppins text-xl leading-snug font-semibold text-[#2a2a2a]">Personal Information</h2>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<Field label="First Name" value={firstName} onChange={setFirstName} error={errors.firstName} prefilled />
|
||||
<Field label="Last Name" value={lastName} onChange={setLastName} error={errors.lastName} prefilled />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<Field label="Email Address" value={email} onChange={setEmail} type="email" error={errors.email} prefilled />
|
||||
<Field label="Phone Number" value={phone} onChange={setPhone} type="tel" error={errors.phone} inputMode="tel" prefilled />
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Gift recipient section */}
|
||||
<AnimatePresence>
|
||||
{selectedTab === 'gift' && (
|
||||
<motion.div
|
||||
key="gift-section"
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="bg-[#F95F62]/[0.03] border border-[#F95F62]/15 rounded-xl px-5 py-4 space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Gift className="w-4 h-4 text-[#F95F62]" />
|
||||
<h3 className="font-poppins text-base font-semibold text-[#2a2a2a]">Gift Recipient Details</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<Field label="Recipient Name" value={giftName} onChange={setGiftName} placeholder="Enter recipient's name" />
|
||||
<Field label="Recipient Email" value={giftEmail} onChange={setGiftEmail} type="email" placeholder="Enter recipient's email" />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* 2. Billing Address */}
|
||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.15 }} className="space-y-5">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-[#F95F62] text-white rounded-full flex items-center justify-center font-poppins text-sm font-semibold flex-shrink-0">
|
||||
2
|
||||
</div>
|
||||
<h2 className="font-poppins text-xl leading-snug font-semibold text-[#2a2a2a]">Billing Address</h2>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<Field label="Street Address" value={address} onChange={setAddress} error={errors.address} prefilled />
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<Field label="City / Suburb" value={billingCity} onChange={setBillingCity} error={errors.billingCity} prefilled />
|
||||
<Field label="State" value={state} onChange={setState} prefilled />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<Field label="Postcode" value={postcode} onChange={setPostcode} inputMode="numeric" error={errors.postcode} maxLength={10} prefilled />
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="font-poppins text-sm font-normal text-[#555] leading-relaxed">Country</label>
|
||||
<div className="relative">
|
||||
<select
|
||||
value={country}
|
||||
onChange={(e) => setCountry(e.target.value)}
|
||||
className="w-full border border-[#F95F62]/25 bg-[#F95F62]/[0.02] rounded-xl px-4 py-3 font-poppins text-base font-normal text-[#2a2a2a] outline-none focus:border-[#F95F62] focus:ring-2 focus:ring-[#F95F62]/10 appearance-none transition-all duration-200"
|
||||
>
|
||||
{['Australia', 'New Zealand', 'United Kingdom', 'United States', 'Canada', 'Singapore', 'Japan', 'Other'].map(c => (
|
||||
<option key={c}>{c}</option>
|
||||
))}
|
||||
</select>
|
||||
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[#aaa] pointer-events-none" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
{/* ── RIGHT: Order Summary ── */}
|
||||
<motion.div initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} className="lg:col-span-1">
|
||||
<div className="lg:sticky lg:top-28 space-y-4">
|
||||
|
||||
<div className="bg-white rounded-2xl shadow-[0px_2px_16px_0px_rgba(0,0,0,0.06)] overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-gray-100">
|
||||
<h3 className="font-poppins text-lg leading-snug font-semibold text-[#2a2a2a]">Order Summary</h3>
|
||||
</div>
|
||||
|
||||
{/* Card info */}
|
||||
<div className="px-6 py-5 border-b border-gray-100">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={`w-16 h-10 rounded-lg flex-shrink-0 flex items-center justify-center ${
|
||||
order.cardType === 'Flexi'
|
||||
? 'bg-gradient-to-br from-[#f95faf] to-[#F95F62]'
|
||||
: 'bg-gradient-to-br from-[#F95F62] to-[#c94245]'
|
||||
}`}>
|
||||
<span className="font-poppins text-[10px] font-semibold text-white">{order.cardType}</span>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h4 className="font-poppins text-base font-semibold text-[#2a2a2a]">{order.city}</h4>
|
||||
<CardTypeBadge cardType={order.cardType} />
|
||||
</div>
|
||||
<p className="font-poppins text-sm font-normal text-[#8e8e8e] mt-0.5">
|
||||
{order.cardType === 'Flexi' ? `${order.days} Attractions` : `${order.days} Days`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 space-y-2">
|
||||
{[
|
||||
{ label: 'Adults', value: order.adults },
|
||||
{ label: 'Children', value: order.children },
|
||||
{ label: 'Qty', value: order.quantity },
|
||||
].map(({ label, value }) => (
|
||||
<div key={label} className="flex items-center justify-between">
|
||||
<span className="font-poppins text-sm font-normal text-[#8e8e8e]">{label}</span>
|
||||
<span className="font-poppins text-sm font-medium text-[#2a2a2a]">{value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pricing breakdown */}
|
||||
<div className="px-6 py-5">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-poppins text-sm font-normal text-[#555]">Subtotal</span>
|
||||
<span className="font-poppins text-sm font-normal text-[#2a2a2a]">${subtotal.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-poppins text-sm font-normal text-[#555]">GST (10%)</span>
|
||||
<span className="font-poppins text-sm font-normal text-[#2a2a2a]">${tax.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-poppins text-sm font-normal text-[#555]">Booking fee</span>
|
||||
<span className="font-poppins text-sm font-normal text-[#22a86b]">Free</span>
|
||||
</div>
|
||||
<div className="pt-3 border-t border-gray-100 flex items-center justify-between">
|
||||
<span className="font-poppins text-base font-semibold text-[#2a2a2a]">Total</span>
|
||||
<span className="font-poppins text-2xl font-semibold text-[#F95F62]">${total.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CTA */}
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.01 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
onClick={handleSubmit}
|
||||
disabled={submitting}
|
||||
className="w-full py-4 rounded-2xl bg-[#F95F62] text-white font-poppins text-base font-semibold hover:bg-[#e8545a] transition-colors shadow-lg shadow-[#F95F62]/20 disabled:opacity-70 flex items-center justify-center gap-2"
|
||||
>
|
||||
{submitting ? (
|
||||
<>
|
||||
<motion.div
|
||||
animate={{ rotate: 360 }}
|
||||
transition={{ duration: 1, repeat: Infinity, ease: 'linear' }}
|
||||
className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full"
|
||||
/>
|
||||
Processing…
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Lock className="w-4 h-4" />
|
||||
Complete Payment · ${total.toFixed(2)}
|
||||
</>
|
||||
)}
|
||||
</motion.button>
|
||||
|
||||
<p className="font-poppins text-xs font-normal text-[#aaa] text-center">
|
||||
By completing your purchase you agree to our Terms of Service and Privacy Policy
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer
|
||||
onHomeClick={onHomeClick} onPassesClick={onPassesClick}
|
||||
onAttractionsClick={onAttractionsClick} onBlogsClick={onBlogsClick}
|
||||
onHowItWorksClick={onHowItWorksClick} onFAQClick={onFAQClick}
|
||||
onPrivacyPolicyClick={onPrivacyPolicyClick} onAboutUsClick={onAboutUsClick}
|
||||
onContactUsClick={onContactUsClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user