From f489073118ed41c056a69116d9718794e6313bda Mon Sep 17 00:00:00 2001 From: Piet Ostendorp Date: Wed, 12 Nov 2025 10:33:34 +0100 Subject: [PATCH 1/7] Add order and performance info components to ticket overlay Introduces OrderComponent, PerformanceInfoComponent, and ShoppingCartComponent for the ticket purchase flow. Updates theater-overlay to display seat selection alongside order details and performance info. Refactors seat and performance data loading, improves UI structure, and enhances movie info display components for consistency. --- src/app/app-module.ts | 6 +++ .../menu-header/menu-header.component.html | 8 +++ src/app/menu-header/menu-header.component.ts | 2 + .../movie-category.component.html | 2 +- .../movie-duration.component.html | 6 ++- .../movie-duration.component.ts | 4 ++ .../movie-poster/movie-poster.component.html | 6 +-- .../movie-rating/movie-rating.component.html | 2 +- .../movie-rating/movie-rating.component.ts | 7 ++- src/app/order/order.component.css | 3 ++ src/app/order/order.component.html | 49 +++++++++++++++++++ src/app/order/order.component.ts | 22 +++++++++ .../performance-info.component.css | 14 ++++++ .../performance-info.component.html | 21 ++++++++ .../performance-info.component.ts | 30 ++++++++++++ .../shopping-cart/shopping-cart.component.css | 0 .../shopping-cart.component.html | 1 + .../shopping-cart/shopping-cart.component.ts | 11 +++++ .../theater-layout.component.css | 4 ++ .../theater-layout.component.html | 13 +++-- .../theater-overlay.component.html | 16 +++--- .../theater-overlay.component.ts | 46 +++++++++-------- 22 files changed, 234 insertions(+), 39 deletions(-) create mode 100644 src/app/order/order.component.css create mode 100644 src/app/order/order.component.html create mode 100644 src/app/order/order.component.ts create mode 100644 src/app/performance-info/performance-info.component.css create mode 100644 src/app/performance-info/performance-info.component.html create mode 100644 src/app/performance-info/performance-info.component.ts create mode 100644 src/app/shopping-cart/shopping-cart.component.css create mode 100644 src/app/shopping-cart/shopping-cart.component.html create mode 100644 src/app/shopping-cart/shopping-cart.component.ts diff --git a/src/app/app-module.ts b/src/app/app-module.ts index 3df7d00..140cb49 100644 --- a/src/app/app-module.ts +++ b/src/app/app-module.ts @@ -49,6 +49,9 @@ import { MovieImporterComponent } from './movie-importer/movie-importer.componen import { MovieImportNoSearchResultComponent } from './movie-import-no-search-result/movie-import-no-search-result.component'; import { MovieImportSearchInfoComponent } from './movie-import-search-info/movie-import-search-info.component'; import { LoginDialog } from './login/login.dialog'; +import { PerformanceInfoComponent } from './performance-info/performance-info.component'; +import { ShoppingCartComponent } from './shopping-cart/shopping-cart.component'; +import { OrderComponent } from './order/order.component'; @NgModule({ @@ -81,6 +84,9 @@ import { LoginDialog } from './login/login.dialog'; MovieImportNoSearchResultComponent, MovieImportSearchInfoComponent, LoginDialog, + PerformanceInfoComponent, + ShoppingCartComponent, + OrderComponent, ], imports: [ AppRoutingModule, diff --git a/src/app/menu-header/menu-header.component.html b/src/app/menu-header/menu-header.component.html index 1941a29..beb2792 100644 --- a/src/app/menu-header/menu-header.component.html +++ b/src/app/menu-header/menu-header.component.html @@ -10,5 +10,13 @@ @if ( searchBar() ) { } + @if ( backToSchedule() ) { +
+ + arrow_back + Zurück zur Programmübersicht + +
+ } diff --git a/src/app/menu-header/menu-header.component.ts b/src/app/menu-header/menu-header.component.ts index 18155ff..4dd4ec2 100644 --- a/src/app/menu-header/menu-header.component.ts +++ b/src/app/menu-header/menu-header.component.ts @@ -12,4 +12,6 @@ export class MenuHeaderComponent { searchBar = input(false); movieSearchResult = output(); + + backToSchedule = input(false); } diff --git a/src/app/movie-category/movie-category.component.html b/src/app/movie-category/movie-category.component.html index 72757f5..3ff5e5c 100644 --- a/src/app/movie-category/movie-category.component.html +++ b/src/app/movie-category/movie-category.component.html @@ -1,3 +1,3 @@ - + {{ category() }} diff --git a/src/app/movie-duration/movie-duration.component.html b/src/app/movie-duration/movie-duration.component.html index 73845a3..b42ea62 100644 --- a/src/app/movie-duration/movie-duration.component.html +++ b/src/app/movie-duration/movie-duration.component.html @@ -1,4 +1,6 @@ - - + + @if (showIcon()) { + + } {{ durationText() }} diff --git a/src/app/movie-duration/movie-duration.component.ts b/src/app/movie-duration/movie-duration.component.ts index e029d3e..0bc94b6 100644 --- a/src/app/movie-duration/movie-duration.component.ts +++ b/src/app/movie-duration/movie-duration.component.ts @@ -8,9 +8,13 @@ import { Component, input, computed } from '@angular/core'; }) export class MovieDurationComponent { duration = input(0); + showIcon = input(true); durationText = computed(() => { if (this.duration() > 0) { + if (!this.showIcon()) { + return `${this.duration()} Minuten`; + } return `${this.duration()} Min.`; } return 'N/A'; diff --git a/src/app/movie-poster/movie-poster.component.html b/src/app/movie-poster/movie-poster.component.html index 1378eab..c038336 100644 --- a/src/app/movie-poster/movie-poster.component.html +++ b/src/app/movie-poster/movie-poster.component.html @@ -7,7 +7,7 @@ >
- - - + + +
diff --git a/src/app/movie-rating/movie-rating.component.html b/src/app/movie-rating/movie-rating.component.html index a8e4c76..9427cf9 100644 --- a/src/app/movie-rating/movie-rating.component.html +++ b/src/app/movie-rating/movie-rating.component.html @@ -1,3 +1,3 @@ - + {{ ratingText() }} diff --git a/src/app/movie-rating/movie-rating.component.ts b/src/app/movie-rating/movie-rating.component.ts index 60951ab..8dfb344 100644 --- a/src/app/movie-rating/movie-rating.component.ts +++ b/src/app/movie-rating/movie-rating.component.ts @@ -1,4 +1,4 @@ -import { Component, input, computed } from '@angular/core'; +import { Component, input, computed, HostBinding } from '@angular/core'; @Component({ selector: 'app-movie-rating', @@ -7,6 +7,11 @@ import { Component, input, computed } from '@angular/core'; styleUrl: './movie-rating.component.css' }) export class MovieRatingComponent { + + @HostBinding('class') get hostClasses(): string { + return this.ratingColor(); + } + rating = input(0); ratingColor = computed(() => { diff --git a/src/app/order/order.component.css b/src/app/order/order.component.css new file mode 100644 index 0000000..9008a3b --- /dev/null +++ b/src/app/order/order.component.css @@ -0,0 +1,3 @@ +mat-stepper { + background: transparent !important; +} diff --git a/src/app/order/order.component.html b/src/app/order/order.component.html new file mode 100644 index 0000000..3d94d59 --- /dev/null +++ b/src/app/order/order.component.html @@ -0,0 +1,49 @@ +
+ + +
+ Warenkorb + + @if (performance()) { + + } + +
+ +
+
+
+ + +
+ Anschrift + + Address + + +
+ + +
+
+
+- + + Zahlung +

You are now done.

+
+ + +
+
+
+
diff --git a/src/app/order/order.component.ts b/src/app/order/order.component.ts new file mode 100644 index 0000000..330041b --- /dev/null +++ b/src/app/order/order.component.ts @@ -0,0 +1,22 @@ +import { Vorstellung } from '@infinimotion/model-frontend'; +import { Component, inject, input } from '@angular/core'; +import { FormBuilder, Validators } from '@angular/forms'; + +@Component({ + selector: 'app-order', + standalone: false, + templateUrl: './order.component.html', + styleUrl: './order.component.css' +}) +export class OrderComponent { + performance = input(); + + private _formBuilder = inject(FormBuilder); + + firstFormGroup = this._formBuilder.group({ + firstCtrl: ['', Validators.required], + }); + secondFormGroup = this._formBuilder.group({ + secondCtrl: ['', Validators.required], + }); +} diff --git a/src/app/performance-info/performance-info.component.css b/src/app/performance-info/performance-info.component.css new file mode 100644 index 0000000..69e9094 --- /dev/null +++ b/src/app/performance-info/performance-info.component.css @@ -0,0 +1,14 @@ +.info-box { + color: var(--mat-sys-on-surface); +} + +::ng-deep .mat-step-header .mat-step-icon { + background-color: #ccc; + color: white; +} + + +::ng-deep .mat-step-header .mat-step-icon-selected, +::ng-deep .mat-step-icon-state-edit { + background-color: var(--color-indigo-500) !important; +} diff --git a/src/app/performance-info/performance-info.component.html b/src/app/performance-info/performance-info.component.html new file mode 100644 index 0000000..4ed4613 --- /dev/null +++ b/src/app/performance-info/performance-info.component.html @@ -0,0 +1,21 @@ +
+ +
+ Movie Poster +
+ +
+

{{ getStartTimeString() }} • {{ performance().hall.name }}

+

{{ movie().title }}

+
+ + +
+
+ +
diff --git a/src/app/performance-info/performance-info.component.ts b/src/app/performance-info/performance-info.component.ts new file mode 100644 index 0000000..bc35f8b --- /dev/null +++ b/src/app/performance-info/performance-info.component.ts @@ -0,0 +1,30 @@ +import { Component, computed, input } from '@angular/core'; +import { Vorstellung } from '@infinimotion/model-frontend'; + +@Component({ + selector: 'app-performance-info', + standalone: false, + templateUrl: './performance-info.component.html', + styleUrl: './performance-info.component.css' +}) +export class PerformanceInfoComponent { + performance = input.required(); + + getStartTimeString(): string { + const date = new Date(this.performance().start); + return date.toLocaleDateString('de-DE', { weekday: 'short' }) + '. ' + date.toLocaleDateString('de-DE') + ', ' + date.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }) + ' Uhr'; + } + + movie() { + return this.performance().movie + } + + onPosterError(event: Event) { + const img = event.target as HTMLImageElement; + const placeholder = 'assets/poster_placeholder.png'; + + if (img.src !== window.location.origin + placeholder) { + img.src = placeholder; + } + } +} diff --git a/src/app/shopping-cart/shopping-cart.component.css b/src/app/shopping-cart/shopping-cart.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/shopping-cart/shopping-cart.component.html b/src/app/shopping-cart/shopping-cart.component.html new file mode 100644 index 0000000..5aff33e --- /dev/null +++ b/src/app/shopping-cart/shopping-cart.component.html @@ -0,0 +1 @@ +

shopping-cart works!

diff --git a/src/app/shopping-cart/shopping-cart.component.ts b/src/app/shopping-cart/shopping-cart.component.ts new file mode 100644 index 0000000..382aba1 --- /dev/null +++ b/src/app/shopping-cart/shopping-cart.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-shopping-cart', + standalone: false, + templateUrl: './shopping-cart.component.html', + styleUrl: './shopping-cart.component.css' +}) +export class ShoppingCartComponent { + +} diff --git a/src/app/theater-layout/theater-layout.component.css b/src/app/theater-layout/theater-layout.component.css index e69de29..055c8c8 100644 --- a/src/app/theater-layout/theater-layout.component.css +++ b/src/app/theater-layout/theater-layout.component.css @@ -0,0 +1,4 @@ +:host { +border-radius: 0.5rem; + background-color: white; +} diff --git a/src/app/theater-layout/theater-layout.component.html b/src/app/theater-layout/theater-layout.component.html index a864e27..cd0560d 100644 --- a/src/app/theater-layout/theater-layout.component.html +++ b/src/app/theater-layout/theater-layout.component.html @@ -1,3 +1,10 @@ -@for (row of seatsPerRow(); track $index) { - -} +
+

+ Leinwand +

+
+
+ @for (row of seatsPerRow(); track $index) { + + } +
diff --git a/src/app/theater-overlay/theater-overlay.component.html b/src/app/theater-overlay/theater-overlay.component.html index bb9e257..d709d18 100644 --- a/src/app/theater-overlay/theater-overlay.component.html +++ b/src/app/theater-overlay/theater-overlay.component.html @@ -1,9 +1,9 @@ -
-

- Leinwand -

-
-
- -
+ +
+ + + + + +
diff --git a/src/app/theater-overlay/theater-overlay.component.ts b/src/app/theater-overlay/theater-overlay.component.ts index a87cb30..c8a0e35 100644 --- a/src/app/theater-overlay/theater-overlay.component.ts +++ b/src/app/theater-overlay/theater-overlay.component.ts @@ -1,8 +1,8 @@ import {Component, inject, OnInit} from '@angular/core'; import {HttpService} from '../http.service'; import {LoadingService} from '../loading.service'; -import {catchError, of, tap} from 'rxjs'; -import {Sitzplatz} from '@infinimotion/model-frontend'; +import {catchError, forkJoin, of, tap} from 'rxjs'; +import {Sitzplatz, Vorstellung} from '@infinimotion/model-frontend'; import {TheaterSeatState} from '../model/theater-seat-state.model'; import {ActivatedRoute} from '@angular/router'; @@ -18,28 +18,34 @@ export class TheaterOverlayComponent implements OnInit { showId!: number; seatsPerRow: { seat: Sitzplatz, state: TheaterSeatState }[][] = [] + performance: Vorstellung | undefined; constructor(private route: ActivatedRoute) {} - ngOnInit() { - this.showId = Number(this.route.snapshot.paramMap.get('id')!); - this.loadShowSeats(); - } +ngOnInit() { + this.showId = Number(this.route.snapshot.paramMap.get('id')!); + this.loadPerformanceAndSeats(); +} - loadShowSeats() { - this.loading.show(); - this.http.getSeatsByShowId(this.showId).pipe( - tap((data) => { - this.seatsPerRow = this.converter(data) - this.loading.hide(); - }), - catchError(err => { - this.loading.showError(err); - console.error('Fehler beim Laden der Vorstellung', err); - return of([]); - }) - ).subscribe(); - } +loadPerformanceAndSeats() { + this.loading.show(); + + forkJoin({ + performance: this.http.getPerformaceById(this.showId), + seats: this.http.getSeatsByShowId(this.showId) + }).pipe( + tap(({ performance, seats }) => { + this.performance = performance; + this.seatsPerRow = this.converter(seats); + this.loading.hide(); + }), + catchError(err => { + this.loading.showError(err); + console.error('Fehler beim Laden', err); + return of({ performance: null, seats: [] }); + }) + ).subscribe(); +} converter(resp: { seats: Sitzplatz[], reserved: Sitzplatz[], booked: Sitzplatz[] }): { seat: Sitzplatz, From 8e4a024299afc5afaf4ca18eb3d4a2b8ed5d6f08 Mon Sep 17 00:00:00 2001 From: Piet Ostendorp Date: Wed, 12 Nov 2025 23:21:51 +0100 Subject: [PATCH 2/7] Get custom theme colors working Introduces _theme-colors.scss with generated palettes and integrates them into custom-theme.scss for both light and dark themes. Removes stepper icon styles from performance-info.component.css and moves them to custom-theme.scss for better maintainability and consistency. --- src/_theme-colors.scss | 137 ++++++++++++++++++ .../performance-info.component.css | 11 -- src/custom-theme.scss | 40 +++-- 3 files changed, 167 insertions(+), 21 deletions(-) create mode 100644 src/_theme-colors.scss diff --git a/src/_theme-colors.scss b/src/_theme-colors.scss new file mode 100644 index 0000000..5c6e044 --- /dev/null +++ b/src/_theme-colors.scss @@ -0,0 +1,137 @@ +// This file was generated by running 'ng generate @angular/material:theme-color'. +// Proceed with caution if making changes to this file. + +@use 'sass:map'; +@use '@angular/material' as mat; + +// Note: Color palettes are generated from primary: 6366f1, tertiary: dd2979 +$_palettes: ( + primary: ( + 0: #000000, + 10: #07006c, + 20: #1000a9, + 25: #201cb4, + 30: #2f2ebe, + 35: #3c3dca, + 40: #494bd6, + 50: #6366f1, + 60: #8083ff, + 70: #a0a3ff, + 80: #c0c1ff, + 90: #e1e0ff, + 95: #f2efff, + 98: #fcf8ff, + 99: #fffbff, + 100: #ffffff, + ), + secondary: ( + 0: #000000, + 10: #13144a, + 20: #292a60, + 25: #34366c, + 30: #404178, + 35: #4b4d85, + 40: #575992, + 50: #7072ac, + 60: #8a8bc8, + 70: #a5a6e4, + 80: #c0c1ff, + 90: #e1e0ff, + 95: #f2efff, + 98: #fcf8ff, + 99: #fffbff, + 100: #ffffff, + ), + tertiary: ( + 0: #000000, + 10: #3f001c, + 20: #650031, + 25: #79003d, + 30: #8e0048, + 35: #a40054, + 40: #ba0060, + 50: #dd2979, + 60: #ff4993, + 70: #ff84ad, + 80: #ffb1c7, + 90: #ffd9e2, + 95: #ffecef, + 98: #fff8f8, + 99: #fffbff, + 100: #ffffff, + ), + neutral: ( + 0: #000000, + 10: #1b1b23, + 20: #303038, + 25: #3b3a44, + 30: #46464f, + 35: #52515b, + 40: #5e5d67, + 50: #777680, + 60: #918f9a, + 70: #acaab5, + 80: #c7c5d1, + 90: #e4e1ed, + 95: #f2effb, + 98: #fcf8ff, + 99: #fffbff, + 100: #ffffff, + 4: #0d0d15, + 6: #13131b, + 12: #1f1f27, + 17: #292932, + 22: #34343d, + 24: #393841, + 87: #dbd8e4, + 92: #e9e6f3, + 94: #efecf8, + 96: #f5f2fe, + ), + neutral-variant: ( + 0: #000000, + 10: #1a1a28, + 20: #2f2f3d, + 25: #3a3a49, + 30: #464554, + 35: #515160, + 40: #5d5d6d, + 50: #767586, + 60: #908fa0, + 70: #aba9bb, + 80: #c7c4d7, + 90: #e3e0f3, + 95: #f2efff, + 98: #fcf8ff, + 99: #fffbff, + 100: #ffffff, + ), + error: ( + 0: #000000, + 10: #410002, + 20: #690005, + 25: #7e0007, + 30: #93000a, + 35: #a80710, + 40: #ba1a1a, + 50: #de3730, + 60: #ff5449, + 70: #ff897d, + 80: #ffb4ab, + 90: #ffdad6, + 95: #ffedea, + 98: #fff8f7, + 99: #fffbff, + 100: #ffffff, + ), +); + +$_rest: ( + secondary: map.get($_palettes, secondary), + neutral: map.get($_palettes, neutral), + neutral-variant: map.get($_palettes, neutral-variant), + error: map.get($_palettes, error), +); + +$primary-palette: map.merge(map.get($_palettes, primary), $_rest); +$tertiary-palette: map.merge(map.get($_palettes, tertiary), $_rest); \ No newline at end of file diff --git a/src/app/performance-info/performance-info.component.css b/src/app/performance-info/performance-info.component.css index 69e9094..28d36f0 100644 --- a/src/app/performance-info/performance-info.component.css +++ b/src/app/performance-info/performance-info.component.css @@ -1,14 +1,3 @@ .info-box { color: var(--mat-sys-on-surface); } - -::ng-deep .mat-step-header .mat-step-icon { - background-color: #ccc; - color: white; -} - - -::ng-deep .mat-step-header .mat-step-icon-selected, -::ng-deep .mat-step-icon-state-edit { - background-color: var(--color-indigo-500) !important; -} diff --git a/src/custom-theme.scss b/src/custom-theme.scss index 5615bb5..3de7b38 100644 --- a/src/custom-theme.scss +++ b/src/custom-theme.scss @@ -5,6 +5,33 @@ // Learn more about theming and how to use it for your application's // custom components at https://material.angular.dev/guide/theming @use '@angular/material' as mat; +@use './_theme-colors' as theme-colors; + +// Light Theme +html { + color-scheme: light; + @include mat.theme(( + color: ( + theme-type: light, + primary: theme-colors.$primary-palette, + tertiary: theme-colors.$tertiary-palette, + ), + )); +} + +// Dark Theme +html.dark { + color-scheme: dark; + @include mat.theme(( + color: ( + theme-type: dark, + primary: theme-colors.$primary-palette, + tertiary: theme-colors.$tertiary-palette, + ), + )); +} + + @include mat.progress-bar-overrides(( active-indicator-color: white, @@ -34,16 +61,9 @@ backdrop-filter: blur(2px); } - -html { - @include mat.theme(( - color: ( - primary: mat.$azure-palette, - tertiary: mat.$blue-palette, - ), - typography: Roboto, - density: 0, - )); +.mat-step-header .mat-step-icon:not(.mat-step-icon-selected):not(.mat-step-icon-completed) { + background-color: #ccc; + color: white; } body { From db0322d44337204c98154451d6ce36fe6c9323de Mon Sep 17 00:00:00 2001 From: Piet Ostendorp Date: Thu, 13 Nov 2025 01:48:28 +0100 Subject: [PATCH 3/7] Add seat selection and no seats components Introduces SeatSelectionComponent and NoSeatsInHallComponent for improved seat category display and handling cases with no available seats. Updates order flow to show seat categories, loading spinner, and total price. Refactors TheaterOverlayComponent to provide seat categories, and updates styles and dependencies. --- package-lock.json | 8 ++-- package.json | 2 +- src/app/app-module.ts | 6 +++ .../no-seats-in-hall.component.css | 0 .../no-seats-in-hall.component.html | 7 +++ .../no-seats-in-hall.component.ts | 11 +++++ src/app/order/order.component.css | 4 ++ src/app/order/order.component.html | 47 +++++++++++++++++-- src/app/order/order.component.ts | 5 +- .../performance-info.component.html | 4 +- .../seat-selection.component.css | 3 ++ .../seat-selection.component.html | 11 +++++ .../seat-selection.component.ts | 18 +++++++ .../theater-overlay.component.html | 2 +- .../theater-overlay.component.ts | 18 ++++++- src/index.html | 1 + 16 files changed, 131 insertions(+), 16 deletions(-) create mode 100644 src/app/no-seats-in-hall/no-seats-in-hall.component.css create mode 100644 src/app/no-seats-in-hall/no-seats-in-hall.component.html create mode 100644 src/app/no-seats-in-hall/no-seats-in-hall.component.ts create mode 100644 src/app/seat-selection/seat-selection.component.css create mode 100644 src/app/seat-selection/seat-selection.component.html create mode 100644 src/app/seat-selection/seat-selection.component.ts diff --git a/package-lock.json b/package-lock.json index ea5c339..0002880 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@angular/material": "^20.2.9", "@angular/platform-browser": "^20.3.0", "@angular/router": "^20.3.0", - "@infinimotion/model-frontend": "^0.0.85", + "@infinimotion/model-frontend": "^0.0.89", "@tailwindcss/postcss": "^4.1.14", "postcss": "^8.5.6", "rxjs": "~7.8.0", @@ -1298,9 +1298,9 @@ } }, "node_modules/@infinimotion/model-frontend": { - "version": "0.0.85", - "resolved": "https://git.infinimotion.de/api/packages/infinimotion/npm/%40infinimotion%2Fmodel-frontend/-/0.0.85/model-frontend-0.0.85.tgz", - "integrity": "sha512-QPiZNl//Y1JdxtXk+VScc67h1K664z68PUCXRff9fRf4IHlYXtqutc+ainK8vxOVSqqL6EEmDAtbLsRwrG6kRg==", + "version": "0.0.89", + "resolved": "https://git.infinimotion.de/api/packages/infinimotion/npm/%40infinimotion%2Fmodel-frontend/-/0.0.89/model-frontend-0.0.89.tgz", + "integrity": "sha512-lvvQy8RWs41Bz52uBgsUKkwn8teGlgxlmG8Rvsgkh+v1IMVWFWVQmfMS7Rznd0lCZRgK1ByihH80X9eAN12idA==", "license": "ISC" }, "node_modules/@inquirer/ansi": { diff --git a/package.json b/package.json index 5027aa3..a8410ad 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@angular/material": "^20.2.9", "@angular/platform-browser": "^20.3.0", "@angular/router": "^20.3.0", - "@infinimotion/model-frontend": "^0.0.85", + "@infinimotion/model-frontend": "^0.0.89", "@tailwindcss/postcss": "^4.1.14", "postcss": "^8.5.6", "rxjs": "~7.8.0", diff --git a/src/app/app-module.ts b/src/app/app-module.ts index 140cb49..7bdff2c 100644 --- a/src/app/app-module.ts +++ b/src/app/app-module.ts @@ -12,6 +12,7 @@ import { MatIconModule } from '@angular/material/icon'; import { MatTabsModule } from '@angular/material/tabs'; import { MatToolbarModule } from '@angular/material/toolbar'; import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatInputModule } from '@angular/material/input'; @@ -52,6 +53,8 @@ import { LoginDialog } from './login/login.dialog'; import { PerformanceInfoComponent } from './performance-info/performance-info.component'; import { ShoppingCartComponent } from './shopping-cart/shopping-cart.component'; import { OrderComponent } from './order/order.component'; +import { SeatSelectionComponent } from './seat-selection/seat-selection.component'; +import { NoSeatsInHallComponent } from './no-seats-in-hall/no-seats-in-hall.component'; @NgModule({ @@ -87,6 +90,8 @@ import { OrderComponent } from './order/order.component'; PerformanceInfoComponent, ShoppingCartComponent, OrderComponent, + SeatSelectionComponent, + NoSeatsInHallComponent, ], imports: [ AppRoutingModule, @@ -98,6 +103,7 @@ import { OrderComponent } from './order/order.component'; MatTabsModule, MatToolbarModule, MatProgressBarModule, + MatProgressSpinnerModule, MatSnackBarModule, MatAutocompleteModule, MatInputModule, diff --git a/src/app/no-seats-in-hall/no-seats-in-hall.component.css b/src/app/no-seats-in-hall/no-seats-in-hall.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/no-seats-in-hall/no-seats-in-hall.component.html b/src/app/no-seats-in-hall/no-seats-in-hall.component.html new file mode 100644 index 0000000..08cea86 --- /dev/null +++ b/src/app/no-seats-in-hall/no-seats-in-hall.component.html @@ -0,0 +1,7 @@ +
+ + brightness_alert + +

Huch?! Keine Sitzplätze?

+

Hast du ein Glück, Stehplätze sind kostenlos.

+
diff --git a/src/app/no-seats-in-hall/no-seats-in-hall.component.ts b/src/app/no-seats-in-hall/no-seats-in-hall.component.ts new file mode 100644 index 0000000..bf6ce88 --- /dev/null +++ b/src/app/no-seats-in-hall/no-seats-in-hall.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-no-seats-in-hall', + standalone: false, + templateUrl: './no-seats-in-hall.component.html', + styleUrl: './no-seats-in-hall.component.css' +}) +export class NoSeatsInHallComponent { + +} diff --git a/src/app/order/order.component.css b/src/app/order/order.component.css index 9008a3b..b19ef5b 100644 --- a/src/app/order/order.component.css +++ b/src/app/order/order.component.css @@ -1,3 +1,7 @@ mat-stepper { background: transparent !important; } + +::ng-deep .mat-horizontal-stepper-header{ + pointer-events: none !important; +} diff --git a/src/app/order/order.component.html b/src/app/order/order.component.html index 3d94d59..4f31b79 100644 --- a/src/app/order/order.component.html +++ b/src/app/order/order.component.html @@ -1,4 +1,15 @@
+ +@if (loadingService.loading$ | async){ +
+ +
+} +@else if (performance()) { + Warenkorb - @if (performance()) { - - } + -
- +
+ + @for (seatCategory of seatCategories(); track $index) { +
+ + } + @empty { + + } + +
+ + + + +
+

+ Tickets gesamt: +

+

+ 75,00 € +

+
+ +
+ +
@@ -46,4 +82,5 @@
+}
diff --git a/src/app/order/order.component.ts b/src/app/order/order.component.ts index 330041b..3f3b094 100644 --- a/src/app/order/order.component.ts +++ b/src/app/order/order.component.ts @@ -1,4 +1,5 @@ -import { Vorstellung } from '@infinimotion/model-frontend'; +import { LoadingService } from './../loading.service'; +import { Sitzkategorie, Vorstellung } from '@infinimotion/model-frontend'; import { Component, inject, input } from '@angular/core'; import { FormBuilder, Validators } from '@angular/forms'; @@ -10,8 +11,10 @@ import { FormBuilder, Validators } from '@angular/forms'; }) export class OrderComponent { performance = input(); + seatCategories = input.required(); private _formBuilder = inject(FormBuilder); + loadingService = inject(LoadingService); firstFormGroup = this._formBuilder.group({ firstCtrl: ['', Validators.required], diff --git a/src/app/performance-info/performance-info.component.html b/src/app/performance-info/performance-info.component.html index 4ed4613..06435e9 100644 --- a/src/app/performance-info/performance-info.component.html +++ b/src/app/performance-info/performance-info.component.html @@ -11,9 +11,9 @@

{{ getStartTimeString() }} • {{ performance().hall.name }}

-

{{ movie().title }}

+

{{ movie().title }}

- +
diff --git a/src/app/seat-selection/seat-selection.component.css b/src/app/seat-selection/seat-selection.component.css new file mode 100644 index 0000000..c206bd7 --- /dev/null +++ b/src/app/seat-selection/seat-selection.component.css @@ -0,0 +1,3 @@ +.seat-name { + color: var(--mat-sys-primary); +} diff --git a/src/app/seat-selection/seat-selection.component.html b/src/app/seat-selection/seat-selection.component.html new file mode 100644 index 0000000..853700b --- /dev/null +++ b/src/app/seat-selection/seat-selection.component.html @@ -0,0 +1,11 @@ +

{{ seatCategory().name }}

+
+
+ + {{ seatCategory().icon }} + +

{{ getPriceDisplay(seatCategory().price) }}

+
+

× 2

+

25.00 €

+
diff --git a/src/app/seat-selection/seat-selection.component.ts b/src/app/seat-selection/seat-selection.component.ts new file mode 100644 index 0000000..e9f0f8d --- /dev/null +++ b/src/app/seat-selection/seat-selection.component.ts @@ -0,0 +1,18 @@ +import { Component, input } from '@angular/core'; +import { Sitzkategorie } from '@infinimotion/model-frontend'; + +@Component({ + selector: 'app-seat-selection', + standalone: false, + templateUrl: './seat-selection.component.html', + styleUrl: './seat-selection.component.css' +}) +export class SeatSelectionComponent { + seatCategory = input.required(); + amount: number = 1; + + getPriceDisplay(price: number): string { + return `${(price / 100).toFixed(2)} €`; + } +} + diff --git a/src/app/theater-overlay/theater-overlay.component.html b/src/app/theater-overlay/theater-overlay.component.html index d709d18..c4c736e 100644 --- a/src/app/theater-overlay/theater-overlay.component.html +++ b/src/app/theater-overlay/theater-overlay.component.html @@ -4,6 +4,6 @@ - + diff --git a/src/app/theater-overlay/theater-overlay.component.ts b/src/app/theater-overlay/theater-overlay.component.ts index c8a0e35..0052b0a 100644 --- a/src/app/theater-overlay/theater-overlay.component.ts +++ b/src/app/theater-overlay/theater-overlay.component.ts @@ -2,7 +2,7 @@ import {Component, inject, OnInit} from '@angular/core'; import {HttpService} from '../http.service'; import {LoadingService} from '../loading.service'; import {catchError, forkJoin, of, tap} from 'rxjs'; -import {Sitzplatz, Vorstellung} from '@infinimotion/model-frontend'; +import {Sitzkategorie, Sitzplatz, Vorstellung} from '@infinimotion/model-frontend'; import {TheaterSeatState} from '../model/theater-seat-state.model'; import {ActivatedRoute} from '@angular/router'; @@ -19,6 +19,7 @@ export class TheaterOverlayComponent implements OnInit { showId!: number; seatsPerRow: { seat: Sitzplatz, state: TheaterSeatState }[][] = [] performance: Vorstellung | undefined; + seatCategories: Sitzkategorie[] = []; constructor(private route: ActivatedRoute) {} @@ -52,13 +53,26 @@ loadPerformanceAndSeats() { state: TheaterSeatState }[][] { let rows: { seat: Sitzplatz, state: TheaterSeatState }[][] = []; + const categoryMap = new Map(); + resp.seats.forEach(seat => { if (!rows[seat.row.position]) { rows[seat.row.position] = []; } - let state = resp.booked.find(other => other.id == seat.id) ? TheaterSeatState.BOOKED : resp.reserved.find(other => other.id == seat.id) ? TheaterSeatState.RESERVED : TheaterSeatState.AVAILABLE; + + let state = resp.booked.find(other => other.id == seat.id) ? TheaterSeatState.BOOKED + : resp.reserved.find(other => other.id == seat.id) ? TheaterSeatState.RESERVED + : TheaterSeatState.AVAILABLE; + rows[seat.row.position].push({seat: seat, state: state}); + + if (seat.row.category && !categoryMap.has(seat.row.category.id)) { + categoryMap.set(seat.row.category.id, seat.row.category); + } }); + + this.seatCategories = Array.from(categoryMap.values()).sort((a, b) => a.id - b.id); + rows = rows.filter(row => row.length > 0).sort((a, b) => a[0].seat.row.position - b[0].seat.row.position); rows.forEach(row => row.sort((a, b) => a.seat.position - b.seat.position)); return rows; diff --git a/src/index.html b/src/index.html index da193c0..2df19e0 100644 --- a/src/index.html +++ b/src/index.html @@ -8,6 +8,7 @@ + From 9c9e9becfb04561b857ec7e3d0355f4fa6268151 Mon Sep 17 00:00:00 2001 From: Piet Ostendorp Date: Thu, 13 Nov 2025 02:10:30 +0100 Subject: [PATCH 4/7] Refactor SelectedSeatsService to use signals Replaces internal array state with Angular signals for selected seats. Updates methods to use signal API and adds a new method to filter selected seats by category. --- src/app/selected-seats.service.ts | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/app/selected-seats.service.ts b/src/app/selected-seats.service.ts index 38fdf1d..5a30cc1 100644 --- a/src/app/selected-seats.service.ts +++ b/src/app/selected-seats.service.ts @@ -1,30 +1,31 @@ -import {Injectable} from '@angular/core'; +import { Injectable, signal } from '@angular/core'; import {Sitzplatz} from '@infinimotion/model-frontend'; @Injectable({ providedIn: 'root', }) export class SelectedSeatsService { - private selectedSeatsList: Sitzplatz[] = []; + private selectedSeatsSignal = signal([]); + + get selectedSeats() { + return this.selectedSeatsSignal; + } pushSelectedSeat(selectedSeat: Sitzplatz): void { - this.selectedSeatsList.push(selectedSeat); - //console.log("Added" + selectedSeat); - //console.log(this.selectedSeatsList); + this.selectedSeatsSignal.update(seats => [...seats, selectedSeat]); } removeSelectedSeat(selectedSeat: Sitzplatz): void { - let removeId = this.selectedSeatsList.indexOf(selectedSeat); - - if(removeId !== -1) { - this.selectedSeatsList.splice(removeId, 1) - } - //console.log("Removed" + selectedSeat) - //console.log(this.selectedSeatsList); + this.selectedSeatsSignal.update(seats => + seats.filter(seat => seat.id !== selectedSeat.id) + ); } getSelectedSeatsList(): Sitzplatz[] { - return this.selectedSeatsList; + return this.selectedSeatsSignal(); } + getSelectedSeatsByCategory(categoryId: number): Sitzplatz[] { + return this.selectedSeatsSignal().filter(seat => seat.row.category.id === categoryId); + } } From 31108859dae299372d031b6b13e6da4c9c1fe1d3 Mon Sep 17 00:00:00 2001 From: Piet Ostendorp Date: Thu, 13 Nov 2025 02:14:40 +0100 Subject: [PATCH 5/7] Display dynamic seat count and total price per category Updated seat-selection component to show the actual number of selected seats and calculate the total price per seat category. Integrated SelectedSeatsService and replaced hardcoded values with computed properties for better accuracy and maintainability. --- src/app/order/order.component.html | 4 +--- .../seat-selection/seat-selection.component.html | 4 ++-- .../seat-selection/seat-selection.component.ts | 15 ++++++++++++--- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/app/order/order.component.html b/src/app/order/order.component.html index 4f31b79..951bfdc 100644 --- a/src/app/order/order.component.html +++ b/src/app/order/order.component.html @@ -25,9 +25,7 @@ @for (seatCategory of seatCategories(); track $index) {
- + } @empty { diff --git a/src/app/seat-selection/seat-selection.component.html b/src/app/seat-selection/seat-selection.component.html index 853700b..0531616 100644 --- a/src/app/seat-selection/seat-selection.component.html +++ b/src/app/seat-selection/seat-selection.component.html @@ -6,6 +6,6 @@

{{ getPriceDisplay(seatCategory().price) }}

-

× 2

-

25.00 €

+

× {{selectedSeatsByCategory()}}

+

{{ getPriceDisplay(totalPrice())}}

diff --git a/src/app/seat-selection/seat-selection.component.ts b/src/app/seat-selection/seat-selection.component.ts index e9f0f8d..07d4307 100644 --- a/src/app/seat-selection/seat-selection.component.ts +++ b/src/app/seat-selection/seat-selection.component.ts @@ -1,4 +1,5 @@ -import { Component, input } from '@angular/core'; +import { SelectedSeatsService } from './../selected-seats.service'; +import { Component, computed, inject, input } from '@angular/core'; import { Sitzkategorie } from '@infinimotion/model-frontend'; @Component({ @@ -9,10 +10,18 @@ import { Sitzkategorie } from '@infinimotion/model-frontend'; }) export class SeatSelectionComponent { seatCategory = input.required(); - amount: number = 1; + + SelectedSeatsService = inject(SelectedSeatsService); + + selectedSeatsByCategory = computed(() => + this.SelectedSeatsService.getSelectedSeatsByCategory(this.seatCategory().id).length + ); + + totalPrice = computed(() => + this.selectedSeatsByCategory() * this.seatCategory().price + ); getPriceDisplay(price: number): string { return `${(price / 100).toFixed(2)} €`; } } - From cb065c8e39470ad79eebacc6fc34e87a82f1d36e Mon Sep 17 00:00:00 2001 From: Piet Ostendorp Date: Thu, 13 Nov 2025 09:03:32 +0100 Subject: [PATCH 6/7] Make total ticket price dynamic Refactored price display logic to use computed total price and formatted output in order and seat selection components. Disabled reservation and booking buttons when no seats are selected. Improved clarity by renaming totalPrice to totalCategoryPrice in seat selection. --- src/app/order/order.component.html | 6 +++--- src/app/order/order.component.ts | 12 +++++++++++- src/app/seat-selection/seat-selection.component.html | 4 ++-- src/app/seat-selection/seat-selection.component.ts | 2 +- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/app/order/order.component.html b/src/app/order/order.component.html index 951bfdc..4ee64b6 100644 --- a/src/app/order/order.component.html +++ b/src/app/order/order.component.html @@ -41,13 +41,13 @@ Tickets gesamt:

- 75,00 € + {{ getPriceDisplay(totalPrice()) }}

- - + +
diff --git a/src/app/order/order.component.ts b/src/app/order/order.component.ts index 3f3b094..5e66746 100644 --- a/src/app/order/order.component.ts +++ b/src/app/order/order.component.ts @@ -1,6 +1,7 @@ +import { SelectedSeatsService } from './../selected-seats.service'; import { LoadingService } from './../loading.service'; import { Sitzkategorie, Vorstellung } from '@infinimotion/model-frontend'; -import { Component, inject, input } from '@angular/core'; +import { Component, computed, inject, input } from '@angular/core'; import { FormBuilder, Validators } from '@angular/forms'; @Component({ @@ -15,6 +16,7 @@ export class OrderComponent { private _formBuilder = inject(FormBuilder); loadingService = inject(LoadingService); + private selectedSeatsService = inject(SelectedSeatsService); firstFormGroup = this._formBuilder.group({ firstCtrl: ['', Validators.required], @@ -22,4 +24,12 @@ export class OrderComponent { secondFormGroup = this._formBuilder.group({ secondCtrl: ['', Validators.required], }); + + totalPrice = computed(() => + this.selectedSeatsService.getSelectedSeatsList().reduce((sum, seat) => sum + seat.row.category.price, 0) + ); + + getPriceDisplay(price: number): string { + return `${(price / 100).toFixed(2)} €`; + } } diff --git a/src/app/seat-selection/seat-selection.component.html b/src/app/seat-selection/seat-selection.component.html index 0531616..53562eb 100644 --- a/src/app/seat-selection/seat-selection.component.html +++ b/src/app/seat-selection/seat-selection.component.html @@ -6,6 +6,6 @@

{{ getPriceDisplay(seatCategory().price) }}

-

× {{selectedSeatsByCategory()}}

-

{{ getPriceDisplay(totalPrice())}}

+

× {{ selectedSeatsByCategory() }}

+

{{ getPriceDisplay(totalCategoryPrice()) }}

diff --git a/src/app/seat-selection/seat-selection.component.ts b/src/app/seat-selection/seat-selection.component.ts index 07d4307..9c5ca14 100644 --- a/src/app/seat-selection/seat-selection.component.ts +++ b/src/app/seat-selection/seat-selection.component.ts @@ -17,7 +17,7 @@ export class SeatSelectionComponent { this.SelectedSeatsService.getSelectedSeatsByCategory(this.seatCategory().id).length ); - totalPrice = computed(() => + totalCategoryPrice = computed(() => this.selectedSeatsByCategory() * this.seatCategory().price ); From 41c9d85e9bd798062cd4a279e8791081e61f656b Mon Sep 17 00:00:00 2001 From: Piet Ostendorp Date: Thu, 13 Nov 2025 22:36:09 +0100 Subject: [PATCH 7/7] Add payment form and improve order stepper UI Introduces a payment step with card input masking using ngx-mask, refactors the order stepper to include address and payment forms with validation, and enhances UI/UX with new styles and layout adjustments. Also updates dependencies and module imports to support ngx-mask and Material Checkbox. --- package-lock.json | 43 ++++- package.json | 1 + src/app/app-module.ts | 16 +- src/app/order/order.component.css | 12 ++ src/app/order/order.component.html | 163 +++++++++++++----- src/app/order/order.component.ts | 53 +++++- .../performance-info.component.html | 2 +- .../performance-info.component.ts | 2 +- src/custom-theme.scss | 4 +- 9 files changed, 231 insertions(+), 65 deletions(-) diff --git a/package-lock.json b/package-lock.json index 98caa25..0000198 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@angular/router": "^20.3.0", "@infinimotion/model-frontend": "^0.0.89", "@tailwindcss/postcss": "^4.1.14", + "ngx-mask": "^20.0.3", "postcss": "^8.5.6", "rxjs": "~7.8.0", "tailwindcss": "^4.1.14", @@ -440,6 +441,7 @@ "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.2.12.tgz", "integrity": "sha512-hz8GtiMy3N9/e8407ZfrByHD5GEC4SkWtxyUknWuTM9P88AOie0jDZ6CfQg9gQ0OJX+6BAbJV3RpYZA1uzNUqA==", "license": "MIT", + "peer": true, "dependencies": { "parse5": "^8.0.0", "tslib": "^2.3.0" @@ -490,6 +492,7 @@ "resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.10.tgz", "integrity": "sha512-12fEzvKbEqjqy1fSk9DMYlJz6dF1MJVXuC5BB+oWWJpd+2lfh4xJ62pkvvLGAICI89hfM5n9Cy5kWnXwnqPZsA==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -506,6 +509,7 @@ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.10.tgz", "integrity": "sha512-cW939Lr8GZjPSYfbQKIDNrUaHWmn2M+zBbERThfq5skLuY+xM60bJFv4NqBekfX6YqKLCY62ilUZlnImYIXaqA==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -519,6 +523,7 @@ "integrity": "sha512-9BemvpFxA26yIVdu8ROffadMkEdlk/AQQ2Jb486w7RPkrvUQ0pbEJukhv9aryJvhbMopT66S5H/j4ipOUMzmzQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "7.28.3", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -551,6 +556,7 @@ "resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.10.tgz", "integrity": "sha512-g99Qe+NOVo72OLxowVF9NjCckswWYHmvO7MgeiZTDJbTjF9tXH96dMx7AWq76/GUinV10sNzDysVW16NoAbCRQ==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -576,6 +582,7 @@ "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.10.tgz", "integrity": "sha512-9yWr51EUauTEINB745AaHwZNTHLpXIm4uxuykxzOg+g2QskEgVfH26uS8G2ogdNuwYpB8wnsXWr34qhM3qgOWw==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -611,6 +618,7 @@ "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.10.tgz", "integrity": "sha512-UV8CGoB5P3FmJciI3/I/n3L7C3NVgGh7bIlZ1BaB/qJDtv0Wq0rRAGwmT/Z3gwmrRtfHZWme7/CeQ2CYJmMyUQ==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -677,6 +685,7 @@ "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -1633,6 +1642,7 @@ "integrity": "sha512-nqhDw2ZcAUrKNPwhjinJny903bRhI0rQhiDz1LksjeRxqa36i3l75+4iXbOy0rlDpLJGxqtgoPavQjmmyS5UJw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@inquirer/checkbox": "^4.2.1", "@inquirer/confirm": "^5.1.14", @@ -3865,6 +3875,7 @@ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -4198,6 +4209,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -5294,6 +5306,7 @@ "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -6245,7 +6258,8 @@ "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.9.0.tgz", "integrity": "sha512-OMUvF1iI6+gSRYOhMrH4QYothVLN9C3EJ6wm4g7zLJlnaTl8zbaPOr0bTw70l7QxkoM7sVFOWo83u9B2Fe2Zng==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/jiti": { "version": "2.6.1", @@ -6339,6 +6353,7 @@ "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@colors/colors": "1.5.0", "body-parser": "^1.19.0", @@ -7055,6 +7070,7 @@ "integrity": "sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", @@ -7693,6 +7709,20 @@ "node": ">= 0.6" } }, + "node_modules/ngx-mask": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/ngx-mask/-/ngx-mask-20.0.3.tgz", + "integrity": "sha512-5bmrgbFGudj0mFN6cPv/TI+cFJxT4l61mLIFskdvaXsJL/Oj7thRmWYqvqHXjCboOcx8gT6T/Zypl5u9l2J8Jg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=14.0.0", + "@angular/core": ">=14.0.0", + "@angular/forms": ">=14.0.0" + } + }, "node_modules/node-addon-api": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", @@ -8712,6 +8742,7 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -8768,6 +8799,7 @@ "integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -9616,7 +9648,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tuf-js": { "version": "3.1.0", @@ -9654,6 +9687,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9840,6 +9874,7 @@ "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -10243,6 +10278,7 @@ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -10261,7 +10297,8 @@ "version": "0.15.1", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz", "integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==", - "license": "MIT" + "license": "MIT", + "peer": true } } } diff --git a/package.json b/package.json index a8410ad..0a846b3 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@angular/router": "^20.3.0", "@infinimotion/model-frontend": "^0.0.89", "@tailwindcss/postcss": "^4.1.14", + "ngx-mask": "^20.0.3", "postcss": "^8.5.6", "rxjs": "~7.8.0", "tailwindcss": "^4.1.14", diff --git a/src/app/app-module.ts b/src/app/app-module.ts index 7bdff2c..f692e72 100644 --- a/src/app/app-module.ts +++ b/src/app/app-module.ts @@ -3,11 +3,11 @@ import { NgModule, provideBrowserGlobalErrorListeners } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { BrowserModule } from '@angular/platform-browser'; import { provideHttpClient, withFetch } from '@angular/common/http'; - - import { AppRoutingModule } from './app-routing-module'; import { App } from './app'; +import { NgxMaskDirective, NgxMaskPipe, provideNgxMask } from 'ngx-mask'; + import { MatIconModule } from '@angular/material/icon'; import { MatTabsModule } from '@angular/material/tabs'; import { MatToolbarModule } from '@angular/material/toolbar'; @@ -21,6 +21,7 @@ import { MatButtonModule, MatIconButton } from '@angular/material/button'; import { MatDividerModule } from '@angular/material/divider'; import { MatDialogClose, MatDialogTitle, MatDialogContent, MatDialogActions } from "@angular/material/dialog"; import { MatStepperModule } from '@angular/material/stepper'; +import { MatCheckboxModule } from '@angular/material/checkbox'; import { HeaderComponent } from './header/header.component'; import { HomeComponent } from './home/home.component'; @@ -29,7 +30,6 @@ import { MainLayoutComponent } from './layouts/main-layout/main-layout.component import { NavbarComponent } from './navbar/navbar.component'; import { PocModelComponent } from './poc-model-component/poc-model-component'; import { ScheduleComponent } from './schedule/schedule.component'; - import { MovieDurationComponent } from './movie-duration/movie-duration.component'; import { MoviePerformanceComponent } from './movie-performance/movie-performance.component'; import { MoviePosterComponent } from './movie-poster/movie-poster.component'; @@ -115,13 +115,17 @@ import { NoSeatsInHallComponent } from './no-seats-in-hall/no-seats-in-hall.comp MatDialogTitle, MatDialogContent, MatDialogActions, - MatStepperModule -], + MatCheckboxModule, + MatStepperModule, + NgxMaskDirective, + NgxMaskPipe, + ], providers: [ provideBrowserGlobalErrorListeners(), provideHttpClient( withFetch(), - ) + ), + provideNgxMask(), ], bootstrap: [App] }) diff --git a/src/app/order/order.component.css b/src/app/order/order.component.css index b19ef5b..367c809 100644 --- a/src/app/order/order.component.css +++ b/src/app/order/order.component.css @@ -1,7 +1,19 @@ mat-stepper { background: transparent !important; } +::ng-deep .mat-step-header { + background-color: transparent !important; +} + ::ng-deep .mat-horizontal-stepper-header{ pointer-events: none !important; } + +.performance-info-space { + margin-top: calc(var(--spacing) * 24) +} + +::ng-deep .checkbox-invalid.mat-mdc-checkbox .mat-internal-form-field { + color: red !important; +} diff --git a/src/app/order/order.component.html b/src/app/order/order.component.html index 4ee64b6..dce14e7 100644 --- a/src/app/order/order.component.html +++ b/src/app/order/order.component.html @@ -1,4 +1,4 @@ -
+
@if (loadingService.loading$ | async){
@@ -10,19 +10,21 @@ } @else if (performance()) { - - -
+
+ +
+ + + Warenkorb - - -
+
+ +
@for (seatCategory of seatCategories(); track $index) {
@@ -30,55 +32,130 @@ @empty { } -
- +

Tickets gesamt:

-

+

{{ getPriceDisplay(totalPrice()) }}

+
- - + + +
+ + + + + Anschrift + +
+ + + + Name + + @if (fData['name'].hasError('minlength')) { Mindestens 3 Zeichen } + + + + + E-Mail Adresse + + @if (fData['email'].hasError('email')) { Ungültige E-Mail-Adresse } + + + +
+ + Ich akzeptiere die AGB und die Datenbestimmung + +
+ + +
+ + +
+ +
+ +
+ Zahlung + +
+ + + + Kartennummer + + @if (fPayment['cardNumber'].hasError('pattern')) { Ungültige Kartennummer } + + + + + + Kartenname + + @if (fPayment['cardName'].hasError('minlength')) { Mindestens 3 Zeichen } + + + +
+ + Gültig bis (MM/YY) + + @if (fPayment['expiry'].hasError('pattern')) { Ungültiges Format } + + + + CVV + + @if (fPayment['cvv'].hasError('pattern')) { 3–4 Ziffernt } + +
+ + +
+ + encrypted + +

+ Ihre Zahlung wird sicher über unsere Partner verarbeitet.
Wir speichern keine Zahlungsinformationen. +

+
+ + +
+ +
- -
- Anschrift - - Address - - -
- - -
-
-
-- - - Zahlung -

You are now done.

-
- - -
-
}
diff --git a/src/app/order/order.component.ts b/src/app/order/order.component.ts index 5e66746..f1c0989 100644 --- a/src/app/order/order.component.ts +++ b/src/app/order/order.component.ts @@ -2,7 +2,8 @@ import { SelectedSeatsService } from './../selected-seats.service'; import { LoadingService } from './../loading.service'; import { Sitzkategorie, Vorstellung } from '@infinimotion/model-frontend'; import { Component, computed, inject, input } from '@angular/core'; -import { FormBuilder, Validators } from '@angular/forms'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { StepperSelectionEvent } from '@angular/cdk/stepper'; @Component({ selector: 'app-order', @@ -11,25 +12,59 @@ import { FormBuilder, Validators } from '@angular/forms'; styleUrl: './order.component.css' }) export class OrderComponent { + paymentForm!: FormGroup; + dataForm!: FormGroup; + + submitted = false; + + constructor(private fb: FormBuilder) {} + + ngOnInit(): void { + this.paymentForm = this.fb.group({ + cardNumber: ['', [Validators.required, Validators.pattern(/^\d{16}$/)]], + cardName: ['', [Validators.required, Validators.minLength(3)]], + expiry: ['', [Validators.required, Validators.pattern(/^(0[1-9]|1[0-2])\/\d{2}$/)]], + cvv: ['', [Validators.required, Validators.pattern(/^\d{3,4}$/)]], + }); + this.dataForm = this.fb.group({ + name: ['', [Validators.required, Validators.minLength(3)]], + email: ['', [Validators.required, Validators.email]], + accept: ['', Validators.requiredTrue], + }); + } + + get fData() { return this.dataForm.controls; } + get fPayment() { return this.paymentForm.controls; } + + onSubmit() { + if (this.paymentForm.invalid) return; + console.log('Zahlungsdaten:', this.paymentForm.value); + } + + onStepChange(event: StepperSelectionEvent) { + this.submitted = false; + } + + stupidCheckboxWorkaround() { + this.submitted = true; + } + performance = input(); seatCategories = input.required(); - private _formBuilder = inject(FormBuilder); loadingService = inject(LoadingService); private selectedSeatsService = inject(SelectedSeatsService); - firstFormGroup = this._formBuilder.group({ - firstCtrl: ['', Validators.required], - }); - secondFormGroup = this._formBuilder.group({ - secondCtrl: ['', Validators.required], - }); - totalPrice = computed(() => this.selectedSeatsService.getSelectedSeatsList().reduce((sum, seat) => sum + seat.row.category.price, 0) ); + totalSeats = computed(() => + this.selectedSeatsService.getSelectedSeatsList().length + ); + getPriceDisplay(price: number): string { return `${(price / 100).toFixed(2)} €`; } + } diff --git a/src/app/performance-info/performance-info.component.html b/src/app/performance-info/performance-info.component.html index 06435e9..786c373 100644 --- a/src/app/performance-info/performance-info.component.html +++ b/src/app/performance-info/performance-info.component.html @@ -11,7 +11,7 @@

{{ getStartTimeString() }} • {{ performance().hall.name }}

-

{{ movie().title }}

+

{{ movie().title }}

diff --git a/src/app/performance-info/performance-info.component.ts b/src/app/performance-info/performance-info.component.ts index bc35f8b..f648154 100644 --- a/src/app/performance-info/performance-info.component.ts +++ b/src/app/performance-info/performance-info.component.ts @@ -1,4 +1,4 @@ -import { Component, computed, input } from '@angular/core'; +import { Component, input } from '@angular/core'; import { Vorstellung } from '@infinimotion/model-frontend'; @Component({ diff --git a/src/custom-theme.scss b/src/custom-theme.scss index 3de7b38..640a19c 100644 --- a/src/custom-theme.scss +++ b/src/custom-theme.scss @@ -61,8 +61,8 @@ html.dark { backdrop-filter: blur(2px); } -.mat-step-header .mat-step-icon:not(.mat-step-icon-selected):not(.mat-step-icon-completed) { - background-color: #ccc; +.mat-step-header .mat-step-icon:not(.mat-step-icon-selected):not(.mat-step-icon-completed):not(.mat-step-icon-state-edit) { + background-color: #bbb; color: white; }