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.
This commit is contained in:
@@ -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]
|
||||
})
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="w-full h-full">
|
||||
<div class="w-full h-full relative">
|
||||
|
||||
@if (loadingService.loading$ | async){
|
||||
<div class="w-full h-full flex items-center justify-center">
|
||||
@@ -10,19 +10,21 @@
|
||||
}
|
||||
@else if (performance()) {
|
||||
|
||||
<mat-stepper
|
||||
orientation="horizontal"
|
||||
linear="true"
|
||||
#stepper
|
||||
>
|
||||
<mat-step [stepControl]="firstFormGroup">
|
||||
<form [formGroup]="firstFormGroup">
|
||||
<div class="absolute top-18 z-20 w-full px-6">
|
||||
<app-performance-info
|
||||
class="w-full h-10"
|
||||
[performance]="performance()!"
|
||||
></app-performance-info>
|
||||
</div>
|
||||
|
||||
<mat-stepper orientation="horizontal" linear="true" [disableRipple]="true" (selectionChange)="onStepChange($event)" #stepper>
|
||||
<mat-step>
|
||||
<ng-template matStepLabel>Warenkorb</ng-template>
|
||||
|
||||
<app-performance-info class="w-full h-50" [performance]="performance()!"></app-performance-info>
|
||||
|
||||
<div class="mb-4 mt-2 p-2">
|
||||
<div class="performance-info-space"></div>
|
||||
|
||||
<!-- Seat-Selection Overview -->
|
||||
<div class="mb-4 p-2">
|
||||
@for (seatCategory of seatCategories(); track $index) {
|
||||
<div class="h-2"></div>
|
||||
<app-seat-selection [seatCategory]="seatCategory"></app-seat-selection>
|
||||
@@ -30,55 +32,130 @@
|
||||
@empty {
|
||||
<app-no-seats-in-hall></app-no-seats-in-hall>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
<!-- Total Price -->
|
||||
<div class="flex justify-between p-2 mt-1 items-baseline">
|
||||
<p class="font-semibold text-lg">
|
||||
Tickets gesamt:
|
||||
</p>
|
||||
<p class="font-semibold text-2xl bg-gradient-to-r from-indigo-500 to-pink-600 bg-clip-text text-transparent">
|
||||
<p class="font-semibold text-2xl bg-linear-to-r from-indigo-500 to-pink-600 bg-clip-text text-transparent">
|
||||
{{ getPriceDisplay(totalPrice()) }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex space-x-5 mt-10">
|
||||
<button mat-button matButton="outlined" matStepperNext [disabled]="totalPrice()==0" class="w-1/2">Reservieren</button>
|
||||
<button mat-button matButton="filled" matStepperNext class="w-1/2" [disabled]="totalPrice()==0">Buchen</button>
|
||||
<button mat-button matButton="outlined" matStepperNext class="w-1/2" [disabled]="totalSeats()==0">Reservieren</button>
|
||||
<button mat-button matButton="filled" matStepperNext class="w-1/2" [disabled]="totalSeats()==0">Buchen</button>
|
||||
</div>
|
||||
</mat-step>
|
||||
|
||||
<mat-step [stepControl]="dataForm">
|
||||
<form [formGroup]="dataForm">
|
||||
<ng-template matStepLabel>Anschrift</ng-template>
|
||||
|
||||
<div class="performance-info-space"></div>
|
||||
|
||||
<!-- Name -->
|
||||
<mat-form-field class="w-full mt-8">
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput formControlName="name" placeholder="Max Mustermann" />
|
||||
@if (fData['name'].hasError('minlength')) { <mat-error>Mindestens 3 Zeichen</mat-error> }
|
||||
</mat-form-field>
|
||||
|
||||
<!-- E-Mail -->
|
||||
<mat-form-field class="w-full mt-2">
|
||||
<mat-label>E-Mail Adresse</mat-label>
|
||||
<input matInput formControlName="email" placeholder="max.mustermann@edu.fhdw.de" />
|
||||
@if (fData['email'].hasError('email')) { <mat-error>Ungültige E-Mail-Adresse</mat-error> }
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Checkbox -->
|
||||
<div class="w-full my-4">
|
||||
<mat-checkbox required formControlName="accept" class="checkbox-invalid" [class]="{ 'checkbox-invalid': submitted && fData['accept'].hasError('required') }">
|
||||
Ich akzeptiere die AGB und die Datenbestimmung
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex space-x-5 mt-10">
|
||||
<button type="button" mat-button matButton="outlined" (click)="stepper.reset()" class="w-1/3">Zurück</button>
|
||||
<button type="submit" mat-button matButton="filled" matStepperNext (click)="stupidCheckboxWorkaround()" class="w-2/3">Sitzplätze reservieren</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-step>
|
||||
<mat-step [stepControl]="paymentForm">
|
||||
<form [formGroup]="paymentForm" (ngSubmit)="onSubmit()">
|
||||
<ng-template matStepLabel>Zahlung</ng-template>
|
||||
|
||||
<div class="performance-info-space"></div>
|
||||
|
||||
<!-- Card Number -->
|
||||
<mat-form-field class="w-full mt-8">
|
||||
<mat-label>Kartennummer</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="cardNumber"
|
||||
mask="0000 0000 0000 0000"
|
||||
placeholder="1111 2222 3333 4444"
|
||||
/>
|
||||
@if (fPayment['cardNumber'].hasError('pattern')) { <mat-error>Ungültige Kartennummer</mat-error> }
|
||||
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Card Name -->
|
||||
<mat-form-field class="w-full">
|
||||
<mat-label>Kartenname</mat-label>
|
||||
<input matInput formControlName="cardName" />
|
||||
@if (fPayment['cardName'].hasError('minlength')) { <mat-error>Mindestens 3 Zeichen</mat-error> }
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Expiry & CVV -->
|
||||
<div class="flex space-x-4">
|
||||
<mat-form-field class="flex-1">
|
||||
<mat-label>Gültig bis (MM/YY)</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="expiry"
|
||||
mask="00/00"
|
||||
placeholder="MM/YY"
|
||||
[dropSpecialCharacters]="false"
|
||||
/>
|
||||
@if (fPayment['expiry'].hasError('pattern')) { <mat-error>Ungültiges Format</mat-error> }
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="flex-1">
|
||||
<mat-label>CVV</mat-label>
|
||||
<input matInput type="password" maxlength="4" formControlName="cvv" />
|
||||
@if (fPayment['cvv'].hasError('pattern')) { <mat-error>3–4 Ziffernt</mat-error> }
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Info -->
|
||||
<div class="flex w-full space-x-2 mt-2 items-center">
|
||||
<mat-icon class="material-symbols-outlined opacity-50" style="font-size: 32px; width: 32px; height: 32px">
|
||||
encrypted
|
||||
</mat-icon>
|
||||
<p class="text-sm opacity-75">
|
||||
Ihre Zahlung wird sicher über unsere Partner verarbeitet.<br>Wir speichern keine Zahlungsinformationen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex space-x-4 mt-8">
|
||||
<button mat-stroked-button color="primary" matStepperPrevious type="button" class="w-1/3">
|
||||
Zurück
|
||||
</button>
|
||||
<button mat-flat-button color="accent" class="w-2/3" matStepperNext type="submit">
|
||||
{{ getPriceDisplay(totalPrice()) }} jetzt bezahlen
|
||||
</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>
|
||||
|
||||
@@ -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<Vorstellung>();
|
||||
seatCategories = input.required<Sitzkategorie[]>();
|
||||
|
||||
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)} €`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<div class="text-md">
|
||||
<h3 class="opacity-75">{{ getStartTimeString() }} • {{ performance().hall.name }}</h3>
|
||||
<h1 class="font-semibold mb-1.5">{{ movie().title }}</h1>
|
||||
<h1 class="font-semibold mb-0.5">{{ movie().title }}</h1>
|
||||
<div class="flex items-center">
|
||||
<app-movie-rating [rating]="movie().rating" class="rounded-sm shadow-xs px-1 py-0.25 text-sm"></app-movie-rating>
|
||||
<app-movie-duration [duration]="movie().duration" [showIcon]="false" class="ml-1.5 opacity-75"></app-movie-duration>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, computed, input } from '@angular/core';
|
||||
import { Component, input } from '@angular/core';
|
||||
import { Vorstellung } from '@infinimotion/model-frontend';
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user