feat: polish booking and confirmation UI

This commit is contained in:
bisco
2026-04-30 00:59:43 +02:00
parent 3dca43bc5c
commit 05de8c75a2
4 changed files with 357 additions and 47 deletions

View File

@@ -25,12 +25,13 @@ type ConfirmationState = 'loading' | 'success' | 'invalid' | 'expired' | 'error'
<header class="page-header">
<p class="eyebrow">Reservation confirmation</p>
<h1>Email confirmation</h1>
<p class="supporting">Use the link from your inbox to confirm your reservation and retrieve the QR code for venue check-in.</p>
</header>
<mat-card class="status-card">
<mat-card-content>
@if (state() === 'loading') {
<div class="status-copy" aria-live="polite">
<div class="status-panel loading" aria-live="polite">
<mat-progress-spinner mode="indeterminate" diameter="36"></mat-progress-spinner>
<div>
<h2>Confirming reservation...</h2>
@@ -40,28 +41,40 @@ type ConfirmationState = 'loading' | 'success' | 'invalid' | 'expired' | 'error'
}
@if (state() === 'success' && confirmation()) {
<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">verified</mat-icon>
</div>
<div>
<h2>Reservation confirmed</h2>
<p>Your seats are confirmed. Present this QR code at check-in.</p>
<p>Your seats are confirmed. Present this QR code at check-in and keep the link handy if staff needs manual access.</p>
<div class="success-points">
<span><mat-icon fontSet="material-symbols-outlined">qr_code_2</mat-icon> Ready for entry</span>
<span><mat-icon fontSet="material-symbols-outlined">theater_comedy</mat-icon> See you at the performance</span>
</div>
</div>
</div>
@if (confirmation()!.qr_code_image) {
<div class="qr-panel">
<p class="panel-label">Your check-in QR code</p>
<img [src]="confirmation()!.qr_code_image" alt="Reservation QR code" />
</div>
}
@if (confirmation()!.qr_code_url) {
<p class="meta">Check-in URL: <a [href]="confirmation()!.qr_code_url">{{ confirmation()!.qr_code_url }}</a></p>
<div class="meta-card">
<mat-icon fontSet="material-symbols-outlined">link</mat-icon>
<p>Check-in URL: <a [href]="confirmation()!.qr_code_url">{{ confirmation()!.qr_code_url }}</a></p>
</div>
}
}
@if (state() === 'invalid') {
<div class="status-copy" aria-live="assertive">
<mat-icon>error</mat-icon>
<div class="status-panel error" aria-live="assertive">
<div class="status-icon">
<mat-icon fontSet="material-symbols-outlined">error</mat-icon>
</div>
<div>
<h2>Invalid confirmation link</h2>
<p>This token is not valid. Please use the latest email confirmation link.</p>
@@ -70,8 +83,10 @@ type ConfirmationState = 'loading' | 'success' | 'invalid' | 'expired' | 'error'
}
@if (state() === 'expired') {
<div class="status-copy" aria-live="assertive">
<mat-icon>schedule</mat-icon>
<div class="status-panel warning" aria-live="assertive">
<div class="status-icon">
<mat-icon fontSet="material-symbols-outlined">schedule</mat-icon>
</div>
<div>
<h2>Confirmation link expired</h2>
<p>This link has expired. Please create a new reservation.</p>
@@ -80,8 +95,10 @@ type ConfirmationState = 'loading' | 'success' | 'invalid' | 'expired' | 'error'
}
@if (state() === 'error') {
<div class="status-copy" aria-live="assertive">
<mat-icon>warning</mat-icon>
<div class="status-panel error" aria-live="assertive">
<div class="status-icon">
<mat-icon fontSet="material-symbols-outlined">warning</mat-icon>
</div>
<div>
<h2>Could not confirm reservation</h2>
<p>Please try again in a moment.</p>
@@ -120,54 +137,195 @@ type ConfirmationState = 'loading' | 'success' | 'invalid' | 'expired' | 'error'
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);
.supporting {
color: var(--azionelab-muted);
line-height: 1.6;
max-width: 58ch;
margin: 14px 0 0;
}
.status-copy {
.status-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;
}
mat-card-actions {
padding: 0 28px 24px !important;
gap: 8px;
}
.status-panel {
display: flex;
align-items: flex-start;
gap: 14px;
gap: 16px;
padding: 20px;
border-radius: 18px;
border: 1px solid transparent;
}
.status-copy h2 {
.status-panel h2 {
margin: 0 0 6px;
font-size: 1.2rem;
}
.status-copy p {
.status-panel p {
margin: 0;
color: var(--azionelab-muted);
line-height: 1.5;
}
.status-copy.success mat-icon {
color: #2e7d32;
.status-panel.loading {
background: rgba(159, 47, 40, 0.04);
border-color: rgba(159, 47, 40, 0.1);
}
.status-panel.success {
background: var(--azionelab-success-bg);
border-color: var(--azionelab-success-border);
}
.status-panel.warning {
background: #fff7ea;
border-color: rgba(181, 126, 0, 0.15);
}
.status-panel.error {
background: var(--azionelab-error-bg);
border-color: var(--azionelab-error-border);
}
.status-icon {
display: grid;
place-items: center;
width: 52px;
height: 52px;
border-radius: 16px;
flex: 0 0 auto;
background: rgba(30, 27, 24, 0.06);
}
.status-panel.success .status-icon {
background: rgba(46, 125, 50, 0.12);
}
.status-panel.warning .status-icon {
background: rgba(181, 126, 0, 0.14);
}
.status-panel.error .status-icon {
background: rgba(179, 38, 30, 0.12);
}
.status-panel.success .status-icon mat-icon {
color: var(--azionelab-success-ink);
}
.status-panel.warning .status-icon mat-icon {
color: #9b6c00;
}
.status-panel.error .status-icon mat-icon {
color: var(--azionelab-error-ink);
}
.success-points {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 14px;
}
.success-points 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;
}
.success-points mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
.qr-panel {
margin-top: 18px;
padding: 14px;
border-radius: 8px;
padding: 16px;
border-radius: 18px;
border: 1px solid var(--azionelab-border);
display: inline-block;
background: white;
}
.panel-label {
margin: 0 0 12px;
font-size: 0.88rem;
font-weight: 700;
color: var(--azionelab-muted);
text-transform: uppercase;
letter-spacing: 0.08em;
}
.qr-panel img {
width: min(280px, 100%);
height: auto;
display: block;
}
.meta {
margin: 14px 0 0;
word-break: break-word;
.meta-card {
display: flex;
align-items: flex-start;
gap: 10px;
margin-top: 16px;
padding: 14px 16px;
border-radius: 16px;
background: rgba(159, 47, 40, 0.05);
color: var(--azionelab-muted);
}
.meta-card p {
margin: 0;
word-break: break-word;
}
.meta-card a {
color: var(--azionelab-accent-strong);
}
@media (max-width: 640px) {
mat-card-content {
padding: 22px !important;
}
mat-card-actions {
padding: 0 22px 20px !important;
}
.status-panel {
padding: 18px;
border-radius: 16px;
}
.qr-panel {
width: 100%;
}
.qr-panel img {
width: min(100%, 280px);
margin: 0 auto;
}
}
`],
changeDetection: ChangeDetectionStrategy.OnPush,
})