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.
This commit is contained in:
@@ -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 { 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 { MovieImportSearchInfoComponent } from './movie-import-search-info/movie-import-search-info.component';
|
||||||
import { LoginDialog } from './login/login.dialog';
|
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({
|
@NgModule({
|
||||||
@@ -81,6 +84,9 @@ import { LoginDialog } from './login/login.dialog';
|
|||||||
MovieImportNoSearchResultComponent,
|
MovieImportNoSearchResultComponent,
|
||||||
MovieImportSearchInfoComponent,
|
MovieImportSearchInfoComponent,
|
||||||
LoginDialog,
|
LoginDialog,
|
||||||
|
PerformanceInfoComponent,
|
||||||
|
ShoppingCartComponent,
|
||||||
|
OrderComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
|
|||||||
@@ -10,5 +10,13 @@
|
|||||||
@if ( searchBar() ) {
|
@if ( searchBar() ) {
|
||||||
<app-movie-search (movieSearchResult)="movieSearchResult.emit($event)"></app-movie-search>
|
<app-movie-search (movieSearchResult)="movieSearchResult.emit($event)"></app-movie-search>
|
||||||
}
|
}
|
||||||
|
@if ( backToSchedule() ) {
|
||||||
|
<div class="mt-1 mr-4">
|
||||||
|
<a [routerLink]="['/schedule']" class="opacity-50 hover:underline flex items-center gap-x-1">
|
||||||
|
<mat-icon style="font-size: 20px; width: 20px; height: 20px;">arrow_back</mat-icon>
|
||||||
|
<span>Zurück zur Programmübersicht</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
|
|||||||
@@ -12,4 +12,6 @@ export class MenuHeaderComponent {
|
|||||||
|
|
||||||
searchBar = input<boolean>(false);
|
searchBar = input<boolean>(false);
|
||||||
movieSearchResult = output<string>();
|
movieSearchResult = output<string>();
|
||||||
|
|
||||||
|
backToSchedule = input<boolean>(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
<span class="flex rounded-sm text-black text-sm px-2 py-1.5">
|
<span>
|
||||||
{{ category() }}
|
{{ category() }}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
<span class="flex items-center text-black text-sm rounded-sm px-2 py-1.5">
|
<span class="flex items-center">
|
||||||
<mat-icon class="mr-1" style="font-size: 20px; width: 20px; height: 20px;" fontIcon="schedule"></mat-icon>
|
@if (showIcon()) {
|
||||||
|
<mat-icon class="mr-1" style="font-size: 20px; width: 20px; height: 20px;" fontIcon="schedule"></mat-icon>
|
||||||
|
}
|
||||||
{{ durationText() }}
|
{{ durationText() }}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -8,9 +8,13 @@ import { Component, input, computed } from '@angular/core';
|
|||||||
})
|
})
|
||||||
export class MovieDurationComponent {
|
export class MovieDurationComponent {
|
||||||
duration = input<number>(0);
|
duration = input<number>(0);
|
||||||
|
showIcon = input<boolean>(true);
|
||||||
|
|
||||||
durationText = computed(() => {
|
durationText = computed(() => {
|
||||||
if (this.duration() > 0) {
|
if (this.duration() > 0) {
|
||||||
|
if (!this.showIcon()) {
|
||||||
|
return `${this.duration()} Minuten`;
|
||||||
|
}
|
||||||
return `${this.duration()} Min.`;
|
return `${this.duration()} Min.`;
|
||||||
}
|
}
|
||||||
return 'N/A';
|
return 'N/A';
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-1 justify-between">
|
<div class="flex gap-1 justify-between">
|
||||||
<app-movie-rating [rating]="movie().rating"></app-movie-rating>
|
<app-movie-rating [rating]="movie().rating" class="text-black text-sm rounded-sm shadow-md px-2 py-1.5"></app-movie-rating>
|
||||||
<app-movie-duration [duration]="movie().duration"></app-movie-duration>
|
<app-movie-duration [duration]="movie().duration" class="text-black text-sm rounded-sm px-2 py-1.5"></app-movie-duration>
|
||||||
<app-movie-category [category]="movie().category.name"></app-movie-category>
|
<app-movie-category [category]="movie().category.name" class="text-black text-sm px-2 py-1.5"></app-movie-category>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
<span [class]="ratingColor()" class="text-black flex rounded-sm shadow-md text-sm px-2 py-1.5">
|
<span>
|
||||||
{{ ratingText() }}
|
{{ ratingText() }}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component, input, computed } from '@angular/core';
|
import { Component, input, computed, HostBinding } from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-movie-rating',
|
selector: 'app-movie-rating',
|
||||||
@@ -7,6 +7,11 @@ import { Component, input, computed } from '@angular/core';
|
|||||||
styleUrl: './movie-rating.component.css'
|
styleUrl: './movie-rating.component.css'
|
||||||
})
|
})
|
||||||
export class MovieRatingComponent {
|
export class MovieRatingComponent {
|
||||||
|
|
||||||
|
@HostBinding('class') get hostClasses(): string {
|
||||||
|
return this.ratingColor();
|
||||||
|
}
|
||||||
|
|
||||||
rating = input<number>(0);
|
rating = input<number>(0);
|
||||||
|
|
||||||
ratingColor = computed(() => {
|
ratingColor = computed(() => {
|
||||||
|
|||||||
3
src/app/order/order.component.css
Normal file
3
src/app/order/order.component.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
mat-stepper {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
49
src/app/order/order.component.html
Normal file
49
src/app/order/order.component.html
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<div class="w-full h-full">
|
||||||
|
<mat-stepper
|
||||||
|
orientation="horizontal"
|
||||||
|
linear="true"
|
||||||
|
#stepper
|
||||||
|
>
|
||||||
|
<mat-step [stepControl]="firstFormGroup">
|
||||||
|
<form [formGroup]="firstFormGroup">
|
||||||
|
<ng-template matStepLabel>Warenkorb</ng-template>
|
||||||
|
|
||||||
|
@if (performance()) {
|
||||||
|
<app-performance-info class="w-full h-50" [performance]="performance()!"></app-performance-info>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button mat-button matStepperNext>Next</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</mat-step>
|
||||||
|
|
||||||
|
<mat-step [stepControl]="secondFormGroup">
|
||||||
|
<form [formGroup]="secondFormGroup">
|
||||||
|
<ng-template matStepLabel>Anschrift</ng-template>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Address</mat-label>
|
||||||
|
<input
|
||||||
|
matInput
|
||||||
|
formControlName="secondCtrl"
|
||||||
|
placeholder="Ex. 1 Main St, New York, NY"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
<div>
|
||||||
|
<button mat-button matStepperPrevious>Back</button>
|
||||||
|
<button mat-button matStepperNext>Next</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</mat-step>
|
||||||
|
-
|
||||||
|
<mat-step>
|
||||||
|
<ng-template matStepLabel>Zahlung</ng-template>
|
||||||
|
<p>You are now done.</p>
|
||||||
|
<div>
|
||||||
|
<button mat-button matStepperPrevious>Back</button>
|
||||||
|
<button mat-button (click)="stepper.reset()">Reset</button>
|
||||||
|
</div>
|
||||||
|
</mat-step>
|
||||||
|
</mat-stepper>
|
||||||
|
</div>
|
||||||
22
src/app/order/order.component.ts
Normal file
22
src/app/order/order.component.ts
Normal file
@@ -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<Vorstellung>();
|
||||||
|
|
||||||
|
private _formBuilder = inject(FormBuilder);
|
||||||
|
|
||||||
|
firstFormGroup = this._formBuilder.group({
|
||||||
|
firstCtrl: ['', Validators.required],
|
||||||
|
});
|
||||||
|
secondFormGroup = this._formBuilder.group({
|
||||||
|
secondCtrl: ['', Validators.required],
|
||||||
|
});
|
||||||
|
}
|
||||||
14
src/app/performance-info/performance-info.component.css
Normal file
14
src/app/performance-info/performance-info.component.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
21
src/app/performance-info/performance-info.component.html
Normal file
21
src/app/performance-info/performance-info.component.html
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<div class="info-box bg-indigo-100 h-22 flex items-center space-x-3 rounded-md p-1 shadow-sm">
|
||||||
|
|
||||||
|
<div class="h-full">
|
||||||
|
<img
|
||||||
|
[src]="movie().image && movie().image !== 'N/A' ? movie().image : 'assets/poster_placeholder.png'"
|
||||||
|
alt="Movie Poster"
|
||||||
|
class="h-full w-auto shadow-xs rounded-sm"
|
||||||
|
(error)="onPosterError($event)"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-md">
|
||||||
|
<h3 class="opacity-75">{{ getStartTimeString() }} • {{ performance().hall.name }}</h3>
|
||||||
|
<h1 class="font-semibold my-1">{{ movie().title }}</h1>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<app-movie-rating [rating]="movie().rating" class="rounded-sm shadow-xs px-0.75 py-0.2 text-sm"></app-movie-rating>
|
||||||
|
<app-movie-duration [duration]="movie().duration" [showIcon]="false" class="ml-1.5 opacity-75"></app-movie-duration>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
30
src/app/performance-info/performance-info.component.ts
Normal file
30
src/app/performance-info/performance-info.component.ts
Normal file
@@ -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<Vorstellung>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
0
src/app/shopping-cart/shopping-cart.component.css
Normal file
0
src/app/shopping-cart/shopping-cart.component.css
Normal file
1
src/app/shopping-cart/shopping-cart.component.html
Normal file
1
src/app/shopping-cart/shopping-cart.component.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<p>shopping-cart works!</p>
|
||||||
11
src/app/shopping-cart/shopping-cart.component.ts
Normal file
11
src/app/shopping-cart/shopping-cart.component.ts
Normal file
@@ -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 {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
:host {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
@for (row of seatsPerRow(); track $index) {
|
<div class="m-auto w-200 h-10 bg-gray-300 mb-20" style="clip-path: polygon(0% 0%,100% 0%,90% 100%,10% 100%);">
|
||||||
<app-seat-row class="flex justify-center" [rowSeatList]="row"></app-seat-row>
|
<p class="flex justify-center text-2xl fond-bold p-1">
|
||||||
}
|
Leinwand
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
@for (row of seatsPerRow(); track $index) {
|
||||||
|
<app-seat-row class="flex justify-center" [rowSeatList]="row"></app-seat-row>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<div class="m-auto w-200 h-10 bg-gray-300 mb-20" style="clip-path: polygon(0% 0%,100% 0%,90% 100%,10% 100%);">
|
<app-menu-header title="Vorstellungstickets kaufen" icon="local_activity" [backToSchedule]="true"></app-menu-header>
|
||||||
<p class="flex justify-center text-2xl fond-bold p-1">
|
|
||||||
Leinwand
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="">
|
|
||||||
<app-theater-layout [seatsPerRow]="seatsPerRow"></app-theater-layout>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<div class="flex justify-between h-max">
|
||||||
|
|
||||||
|
<app-theater-layout [seatsPerRow]="seatsPerRow" class="m-10 w-7/10"></app-theater-layout>
|
||||||
|
|
||||||
|
<app-order class="m-10 w-3/10" [performance]="performance"></app-order>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import {Component, inject, OnInit} from '@angular/core';
|
import {Component, inject, OnInit} from '@angular/core';
|
||||||
import {HttpService} from '../http.service';
|
import {HttpService} from '../http.service';
|
||||||
import {LoadingService} from '../loading.service';
|
import {LoadingService} from '../loading.service';
|
||||||
import {catchError, of, tap} from 'rxjs';
|
import {catchError, forkJoin, of, tap} from 'rxjs';
|
||||||
import {Sitzplatz} from '@infinimotion/model-frontend';
|
import {Sitzplatz, Vorstellung} from '@infinimotion/model-frontend';
|
||||||
import {TheaterSeatState} from '../model/theater-seat-state.model';
|
import {TheaterSeatState} from '../model/theater-seat-state.model';
|
||||||
import {ActivatedRoute} from '@angular/router';
|
import {ActivatedRoute} from '@angular/router';
|
||||||
|
|
||||||
@@ -18,28 +18,34 @@ export class TheaterOverlayComponent implements OnInit {
|
|||||||
|
|
||||||
showId!: number;
|
showId!: number;
|
||||||
seatsPerRow: { seat: Sitzplatz, state: TheaterSeatState }[][] = []
|
seatsPerRow: { seat: Sitzplatz, state: TheaterSeatState }[][] = []
|
||||||
|
performance: Vorstellung | undefined;
|
||||||
|
|
||||||
constructor(private route: ActivatedRoute) {}
|
constructor(private route: ActivatedRoute) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.showId = Number(this.route.snapshot.paramMap.get('id')!);
|
this.showId = Number(this.route.snapshot.paramMap.get('id')!);
|
||||||
this.loadShowSeats();
|
this.loadPerformanceAndSeats();
|
||||||
}
|
}
|
||||||
|
|
||||||
loadShowSeats() {
|
loadPerformanceAndSeats() {
|
||||||
this.loading.show();
|
this.loading.show();
|
||||||
this.http.getSeatsByShowId(this.showId).pipe(
|
|
||||||
tap((data) => {
|
forkJoin({
|
||||||
this.seatsPerRow = this.converter(data)
|
performance: this.http.getPerformaceById(this.showId),
|
||||||
this.loading.hide();
|
seats: this.http.getSeatsByShowId(this.showId)
|
||||||
}),
|
}).pipe(
|
||||||
catchError(err => {
|
tap(({ performance, seats }) => {
|
||||||
this.loading.showError(err);
|
this.performance = performance;
|
||||||
console.error('Fehler beim Laden der Vorstellung', err);
|
this.seatsPerRow = this.converter(seats);
|
||||||
return of([]);
|
this.loading.hide();
|
||||||
})
|
}),
|
||||||
).subscribe();
|
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[] }): {
|
converter(resp: { seats: Sitzplatz[], reserved: Sitzplatz[], booked: Sitzplatz[] }): {
|
||||||
seat: Sitzplatz,
|
seat: Sitzplatz,
|
||||||
|
|||||||
Reference in New Issue
Block a user