Refactor monolithic modules and harden booking, email, and auth infrastructure.
Split oversized frontend API client, email service, and admin/booking pages into focused modules while preserving import surfaces, and add Redis-backed queues, stale booking cleanup, stronger auth, and scale deployment configs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
import Link from 'next/link';
|
||||
import Card from '@/components/ui/Card';
|
||||
import Button from '@/components/ui/Button';
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
TicketIcon,
|
||||
ArrowDownTrayIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import { Event } from '@/lib/api';
|
||||
import { getSuccessContent } from '../_logic/booking';
|
||||
import type { BookingResult } from '../_types';
|
||||
|
||||
interface SuccessStepProps {
|
||||
bookingResult: BookingResult;
|
||||
event: Event;
|
||||
locale: string;
|
||||
t: (key: string) => string;
|
||||
formatDate: (dateStr: string) => string;
|
||||
fmtTime: (dateStr: string) => string;
|
||||
}
|
||||
|
||||
export function SuccessStep({
|
||||
bookingResult,
|
||||
event,
|
||||
locale,
|
||||
t,
|
||||
formatDate,
|
||||
fmtTime,
|
||||
}: SuccessStepProps) {
|
||||
const successContent = getSuccessContent(bookingResult, locale, t);
|
||||
|
||||
return (
|
||||
<div className="section-padding">
|
||||
<div className="container-page max-w-2xl">
|
||||
<Card className="p-8 text-center">
|
||||
<div className={`w-16 h-16 rounded-full ${successContent.iconColor} flex items-center justify-center mx-auto mb-6`}>
|
||||
<CheckCircleIcon className={`w-10 h-10 ${successContent.iconTextColor}`} />
|
||||
</div>
|
||||
|
||||
<h1 className="text-2xl font-bold text-primary-dark mb-2">
|
||||
{successContent.title}
|
||||
</h1>
|
||||
<p className="text-gray-600 mb-6">
|
||||
{successContent.description}
|
||||
</p>
|
||||
|
||||
<div className="bg-secondary-gray rounded-lg p-6 mb-6">
|
||||
{/* Multi-ticket indicator */}
|
||||
{bookingResult.ticketCount && bookingResult.ticketCount > 1 && (
|
||||
<div className="mb-4 pb-4 border-b border-gray-300">
|
||||
<p className="text-lg font-semibold text-primary-dark">
|
||||
{locale === 'es'
|
||||
? `${bookingResult.ticketCount} tickets reservados`
|
||||
: `${bookingResult.ticketCount} tickets booked`}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
{locale === 'es'
|
||||
? 'Cada asistente recibirá su propio código QR'
|
||||
: 'Each attendee will receive their own QR code'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<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">{bookingResult.qrCode}</span>
|
||||
{bookingResult.ticketCount && bookingResult.ticketCount > 1 && (
|
||||
<span className="text-xs bg-purple-100 text-purple-700 px-2 py-1 rounded-full">
|
||||
+{bookingResult.ticketCount - 1} {locale === 'es' ? 'más' : 'more'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-600 space-y-2">
|
||||
<p><strong>{t('booking.success.event')}:</strong> {event.title}</p>
|
||||
<p><strong>{t('booking.success.date')}:</strong> {formatDate(event.startDatetime)}</p>
|
||||
<p><strong>{t('booking.success.time')}:</strong> {fmtTime(event.startDatetime)}</p>
|
||||
<p><strong>{t('booking.success.location')}:</strong> {event.location}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{bookingResult.paymentMethod === 'cash' && (
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
|
||||
<p className="text-yellow-800 text-sm">
|
||||
<strong>{t('booking.success.cashNote')}:</strong> {t('booking.success.cashDescription')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{bookingResult.paymentMethod === 'bancard' && (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
|
||||
<p className="text-blue-800 text-sm">
|
||||
{t('booking.success.cardNote')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{bookingResult.paymentMethod === 'lightning' && (
|
||||
<div className="bg-green-50 border border-green-200 rounded-lg p-4 mb-6">
|
||||
<p className="text-green-800 text-sm flex items-center gap-2">
|
||||
<CheckCircleIcon className="w-5 h-5" />
|
||||
{locale === 'es'
|
||||
? '¡Pago con Bitcoin Lightning recibido exitosamente!'
|
||||
: 'Bitcoin Lightning payment received successfully!'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-sm text-gray-500 mb-6">
|
||||
{t('booking.success.emailSent')}
|
||||
</p>
|
||||
|
||||
{/* Download Ticket Button - only for instant confirmation (Lightning) */}
|
||||
{bookingResult.paymentMethod === 'lightning' && (
|
||||
<div className="mb-6">
|
||||
<a
|
||||
href={bookingResult.bookingId
|
||||
? `/api/tickets/booking/${bookingResult.bookingId}/pdf`
|
||||
: `/api/tickets/${bookingResult.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'
|
||||
? (bookingResult.ticketCount && bookingResult.ticketCount > 1 ? 'Descargar Tickets' : 'Descargar Ticket')
|
||||
: (bookingResult.ticketCount && bookingResult.ticketCount > 1 ? 'Download Tickets' : 'Download Ticket')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
||||
<Link href="/events">
|
||||
<Button variant="outline">{t('booking.success.browseEvents')}</Button>
|
||||
</Link>
|
||||
<Link href="/">
|
||||
<Button>{t('booking.success.backHome')}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user