'use client'; import { useState, useEffect } from 'react'; import { useParams, useRouter, useSearchParams } from 'next/navigation'; import Link from 'next/link'; import { useLanguage } from '@/context/LanguageContext'; import { useAuth } from '@/context/AuthContext'; import { eventsApi, ticketsApi, paymentOptionsApi, Event, PaymentOptionsConfig } from '@/lib/api'; import { formatPrice, formatDateLong, formatTime } from '@/lib/utils'; import Card from '@/components/ui/Card'; import Button from '@/components/ui/Button'; import Input from '@/components/ui/Input'; import { QRCodeSVG } from 'qrcode.react'; import { CalendarIcon, MapPinIcon, UserGroupIcon, CurrencyDollarIcon, ArrowLeftIcon, CheckCircleIcon, CreditCardIcon, BanknotesIcon, BoltIcon, TicketIcon, ClipboardDocumentIcon, BuildingLibraryIcon, ClockIcon, ArrowTopRightOnSquareIcon, UserIcon, ArrowDownTrayIcon, } from '@heroicons/react/24/outline'; import toast from 'react-hot-toast'; // Attendee info for each ticket interface AttendeeInfo { firstName: string; lastName: string; } type PaymentMethod = 'bancard' | 'lightning' | 'cash' | 'bank_transfer' | 'tpago'; interface BookingFormData { firstName: string; lastName: string; email: string; phone: string; preferredLanguage: 'en' | 'es'; paymentMethod: PaymentMethod; ruc: string; } interface LightningInvoice { paymentHash: string; paymentRequest: string; // BOLT11 invoice amount: number; // Amount in satoshis fiatAmount?: number; // Original fiat amount fiatCurrency?: string; // Original fiat currency expiry?: string; } interface BookingResult { ticketId: string; ticketIds?: string[]; // For multi-ticket bookings bookingId?: string; qrCode: string; qrCodes?: string[]; // For multi-ticket bookings paymentMethod: PaymentMethod; lightningInvoice?: LightningInvoice; ticketCount?: number; } export default function BookingPage() { const params = useParams(); const router = useRouter(); const searchParams = useSearchParams(); const { t, locale } = useLanguage(); const { user } = useAuth(); const [event, setEvent] = useState(null); const [paymentConfig, setPaymentConfig] = useState(null); const [loading, setLoading] = useState(true); const [step, setStep] = useState<'form' | 'paying' | 'manual_payment' | 'pending_approval' | 'success'>('form'); const [submitting, setSubmitting] = useState(false); const [bookingResult, setBookingResult] = useState(null); const [paymentPending, setPaymentPending] = useState(false); const [markingPaid, setMarkingPaid] = useState(false); // State for payer name (when paid under different name) const [paidUnderDifferentName, setPaidUnderDifferentName] = useState(false); const [payerName, setPayerName] = useState(''); // Quantity from URL param (default 1) const initialQuantity = Math.max(1, parseInt(searchParams.get('qty') || '1', 10)); const [ticketQuantity, setTicketQuantity] = useState(initialQuantity); // Attendees for multi-ticket bookings (ticket 1 uses main formData) const [attendees, setAttendees] = useState(() => Array(Math.max(0, initialQuantity - 1)).fill(null).map(() => ({ firstName: '', lastName: '' })) ); const [attendeeErrors, setAttendeeErrors] = useState<{ [key: number]: string }>({}); const [formData, setFormData] = useState({ firstName: '', lastName: '', email: '', phone: '', preferredLanguage: locale as 'en' | 'es', paymentMethod: 'cash', ruc: '', }); const [errors, setErrors] = useState>>({}); const rucPattern = /^\d{6,10}$/; // Format RUC input: digits only, max 10 const formatRuc = (value: string): string => { const digits = value.replace(/\D/g, '').slice(0, 10); return digits; }; // Handle RUC input change const handleRucChange = (e: React.ChangeEvent) => { const formatted = formatRuc(e.target.value); setFormData({ ...formData, ruc: formatted }); // Clear error on change if (errors.ruc) { setErrors({ ...errors, ruc: undefined }); } }; // Validate RUC on blur (optional field: 6–10 digits) const handleRucBlur = () => { if (!formData.ruc) return; const digits = formData.ruc.replace(/\D/g, ''); if (digits.length > 0 && !rucPattern.test(digits)) { setErrors({ ...errors, ruc: t('booking.form.errors.rucInvalidFormat') }); } }; useEffect(() => { if (params.eventId) { Promise.all([ eventsApi.getById(params.eventId as string), paymentOptionsApi.getForEvent(params.eventId as string), ]) .then(([eventRes, paymentRes]) => { if (!eventRes.event || !['published', 'unlisted'].includes(eventRes.event.status)) { toast.error('Event is not available for booking'); router.push('/events'); return; } // Redirect to external booking if enabled if (eventRes.event.externalBookingEnabled && eventRes.event.externalBookingUrl) { window.location.href = eventRes.event.externalBookingUrl; return; } const bookedCount = eventRes.event.bookedCount ?? 0; const capacity = eventRes.event.capacity ?? 0; const soldOut = bookedCount >= capacity; if (soldOut) { toast.error(t('events.details.soldOut')); router.push(`/events/${eventRes.event.id}`); return; } const spotsLeft = Math.max(0, capacity - bookedCount); setEvent(eventRes.event); // Cap quantity by available spots (never allow requesting more than spotsLeft) setTicketQuantity((q) => Math.min(q, Math.max(1, spotsLeft))); setAttendees((prev) => { const newQty = Math.min(initialQuantity, Math.max(1, spotsLeft)); const need = Math.max(0, newQty - 1); if (need === prev.length) return prev; return Array(need).fill(null).map((_, i) => prev[i] ?? { firstName: '', lastName: '' }); }); setPaymentConfig(paymentRes.paymentOptions); // Set default payment method based on what's enabled const config = paymentRes.paymentOptions; if (config.lightningEnabled) { setFormData(prev => ({ ...prev, paymentMethod: 'lightning' })); } else if (config.cashEnabled) { setFormData(prev => ({ ...prev, paymentMethod: 'cash' })); } else if (config.bankTransferEnabled) { setFormData(prev => ({ ...prev, paymentMethod: 'bank_transfer' })); } else if (config.tpagoEnabled) { setFormData(prev => ({ ...prev, paymentMethod: 'tpago' })); } }) .catch(() => router.push('/events')) .finally(() => setLoading(false)); } }, [params.eventId, router]); // Auto-fill form fields when user is logged in useEffect(() => { if (user) { setFormData(prev => { // Split user's full name into first and last name const nameParts = (user.name || '').trim().split(' '); const firstName = nameParts[0] || ''; const lastName = nameParts.slice(1).join(' ') || ''; return { ...prev, firstName: prev.firstName || firstName, lastName: prev.lastName || lastName, email: prev.email || user.email || '', phone: prev.phone || user.phone || '', preferredLanguage: (user.languagePreference as 'en' | 'es') || prev.preferredLanguage, ruc: prev.ruc || user.rucNumber || '', }; }); } }, [user]); const formatDate = (dateStr: string) => formatDateLong(dateStr, locale as 'en' | 'es'); const fmtTime = (dateStr: string) => formatTime(dateStr, locale as 'en' | 'es'); const validateForm = (): boolean => { const newErrors: Partial> = {}; const newAttendeeErrors: { [key: number]: string } = {}; if (!formData.firstName.trim() || formData.firstName.length < 2) { newErrors.firstName = t('booking.form.errors.firstNameRequired'); } // lastName is optional - only validate if provided if (formData.lastName.trim() && formData.lastName.length < 2) { newErrors.lastName = t('booking.form.errors.lastNameTooShort'); } if (!formData.email.trim() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) { newErrors.email = t('booking.form.errors.emailInvalid'); } // phone is optional - only validate if provided if (formData.phone.trim() && formData.phone.length < 6) { newErrors.phone = t('booking.form.errors.phoneTooShort'); } // RUC validation (optional field - 6–10 digits if filled) if (formData.ruc.trim()) { const digits = formData.ruc.replace(/\D/g, ''); if (!/^\d{6,10}$/.test(digits)) { newErrors.ruc = t('booking.form.errors.rucInvalidFormat'); } } // Validate additional attendees (if multi-ticket) attendees.forEach((attendee, index) => { if (!attendee.firstName.trim() || attendee.firstName.length < 2) { newAttendeeErrors[index] = locale === 'es' ? 'Ingresa el nombre del asistente' : 'Enter attendee name'; } }); setErrors(newErrors); setAttendeeErrors(newAttendeeErrors); return Object.keys(newErrors).length === 0 && Object.keys(newAttendeeErrors).length === 0; }; // Connect to SSE for real-time payment updates const connectPaymentStream = (ticketId: string) => { const apiUrl = process.env.NEXT_PUBLIC_API_URL || ''; const eventSource = new EventSource(`${apiUrl}/api/lnbits/stream/${ticketId}`); eventSource.addEventListener('payment', (event) => { try { const data = JSON.parse(event.data); console.log('Payment event:', data); if (data.type === 'paid') { toast.success(locale === 'es' ? '¡Pago confirmado!' : 'Payment confirmed!'); setPaymentPending(false); setStep('success'); eventSource.close(); } else if (data.type === 'expired') { toast.error(locale === 'es' ? 'La factura ha expirado' : 'Invoice has expired'); setPaymentPending(false); eventSource.close(); } else if (data.type === 'already_paid') { setPaymentPending(false); setStep('success'); eventSource.close(); } } catch (e) { console.error('Error parsing payment event:', e); } }); eventSource.onerror = (error) => { console.error('SSE error:', error); // Fallback to polling if SSE fails eventSource.close(); fallbackPoll(ticketId); }; return eventSource; }; // Fallback polling if SSE is not available const fallbackPoll = async (ticketId: string) => { const maxAttempts = 60; let attempts = 0; const poll = async () => { attempts++; try { const status = await ticketsApi.checkPaymentStatus(ticketId); if (status.isPaid) { toast.success(locale === 'es' ? '¡Pago confirmado!' : 'Payment confirmed!'); setPaymentPending(false); setStep('success'); return; } } catch (error) { console.error('Error checking payment status:', error); } if (attempts < maxAttempts && paymentPending) { setTimeout(poll, 5000); } }; poll(); }; // Copy invoice to clipboard const copyInvoiceToClipboard = (invoice: string) => { navigator.clipboard.writeText(invoice).then(() => { toast.success(locale === 'es' ? '¡Copiado!' : 'Copied!'); }).catch(() => { toast.error(locale === 'es' ? 'Error al copiar' : 'Failed to copy'); }); }; // Truncate invoice for display const truncateInvoice = (invoice: string, chars: number = 20) => { if (invoice.length <= chars * 2) return invoice; return `${invoice.slice(0, chars)}...${invoice.slice(-chars)}`; }; // Handle "I Have Paid" button click const handleMarkPaymentSent = async () => { if (!bookingResult) return; // Validate payer name if paid under different name if (paidUnderDifferentName && !payerName.trim()) { toast.error(locale === 'es' ? 'Por favor ingresa el nombre del pagador' : 'Please enter the payer name'); return; } setMarkingPaid(true); try { await ticketsApi.markPaymentSent( bookingResult.ticketId, paidUnderDifferentName ? payerName.trim() : undefined ); setStep('pending_approval'); toast.success(locale === 'es' ? 'Pago marcado como enviado. Esperando aprobación.' : 'Payment marked as sent. Waiting for approval.'); } catch (error: any) { toast.error(error.message || 'Failed to mark payment as sent'); } finally { setMarkingPaid(false); } }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!event || !validateForm()) return; setSubmitting(true); try { // Build attendees array: first attendee from main form, rest from attendees state const allAttendees = [ { firstName: formData.firstName, lastName: formData.lastName }, ...attendees ]; const response = await ticketsApi.book({ eventId: event.id, firstName: formData.firstName, lastName: formData.lastName, email: formData.email, phone: formData.phone, preferredLanguage: formData.preferredLanguage, paymentMethod: formData.paymentMethod, ...(formData.ruc.trim() && { ruc: formData.ruc.replace(/\D/g, '') }), // Include attendees array for multi-ticket bookings ...(allAttendees.length > 1 && { attendees: allAttendees }), }); const { ticket, tickets: ticketsList, bookingId, lightningInvoice } = response as any; const ticketCount = ticketsList?.length || 1; const primaryTicket = ticket || ticketsList?.[0]; // If Lightning payment with invoice, go to paying step if (formData.paymentMethod === 'lightning' && lightningInvoice?.paymentRequest) { const result: BookingResult = { ticketId: primaryTicket.id, ticketIds: ticketsList?.map((t: any) => t.id), bookingId, qrCode: primaryTicket.qrCode, qrCodes: ticketsList?.map((t: any) => t.qrCode), paymentMethod: formData.paymentMethod as PaymentMethod, ticketCount, lightningInvoice: { paymentHash: lightningInvoice.paymentHash, paymentRequest: lightningInvoice.paymentRequest, amount: lightningInvoice.amount, fiatAmount: lightningInvoice.fiatAmount, fiatCurrency: lightningInvoice.fiatCurrency, expiry: lightningInvoice.expiry, }, }; setBookingResult(result); setStep('paying'); setPaymentPending(true); // Connect to SSE for real-time payment updates connectPaymentStream(primaryTicket.id); } else if (formData.paymentMethod === 'bank_transfer' || formData.paymentMethod === 'tpago') { // Manual payment methods - show payment details setBookingResult({ ticketId: primaryTicket.id, ticketIds: ticketsList?.map((t: any) => t.id), bookingId, qrCode: primaryTicket.qrCode, qrCodes: ticketsList?.map((t: any) => t.qrCode), paymentMethod: formData.paymentMethod, ticketCount, }); setStep('manual_payment'); } else { // Cash payment - go straight to success setBookingResult({ ticketId: primaryTicket.id, ticketIds: ticketsList?.map((t: any) => t.id), bookingId, qrCode: primaryTicket.qrCode, qrCodes: ticketsList?.map((t: any) => t.qrCode), paymentMethod: formData.paymentMethod, ticketCount, }); setStep('success'); toast.success(t('booking.success.message')); } } catch (error: any) { toast.error(error.message || t('booking.form.errors.bookingFailed')); } finally { setSubmitting(false); } }; // Build payment methods list based on configuration const paymentMethods: { id: PaymentMethod; icon: typeof CreditCardIcon; label: string; description: string; badge?: string }[] = []; if (paymentConfig?.lightningEnabled) { paymentMethods.push({ id: 'lightning', icon: BoltIcon, label: 'Bitcoin Lightning', description: locale === 'es' ? 'Pago instantáneo con Bitcoin' : 'Instant payment with Bitcoin', badge: locale === 'es' ? 'Instantáneo' : 'Instant', }); } if (paymentConfig?.tpagoEnabled) { paymentMethods.push({ id: 'tpago', icon: CreditCardIcon, label: locale === 'es' ? 'TPago / Tarjetas de Crédito' : 'TPago / Credit Cards', description: locale === 'es' ? 'Pagá con tarjetas de crédito locales o internacionales' : 'Pay with local or international credit cards', badge: locale === 'es' ? 'Manual' : 'Manual', }); } if (paymentConfig?.bankTransferEnabled) { paymentMethods.push({ id: 'bank_transfer', icon: BuildingLibraryIcon, label: locale === 'es' ? 'Transferencia Bancaria Local' : 'Local Bank Transfer', description: locale === 'es' ? 'Pago por transferencia bancaria en Paraguay' : 'Pay via Paraguayan bank transfer', badge: locale === 'es' ? 'Manual' : 'Manual', }); } if (paymentConfig?.cashEnabled) { paymentMethods.push({ id: 'cash', icon: BanknotesIcon, label: locale === 'es' ? 'Efectivo en el Evento' : 'Cash at Event', description: locale === 'es' ? 'Paga cuando llegues al evento' : 'Pay when you arrive at the event', badge: locale === 'es' ? 'Manual' : 'Manual', }); } if (loading) { return (
); } if (!event) { return null; } const spotsLeft = Math.max(0, event.capacity - (event.bookedCount ?? 0)); const isSoldOut = (event.bookedCount ?? 0) >= event.capacity; // Get title and description based on payment method const getSuccessContent = () => { if (bookingResult?.paymentMethod === 'cash') { return { title: locale === 'es' ? '¡Reserva Recibida!' : 'Reservation Received!', description: locale === 'es' ? 'Tu lugar está reservado. El pago se realizará en el evento.' : 'Your spot is reserved. Payment will be collected at the event.', iconColor: 'bg-yellow-100', iconTextColor: 'text-yellow-600', }; } if (bookingResult?.paymentMethod === 'lightning') { // For Lightning, if we're on success step, payment was confirmed return { title: locale === 'es' ? '¡Pago Confirmado!' : 'Payment Confirmed!', description: locale === 'es' ? '¡Tu reserva está confirmada! Te esperamos en el evento.' : 'Your booking is confirmed! See you at the event.', iconColor: 'bg-green-100', iconTextColor: 'text-green-600', }; } return { title: t('booking.success.title'), description: t('booking.success.description'), iconColor: 'bg-green-100', iconTextColor: 'text-green-600', }; }; // Paying step - waiting for Lightning payment (compact design) if (step === 'paying' && bookingResult && bookingResult.lightningInvoice) { const invoice = bookingResult.lightningInvoice; return (
{/* Amount - prominent at top */}
{invoice.fiatAmount && invoice.fiatCurrency && (

{invoice.fiatAmount.toLocaleString()} {invoice.fiatCurrency}

)}

