From 24d3f4d30f1635ec2663ed7308df9908ae670d16 Mon Sep 17 00:00:00 2001 From: bisco Date: Wed, 29 Apr 2026 18:19:05 +0200 Subject: [PATCH] feat(frontend): add reservation confirmation page --- frontend/src/app/app.routes.ts | 2 + .../reservation-confirm-page.component.ts | 210 ++++++++++++++++++ .../src/app/services/shows-api.service.ts | 14 ++ 3 files changed, 226 insertions(+) create mode 100644 frontend/src/app/pages/reservation-confirm-page.component.ts diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 5858c48..5ff2f8d 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -3,6 +3,7 @@ import { Routes } from '@angular/router'; import { BookingPlaceholderPageComponent } from './pages/booking-placeholder-page.component'; import { CheckInPlaceholderPageComponent } from './pages/check-in-placeholder-page.component'; import { HomePageComponent } from './pages/home-page.component'; +import { ReservationConfirmPageComponent } from './pages/reservation-confirm-page.component'; import { ShowDetailPlaceholderPageComponent } from './pages/show-detail-placeholder-page.component'; import { ShowListPageComponent } from './pages/show-list-page.component'; @@ -11,6 +12,7 @@ export const appRoutes: Routes = [ { path: 'shows', component: ShowListPageComponent, title: 'Shows | AzioneLab' }, { path: 'shows/:slug', component: ShowDetailPlaceholderPageComponent, title: 'Show detail | AzioneLab' }, { path: 'performances/:id/book', component: BookingPlaceholderPageComponent, title: 'Book | AzioneLab' }, + { path: 'reservations/confirm', component: ReservationConfirmPageComponent, title: 'Confirm reservation | AzioneLab' }, { path: 'check-in', component: CheckInPlaceholderPageComponent, title: 'Check-in | AzioneLab' }, { path: '**', redirectTo: '' }, ]; diff --git a/frontend/src/app/pages/reservation-confirm-page.component.ts b/frontend/src/app/pages/reservation-confirm-page.component.ts new file mode 100644 index 0000000..013e329 --- /dev/null +++ b/frontend/src/app/pages/reservation-confirm-page.component.ts @@ -0,0 +1,210 @@ +import { ChangeDetectionStrategy, Component, DestroyRef, inject, signal } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { HttpErrorResponse } from '@angular/common/http'; +import { ActivatedRoute, RouterLink } from '@angular/router'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { MatIconModule } from '@angular/material/icon'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; + +import { ReservationConfirmResponse, ShowsApiService } from '../services/shows-api.service'; + +type ConfirmationState = 'loading' | 'success' | 'invalid' | 'expired' | 'error'; + +@Component({ + standalone: true, + imports: [ + RouterLink, + MatButtonModule, + MatCardModule, + MatIconModule, + MatProgressSpinnerModule, + ], + template: ` +
+ + + + + @if (state() === 'loading') { +
+ +
+

Confirming reservation...

+

Please wait while we validate your link.

+
+
+ } + + @if (state() === 'success' && confirmation()) { +
+ check_circle +
+

Reservation confirmed

+

Your seats are confirmed. Present this QR code at check-in.

+
+
+ + @if (confirmation()!.qr_code_image) { +
+ Reservation QR code +
+ } + + @if (confirmation()!.qr_code_url) { +

Check-in URL: {{ confirmation()!.qr_code_url }}

+ } + } + + @if (state() === 'invalid') { +
+ error +
+

Invalid confirmation link

+

This token is not valid. Please use the latest email confirmation link.

+
+
+ } + + @if (state() === 'expired') { +
+ schedule +
+

Confirmation link expired

+

This link has expired. Please create a new reservation.

+
+
+ } + + @if (state() === 'error') { +
+ warning +
+

Could not confirm reservation

+

Please try again in a moment.

+
+
+ } +
+ + + Home + 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); + } + + .status-card { + border-radius: 8px; + border: 1px solid var(--azionelab-border); + background: var(--azionelab-surface); + box-shadow: var(--azionelab-shadow); + } + + .status-copy { + display: flex; + align-items: flex-start; + gap: 14px; + } + + .status-copy h2 { + margin: 0 0 6px; + font-size: 1.2rem; + } + + .status-copy p { + margin: 0; + color: var(--azionelab-muted); + line-height: 1.5; + } + + .status-copy.success mat-icon { + color: #2e7d32; + } + + .qr-panel { + margin-top: 18px; + padding: 14px; + border-radius: 8px; + border: 1px solid var(--azionelab-border); + display: inline-block; + background: white; + } + + .qr-panel img { + width: min(280px, 100%); + height: auto; + display: block; + } + + .meta { + margin: 14px 0 0; + word-break: break-word; + color: var(--azionelab-muted); + } + `], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ReservationConfirmPageComponent { + private readonly destroyRef = inject(DestroyRef); + private readonly route = inject(ActivatedRoute); + private readonly showsApi = inject(ShowsApiService); + + protected readonly state = signal('loading'); + protected readonly confirmation = signal(null); + + constructor() { + const token = this.route.snapshot.queryParamMap.get('token')?.trim() ?? ''; + + if (!token) { + this.state.set('invalid'); + return; + } + + this.showsApi.confirmReservation(token) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next: (response) => { + this.confirmation.set(response); + this.state.set('success'); + }, + error: (error: HttpErrorResponse) => { + if (error.status === 404 || error.status === 400) { + this.state.set('invalid'); + return; + } + if (error.status === 410) { + this.state.set('expired'); + return; + } + this.state.set('error'); + }, + }); + } +} diff --git a/frontend/src/app/services/shows-api.service.ts b/frontend/src/app/services/shows-api.service.ts index c750831..896a00e 100644 --- a/frontend/src/app/services/shows-api.service.ts +++ b/frontend/src/app/services/shows-api.service.ts @@ -44,6 +44,14 @@ export type ReservationCreateResponse = { message: string; }; +export type ReservationConfirmResponse = { + reservation_id: number; + status: string; + party_size: number; + qr_code_url?: string; + qr_code_image?: string; +}; + type ShowListResponse = { results: ShowListItem[]; }; @@ -79,4 +87,10 @@ export class ShowsApiService { payload, ); } + + confirmReservation(token: string): Observable { + return this.http.get(`${this.apiBaseUrl}/reservations/confirm/`, { + params: { token }, + }); + } }