generated from bisco/codex-bootstrap
feat: polish booking and confirmation UI
This commit is contained in:
@@ -31,23 +31,37 @@ type ApiValidationErrors = Record<string, string[]>;
|
||||
<header class="page-header">
|
||||
<p class="eyebrow">Booking</p>
|
||||
<h1>Reserve seats</h1>
|
||||
<p class="supporting">Performance {{ performanceId }}. Complete the form and we will send a confirmation email.</p>
|
||||
<p class="supporting">
|
||||
Performance {{ performanceId }}. Complete the form and we will email a confirmation link before any reservation becomes active.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<mat-card class="content-card">
|
||||
<mat-card-content>
|
||||
@if (isSuccess()) {
|
||||
<div class="status-copy success" aria-live="polite">
|
||||
<mat-icon>check_circle</mat-icon>
|
||||
<div class="status-panel success" aria-live="polite">
|
||||
<div class="status-icon">
|
||||
<mat-icon fontSet="material-symbols-outlined">mark_email_read</mat-icon>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Reservation created</h2>
|
||||
<p>check your email</p>
|
||||
<p>Check your email to confirm the booking and unlock the QR code for admission.</p>
|
||||
<div class="status-steps">
|
||||
<span><mat-icon fontSet="material-symbols-outlined">mail</mat-icon> Open the confirmation email</span>
|
||||
<span><mat-icon fontSet="material-symbols-outlined">verified</mat-icon> Confirm your reservation</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<form [formGroup]="bookingForm" (ngSubmit)="submit()" novalidate>
|
||||
<div class="intro-note">
|
||||
<mat-icon fontSet="material-symbols-outlined">info</mat-icon>
|
||||
<p>We only ask for the essentials. Your seats are held only after email confirmation.</p>
|
||||
</div>
|
||||
|
||||
<div class="form-grid">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-icon matPrefix fontSet="material-symbols-outlined">person</mat-icon>
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput type="text" formControlName="name" autocomplete="name" />
|
||||
@if (bookingForm.controls.name.touched && bookingForm.controls.name.hasError('required')) {
|
||||
@@ -56,8 +70,10 @@ type ApiValidationErrors = Record<string, string[]>;
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-icon matPrefix fontSet="material-symbols-outlined">mail</mat-icon>
|
||||
<mat-label>Email</mat-label>
|
||||
<input matInput type="email" formControlName="email" autocomplete="email" />
|
||||
<mat-hint>We will send the confirmation link here.</mat-hint>
|
||||
@if (bookingForm.controls.email.touched && bookingForm.controls.email.hasError('required')) {
|
||||
<mat-error>Email is required.</mat-error>
|
||||
}
|
||||
@@ -67,8 +83,10 @@ type ApiValidationErrors = Record<string, string[]>;
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-icon matPrefix fontSet="material-symbols-outlined">group</mat-icon>
|
||||
<mat-label>Number of seats</mat-label>
|
||||
<input matInput type="number" min="1" step="1" formControlName="partySize" />
|
||||
<mat-hint>Enter the total number of guests in your party.</mat-hint>
|
||||
@if (bookingForm.controls.partySize.touched && bookingForm.controls.partySize.hasError('required')) {
|
||||
<mat-error>Number of seats is required.</mat-error>
|
||||
}
|
||||
@@ -79,14 +97,21 @@ type ApiValidationErrors = Record<string, string[]>;
|
||||
</div>
|
||||
|
||||
@if (submitError()) {
|
||||
<p class="error-message" aria-live="assertive">{{ submitError() }}</p>
|
||||
<div class="message-panel error" aria-live="assertive">
|
||||
<mat-icon fontSet="material-symbols-outlined">error</mat-icon>
|
||||
<p>{{ submitError() }}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (fieldErrors().length > 0) {
|
||||
<div class="field-errors" aria-live="assertive">
|
||||
<div class="message-panel error field-errors" aria-live="assertive">
|
||||
<mat-icon fontSet="material-symbols-outlined">warning</mat-icon>
|
||||
<div>
|
||||
<p class="message-title">Please check the highlighted details:</p>
|
||||
@for (message of fieldErrors(); track message) {
|
||||
<p>{{ message }}</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -96,7 +121,10 @@ type ApiValidationErrors = Record<string, string[]>;
|
||||
<mat-progress-spinner mode="indeterminate" diameter="18"></mat-progress-spinner>
|
||||
<span>Submitting...</span>
|
||||
} @else {
|
||||
<span>Reserve</span>
|
||||
<ng-container>
|
||||
<mat-icon fontSet="material-symbols-outlined">confirmation_number</mat-icon>
|
||||
<span>Reserve</span>
|
||||
</ng-container>
|
||||
}
|
||||
</button>
|
||||
<a mat-button routerLink="/shows">Back to shows</a>
|
||||
@@ -138,10 +166,35 @@ type ApiValidationErrors = Record<string, string[]>;
|
||||
}
|
||||
|
||||
.content-card {
|
||||
border-radius: 8px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid var(--azionelab-border);
|
||||
background: var(--azionelab-surface);
|
||||
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 {
|
||||
@@ -153,18 +206,37 @@ type ApiValidationErrors = Record<string, string[]>;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.error-message,
|
||||
.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;
|
||||
color: #b3261e;
|
||||
line-height: 1.4;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.field-errors {
|
||||
.message-title {
|
||||
font-weight: 700;
|
||||
margin-bottom: 6px !important;
|
||||
}
|
||||
|
||||
.field-errors > div {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
@@ -183,23 +255,77 @@ type ApiValidationErrors = Record<string, string[]>;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.status-copy {
|
||||
.status-panel {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 14px;
|
||||
gap: 16px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.status-copy h2 {
|
||||
.status-panel h2 {
|
||||
margin: 0 0 6px;
|
||||
}
|
||||
|
||||
.status-copy p {
|
||||
.status-panel p {
|
||||
margin: 0;
|
||||
color: var(--azionelab-muted);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.status-copy.success mat-icon {
|
||||
color: #2e7d32;
|
||||
.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,
|
||||
|
||||
Reference in New Issue
Block a user