≈ {invoice.amount.toLocaleString()} sats

{/* QR Code - clickable to copy */}
copyInvoiceToClipboard(invoice.paymentRequest)} title={locale === 'es' ? 'Clic para copiar' : 'Click to copy'} >
{/* Invoice string - truncated, clickable */}
copyInvoiceToClipboard(invoice.paymentRequest)} >

{truncateInvoice(invoice.paymentRequest, 16)}

{locale === 'es' ? 'Toca para copiar' : 'Tap to copy'}

{/* Open in Wallet - primary action */} {locale === 'es' ? 'Abrir en Billetera' : 'Open in Wallet'} {/* Status indicator */}
{locale === 'es' ? 'Esperando pago...' : 'Waiting for payment...'}
{/* Ticket reference - small */}

{locale === 'es' ? 'Ref' : 'Ref'}: {bookingResult.qrCode}

); } // Manual payment step - showing bank transfer details or TPago link if (step === 'manual_payment' && bookingResult && paymentConfig) { const isBankTransfer = bookingResult.paymentMethod === 'bank_transfer'; const isTpago = bookingResult.paymentMethod === 'tpago'; const ticketCount = bookingResult.ticketCount || 1; const totalAmount = (event?.price || 0) * ticketCount; return (
{isBankTransfer ? ( ) : ( )}

