From 41c9d85e9bd798062cd4a279e8791081e61f656b Mon Sep 17 00:00:00 2001 From: Piet Ostendorp Date: Thu, 13 Nov 2025 22:36:09 +0100 Subject: [PATCH] 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; }