Files
Spanglish/frontend/src/app/(public)/booking/success/[ticketId]/page.tsx
Michilis 9410e83b89 Add ticket system with QR scanner and PDF generation
- Add ticket validation and check-in API endpoints
- Add PDF ticket generation with QR codes (pdfkit)
- Add admin QR scanner page with camera support
- Add admin site settings page
- Update email templates with PDF ticket download link
- Add checked_in_by_admin_id field for audit tracking
- Update booking success page with ticket download
- Various UI improvements to events and booking pages
2026-02-02 00:45:12 +00:00

260 lines
9.2 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { useParams } from 'next/navigation';
import Link from 'next/link';
import { useLanguage } from '@/context/LanguageContext';
import { ticketsApi, Ticket } from '@/lib/api';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import {
CheckCircleIcon,
ClockIcon,
XCircleIcon,
TicketIcon,
ArrowPathIcon,
ArrowDownTrayIcon,
} from '@heroicons/react/24/outline';
export default function BookingSuccessPage() {
const params = useParams();
const { t, locale } = useLanguage();
const [ticket, setTicket] = useState<Ticket | null>(null);
const [loading, setLoading] = useState(true);
const [polling, setPolling] = useState(false);
const ticketId = params.ticketId as string;
const checkPaymentStatus = async () => {
try {
const { ticket: ticketData } = await ticketsApi.getById(ticketId);
setTicket(ticketData);
// If still pending, continue polling
if (ticketData.status === 'pending' && ticketData.payment?.status === 'pending') {
return false; // Not done yet
}
return true; // Done polling
} catch (error) {
console.error('Error checking payment status:', error);
return true; // Stop polling on error
}
};
useEffect(() => {
if (!ticketId) return;
// Initial load
checkPaymentStatus().finally(() => setLoading(false));
// Poll for payment status every 3 seconds
setPolling(true);
const interval = setInterval(async () => {
const isDone = await checkPaymentStatus();
if (isDone) {
setPolling(false);
clearInterval(interval);
}
}, 3000);
// Stop polling after 5 minutes
const timeout = setTimeout(() => {
setPolling(false);
clearInterval(interval);
}, 5 * 60 * 1000);
return () => {
clearInterval(interval);
clearTimeout(timeout);
};
}, [ticketId]);
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString(locale === 'es' ? 'es-ES' : 'en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
});
};
const formatTime = (dateStr: string) => {
return new Date(dateStr).toLocaleTimeString(locale === 'es' ? 'es-ES' : 'en-US', {
hour: '2-digit',
minute: '2-digit',
});
};
if (loading) {
return (
<div className="section-padding">
<div className="container-page max-w-2xl text-center">
<div className="animate-spin w-8 h-8 border-4 border-primary-yellow border-t-transparent rounded-full mx-auto" />
<p className="mt-4 text-gray-600">
{locale === 'es' ? 'Verificando pago...' : 'Verifying payment...'}
</p>
</div>
</div>
);
}
if (!ticket) {
return (
<div className="section-padding">
<div className="container-page max-w-2xl">
<Card className="p-8 text-center">
<XCircleIcon className="w-16 h-16 text-red-500 mx-auto mb-4" />
<h1 className="text-2xl font-bold text-primary-dark mb-2">
{locale === 'es' ? 'Reserva no encontrada' : 'Booking not found'}
</h1>
<p className="text-gray-600 mb-6">
{locale === 'es'
? 'No pudimos encontrar tu reserva. Por favor, contacta con soporte.'
: 'We could not find your booking. Please contact support.'}
</p>
<Link href="/events">
<Button>{locale === 'es' ? 'Ver Eventos' : 'Browse Events'}</Button>
</Link>
</Card>
</div>
</div>
);
}
const isPaid = ticket.status === 'confirmed' || ticket.payment?.status === 'paid';
const isPending = ticket.status === 'pending' && ticket.payment?.status === 'pending';
const isFailed = ticket.payment?.status === 'failed';
return (
<div className="section-padding">
<div className="container-page max-w-2xl">
<Card className="p-8 text-center">
{/* Status Icon */}
{isPaid ? (
<div className="w-16 h-16 rounded-full bg-green-100 flex items-center justify-center mx-auto mb-6">
<CheckCircleIcon className="w-10 h-10 text-green-600" />
</div>
) : isPending ? (
<div className="w-16 h-16 rounded-full bg-yellow-100 flex items-center justify-center mx-auto mb-6">
<ClockIcon className="w-10 h-10 text-yellow-600" />
</div>
) : (
<div className="w-16 h-16 rounded-full bg-red-100 flex items-center justify-center mx-auto mb-6">
<XCircleIcon className="w-10 h-10 text-red-600" />
</div>
)}
{/* Title */}
<h1 className="text-2xl font-bold text-primary-dark mb-2">
{isPaid
? (locale === 'es' ? '¡Pago Confirmado!' : 'Payment Confirmed!')
: isPending
? (locale === 'es' ? 'Esperando Pago...' : 'Waiting for Payment...')
: (locale === 'es' ? 'Pago Fallido' : 'Payment Failed')
}
</h1>
<p className="text-gray-600 mb-6">
{isPaid
? (locale === 'es'
? 'Tu reserva está confirmada. ¡Te esperamos!'
: 'Your booking is confirmed. See you there!')
: isPending
? (locale === 'es'
? 'Estamos verificando tu pago. Esto puede tomar unos segundos.'
: 'We are verifying your payment. This may take a few seconds.')
: (locale === 'es'
? 'Hubo un problema con tu pago. Por favor, intenta de nuevo.'
: 'There was an issue with your payment. Please try again.')
}
</p>
{/* Polling indicator */}
{polling && isPending && (
<div className="flex items-center justify-center gap-2 text-yellow-600 mb-6">
<ArrowPathIcon className="w-5 h-5 animate-spin" />
<span className="text-sm">
{locale === 'es' ? 'Verificando...' : 'Checking...'}
</span>
</div>
)}
{/* Ticket Details */}
<div className="bg-secondary-gray rounded-lg p-6 mb-6">
<div className="flex items-center justify-center gap-2 mb-4">
<TicketIcon className="w-6 h-6 text-primary-yellow" />
<span className="font-mono text-lg font-bold">{ticket.qrCode}</span>
</div>
<div className="text-sm text-gray-600 space-y-2">
{ticket.event && (
<>
<p><strong>{locale === 'es' ? 'Evento' : 'Event'}:</strong> {ticket.event.title}</p>
<p><strong>{locale === 'es' ? 'Fecha' : 'Date'}:</strong> {formatDate(ticket.event.startDatetime)}</p>
<p><strong>{locale === 'es' ? 'Hora' : 'Time'}:</strong> {formatTime(ticket.event.startDatetime)}</p>
<p><strong>{locale === 'es' ? 'Ubicación' : 'Location'}:</strong> {ticket.event.location}</p>
</>
)}
</div>
</div>
{/* Status Badge */}
<div className="mb-6">
<span className={`inline-block px-4 py-2 rounded-full text-sm font-medium ${
isPaid
? 'bg-green-100 text-green-800'
: isPending
? 'bg-yellow-100 text-yellow-800'
: 'bg-red-100 text-red-800'
}`}>
{isPaid
? (locale === 'es' ? 'Confirmado' : 'Confirmed')
: isPending
? (locale === 'es' ? 'Pendiente de Pago' : 'Pending Payment')
: (locale === 'es' ? 'Pago Fallido' : 'Payment Failed')
}
</span>
</div>
{/* Email note */}
{isPaid && (
<p className="text-sm text-gray-500 mb-6">
{locale === 'es'
? 'Un correo de confirmación ha sido enviado a tu bandeja de entrada.'
: 'A confirmation email has been sent to your inbox.'}
</p>
)}
{/* Download Ticket Button */}
{isPaid && (
<div className="mb-6">
<a
href={`/api/tickets/${ticketId}/pdf`}
download
className="inline-flex items-center gap-2 px-4 py-2 bg-primary-yellow text-primary-dark font-medium rounded-btn hover:bg-primary-yellow/90 transition-colors"
>
<ArrowDownTrayIcon className="w-5 h-5" />
{locale === 'es' ? 'Descargar Ticket' : 'Download Ticket'}
</a>
</div>
)}
{/* Actions */}
<div className="flex flex-col sm:flex-row gap-3 justify-center">
<Link href="/events">
<Button variant="outline">
{locale === 'es' ? 'Ver Más Eventos' : 'Browse More Events'}
</Button>
</Link>
<Link href="/">
<Button>
{locale === 'es' ? 'Volver al Inicio' : 'Back to Home'}
</Button>
</Link>
</div>
</Card>
</div>
</div>
);
}