{locale === 'es' ? 'Completa tu Pago' : 'Complete Your Payment'}

{locale === 'es' ? 'Sigue las instrucciones para completar tu pago' : 'Follow the instructions to complete your payment'}

{/* Amount to pay */}

{locale === 'es' ? 'Monto a pagar' : 'Amount to pay'}

{event?.price !== undefined ? formatPrice(totalAmount, event.currency) : ''}

{ticketCount > 1 && (

{ticketCount} tickets × {formatPrice(event?.price || 0, event?.currency || 'PYG')}

)}
{/* Bank Transfer Details */} {isBankTransfer && (

{locale === 'es' ? 'Datos Bancarios' : 'Bank Details'}

{paymentConfig.bankName && (
{locale === 'es' ? 'Banco' : 'Bank'}: {paymentConfig.bankName}
)} {paymentConfig.bankAccountHolder && (
{locale === 'es' ? 'Titular' : 'Account Holder'}: {paymentConfig.bankAccountHolder}
)} {paymentConfig.bankAccountNumber && (
{locale === 'es' ? 'Nro. Cuenta' : 'Account Number'}: {paymentConfig.bankAccountNumber}
)} {paymentConfig.bankAlias && (
Alias: {paymentConfig.bankAlias}
)} {paymentConfig.bankPhone && (
{locale === 'es' ? 'Teléfono' : 'Phone'}: {paymentConfig.bankPhone}
)}
{(locale === 'es' ? paymentConfig.bankNotesEs : paymentConfig.bankNotes) && (

{locale === 'es' ? paymentConfig.bankNotesEs : paymentConfig.bankNotes}

)}
)} {/* TPago Link */} {isTpago && (

{locale === 'es' ? 'Pago con Tarjeta' : 'Card Payment'}

{paymentConfig.tpagoLink && ( {locale === 'es' ? 'Abrir TPago para Pagar' : 'Open TPago to Pay'} )} {(locale === 'es' ? paymentConfig.tpagoInstructionsEs : paymentConfig.tpagoInstructions) && (

{locale === 'es' ? paymentConfig.tpagoInstructionsEs : paymentConfig.tpagoInstructions}

)}
)} {/* Reference */}

