Files
CityCards-Website/src/components/CheckoutPage.tsx
2025-11-05 18:57:24 +05:30

735 lines
32 KiB
TypeScript

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