From fbbb1ae5fee38539361ba1adaa1a25f3f0e6323c Mon Sep 17 00:00:00 2001 From: bisco Date: Tue, 5 May 2026 10:46:10 +0200 Subject: [PATCH] feat: apply azionelab branding to frontend --- frontend/src/app/app.component.ts | 40 ++++----- frontend/src/app/app.routes.ts | 8 +- .../booking-placeholder-page.component.ts | 73 +++++++++------ .../check-in-placeholder-page.component.ts | 90 +++++++++---------- frontend/src/app/pages/home-page.component.ts | 56 +++++++----- .../reservation-confirm-page.component.ts | 48 +++++----- .../show-detail-placeholder-page.component.ts | 40 ++++----- .../src/app/pages/show-list-page.component.ts | 22 ++--- frontend/src/assets/azionelab-logo.svg | 15 ++++ 9 files changed, 213 insertions(+), 179 deletions(-) create mode 100644 frontend/src/assets/azionelab-logo.svg diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 19be632..5eb598f 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -15,29 +15,29 @@ import { MatButtonModule } from '@angular/material/button';
- Rome-based theatre company - Reservations, confirmation, and entrance check-in + Laboratori teatrali & produzioni audio/visive + Direzione artistica a cura di Ernesto Estatico
@@ -50,12 +50,12 @@ import { MatButtonModule } from '@angular/material/button'; @@ -115,16 +115,12 @@ import { MatButtonModule } from '@angular/material/button'; text-decoration: none; } - .brand-mark { - display: inline-grid; - place-items: center; - width: 48px; - height: 48px; - border-radius: 14px; - background: linear-gradient(145deg, var(--azionelab-accent), var(--azionelab-accent-soft)); - color: #fff8f2; - font-weight: 700; - box-shadow: 0 14px 30px rgba(111, 40, 33, 0.18); + .brand-logo { + width: 58px; + height: 58px; + display: block; + object-fit: contain; + filter: drop-shadow(0 14px 30px rgba(111, 40, 33, 0.16)); } .brand-text { diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 5ff2f8d..3e57c3b 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -9,10 +9,10 @@ import { ShowListPageComponent } from './pages/show-list-page.component'; export const appRoutes: Routes = [ { path: '', component: HomePageComponent, title: 'AzioneLab' }, - { 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: 'shows', component: ShowListPageComponent, title: 'Spettacoli | AzioneLab' }, + { path: 'shows/:slug', component: ShowDetailPlaceholderPageComponent, title: 'Scheda spettacolo | AzioneLab' }, + { path: 'performances/:id/book', component: BookingPlaceholderPageComponent, title: 'Prenotazione | AzioneLab' }, + { path: 'reservations/confirm', component: ReservationConfirmPageComponent, title: 'Conferma prenotazione | AzioneLab' }, { path: 'check-in', component: CheckInPlaceholderPageComponent, title: 'Check-in | AzioneLab' }, { path: '**', redirectTo: '' }, ]; diff --git a/frontend/src/app/pages/booking-placeholder-page.component.ts b/frontend/src/app/pages/booking-placeholder-page.component.ts index ac99a7b..eb2704d 100644 --- a/frontend/src/app/pages/booking-placeholder-page.component.ts +++ b/frontend/src/app/pages/booking-placeholder-page.component.ts @@ -29,22 +29,22 @@ type ApiValidationErrors = Record; template: `
-

Before you submit

-

Reservations are activated only after email confirmation.

+

Prima di inviare

+

La prenotazione si attiva solo dopo la conferma via email.

    -
  • Your confirmation link arrives at the email address you provide.
  • -
  • Seat availability is checked on the server before confirmation.
  • -
  • The QR code is generated only after the reservation becomes confirmed.
  • +
  • Il link di conferma arriva all'indirizzo email che inserisci.
  • +
  • La disponibilita' viene verificata sul server prima della conferma.
  • +
  • Il QR code viene generato solo dopo la conferma della prenotazione.
@@ -57,11 +57,11 @@ type ApiValidationErrors = Record; mark_email_read
-

Reservation created

-

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

+

Richiesta inviata

+

Controlla la tua email per confermare la prenotazione e ottenere il QR code per l'ingresso.

- mail Open the confirmation email - verified Confirm your reservation + mail Apri l'email di conferma + verified Conferma la tua prenotazione
@@ -69,16 +69,16 @@ type ApiValidationErrors = Record;
info -

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

+

Ti chiediamo solo l'essenziale. I posti vengono confermati solo dopo la verifica via email.

person - Name + Nome @if (bookingForm.controls.name.touched && bookingForm.controls.name.hasError('required')) { - Name is required. + Il nome e' obbligatorio. } @@ -86,25 +86,25 @@ type ApiValidationErrors = Record; mail Email - We will send the confirmation link here. + Invieremo qui il link di conferma. @if (bookingForm.controls.email.touched && bookingForm.controls.email.hasError('required')) { - Email is required. + L'email e' obbligatoria. } @if (bookingForm.controls.email.touched && bookingForm.controls.email.hasError('email')) { - Enter a valid email address. + Inserisci un indirizzo email valido. } group - Number of seats + Numero di posti - Enter the total number of guests in your party. + Indica il numero totale di persone della prenotazione. @if (bookingForm.controls.partySize.touched && bookingForm.controls.partySize.hasError('required')) { - Number of seats is required. + Il numero di posti e' obbligatorio. } @if (bookingForm.controls.partySize.touched && bookingForm.controls.partySize.hasError('min')) { - At least 1 seat is required. + Devi richiedere almeno 1 posto. }
@@ -120,7 +120,7 @@ type ApiValidationErrors = Record;
warning
-

Please check the highlighted details:

+

Controlla i dati evidenziati:

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

{{ message }}

} @@ -132,15 +132,15 @@ type ApiValidationErrors = Record; - Back to shows + Torna agli spettacoli
} @@ -413,15 +413,30 @@ export class BookingPlaceholderPageComponent { this.fieldErrors.set(this.flattenValidationErrors(error.error as ApiValidationErrors)); return; } - this.submitError.set('Could not create reservation. Please try again.'); + this.submitError.set('Non siamo riusciti a creare la prenotazione. Riprova.'); }, }); } 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}`); + const labelMap: Record = { + name: 'nome', + email: 'email', + party_size: 'numero di posti', + }; + const label = labelMap[field] ?? field; + return messages.map((message) => `${label}: ${this.translateValidationMessage(message)}`); }); } + + private translateValidationMessage(message: string): string { + const translations: Record = { + 'This field is required.': 'questo campo e\' obbligatorio.', + 'Enter a valid email address.': 'inserisci un indirizzo email valido.', + 'Ensure this value is greater than or equal to 1.': 'inserisci un valore maggiore o uguale a 1.', + }; + + return translations[message] ?? message; + } } diff --git a/frontend/src/app/pages/check-in-placeholder-page.component.ts b/frontend/src/app/pages/check-in-placeholder-page.component.ts index b608896..8c55ebe 100644 --- a/frontend/src/app/pages/check-in-placeholder-page.component.ts +++ b/frontend/src/app/pages/check-in-placeholder-page.component.ts @@ -63,20 +63,20 @@ type BarcodeDetectorConstructor = new (options?: { formats?: string[] }) => Barc template: `
-