{locale === 'es' ? 'Referencia de tu reserva' : 'Your booking reference'}

{bookingResult.qrCode}

{/* Manual verification notice */}

{locale === 'es' ? 'Verificación manual' : 'Manual verification'}

{locale === 'es' ? 'El equipo de Spanglish revisará el pago manualmente. Tu reserva solo será confirmada después de recibir un email de confirmación de nuestra parte.' : 'The Spanglish team will review the payment manually. Your booking is only confirmed after you receive a confirmation email from us.'}

{/* Paid under different name option */}
{paidUnderDifferentName && (
setPayerName(e.target.value)} placeholder={locale === 'es' ? 'Nombre completo del titular de la cuenta' : 'Full name of account holder'} required />
)}
{/* Warning before I Have Paid button */}

{locale === 'es' ? 'Solo haz clic aquí después de haber completado el pago.' : 'Only click this after you have actually completed the payment.'}

{/* I Have Paid Button */}

{locale === 'es' ? 'Tu reserva será confirmada una vez que verifiquemos el pago' : 'Your booking will be confirmed once we verify the payment'}

); } // Pending approval step - user has marked payment as sent if (step === 'pending_approval' && bookingResult) { return (

{locale === 'es' ? '¡Pago en Verificación!' : 'Payment Being Verified!'}

