Initial commit from remix
This commit is contained in:
574
src/pages/Checkout.tsx
Normal file
574
src/pages/Checkout.tsx
Normal file
@@ -0,0 +1,574 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useCart } from '@/contexts/CartContext';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ArrowLeft, CreditCard, Smartphone, Building, Truck, Shield, Check, X } from 'lucide-react';
|
||||
import Header from '@/components/Header';
|
||||
import Footer from '@/components/Footer';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
|
||||
interface CustomerInfo {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
address: string;
|
||||
city: string;
|
||||
country: string;
|
||||
zipCode: string;
|
||||
}
|
||||
|
||||
interface PaymentInfo {
|
||||
method: 'credit_card' | 'paypal' | 'bank_transfer' | 'cash';
|
||||
cardNumber: string;
|
||||
expiryDate: string;
|
||||
cvv: string;
|
||||
cardName: string;
|
||||
}
|
||||
|
||||
const Checkout = () => {
|
||||
const navigate = useNavigate();
|
||||
const { items, getTotalPrice, clearCart } = useCart();
|
||||
const { toast } = useToast();
|
||||
const [step, setStep] = useState(1);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
|
||||
const [customerInfo, setCustomerInfo] = useState<CustomerInfo>({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
city: '',
|
||||
country: 'Dominican Republic',
|
||||
zipCode: ''
|
||||
});
|
||||
|
||||
const [paymentInfo, setPaymentInfo] = useState<PaymentInfo>({
|
||||
method: 'credit_card',
|
||||
cardNumber: '',
|
||||
expiryDate: '',
|
||||
cvv: '',
|
||||
cardName: ''
|
||||
});
|
||||
|
||||
const [specialRequests, setSpecialRequests] = useState('');
|
||||
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Header />
|
||||
<div className="container mx-auto px-4 pt-24 pb-12">
|
||||
<div className="text-center py-12">
|
||||
<h1 className="text-2xl font-bold mb-4">Tu carrito está vacío</h1>
|
||||
<p className="text-gray-600 mb-6">Agrega algunas ofertas a tu carrito para continuar.</p>
|
||||
<Button onClick={() => navigate('/explore')}>
|
||||
Explorar Ofertas
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const totalPrice = getTotalPrice();
|
||||
const taxes = totalPrice * 0.18; // 18% ITBIS (impuesto dominicano)
|
||||
const serviceFee = totalPrice * 0.05; // 5% fee de servicio
|
||||
const finalTotal = totalPrice + taxes + serviceFee;
|
||||
|
||||
const paymentMethods = [
|
||||
{ id: 'credit_card', name: 'Tarjeta de Crédito/Débito', icon: CreditCard },
|
||||
{ id: 'paypal', name: 'PayPal', icon: Smartphone },
|
||||
{ id: 'bank_transfer', name: 'Transferencia Bancaria', icon: Building },
|
||||
{ id: 'cash', name: 'Pago en Efectivo', icon: Truck }
|
||||
];
|
||||
|
||||
const handleCustomerInfoChange = (field: keyof CustomerInfo, value: string) => {
|
||||
setCustomerInfo(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handlePaymentInfoChange = (field: keyof PaymentInfo, value: string) => {
|
||||
setPaymentInfo(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const saveOrderToJSON = (orderData: any) => {
|
||||
const orders = JSON.parse(localStorage.getItem('karibeo_orders') || '[]');
|
||||
orders.push(orderData);
|
||||
localStorage.setItem('karibeo_orders', JSON.stringify(orders));
|
||||
};
|
||||
|
||||
const processPayment = async () => {
|
||||
setIsProcessing(true);
|
||||
|
||||
// Simulate payment processing
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
|
||||
const orderData = {
|
||||
id: Date.now().toString(),
|
||||
date: new Date().toISOString(),
|
||||
items: items,
|
||||
customerInfo,
|
||||
paymentInfo: {
|
||||
method: paymentInfo.method,
|
||||
// Don't save sensitive payment data in real implementation
|
||||
cardLast4: paymentInfo.cardNumber.slice(-4)
|
||||
},
|
||||
specialRequests,
|
||||
pricing: {
|
||||
subtotal: totalPrice,
|
||||
taxes,
|
||||
serviceFee,
|
||||
total: finalTotal
|
||||
},
|
||||
status: 'confirmed'
|
||||
};
|
||||
|
||||
saveOrderToJSON(orderData);
|
||||
clearCart();
|
||||
|
||||
toast({
|
||||
title: "¡Pago procesado exitosamente!",
|
||||
description: "Tu reserva ha sido confirmada. Recibirás un email de confirmación.",
|
||||
});
|
||||
|
||||
navigate('/order-confirmation', { state: { orderData } });
|
||||
setIsProcessing(false);
|
||||
};
|
||||
|
||||
const validateStep1 = () => {
|
||||
return customerInfo.firstName && customerInfo.lastName && customerInfo.email && customerInfo.phone;
|
||||
};
|
||||
|
||||
const validateStep2 = () => {
|
||||
if (paymentInfo.method === 'credit_card') {
|
||||
return paymentInfo.cardNumber && paymentInfo.expiryDate && paymentInfo.cvv && paymentInfo.cardName;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Header />
|
||||
|
||||
<div className="container mx-auto px-4 pt-24 pb-12">
|
||||
<div className="mb-6">
|
||||
<Button variant="ghost" onClick={() => navigate(-1)} className="mb-4">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Volver
|
||||
</Button>
|
||||
<h1 className="text-3xl font-bold">Checkout</h1>
|
||||
|
||||
{/* Progress Steps */}
|
||||
<div className="flex items-center mt-6 mb-8">
|
||||
<div className={`flex items-center ${step >= 1 ? 'text-primary' : 'text-gray-400'}`}>
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center border-2 ${
|
||||
step >= 1 ? 'border-primary bg-primary text-white' : 'border-gray-300'
|
||||
}`}>
|
||||
{step > 1 ? <Check className="w-4 h-4" /> : '1'}
|
||||
</div>
|
||||
<span className="ml-2 font-medium">Información Personal</span>
|
||||
</div>
|
||||
<div className={`flex-1 h-px mx-4 ${step > 1 ? 'bg-primary' : 'bg-gray-300'}`} />
|
||||
<div className={`flex items-center ${step >= 2 ? 'text-primary' : 'text-gray-400'}`}>
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center border-2 ${
|
||||
step >= 2 ? 'border-primary bg-primary text-white' : 'border-gray-300'
|
||||
}`}>
|
||||
{step > 2 ? <Check className="w-4 h-4" /> : '2'}
|
||||
</div>
|
||||
<span className="ml-2 font-medium">Pago</span>
|
||||
</div>
|
||||
<div className={`flex-1 h-px mx-4 ${step > 2 ? 'bg-primary' : 'bg-gray-300'}`} />
|
||||
<div className={`flex items-center ${step >= 3 ? 'text-primary' : 'text-gray-400'}`}>
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center border-2 ${
|
||||
step >= 3 ? 'border-primary bg-primary text-white' : 'border-gray-300'
|
||||
}`}>
|
||||
3
|
||||
</div>
|
||||
<span className="ml-2 font-medium">Confirmación</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Main Content */}
|
||||
<div className="lg:col-span-2">
|
||||
{step === 1 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Información Personal</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="firstName">Nombre *</Label>
|
||||
<Input
|
||||
id="firstName"
|
||||
value={customerInfo.firstName}
|
||||
onChange={(e) => handleCustomerInfoChange('firstName', e.target.value)}
|
||||
placeholder="Tu nombre"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="lastName">Apellido *</Label>
|
||||
<Input
|
||||
id="lastName"
|
||||
value={customerInfo.lastName}
|
||||
onChange={(e) => handleCustomerInfoChange('lastName', e.target.value)}
|
||||
placeholder="Tu apellido"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="email">Email *</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={customerInfo.email}
|
||||
onChange={(e) => handleCustomerInfoChange('email', e.target.value)}
|
||||
placeholder="tu@email.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="phone">Teléfono *</Label>
|
||||
<Input
|
||||
id="phone"
|
||||
value={customerInfo.phone}
|
||||
onChange={(e) => handleCustomerInfoChange('phone', e.target.value)}
|
||||
placeholder="+1 (809) 123-4567"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="address">Dirección</Label>
|
||||
<Input
|
||||
id="address"
|
||||
value={customerInfo.address}
|
||||
onChange={(e) => handleCustomerInfoChange('address', e.target.value)}
|
||||
placeholder="Calle, número, sector"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="city">Ciudad</Label>
|
||||
<Input
|
||||
id="city"
|
||||
value={customerInfo.city}
|
||||
onChange={(e) => handleCustomerInfoChange('city', e.target.value)}
|
||||
placeholder="Santo Domingo"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="country">País</Label>
|
||||
<Select
|
||||
value={customerInfo.country}
|
||||
onValueChange={(value) => handleCustomerInfoChange('country', value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Dominican Republic">República Dominicana</SelectItem>
|
||||
<SelectItem value="United States">Estados Unidos</SelectItem>
|
||||
<SelectItem value="Canada">Canadá</SelectItem>
|
||||
<SelectItem value="Other">Otro</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="zipCode">Código Postal</Label>
|
||||
<Input
|
||||
id="zipCode"
|
||||
value={customerInfo.zipCode}
|
||||
onChange={(e) => handleCustomerInfoChange('zipCode', e.target.value)}
|
||||
placeholder="10001"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="requests">Solicitudes Especiales</Label>
|
||||
<Textarea
|
||||
id="requests"
|
||||
value={specialRequests}
|
||||
onChange={(e) => setSpecialRequests(e.target.value)}
|
||||
placeholder="Algún requerimiento especial para tu reserva..."
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={() => setStep(2)}
|
||||
disabled={!validateStep1()}
|
||||
>
|
||||
Continuar al Pago
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{step === 2 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Método de Pago</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Payment Method Selection */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{paymentMethods.map((method) => {
|
||||
const IconComponent = method.icon;
|
||||
return (
|
||||
<button
|
||||
key={method.id}
|
||||
onClick={() => handlePaymentInfoChange('method', method.id as any)}
|
||||
className={`p-4 border-2 rounded-lg flex items-center space-x-3 transition-colors ${
|
||||
paymentInfo.method === method.id
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<IconComponent className="w-5 h-5" />
|
||||
<span className="font-medium">{method.name}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Credit Card Form */}
|
||||
{paymentInfo.method === 'credit_card' && (
|
||||
<div className="space-y-4 border-t pt-6">
|
||||
<div>
|
||||
<Label htmlFor="cardName">Nombre en la Tarjeta *</Label>
|
||||
<Input
|
||||
id="cardName"
|
||||
value={paymentInfo.cardName}
|
||||
onChange={(e) => handlePaymentInfoChange('cardName', e.target.value)}
|
||||
placeholder="Como aparece en la tarjeta"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="cardNumber">Número de Tarjeta *</Label>
|
||||
<Input
|
||||
id="cardNumber"
|
||||
value={paymentInfo.cardNumber}
|
||||
onChange={(e) => handlePaymentInfoChange('cardNumber', e.target.value)}
|
||||
placeholder="1234 5678 9012 3456"
|
||||
maxLength={19}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="expiryDate">Fecha de Vencimiento *</Label>
|
||||
<Input
|
||||
id="expiryDate"
|
||||
value={paymentInfo.expiryDate}
|
||||
onChange={(e) => handlePaymentInfoChange('expiryDate', e.target.value)}
|
||||
placeholder="MM/YY"
|
||||
maxLength={5}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="cvv">CVV *</Label>
|
||||
<Input
|
||||
id="cvv"
|
||||
value={paymentInfo.cvv}
|
||||
onChange={(e) => handlePaymentInfoChange('cvv', e.target.value)}
|
||||
placeholder="123"
|
||||
maxLength={4}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Other Payment Methods Info */}
|
||||
{paymentInfo.method === 'paypal' && (
|
||||
<div className="bg-blue-50 p-4 rounded-lg border-t">
|
||||
<p className="text-sm text-blue-700">
|
||||
Serás redirigido a PayPal para completar tu pago de forma segura.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{paymentInfo.method === 'bank_transfer' && (
|
||||
<div className="bg-green-50 p-4 rounded-lg border-t">
|
||||
<p className="text-sm text-green-700 mb-2">
|
||||
Te enviaremos los detalles de la cuenta bancaria para realizar la transferencia.
|
||||
</p>
|
||||
<p className="text-xs text-green-600">
|
||||
Tu reserva se confirmará una vez recibamos el comprobante de pago.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{paymentInfo.method === 'cash' && (
|
||||
<div className="bg-orange-50 p-4 rounded-lg border-t">
|
||||
<p className="text-sm text-orange-700 mb-2">
|
||||
Podrás pagar en efectivo al momento del check-in o al llegar al lugar.
|
||||
</p>
|
||||
<p className="text-xs text-orange-600">
|
||||
Tu reserva quedará pendiente hasta el pago.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button variant="outline" onClick={() => setStep(1)}>
|
||||
Volver
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1"
|
||||
onClick={() => setStep(3)}
|
||||
disabled={!validateStep2()}
|
||||
>
|
||||
Revisar Orden
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{step === 3 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Confirmar Reserva</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<h3 className="font-semibold mb-2">Información de Contacto</h3>
|
||||
<p className="text-sm">{customerInfo.firstName} {customerInfo.lastName}</p>
|
||||
<p className="text-sm">{customerInfo.email}</p>
|
||||
<p className="text-sm">{customerInfo.phone}</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<h3 className="font-semibold mb-2">Método de Pago</h3>
|
||||
<p className="text-sm">
|
||||
{paymentMethods.find(m => m.id === paymentInfo.method)?.name}
|
||||
{paymentInfo.method === 'credit_card' && paymentInfo.cardNumber &&
|
||||
` terminada en ${paymentInfo.cardNumber.slice(-4)}`
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{specialRequests && (
|
||||
<div className="bg-gray-50 p-4 rounded-lg">
|
||||
<h3 className="font-semibold mb-2">Solicitudes Especiales</h3>
|
||||
<p className="text-sm">{specialRequests}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center p-4 bg-blue-50 rounded-lg">
|
||||
<Shield className="w-5 h-5 text-blue-600 mr-3" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-blue-900">Pago Seguro</p>
|
||||
<p className="text-xs text-blue-700">
|
||||
Tu información está protegida con encriptación SSL
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button variant="outline" onClick={() => setStep(2)}>
|
||||
Volver
|
||||
</Button>
|
||||
<Button
|
||||
className="flex-1"
|
||||
onClick={processPayment}
|
||||
disabled={isProcessing}
|
||||
>
|
||||
{isProcessing ? 'Procesando...' : `Confirmar y Pagar $${finalTotal.toFixed(2)}`}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Order Summary Sidebar */}
|
||||
<div className="lg:col-span-1">
|
||||
<Card className="sticky top-24">
|
||||
<CardHeader>
|
||||
<CardTitle>Resumen de Orden</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Cart Items */}
|
||||
<div className="space-y-3">
|
||||
{items.map((item) => (
|
||||
<div key={item.id} className="flex space-x-3">
|
||||
<img
|
||||
src={item.image}
|
||||
alt={item.title}
|
||||
className="w-16 h-16 object-cover rounded"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-medium text-sm truncate">{item.title}</h4>
|
||||
<p className="text-xs text-gray-500">{item.location}</p>
|
||||
{item.selectedDate && (
|
||||
<p className="text-xs text-gray-600">Fecha: {item.selectedDate}</p>
|
||||
)}
|
||||
{item.guests && (
|
||||
<p className="text-xs text-gray-600">Personas: {item.guests}</p>
|
||||
)}
|
||||
<div className="flex justify-between items-center mt-1">
|
||||
<span className="text-xs text-gray-500">Qty: {item.quantity}</span>
|
||||
<span className="text-sm font-semibold">${(item.price * item.quantity).toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Pricing Breakdown */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>Subtotal</span>
|
||||
<span>${totalPrice.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>ITBIS (18%)</span>
|
||||
<span>${taxes.toFixed(2)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>Fee de servicio (5%)</span>
|
||||
<span>${serviceFee.toFixed(2)}</span>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex justify-between font-semibold">
|
||||
<span>Total</span>
|
||||
<span>${finalTotal.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-gray-500 mt-4">
|
||||
<p>• Cancelación gratuita hasta 24 horas antes</p>
|
||||
<p>• Recibirás confirmación por email</p>
|
||||
<p>• Soporte al cliente 24/7</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Checkout;
|
||||
Reference in New Issue
Block a user