import { ChangeDetectionStrategy, Component, DestroyRef, inject, signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; import { ActivatedRoute, RouterLink } from '@angular/router'; import { HttpErrorResponse } from '@angular/common/http'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { ReservationCreatePayload, ShowsApiService } from '../services/shows-api.service'; type ApiValidationErrors = Record; @Component({ standalone: true, imports: [ ReactiveFormsModule, RouterLink, MatButtonModule, MatCardModule, MatFormFieldModule, MatIconModule, MatInputModule, MatProgressSpinnerModule, ], template: `
@if (isSuccess()) {
mark_email_read

Reservation created

Check your email to confirm the booking and unlock the QR code for admission.

mail Open the confirmation email verified Confirm your reservation
} @else {
info

We only ask for the essentials. Your seats are held only after email confirmation.

person Name @if (bookingForm.controls.name.touched && bookingForm.controls.name.hasError('required')) { Name is required. } mail Email We will send the confirmation link here. @if (bookingForm.controls.email.touched && bookingForm.controls.email.hasError('required')) { Email is required. } @if (bookingForm.controls.email.touched && bookingForm.controls.email.hasError('email')) { Enter a valid email address. } group Number of seats Enter the total number of guests in your party. @if (bookingForm.controls.partySize.touched && bookingForm.controls.partySize.hasError('required')) { Number of seats is required. } @if (bookingForm.controls.partySize.touched && bookingForm.controls.partySize.hasError('min')) { At least 1 seat is required. }
@if (submitError()) {
error

{{ submitError() }}

} @if (fieldErrors().length > 0) {
warning

Please check the highlighted details:

@for (message of fieldErrors(); track message) {

{{ message }}

}
}
Back to shows
}
`, styles: [` .page { max-width: 760px; margin: 0 auto; } .page-header { margin-bottom: 24px; } .eyebrow { margin: 0 0 10px; color: var(--azionelab-accent); text-transform: uppercase; font-size: 0.78rem; font-weight: 700; } h1 { margin: 0; font-size: clamp(2rem, 4vw, 3rem); } .supporting { color: var(--azionelab-muted); line-height: 1.6; max-width: 56ch; margin: 14px 0 0; } .content-card { border-radius: 20px; border: 1px solid var(--azionelab-border); background: var(--azionelab-surface-strong); box-shadow: var(--azionelab-shadow); overflow: hidden; } mat-card-content { padding: 28px !important; } .intro-note { display: flex; align-items: flex-start; gap: 12px; margin-bottom: 20px; padding: 14px 16px; border-radius: 16px; background: rgba(159, 47, 40, 0.06); color: var(--azionelab-muted); } .intro-note p { margin: 0; line-height: 1.55; } .intro-note mat-icon { color: var(--azionelab-accent); } .form-grid { display: grid; gap: 14px; } mat-form-field { width: 100%; } .message-panel { display: flex; align-items: flex-start; gap: 12px; margin-top: 14px; padding: 14px 16px; border-radius: 16px; border: 1px solid transparent; } .message-panel.error { background: var(--azionelab-error-bg); border-color: var(--azionelab-error-border); color: var(--azionelab-error-ink); } .message-panel p, .field-errors p { margin: 0; line-height: 1.4; font-size: 0.92rem; } .message-title { font-weight: 700; margin-bottom: 6px !important; } .field-errors > div { display: grid; gap: 6px; } .actions { display: flex; align-items: center; gap: 12px; margin-top: 18px; flex-wrap: wrap; } .actions button[mat-flat-button] { min-width: 130px; display: inline-flex; align-items: center; justify-content: center; gap: 8px; } .status-panel { display: flex; align-items: flex-start; gap: 16px; padding: 4px 0; } .status-panel h2 { margin: 0 0 6px; } .status-panel p { margin: 0; color: var(--azionelab-muted); line-height: 1.55; } .status-panel.success { padding: 22px; border-radius: 18px; background: var(--azionelab-success-bg); border: 1px solid var(--azionelab-success-border); } .status-icon { display: grid; place-items: center; width: 52px; height: 52px; border-radius: 16px; background: rgba(46, 125, 50, 0.12); } .status-icon mat-icon { color: var(--azionelab-success-ink); } .status-steps { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 14px; } .status-steps span { display: inline-flex; align-items: center; gap: 6px; padding: 8px 12px; border-radius: 999px; background: rgba(255, 255, 255, 0.72); color: var(--azionelab-success-ink); font-size: 0.92rem; } .status-steps mat-icon { font-size: 18px; width: 18px; height: 18px; } @media (max-width: 640px) { mat-card-content { padding: 22px !important; } .status-panel, .message-panel, .intro-note { border-radius: 14px; } } `], changeDetection: ChangeDetectionStrategy.OnPush, }) export class BookingPlaceholderPageComponent { private readonly destroyRef = inject(DestroyRef); private readonly formBuilder = inject(FormBuilder); private readonly route = inject(ActivatedRoute); private readonly showsApi = inject(ShowsApiService); protected readonly performanceId = this.route.snapshot.paramMap.get('id') ?? ''; protected readonly isSubmitting = signal(false); protected readonly isSuccess = signal(false); protected readonly submitError = signal(''); protected readonly fieldErrors = signal([]); protected readonly bookingForm = this.formBuilder.nonNullable.group({ name: ['', [Validators.required, Validators.maxLength(200)]], email: ['', [Validators.required, Validators.email]], partySize: [1, [Validators.required, Validators.min(1)]], }); protected submit(): void { this.submitError.set(''); this.fieldErrors.set([]); if (this.bookingForm.invalid) { this.bookingForm.markAllAsTouched(); return; } const payload: ReservationCreatePayload = { name: this.bookingForm.controls.name.value.trim(), email: this.bookingForm.controls.email.value.trim(), party_size: this.bookingForm.controls.partySize.value, }; this.isSubmitting.set(true); this.showsApi.createReservation(this.performanceId, payload) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe({ next: () => { this.isSubmitting.set(false); this.isSuccess.set(true); this.bookingForm.disable(); }, error: (error: HttpErrorResponse) => { this.isSubmitting.set(false); if (error.status === 400 && error.error && typeof error.error === 'object') { this.fieldErrors.set(this.flattenValidationErrors(error.error as ApiValidationErrors)); return; } this.submitError.set('Could not create reservation. Please try again.'); }, }); } private flattenValidationErrors(errors: ApiValidationErrors): string[] { return Object.entries(errors).flatMap(([field, messages]) => { const label = field === 'party_size' ? 'number of seats' : field; return messages.map((message) => `${label}: ${message}`); }); } }