{locale === 'es' ? 'Estamos verificando tu pago. Recibirás un email de confirmación una vez aprobado.' : 'We are verifying your payment. You will receive a confirmation email once approved.'}

{bookingResult.qrCode}

{t('booking.success.event')}: {event?.title}

{t('booking.success.date')}: {event && formatDate(event.startDatetime)}

{t('booking.success.time')}: {event && fmtTime(event.startDatetime)}

{t('booking.success.location')}: {event?.location}

{locale === 'es' ? 'La verificación del pago puede tomar hasta 24 horas hábiles. Por favor revisa tu email regularmente.' : 'Payment verification may take up to 24 business hours. Please check your email regularly.'}

); } // Success step if (step === 'success' && bookingResult) { const successContent = getSuccessContent(); return (

{successContent.title}

{successContent.description}

{/* Multi-ticket indicator */} {bookingResult.ticketCount && bookingResult.ticketCount > 1 && (

{locale === 'es' ? `${bookingResult.ticketCount} tickets reservados` : `${bookingResult.ticketCount} tickets booked`}

{locale === 'es' ? 'Cada asistente recibirá su propio código QR' : 'Each attendee will receive their own QR code'}

)}
{bookingResult.qrCode} {bookingResult.ticketCount && bookingResult.ticketCount > 1 && ( +{bookingResult.ticketCount - 1} {locale === 'es' ? 'más' : 'more'} )}