Front of house

-

Designed for quick, low-friction arrivals.

+

Ingresso sala

+

Uno strumento pensato per un'accoglienza rapida e chiara.

    -
  • Scan a QR code when a device camera is available.
  • -
  • Enter the token manually if scanning is not possible.
  • -
  • Confirm admission only after the preview data matches the guest.
  • +
  • Inquadra il QR code quando la fotocamera del dispositivo e' disponibile.
  • +
  • Inserisci il token manualmente se la scansione non e' possibile.
  • +
  • Conferma l'ingresso solo dopo aver verificato che i dati corrispondano alla prenotazione.
@@ -85,13 +85,13 @@ type BarcodeDetectorConstructor = new (options?: { formats?: string[] }) => Barc
-

Camera scan

-

Optional on supported browsers. If the QR contains a full check-in URL, the token is extracted automatically.

+

Scansione con fotocamera

+

Disponibile nei browser compatibili. Se il QR contiene l'intero link di check-in, il token viene estratto automaticamente.

@if (cameraState() === 'active') { - + } @else { } @@ -123,10 +123,10 @@ type BarcodeDetectorConstructor = new (options?: { formats?: string[] }) => Barc
- Opaque token + Token opaco @if (tokenForm.controls.token.touched && tokenForm.controls.token.hasError('required')) { - Token is required. + Il token e' obbligatorio. } @@ -134,34 +134,34 @@ type BarcodeDetectorConstructor = new (options?: { formats?: string[] }) => Barc - Home - Shows + Inizio + Spettacoli
@if (previewData() && shouldShowPreview()) {
-

Admission preview

+

Anteprima accesso

-
Show
{{ previewData()!.show_title }}
-
Venue
{{ previewData()!.venue_name }}
-
Starts at
{{ previewData()!.starts_at | date: 'EEEE d MMMM, HH:mm' }}
-
Party size
{{ previewData()!.party_size }}
-
Reservation
#{{ previewData()!.reservation_id }}
+
Spettacolo
{{ previewData()!.show_title }}
+
Spazio
{{ previewData()!.venue_name }}
+
Inizio
{{ previewData()!.starts_at | date: 'EEEE d MMMM, HH:mm' }}
+
Posti
{{ previewData()!.party_size }}
+
Prenotazione
#{{ previewData()!.reservation_id }}
@@ -169,24 +169,24 @@ type BarcodeDetectorConstructor = new (options?: { formats?: string[] }) => Barc @if (state() === 'confirm_success' && confirmData()) {

- Check-in confirmed at {{ confirmData()!.checked_in_at | date: 'HH:mm' }}. + Ingresso confermato alle {{ confirmData()!.checked_in_at | date: 'HH:mm' }}.

} @if (state() === 'invalid_token') { -

Invalid token.

+

Token non valido.

} @if (state() === 'pending_reservation') { -

Reservation is still pending confirmation.

+

La prenotazione non e' ancora confermata.

} @if (state() === 'already_checked_in') { -

This reservation is already checked in.

+

Questa prenotazione risulta gia' registrata all'ingresso.

} @if (state() === 'unauthorized') { -

You are not authorized. Log into /admin with a staff account, let the page reload with that session, then retry this check-in.

+

Non sei autorizzato. Accedi a /admin con un account staff, lascia ricaricare la pagina con quella sessione e poi riprova.

} @if (state() === 'error') { -

Something went wrong. Please try again.

+

Qualcosa non ha funzionato. Riprova.

} @@ -388,8 +388,8 @@ export class CheckInPlaceholderPageComponent { protected readonly cameraState = signal(this.scannerSupported ? 'ready' : 'unsupported'); protected readonly cameraMessage = signal( this.scannerSupported - ? 'Open the camera to scan a QR code, or keep using manual token entry.' - : 'Camera scanning is not available in this browser. Manual token entry still works.', + ? 'Apri la fotocamera per scansionare un QR code, oppure continua con l\'inserimento manuale del token.' + : 'La scansione con fotocamera non e\' disponibile in questo browser. Puoi comunque inserire il token manualmente.', ); constructor() { @@ -452,13 +452,13 @@ export class CheckInPlaceholderPageComponent { protected async startScanner(): Promise { if (!this.scannerSupported || !this.barcodeDetectorCtor) { this.cameraState.set('unsupported'); - this.cameraMessage.set('Camera scanning is not available in this browser. Manual token entry still works.'); + this.cameraMessage.set('La scansione con fotocamera non e\' disponibile in questo browser. Puoi comunque inserire il token manualmente.'); return; } this.stopScanner(); this.cameraState.set('starting'); - this.cameraMessage.set('Starting camera...'); + this.cameraMessage.set('Avvio della fotocamera in corso...'); try { this.scannerStream = await navigator.mediaDevices.getUserMedia({ @@ -468,19 +468,19 @@ export class CheckInPlaceholderPageComponent { this.detector = new this.barcodeDetectorCtor({ formats: ['qr_code'] }); this.cameraState.set('active'); - this.cameraMessage.set('Point the camera at the visitor QR code.'); + this.cameraMessage.set('Inquadra il QR code del visitatore.'); this.scheduleScan(); } catch (error) { this.stopScanner(); if (error instanceof DOMException && (error.name === 'NotAllowedError' || error.name === 'SecurityError')) { this.cameraState.set('denied'); - this.cameraMessage.set('Camera access was denied. You can continue with manual token entry.'); + this.cameraMessage.set('L\'accesso alla fotocamera e\' stato negato. Puoi continuare con l\'inserimento manuale del token.'); return; } this.cameraState.set('error'); - this.cameraMessage.set('Could not start the camera. You can continue with manual token entry.'); + this.cameraMessage.set('Non siamo riusciti ad avviare la fotocamera. Puoi continuare con l\'inserimento manuale del token.'); } } @@ -507,7 +507,7 @@ export class CheckInPlaceholderPageComponent { if (this.scannerSupported && this.cameraState() === 'active') { this.cameraState.set('ready'); - this.cameraMessage.set('Camera stopped. You can scan again or continue with manual token entry.'); + this.cameraMessage.set('Fotocamera fermata. Puoi riavviare la scansione oppure continuare con l\'inserimento manuale del token.'); } } @@ -559,7 +559,7 @@ export class CheckInPlaceholderPageComponent { const context = canvas.getContext('2d'); if (!context) { this.cameraState.set('error'); - this.cameraMessage.set('Camera scan is not available right now. Please enter the token manually.'); + this.cameraMessage.set('La scansione non e\' disponibile in questo momento. Inserisci il token manualmente.'); this.stopScanner(); return; } @@ -578,14 +578,14 @@ export class CheckInPlaceholderPageComponent { if (token) { this.tokenForm.controls.token.setValue(token); this.tokenForm.controls.token.markAsTouched(); - this.cameraMessage.set('QR captured. Validating token...'); + this.cameraMessage.set('QR acquisito. Verifica del token in corso...'); this.stopScanner(); this.preview(); return; } } catch { this.cameraState.set('error'); - this.cameraMessage.set('Camera scan failed. Please enter the token manually.'); + this.cameraMessage.set('La scansione non e\' andata a buon fine. Inserisci il token manualmente.'); this.stopScanner(); return; } finally { diff --git a/frontend/src/app/pages/home-page.component.ts b/frontend/src/app/pages/home-page.component.ts index 65610cd..97dfd92 100644 --- a/frontend/src/app/pages/home-page.component.ts +++ b/frontend/src/app/pages/home-page.component.ts @@ -11,14 +11,15 @@ import { API_BASE_URL } from '../services/api-config.token'; template: `
-

AzioneLab Theatre Company

-

Small-stage evenings, thoughtful performances, simple reservations.

+ +

AzioneLab

+

Laboratori, scena e produzioni che mettono il pubblico al centro.

- AzioneLab brings contemporary theatre into intimate venues. This public frontend is shaped around clear show discovery, lightweight booking, and a calm arrival experience at the door. + AzioneLab intreccia laboratori teatrali, produzioni audio/visive e una presenza scenica pensata per spazi vivi e raccolti. Il sito accompagna il pubblico con chiarezza: scoperta degli spettacoli, prenotazione essenziale, conferma serena all'ingresso.

@@ -26,8 +27,8 @@ import { API_BASE_URL } from '../services/api-config.token';
- Tonight at AzioneLab - Doors open 30 minutes before the performance + Questa sera da AzioneLab + Le porte aprono 30 minuti prima dell'inizio
@@ -35,29 +36,29 @@ import { API_BASE_URL } from '../services/api-config.token';
-

At a glance

-

Built for a small company, not a sprawling ticketing empire

+

In breve

+

Un sito pensato per una compagnia viva, non per una macchina impersonale di biglietteria

-

The public experience stays simple: browse a show, reserve seats, confirm by email, arrive with a QR code.

+

L'esperienza pubblica resta semplice: scegli uno spettacolo, richiedi i posti, conferma via email, arriva con il tuo QR code.

- Find the right performance + Scegli la replica giusta -

Show listings and detail pages keep venue, schedule, and availability visible without noise.

+

Le schede degli spettacoli tengono in primo piano luogo, orari e disponibilita', senza confusione.

- Confirm by email + Conferma via email -

Reservations stay pending until the audience member confirms, which keeps capacity trustworthy and easy to manage.

+

Le richieste restano in attesa finche' il pubblico non conferma, cosi' la capienza resta affidabile e semplice da gestire.

- Check in quickly + Accoglienza rapida -

Front-of-house staff can preview a token, validate it server-side, and record entry in one compact flow.

+

Lo staff puo' verificare il token, controllarlo lato server e registrare l'ingresso in un flusso compatto.

@@ -65,21 +66,21 @@ import { API_BASE_URL } from '../services/api-config.token';
-

Audience journey

-

From interest to entrance in a few quiet steps

+

Percorso del pubblico

+

Dall'interesse all'ingresso in pochi passaggi chiari

    -
  1. Browse the public programme and open a show page.
  2. -
  3. Reserve seats for a performance and confirm by email.
  4. -
  5. Keep the QR code ready on your phone or on paper for entry.
  6. +
  7. Esplora la programmazione pubblica e apri la scheda di uno spettacolo.
  8. +
  9. Richiedi i posti per una replica e conferma via email.
  10. +
  11. Tieni pronto il QR code sul telefono o su carta per l'ingresso.
- Runtime wiring + Base tecnica -

API base URL

+

URL base API

{{ apiBaseUrl }} -

The frontend remains aligned with the existing Django API surface without changing backend contracts.

+

Il frontend resta allineato alle API Django gia' esistenti, senza cambiare i contratti del backend.

@@ -97,6 +98,13 @@ import { API_BASE_URL } from '../services/api-config.token'; padding: 34px 0 20px; } + .hero-logo { + width: min(220px, 52vw); + height: auto; + display: block; + margin-bottom: 18px; + } + h1 { margin: 0; max-width: 11ch; diff --git a/frontend/src/app/pages/reservation-confirm-page.component.ts b/frontend/src/app/pages/reservation-confirm-page.component.ts index 110d195..f06e16b 100644 --- a/frontend/src/app/pages/reservation-confirm-page.component.ts +++ b/frontend/src/app/pages/reservation-confirm-page.component.ts @@ -23,9 +23,9 @@ type ConfirmationState = 'loading' | 'success' | 'invalid' | 'expired' | 'error' template: `
@@ -34,8 +34,8 @@ type ConfirmationState = 'loading' | 'success' | 'invalid' | 'expired' | 'error'
-

Confirming reservation...

-

Please wait while we validate your link.

+

Stiamo confermando la prenotazione...

+

Attendi qualche istante mentre verifichiamo il link.

} @@ -46,37 +46,37 @@ type ConfirmationState = 'loading' | 'success' | 'invalid' | 'expired' | 'error' verified
-

Reservation confirmed

-

Your seats are confirmed. Present this QR code at check-in and keep the link handy if staff needs manual access.

+

Prenotazione confermata

+

I tuoi posti sono confermati. Mostra questo QR code all'ingresso e conserva il link nel caso lo staff debba aprirlo manualmente.

- qr_code_2 Ready for entry - theater_comedy See you at the performance + qr_code_2 Pronto per l'ingresso + theater_comedy Ci vediamo a teatro
@if (confirmation()!.qr_code_image) {
-

Your check-in QR code

- Reservation QR code +

Il tuo QR code di ingresso

+ QR code della prenotazione
} @if (confirmation()!.qr_code_url) { }
-

What to do next

-

Keep the QR code on your phone or print it. Staff can scan it directly at the entrance.

+

Cosa fare adesso

+

Conserva il QR code sul telefono oppure stampalo. Lo staff potra' leggerlo direttamente all'ingresso.

-

Need the link later?

-

Save the confirmation email so you can reopen the check-in page if the QR needs to be shown again.

+

Ti serve di nuovo il link?

+

Tieni l'email di conferma: potrai riaprire la pagina di check-in in qualsiasi momento se dovrai mostrare ancora il QR.

} @@ -87,8 +87,8 @@ type ConfirmationState = 'loading' | 'success' | 'invalid' | 'expired' | 'error' error
-

Invalid confirmation link

-

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

+

Link di conferma non valido

+

Questo token non e' valido. Usa l'ultimo link di conferma ricevuto via email.

} @@ -99,8 +99,8 @@ type ConfirmationState = 'loading' | 'success' | 'invalid' | 'expired' | 'error' schedule
-

Confirmation link expired

-

This link has expired. Please create a new reservation.

+

Link di conferma scaduto

+

Questo link e' scaduto. Crea una nuova prenotazione.

} @@ -111,16 +111,16 @@ type ConfirmationState = 'loading' | 'success' | 'invalid' | 'expired' | 'error' warning
-

Could not confirm reservation

-

Please try again in a moment.

+

Non siamo riusciti a confermare la prenotazione

+

Riprova tra qualche istante.

} - Home - Shows + Inizio + Spettacoli diff --git a/frontend/src/app/pages/show-detail-placeholder-page.component.ts b/frontend/src/app/pages/show-detail-placeholder-page.component.ts index a74e4fc..4ba5c16 100644 --- a/frontend/src/app/pages/show-detail-placeholder-page.component.ts +++ b/frontend/src/app/pages/show-detail-placeholder-page.component.ts @@ -25,7 +25,7 @@ import { ShowDetail, ShowPerformance, ShowsApiService } from '../services/shows- @if (isLoading()) {
-

Loading show details...

+

Caricamento dei dettagli dello spettacolo...

} @else if (errorMessage()) { @@ -33,25 +33,25 @@ import { ShowDetail, ShowPerformance, ShowsApiService } from '../services/shows-
error
-

Could not load this show

+

Non siamo riusciti a caricare questo spettacolo

{{ errorMessage() }}

- - Back to shows + + Torna agli spettacoli
} @else if (show()) {