first commit
This commit is contained in:
244
frontend/src/app/(public)/booking/success/[ticketId]/page.tsx
Normal file
244
frontend/src/app/(public)/booking/success/[ticketId]/page.tsx
Normal file
@@ -0,0 +1,244 @@
|
||||
'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,
|
||||
} 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>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user