{t('booking.success.event')}: {event.title}

{t('booking.success.date')}: {formatDate(event.startDatetime)}

{t('booking.success.time')}: {fmtTime(event.startDatetime)}

{t('booking.success.location')}: {event.location}

{bookingResult.paymentMethod === 'cash' && (

{t('booking.success.cashNote')}: {t('booking.success.cashDescription')}

)} {bookingResult.paymentMethod === 'bancard' && (

{t('booking.success.cardNote')}

)} {bookingResult.paymentMethod === 'lightning' && (

{locale === 'es' ? '¡Pago con Bitcoin Lightning recibido exitosamente!' : 'Bitcoin Lightning payment received successfully!'}

)}

{t('booking.success.emailSent')}

{/* Download Ticket Button - only for instant confirmation (Lightning) */} {bookingResult.paymentMethod === 'lightning' && ( )}
); } return (
{t('common.back')} {/* Event Summary - Always Visible */}

{locale === 'es' && event.titleEs ? event.titleEs : event.title}

{formatDate(event.startDatetime)} • {fmtTime(event.startDatetime)}
{event.location}
{!event.externalBookingEnabled && (
{spotsLeft} / {event.capacity} {t('events.details.spotsLeft')}
)}
{event.price === 0 ? t('events.details.free') : formatPrice(event.price, event.currency)} {event.price > 0 && ( {locale === 'es' ? 'por persona' : 'per person'} )}
{/* Ticket quantity and total */} {ticketQuantity > 1 && (
{locale === 'es' ? 'Tickets' : 'Tickets'}: {ticketQuantity} {locale === 'es' ? 'Total' : 'Total'}: {formatPrice(event.price * ticketQuantity, event.currency)}
)}
{isSoldOut ? (

{t('events.details.soldOut')}

{t('booking.form.soldOutMessage')}

) : (
{/* User Information Section */}

{attendees.length > 0 && ( 1 )} {t('booking.form.personalInfo')} {attendees.length > 0 && ( ({locale === 'es' ? 'Asistente principal' : 'Primary attendee'}) )}

setFormData({ ...formData, firstName: e.target.value })} placeholder={t('booking.form.firstNamePlaceholder')} error={errors.firstName} required />
({locale === 'es' ? 'Opcional' : 'Optional'})
setFormData({ ...formData, lastName: e.target.value })} placeholder={t('booking.form.lastNamePlaceholder')} error={errors.lastName} />
setFormData({ ...formData, email: e.target.value })} placeholder={t('booking.form.emailPlaceholder')} error={errors.email} required />
({locale === 'es' ? 'Opcional' : 'Optional'})
setFormData({ ...formData, phone: e.target.value })} placeholder={t('booking.form.phonePlaceholder')} error={errors.phone} />
{t('booking.form.rucOptional')}
{/* Additional Attendees Section (for multi-ticket bookings) */} {attendees.length > 0 && (

{locale === 'es' ? 'Información de los Otros Asistentes' : 'Other Attendees Information'}

{locale === 'es' ? 'Ingresa el nombre de cada asistente adicional. Cada persona recibirá su propio ticket.' : 'Enter the name for each additional attendee. Each person will receive their own ticket.'}

{attendees.map((attendee, index) => (
{index + 2} {locale === 'es' ? `Asistente ${index + 2}` : `Attendee ${index + 2}`}
{ const newAttendees = [...attendees]; newAttendees[index].firstName = e.target.value; setAttendees(newAttendees); if (attendeeErrors[index]) { const newErrors = { ...attendeeErrors }; delete newErrors[index]; setAttendeeErrors(newErrors); } }} placeholder={t('booking.form.firstNamePlaceholder')} error={attendeeErrors[index]} required />
({locale === 'es' ? 'Opcional' : 'Optional'})
{ const newAttendees = [...attendees]; newAttendees[index].lastName = e.target.value; setAttendees(newAttendees); }} placeholder={t('booking.form.lastNamePlaceholder')} />
))}
)} {/* Payment Selection Section */}

{t('booking.form.paymentMethod')}

{paymentMethods.length === 0 ? (
{locale === 'es' ? 'No hay métodos de pago disponibles para este evento.' : 'No payment methods available for this event.'}
) : ( <> {paymentMethods.map((method) => ( ))} )}
{/* Submit Button */}

{t('booking.form.termsNote')}

)}
); }