From 44c163703c2e4188c40f3a94379d67b1cc2ee877 Mon Sep 17 00:00:00 2001 From: Igor Propisnov Date: Tue, 23 Jul 2024 23:47:35 +0200 Subject: [PATCH 01/20] added shadow, and new root page for event managment --- frontend/src/app/app.routes.ts | 8 +++++++- .../layout/main-layout/layout.component.html | 18 +++++++++++------- .../app/layout/main-layout/layout.component.ts | 15 ++++++++++++--- .../pages/event-root/event-root.component.html | 1 + .../pages/event-root/event-root.component.ts | 12 ++++++++++++ 5 files changed, 43 insertions(+), 11 deletions(-) create mode 100644 frontend/src/app/pages/event-root/event-root.component.html create mode 100644 frontend/src/app/pages/event-root/event-root.component.ts diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index a9754fc..e445049 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -26,7 +26,13 @@ const protectedRoutes: Routes = [ import('./pages/dashboard-root/dashboard-root.component').then( (m) => m.DashboardRootComponent ), - canActivate: [], + }, + { + path: 'event', + loadComponent: () => + import('./pages/event-root/event-root.component').then( + (m) => m.EventRootComponent + ), }, ]; diff --git a/frontend/src/app/layout/main-layout/layout.component.html b/frontend/src/app/layout/main-layout/layout.component.html index 1ec156f..e923857 100644 --- a/frontend/src/app/layout/main-layout/layout.component.html +++ b/frontend/src/app/layout/main-layout/layout.component.html @@ -3,18 +3,18 @@ [ngStyle]="navigation" [class]=" isCollapsed - ? 'bg-primary w-0 md:w-20 transition-all duration-300 ease-in-out' + ? 'bg-primary w-0 md:w-20 transition-all duration-300 ease-in-out shadow-[5px_0_20px_rgba(0,0,0,0.5)]' : showMobileMenu - ? 'bg-primary w-64 transition-all duration-300 ease-in-out' + ? 'bg-primary w-64 transition-all duration-300 ease-in-out shadow-[5px_0_20px_rgba(0,0,0,0.5)]' : isDesktopCollapsed - ? 'bg-primary w-48 md:w-14 transition-all duration-300 ease-in-out' - : 'bg-primary w-48 md:w-48 transition-all duration-300 ease-in-out' + ? 'bg-primary w-48 md:w-14 transition-all duration-300 ease-in-out shadow-[5px_0_20px_rgba(0,0,0,0.5)]' + : 'bg-primary w-48 md:w-48 transition-all duration-300 ease-in-out shadow-[5px_0_20px_rgba(0,0,0,0.5)]' " class="transform h-full z-20 overflow-y-auto fixed md:relative flex flex-col">
+ class="p-1 w-full h-16 z-50 bg-base-100 flex items-center relative">
@if (!isCollapsed && !isDesktopCollapsed) { @@ -60,7 +60,8 @@ class="cursor-pointer rounded-btn mt-2" [ngClass]="{ 'bg-base-100 text-primary': item.active, - 'text-primary-content hover:text-base-content': !item.active + 'text-primary-content hover:text-accent-content hover:bg-accent': + !item.active }" (click)="setActive(item)" (keydown.enter)="setActive(item)" @@ -114,7 +115,7 @@
+ class="p-4 z-0 md:z-20 relative bg-primary text-primary-content flex items-center h-16 shadow-[0_5px_20px_rgba(0,0,0,0.5)]">
diff --git a/frontend/src/app/layout/main-layout/layout.component.ts b/frontend/src/app/layout/main-layout/layout.component.ts index c711d32..080481c 100644 --- a/frontend/src/app/layout/main-layout/layout.component.ts +++ b/frontend/src/app/layout/main-layout/layout.component.ts @@ -38,7 +38,6 @@ export class LayoutComponent implements OnInit { public isCollapsed: boolean = false; public isDesktopCollapsed: boolean = false; public showMobileMenu: boolean = false; - public userHasInteracted: boolean = false; public menuItems: TopMenuItem[] = [ { name: 'Dashboard', @@ -48,6 +47,14 @@ export class LayoutComponent implements OnInit { `), }, + { + name: 'Event', + route: '/event', + icon: this.sanitizer + .bypassSecurityTrustHtml(` + + `), + }, ]; public bottomMenuItems: BottomMenuItem[] = [ { @@ -134,7 +141,6 @@ export class LayoutComponent implements OnInit { } else { this.isDesktopCollapsed = !this.isDesktopCollapsed; } - this.userHasInteracted = true; } public toggleDesktopSidebar(): void { @@ -145,7 +151,10 @@ export class LayoutComponent implements OnInit { this.menuItems.forEach((menu: TopMenuItem) => { menu.active = false; }); - item.active = true; + this.router.navigate([item.route]); + if (!this.isCollapsed && this.showMobileMenu) { + this.toggleSidebar(); + } } private setActiveItemBasedOnRoute(): void { diff --git a/frontend/src/app/pages/event-root/event-root.component.html b/frontend/src/app/pages/event-root/event-root.component.html new file mode 100644 index 0000000..164b9f2 --- /dev/null +++ b/frontend/src/app/pages/event-root/event-root.component.html @@ -0,0 +1 @@ +

Event Root Works

diff --git a/frontend/src/app/pages/event-root/event-root.component.ts b/frontend/src/app/pages/event-root/event-root.component.ts new file mode 100644 index 0000000..5d02ac9 --- /dev/null +++ b/frontend/src/app/pages/event-root/event-root.component.ts @@ -0,0 +1,12 @@ +import { CommonModule } from '@angular/common'; +import { Component, ChangeDetectionStrategy } from '@angular/core'; + +@Component({ + selector: 'app-event-root', + standalone: true, + imports: [CommonModule], + providers: [], + templateUrl: './event-root.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class EventRootComponent {} -- 2.40.1 From f8e7b816bc5bff0a5371ba7d4e0332bcf341a0a4 Mon Sep 17 00:00:00 2001 From: Igor Propisnov Date: Mon, 29 Jul 2024 23:10:43 +0200 Subject: [PATCH 02/20] Event creating W.I.P --- frontend/src/app/app.routes.ts | 20 ++++- .../layout/main-layout/layout.component.html | 5 +- .../create-event/create-event.component.html | 32 +++++++ .../create-event/create-event.component.ts | 76 +++++++++++++++++ .../steps/basic-step.component.html | 85 +++++++++++++++++++ .../steps/basic-step.component.ts | 13 +++ .../event-empty-state.component.html | 24 ++++++ .../event-empty-state.component.ts | 17 ++++ .../event-root/event-root.component.html | 2 +- .../pages/event-root/event-root.component.ts | 4 +- package.json | 3 + 11 files changed, 271 insertions(+), 10 deletions(-) create mode 100644 frontend/src/app/pages/event-root/create-event/create-event.component.html create mode 100644 frontend/src/app/pages/event-root/create-event/create-event.component.ts create mode 100644 frontend/src/app/pages/event-root/create-event/steps/basic-step.component.html create mode 100644 frontend/src/app/pages/event-root/create-event/steps/basic-step.component.ts create mode 100644 frontend/src/app/pages/event-root/event-empty-state/event-empty-state.component.html create mode 100644 frontend/src/app/pages/event-root/event-empty-state/event-empty-state.component.ts diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index e445049..f0d58b1 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -29,10 +29,22 @@ const protectedRoutes: Routes = [ }, { path: 'event', - loadComponent: () => - import('./pages/event-root/event-root.component').then( - (m) => m.EventRootComponent - ), + children: [ + { + path: '', + loadComponent: () => + import('./pages/event-root/event-root.component').then( + (m) => m.EventRootComponent + ), + }, + { + path: 'create', + loadComponent: () => + import('./pages/event-root/create-event/create-event.component').then( + (m) => m.CreateEventComponent + ), + }, + ], }, ]; diff --git a/frontend/src/app/layout/main-layout/layout.component.html b/frontend/src/app/layout/main-layout/layout.component.html index e923857..2ba8ef2 100644 --- a/frontend/src/app/layout/main-layout/layout.component.html +++ b/frontend/src/app/layout/main-layout/layout.component.html @@ -144,10 +144,7 @@
-
-

isCollapsed: {{ isCollapsed }}

-

isDesktopCollapsed: {{ isDesktopCollapsed }}

-

showMobileMenu: {{ showMobileMenu }}

+
diff --git a/frontend/src/app/pages/event-root/create-event/create-event.component.html b/frontend/src/app/pages/event-root/create-event/create-event.component.html new file mode 100644 index 0000000..0f9bd86 --- /dev/null +++ b/frontend/src/app/pages/event-root/create-event/create-event.component.html @@ -0,0 +1,32 @@ +
+
+
+
+ @if (currentStep() === 0) { + + } +
+
+
+ +
+ + +
    + @for (step of steps; track $index) { +
  • + {{ step }} +
  • + } +
+
+
diff --git a/frontend/src/app/pages/event-root/create-event/create-event.component.ts b/frontend/src/app/pages/event-root/create-event/create-event.component.ts new file mode 100644 index 0000000..307ba6b --- /dev/null +++ b/frontend/src/app/pages/event-root/create-event/create-event.component.ts @@ -0,0 +1,76 @@ +import { CommonModule } from '@angular/common'; +import { + Component, + ChangeDetectionStrategy, + signal, + WritableSignal, + ElementRef, + OnInit, +} from '@angular/core'; + +import { + BackgroundPatternService, + ThemeService, +} from '../../../shared/service'; + +import { BasicStepComponent } from './steps/basic-step.component'; + +@Component({ + selector: 'app-create-event', + standalone: true, + providers: [], + imports: [BasicStepComponent, CommonModule], + templateUrl: './create-event.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CreateEventComponent implements OnInit { + public actionbar: { 'background-image': string } | null = null; + public currentStep: WritableSignal = signal(0); + public readonly steps: string[] = ['Basic', 'Tickets', 'Review']; + + public constructor( + private readonly backgroundPatternService: BackgroundPatternService, + private readonly themeService: ThemeService, + private readonly el: ElementRef + ) {} + + public ngOnInit(): void { + this.setBackground(); + } + + public setBackground(): void { + const theme = this.themeService.getTheme(); + let opacity: number; + + if (theme === 'dark') { + opacity = 0.05; + } else { + opacity = 0.1; + } + + const colorPrimaryC = getComputedStyle( + this.el.nativeElement + ).getPropertyValue('--pc'); + + const svgUrlActionbar = this.backgroundPatternService.getBankNotePattern( + colorPrimaryC, + opacity + ); + + this.actionbar = { + 'background-image': `url("${svgUrlActionbar}")`, + }; + } + + public nextStep(): void { + if (this.currentStep() < this.steps.length - 1) { + this.currentStep.set(this.currentStep() + 1); + } + } + + public prevStep(): void { + if (this.currentStep() > 0) { + this.currentStep.set(this.currentStep() - 1); + } + } +} diff --git a/frontend/src/app/pages/event-root/create-event/steps/basic-step.component.html b/frontend/src/app/pages/event-root/create-event/steps/basic-step.component.html new file mode 100644 index 0000000..335dad5 --- /dev/null +++ b/frontend/src/app/pages/event-root/create-event/steps/basic-step.component.html @@ -0,0 +1,85 @@ +
+

Event Basic Information

+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+
diff --git a/frontend/src/app/pages/event-root/create-event/steps/basic-step.component.ts b/frontend/src/app/pages/event-root/create-event/steps/basic-step.component.ts new file mode 100644 index 0000000..b6b0093 --- /dev/null +++ b/frontend/src/app/pages/event-root/create-event/steps/basic-step.component.ts @@ -0,0 +1,13 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'app-basic-step', + standalone: true, + templateUrl: './basic-step.component.html', + providers: [], + imports: [], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class BasicStepComponent { + public constructor() {} +} diff --git a/frontend/src/app/pages/event-root/event-empty-state/event-empty-state.component.html b/frontend/src/app/pages/event-root/event-empty-state/event-empty-state.component.html new file mode 100644 index 0000000..2505cd9 --- /dev/null +++ b/frontend/src/app/pages/event-root/event-empty-state/event-empty-state.component.html @@ -0,0 +1,24 @@ +
+
+
+

No Events Yet

+ +
+

+ Every great event starts with a single step. Take yours today! +

+
+ +

+ Create your first event and start +
+ selling tickets today! +

+ +
+
+
diff --git a/frontend/src/app/pages/event-root/event-empty-state/event-empty-state.component.ts b/frontend/src/app/pages/event-root/event-empty-state/event-empty-state.component.ts new file mode 100644 index 0000000..bae31d8 --- /dev/null +++ b/frontend/src/app/pages/event-root/event-empty-state/event-empty-state.component.ts @@ -0,0 +1,17 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'app-event-empty-state', + standalone: true, + templateUrl: './event-empty-state.component.html', + providers: [], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class EventEmptyStateComponent { + public constructor(private readonly router: Router) {} + + public navigateToCreateEvent(): void { + this.router.navigate(['/event/create']); + } +} diff --git a/frontend/src/app/pages/event-root/event-root.component.html b/frontend/src/app/pages/event-root/event-root.component.html index 164b9f2..5ca8bb2 100644 --- a/frontend/src/app/pages/event-root/event-root.component.html +++ b/frontend/src/app/pages/event-root/event-root.component.html @@ -1 +1 @@ -

Event Root Works

+ diff --git a/frontend/src/app/pages/event-root/event-root.component.ts b/frontend/src/app/pages/event-root/event-root.component.ts index 5d02ac9..10d2fe9 100644 --- a/frontend/src/app/pages/event-root/event-root.component.ts +++ b/frontend/src/app/pages/event-root/event-root.component.ts @@ -1,10 +1,12 @@ import { CommonModule } from '@angular/common'; import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { EventEmptyStateComponent } from './event-empty-state/event-empty-state.component'; + @Component({ selector: 'app-event-root', standalone: true, - imports: [CommonModule], + imports: [CommonModule, EventEmptyStateComponent], providers: [], templateUrl: './event-root.component.html', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/package.json b/package.json index f932772..d308e65 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,9 @@ { "name": "mvp-ticket", "version": "1.0.0", + "engines": { + "node": "18.17.1" + }, "description": "", "main": "index.js", "scripts": { -- 2.40.1 From 0314855a0d12a81c931f1a3dd0c4df63599b3c13 Mon Sep 17 00:00:00 2001 From: Igor Propisnov Date: Tue, 30 Jul 2024 10:07:08 +0200 Subject: [PATCH 03/20] added partial first page of basic event setup step --- .../create-event/create-event.component.html | 70 ++++++++++---- .../create-event/create-event.component.ts | 67 ++++++------- .../steps/basic-step.component.html | 45 ++++++--- .../steps/basic-step.component.ts | 8 +- .../dropdown/dropdown.component.html | 95 +++++++++++++++++++ .../components/dropdown/dropdown.component.ts | 69 ++++++++++++++ frontend/src/app/shared/components/index.ts | 1 + 7 files changed, 287 insertions(+), 68 deletions(-) create mode 100644 frontend/src/app/shared/components/dropdown/dropdown.component.html create mode 100644 frontend/src/app/shared/components/dropdown/dropdown.component.ts create mode 100644 frontend/src/app/shared/components/index.ts diff --git a/frontend/src/app/pages/event-root/create-event/create-event.component.html b/frontend/src/app/pages/event-root/create-event/create-event.component.html index 0f9bd86..a8f3fc3 100644 --- a/frontend/src/app/pages/event-root/create-event/create-event.component.html +++ b/frontend/src/app/pages/event-root/create-event/create-event.component.html @@ -1,6 +1,25 @@
-
-
+
+
    + @for (step of steps; track $index) { +
  • + {{ step }} +
  • + } +
+
+ +
+
@if (currentStep() === 0) { @@ -10,23 +29,34 @@
- - -
    - @for (step of steps; track $index) { -
  • - {{ step }} -
  • - } -
+ class="w-full bg-neutral max-w-full sticky bottom-0 z-20 px-4 sm:px-8 py-4"> +
+ + +
diff --git a/frontend/src/app/pages/event-root/create-event/create-event.component.ts b/frontend/src/app/pages/event-root/create-event/create-event.component.ts index 307ba6b..11d69d5 100644 --- a/frontend/src/app/pages/event-root/create-event/create-event.component.ts +++ b/frontend/src/app/pages/event-root/create-event/create-event.component.ts @@ -4,15 +4,9 @@ import { ChangeDetectionStrategy, signal, WritableSignal, - ElementRef, OnInit, } from '@angular/core'; -import { - BackgroundPatternService, - ThemeService, -} from '../../../shared/service'; - import { BasicStepComponent } from './steps/basic-step.component'; @Component({ @@ -24,42 +18,51 @@ import { BasicStepComponent } from './steps/basic-step.component'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class CreateEventComponent implements OnInit { - public actionbar: { 'background-image': string } | null = null; public currentStep: WritableSignal = signal(0); public readonly steps: string[] = ['Basic', 'Tickets', 'Review']; - public constructor( - private readonly backgroundPatternService: BackgroundPatternService, - private readonly themeService: ThemeService, - private readonly el: ElementRef - ) {} + public constructor() {} - public ngOnInit(): void { - this.setBackground(); + public ngOnInit(): void {} + + public getStepContent(index: number): string { + if (index < this.currentStep()) { + return this.isStepValid(index) ? '✓' : '?'; + } else { + return (index + 1).toString(); + } } - public setBackground(): void { - const theme = this.themeService.getTheme(); - let opacity: number; - - if (theme === 'dark') { - opacity = 0.05; - } else { - opacity = 0.1; + public goToStep(stepIndex: number): void { + if (stepIndex < this.currentStep()) { + this.currentStep.set(stepIndex); + } else if (this.canAdvanceToStep(stepIndex)) { + this.currentStep.set(stepIndex); } + } - const colorPrimaryC = getComputedStyle( - this.el.nativeElement - ).getPropertyValue('--pc'); + public canAdvanceToStep(stepIndex: number): boolean { + for (let i = this.currentStep(); i < stepIndex; i++) { + if (!this.isStepValid(i)) { + return false; + } + } + return true; + } - const svgUrlActionbar = this.backgroundPatternService.getBankNotePattern( - colorPrimaryC, - opacity - ); + public isStepValid(stepIndex: number): boolean { + // TODO: Implementiere die tatsächliche Validierungslogik hier + // Für Demonstrationszwecke nehmen wir an, dass der erste Schritt immer gültig ist, + // und die anderen zufällig gültig oder ungültig sind + return true; - this.actionbar = { - 'background-image': `url("${svgUrlActionbar}")`, - }; + // In Zukunft könnte es so aussehen: + // switch(stepIndex) { + // case 0: return this.isBasicStepValid(); + // case 1: return this.isTicketsStepValid(); + // case 2: return this.isReviewStepValid(); + // default: return false; + // } } public nextStep(): void { diff --git a/frontend/src/app/pages/event-root/create-event/steps/basic-step.component.html b/frontend/src/app/pages/event-root/create-event/steps/basic-step.component.html index 335dad5..6813275 100644 --- a/frontend/src/app/pages/event-root/create-event/steps/basic-step.component.html +++ b/frontend/src/app/pages/event-root/create-event/steps/basic-step.component.html @@ -1,18 +1,33 @@
-

Event Basic Information

+

Event Basic Information

-
-
@@ -84,6 +76,7 @@
diff --git a/frontend/src/app/pages/event-root/create-event/dialog/create-location.dialog.component.ts b/frontend/src/app/pages/event-root/create-event/dialog/create-location.dialog.component.ts index 3e2dbf7..42e2524 100644 --- a/frontend/src/app/pages/event-root/create-event/dialog/create-location.dialog.component.ts +++ b/frontend/src/app/pages/event-root/create-event/dialog/create-location.dialog.component.ts @@ -1,5 +1,11 @@ import { CommonModule } from '@angular/common'; -import { Component, output, OutputEmitterRef } from '@angular/core'; +import { + Component, + output, + OutputEmitterRef, + signal, + WritableSignal, +} from '@angular/core'; import { FormBuilder, FormGroup, @@ -24,18 +30,33 @@ export class LocationDialogComponent { public locationCreated: OutputEmitterRef = output(); public locationForm: FormGroup = new FormGroup({}); + public formSubmitted: WritableSignal = signal(false); public constructor(private readonly formBuilder: FormBuilder) { this.locationForm = this.formBuilder.group({ + name: ['', [Validators.required, Validators.minLength(3)]], postalCode: ['', [Validators.required, Validators.pattern(/^\d{5}$/)]], - city: ['', Validators.required], - street: ['', Validators.required], - houseNumber: ['', Validators.required], - name: ['', Validators.required], + city: ['', [Validators.required, Validators.minLength(2)]], + street: ['', [Validators.required, Validators.minLength(3)]], + houseNumber: [ + '', + [Validators.required, Validators.pattern(/^[0-9]+[a-zA-Z]?$/)], + ], }); } + public getInputClass(controlName: string): string { + const control = this.locationForm.get(controlName); + + if ((control?.dirty || this.formSubmitted()) && control?.touched) { + return control.valid ? 'input-success' : 'input-error'; + } + return ''; + } + public createLocation(): void { + this.formSubmitted.set(true); + this.locationForm.markAllAsTouched(); if (this.locationForm.valid) { this.locationCreated.emit(this.locationForm.value); this.closeModal(); @@ -47,6 +68,10 @@ export class LocationDialogComponent { 'location_modal' ) as HTMLDialogElement; + this.locationForm.reset(); + this.locationForm.markAsUntouched(); + this.locationForm.markAsPristine(); + this.formSubmitted.set(false); modal.showModal(); } -- 2.40.1 From 95aa8d6b11ca233db1af0a5ea9a934bcfa093702 Mon Sep 17 00:00:00 2001 From: Igor Propisnov Date: Tue, 30 Jul 2024 16:44:04 +0200 Subject: [PATCH 07/20] Implement edit icon to dropdown --- .../dropdown/dropdown.component.html | 36 ++++++++++++++----- .../components/dropdown/dropdown.component.ts | 8 +++++ 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/shared/components/dropdown/dropdown.component.html b/frontend/src/app/shared/components/dropdown/dropdown.component.html index 8425eec..c5e477b 100644 --- a/frontend/src/app/shared/components/dropdown/dropdown.component.html +++ b/frontend/src/app/shared/components/dropdown/dropdown.component.html @@ -35,12 +35,14 @@ @if (filteredItems().length > 0) { @for (item of filteredItems(); track item) {
  • - - {{ item }} +
  • }
    {{ dividerText() }}
    diff --git a/frontend/src/app/shared/components/dropdown/dropdown.component.ts b/frontend/src/app/shared/components/dropdown/dropdown.component.ts index 3de8066..e8817b6 100644 --- a/frontend/src/app/shared/components/dropdown/dropdown.component.ts +++ b/frontend/src/app/shared/components/dropdown/dropdown.component.ts @@ -35,6 +35,7 @@ export class DropdownComponent { ); public itemSelected: OutputEmitterRef = output(); public submitNewItems: OutputEmitterRef = output(); + public itemEdit: OutputEmitterRef = output(); public onInput(event: Event): void { const value = (event.target as HTMLInputElement).value; @@ -62,6 +63,13 @@ export class DropdownComponent { this.showDropdown.set(true); } + public editItem(item: string, event: MouseEvent): void { + event.preventDefault(); + event.stopPropagation(); + // TODO: Implement edit item functionality + this.itemEdit.emit(item); + } + public selectItem(item: string): void { this.searchTerm.set(item); this.selectedItem.set(item); -- 2.40.1 From 4a69d916c6ea873c759746631915a9dfb79ec1f6 Mon Sep 17 00:00:00 2001 From: Igor Propisnov Date: Tue, 30 Jul 2024 20:52:00 +0200 Subject: [PATCH 08/20] connect formgroup with step 1 from the event wizzard --- .../create-event/create-event.component.html | 2 +- .../create-event/create-event.component.ts | 16 ++- .../steps/basic-step.component.html | 23 ++++- .../steps/basic-step.component.ts | 23 ++++- .../dropdown/dropdown.component.html | 9 +- .../components/dropdown/dropdown.component.ts | 99 +++++++++++++++++-- 6 files changed, 151 insertions(+), 21 deletions(-) diff --git a/frontend/src/app/pages/event-root/create-event/create-event.component.html b/frontend/src/app/pages/event-root/create-event/create-event.component.html index 526014c..7faf76d 100644 --- a/frontend/src/app/pages/event-root/create-event/create-event.component.html +++ b/frontend/src/app/pages/event-root/create-event/create-event.component.html @@ -21,7 +21,7 @@
    @if (currentStep() === 0) { - + } @if (currentStep() === 1) { diff --git a/frontend/src/app/pages/event-root/create-event/create-event.component.ts b/frontend/src/app/pages/event-root/create-event/create-event.component.ts index af279e2..b2faf48 100644 --- a/frontend/src/app/pages/event-root/create-event/create-event.component.ts +++ b/frontend/src/app/pages/event-root/create-event/create-event.component.ts @@ -4,8 +4,8 @@ import { ChangeDetectionStrategy, signal, WritableSignal, - OnInit, } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { BasicStepComponent } from './steps/basic-step.component'; import { TicketsStepComponent } from './steps/tickets-step.component'; @@ -18,13 +18,19 @@ import { TicketsStepComponent } from './steps/tickets-step.component'; templateUrl: './create-event.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class CreateEventComponent implements OnInit { +export class CreateEventComponent { public currentStep: WritableSignal = signal(0); public readonly steps: string[] = ['Basic', 'Tickets', 'Review']; + public form!: FormGroup; - public constructor() {} - - public ngOnInit(): void {} + public constructor(private readonly formBuilder: FormBuilder) { + this.form = this.formBuilder.group({ + eventTitle: ['', [Validators.required, Validators.minLength(3)]], + eventLocation: ['', Validators.required], + eventDate: ['', Validators.required], + eventTime: ['', Validators.required], + }); + } public getStepContent(index: number): string { if (index < this.currentStep()) { diff --git a/frontend/src/app/pages/event-root/create-event/steps/basic-step.component.html b/frontend/src/app/pages/event-root/create-event/steps/basic-step.component.html index 4520f5b..63c697d 100644 --- a/frontend/src/app/pages/event-root/create-event/steps/basic-step.component.html +++ b/frontend/src/app/pages/event-root/create-event/steps/basic-step.component.html @@ -1,4 +1,4 @@ -
    +

    Event Basic Information

    @@ -18,6 +20,7 @@
    Date Select the event date
    - +
    @@ -46,12 +54,21 @@ Time Choose the event time
    - +
    + +
    +
    {{ form().value | json }}
    +
    diff --git a/frontend/src/app/pages/event-root/create-event/steps/basic-step.component.ts b/frontend/src/app/pages/event-root/create-event/steps/basic-step.component.ts index a0e85d8..0528008 100644 --- a/frontend/src/app/pages/event-root/create-event/steps/basic-step.component.ts +++ b/frontend/src/app/pages/event-root/create-event/steps/basic-step.component.ts @@ -1,6 +1,12 @@ import { CommonModule } from '@angular/common'; -import { ChangeDetectionStrategy, Component, ViewChild } from '@angular/core'; -import { FormsModule } from '@angular/forms'; +import { + ChangeDetectionStrategy, + Component, + InputSignal, + ViewChild, + input, +} from '@angular/core'; +import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { DropdownComponent } from '../../../../shared/components/dropdown/dropdown.component'; import { @@ -18,6 +24,7 @@ import { FormsModule, DropdownComponent, LocationDialogComponent, + ReactiveFormsModule, ], changeDetection: ChangeDetectionStrategy.OnPush, }) @@ -25,6 +32,7 @@ export class BasicStepComponent { @ViewChild(LocationDialogComponent) public locationModal!: LocationDialogComponent; public items: string[] = ['Nachtigal Köln']; + public form: InputSignal = input.required(); public constructor() {} @@ -32,8 +40,17 @@ export class BasicStepComponent { this.openLocationModal(); } + public getInputClass(controlName: string): string { + const control = this.form().get(controlName); + + if (control?.touched) { + return control.valid ? 'input-success' : 'input-error'; + } + return ''; + } + public onLocationSubmit(location: EventLocation): void { - console.log(location); + //TODO: save location } private openLocationModal(): void { diff --git a/frontend/src/app/shared/components/dropdown/dropdown.component.html b/frontend/src/app/shared/components/dropdown/dropdown.component.html index c5e477b..15ee958 100644 --- a/frontend/src/app/shared/components/dropdown/dropdown.component.html +++ b/frontend/src/app/shared/components/dropdown/dropdown.component.html @@ -1,4 +1,4 @@ -
    +
    + diff --git a/frontend/src/app/shared/components/dropdown/dropdown.component.ts b/frontend/src/app/shared/components/dropdown/dropdown.component.ts index e8817b6..b9d034e 100644 --- a/frontend/src/app/shared/components/dropdown/dropdown.component.ts +++ b/frontend/src/app/shared/components/dropdown/dropdown.component.ts @@ -1,6 +1,7 @@ import { CommonModule } from '@angular/common'; import { Component, + forwardRef, input, InputSignal, output, @@ -8,15 +9,35 @@ import { signal, WritableSignal, } from '@angular/core'; -import { FormsModule } from '@angular/forms'; +import { + AbstractControl, + ControlValueAccessor, + FormsModule, + NG_VALIDATORS, + NG_VALUE_ACCESSOR, + ValidationErrors, + Validator, +} from '@angular/forms'; @Component({ selector: 'app-dropdown', templateUrl: './dropdown.component.html', standalone: true, imports: [CommonModule, FormsModule], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DropdownComponent), + multi: true, + }, + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => DropdownComponent), + multi: true, + }, + ], }) -export class DropdownComponent { +export class DropdownComponent implements ControlValueAccessor, Validator { public label: InputSignal = input.required(); public placeholder: InputSignal = input.required(); public items: InputSignal = input.required(); @@ -37,6 +58,36 @@ export class DropdownComponent { public submitNewItems: OutputEmitterRef = output(); public itemEdit: OutputEmitterRef = output(); + public writeValue(value: string | null): void { + if (value && this.isValidOption(value)) { + this.searchTerm.set(value); + this.selectedItem.set(value); + } else { + this.searchTerm.set(''); + this.selectedItem.set(null); + } + } + + public registerOnChange(fn: (value: string) => void): void { + this.onChange = fn; + } + + public registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } + + public validate(control: AbstractControl): ValidationErrors | null { + const value = control.value; + + if (this.required() && (!value || value.trim().length === 0)) { + return { required: true }; + } + if (value && value.trim().length > 0 && !this.isValidOption(value)) { + return { invalidOption: true }; + } + return null; + } + public onInput(event: Event): void { const value = (event.target as HTMLInputElement).value; @@ -54,13 +105,30 @@ export class DropdownComponent { if (exactMatch) { this.selectedItem.set(exactMatch); + this.onChange(exactMatch); this.itemSelected.emit(exactMatch); } else { this.selectedItem.set(null); + this.onChange(''); this.itemSelected.emit(''); } this.showDropdown.set(true); + this.onTouched(); + } + + public getInputClass(): string { + if (this.selectedItem()) { + return 'input-success'; + } else if ( + this.searchTerm().trim() !== '' && + !this.isValidOption(this.searchTerm()) + ) { + return 'input-error'; + } else if (this.required() && this.searchTerm().trim() === '') { + return 'input-error'; + } + return ''; } public editItem(item: string, event: MouseEvent): void { @@ -71,16 +139,27 @@ export class DropdownComponent { } public selectItem(item: string): void { - this.searchTerm.set(item); - this.selectedItem.set(item); - this.showDropdown.set(false); - this.itemSelected.emit(item); + if (this.isValidOption(item)) { + this.searchTerm.set(item); + this.selectedItem.set(item); + this.showDropdown.set(false); + this.itemSelected.emit(item); + this.onChange(item); + this.onTouched(); + } } public submitNewItem(): void { this.submitNewItems.emit(true); } + public onInputClear(): void { + this.searchTerm.set(''); + this.selectedItem.set(null); + this.onChange(''); + this.onTouched(); + } + public onFocus(): void { this.showDropdown.set(true); this.filteredItems.set(this.items()); @@ -89,6 +168,14 @@ export class DropdownComponent { public onBlur(): void { setTimeout(() => { this.showDropdown.set(false); + this.onTouched(); }, 100); } + + private isValidOption(value: string): boolean { + return this.items().includes(value); + } + + private onChange: (value: string) => void = () => {}; + private onTouched: () => void = () => {}; } -- 2.40.1 From 584f34f947367d179752362e132029dfec6dc620 Mon Sep 17 00:00:00 2001 From: Igor Propisnov Date: Tue, 30 Jul 2024 22:39:48 +0200 Subject: [PATCH 09/20] added some open an close logic for dropdown --- .../create-event/create-event.component.html | 52 ++++++++++--------- .../steps/basic-step.component.html | 4 -- .../dropdown/dropdown.component.html | 7 ++- .../components/dropdown/dropdown.component.ts | 42 ++++++++++++--- 4 files changed, 69 insertions(+), 36 deletions(-) diff --git a/frontend/src/app/pages/event-root/create-event/create-event.component.html b/frontend/src/app/pages/event-root/create-event/create-event.component.html index 7faf76d..ae2d82f 100644 --- a/frontend/src/app/pages/event-root/create-event/create-event.component.html +++ b/frontend/src/app/pages/event-root/create-event/create-event.component.html @@ -1,5 +1,5 @@
    -
    +
      @for (step of steps; track $index) {
    • + class="w-full bg-base-100 max-w-full sticky bottom-0 z-10 px-4 sm:px-8 py-4">
      - } - -
      +
      @if (currentStep() < steps.length - 1) { - - Next to - {{ steps[currentStep() + 1] }} - + } - +
    diff --git a/frontend/src/app/pages/event-root/create-event/steps/basic-step.component.html b/frontend/src/app/pages/event-root/create-event/steps/basic-step.component.html index 63c697d..819fb1b 100644 --- a/frontend/src/app/pages/event-root/create-event/steps/basic-step.component.html +++ b/frontend/src/app/pages/event-root/create-event/steps/basic-step.component.html @@ -65,10 +65,6 @@
    - -
    -
    {{ form().value | json }}
    -
    diff --git a/frontend/src/app/shared/components/dropdown/dropdown.component.html b/frontend/src/app/shared/components/dropdown/dropdown.component.html index 15ee958..2d840d3 100644 --- a/frontend/src/app/shared/components/dropdown/dropdown.component.html +++ b/frontend/src/app/shared/components/dropdown/dropdown.component.html @@ -24,7 +24,8 @@ viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" - class="size-6" + class="size-6 cursor-pointer" + (click)="toggleDropdown()" [class.rotate-180]="showDropdown()"> @if (showDropdown()) {
    + + + + diff --git a/frontend/src/app/pages/event-root/event-empty-state/event-empty-state.component.ts b/frontend/src/app/pages/event-root/event-empty-state/event-empty-state.component.ts index bae31d8..59f79bc 100644 --- a/frontend/src/app/pages/event-root/event-empty-state/event-empty-state.component.ts +++ b/frontend/src/app/pages/event-root/event-empty-state/event-empty-state.component.ts @@ -1,6 +1,13 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + ElementRef, + ViewChild, +} from '@angular/core'; import { Router } from '@angular/router'; +import { VerifyApiService } from '../../../api'; + @Component({ selector: 'app-event-empty-state', standalone: true, @@ -9,9 +16,33 @@ import { Router } from '@angular/router'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class EventEmptyStateComponent { - public constructor(private readonly router: Router) {} + @ViewChild('emailVerificationModal') + public emailVerificationModal!: ElementRef; + + public constructor( + private readonly router: Router, + private readonly verifyApi: VerifyApiService + ) {} public navigateToCreateEvent(): void { - this.router.navigate(['/event/create']); + this.verifyApi + .verifyControllerIsEmailVerified() + .subscribe((isVerified: boolean) => { + if (!isVerified) { + this.openEmailVerificationModal(); + } else { + this.router.navigate(['/event/create']); + } + }); + } + + public closeEmailVerificationModal(): void { + (this.emailVerificationModal.nativeElement as HTMLDialogElement).close(); + } + + private openEmailVerificationModal(): void { + ( + this.emailVerificationModal.nativeElement as HTMLDialogElement + ).showModal(); } } -- 2.40.1 From 1475b13360d3ceacdfbe258e8366b734cb3ae93c Mon Sep 17 00:00:00 2001 From: Igor Propisnov Date: Thu, 22 Aug 2024 10:05:46 +0200 Subject: [PATCH 13/20] added step validation --- .../create-event/create-event.component.html | 9 +-- .../create-event/create-event.component.ts | 58 ++++++++++++++----- 2 files changed, 48 insertions(+), 19 deletions(-) diff --git a/frontend/src/app/pages/event-root/create-event/create-event.component.html b/frontend/src/app/pages/event-root/create-event/create-event.component.html index ae2d82f..e280ad0 100644 --- a/frontend/src/app/pages/event-root/create-event/create-event.component.html +++ b/frontend/src/app/pages/event-root/create-event/create-event.component.html @@ -51,13 +51,10 @@ @if (currentStep() < steps.length - 1) { } diff --git a/frontend/src/app/pages/event-root/create-event/create-event.component.ts b/frontend/src/app/pages/event-root/create-event/create-event.component.ts index b2faf48..eb06dd3 100644 --- a/frontend/src/app/pages/event-root/create-event/create-event.component.ts +++ b/frontend/src/app/pages/event-root/create-event/create-event.component.ts @@ -4,6 +4,7 @@ import { ChangeDetectionStrategy, signal, WritableSignal, + effect, } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; @@ -22,6 +23,8 @@ export class CreateEventComponent { public currentStep: WritableSignal = signal(0); public readonly steps: string[] = ['Basic', 'Tickets', 'Review']; public form!: FormGroup; + public isCurrentStepValid: WritableSignal = signal(false); + public hasAttemptedNextStep: WritableSignal = signal(false); public constructor(private readonly formBuilder: FormBuilder) { this.form = this.formBuilder.group({ @@ -30,6 +33,18 @@ export class CreateEventComponent { eventDate: ['', Validators.required], eventTime: ['', Validators.required], }); + + effect(() => { + this.isCurrentStepValid.set(this.isStepValid(this.currentStep())); + this.hasAttemptedNextStep.set(false); // Reset when step changes + }); + + this.form.valueChanges.subscribe(() => { + this.isCurrentStepValid.set(this.isStepValid(this.currentStep())); + if (this.isCurrentStepValid()) { + this.hasAttemptedNextStep.set(false); // Reset when form becomes valid + } + }); } public getStepContent(index: number): string { @@ -58,23 +73,28 @@ export class CreateEventComponent { } public isStepValid(stepIndex: number): boolean { - // TODO: Implementiere die tatsächliche Validierungslogik hier - // Für Demonstrationszwecke nehmen wir an, dass der erste Schritt immer gültig ist, - // und die anderen zufällig gültig oder ungültig sind - return true; - - // In Zukunft könnte es so aussehen: - // switch(stepIndex) { - // case 0: return this.isBasicStepValid(); - // case 1: return this.isTicketsStepValid(); - // case 2: return this.isReviewStepValid(); - // default: return false; - // } + switch (stepIndex) { + case 0: + return this.form.valid; + case 1: + // TODO: Implement validation for Tickets step + return true; + case 2: + // TODO: Implement validation for Review step + return true; + default: + return false; + } } public nextStep(): void { if (this.currentStep() < this.steps.length - 1) { - this.currentStep.set(this.currentStep() + 1); + if (this.isCurrentStepValid()) { + this.currentStep.set(this.currentStep() + 1); + this.hasAttemptedNextStep.set(false); + } else { + this.hasAttemptedNextStep.set(true); + } } } @@ -83,4 +103,16 @@ export class CreateEventComponent { this.currentStep.set(this.currentStep() - 1); } } + + public getNextButtonClass(): string { + return this.hasAttemptedNextStep() && !this.isCurrentStepValid() + ? 'btn btn-outline btn-warning' + : 'btn btn-primary btn-outline'; + } + + public getNextButtonText(): string { + return this.hasAttemptedNextStep() && !this.isCurrentStepValid() + ? 'Required Fields Missing' + : `Next to ${this.steps[this.currentStep() + 1]}`; + } } -- 2.40.1 From 2805e57e608fe41a94a14bb68ecf05c4c833dc9a Mon Sep 17 00:00:00 2001 From: Igor Propisnov Date: Thu, 22 Aug 2024 11:07:25 +0200 Subject: [PATCH 14/20] added error validation for first create event step --- .../create-event/create-event.component.ts | 14 +++- .../steps/basic-step.component.html | 16 +++- .../steps/basic-step.component.ts | 74 ++++++++++++++++++- .../dropdown/dropdown.component.html | 6 ++ .../components/dropdown/dropdown.component.ts | 35 ++++++--- 5 files changed, 128 insertions(+), 17 deletions(-) diff --git a/frontend/src/app/pages/event-root/create-event/create-event.component.ts b/frontend/src/app/pages/event-root/create-event/create-event.component.ts index eb06dd3..6780d54 100644 --- a/frontend/src/app/pages/event-root/create-event/create-event.component.ts +++ b/frontend/src/app/pages/event-root/create-event/create-event.component.ts @@ -5,6 +5,7 @@ import { signal, WritableSignal, effect, + ViewChild, } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; @@ -20,6 +21,7 @@ import { TicketsStepComponent } from './steps/tickets-step.component'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class CreateEventComponent { + @ViewChild(BasicStepComponent) public basicStep!: BasicStepComponent; public currentStep: WritableSignal = signal(0); public readonly steps: string[] = ['Basic', 'Tickets', 'Review']; public form!: FormGroup; @@ -94,6 +96,7 @@ export class CreateEventComponent { this.hasAttemptedNextStep.set(false); } else { this.hasAttemptedNextStep.set(true); + this.markCurrentStepAsTouched(); } } } @@ -106,13 +109,20 @@ export class CreateEventComponent { public getNextButtonClass(): string { return this.hasAttemptedNextStep() && !this.isCurrentStepValid() - ? 'btn btn-outline btn-warning' + ? 'btn btn-outline btn-error' : 'btn btn-primary btn-outline'; } public getNextButtonText(): string { return this.hasAttemptedNextStep() && !this.isCurrentStepValid() - ? 'Required Fields Missing' + ? 'Required Fields Need Attention' : `Next to ${this.steps[this.currentStep() + 1]}`; } + + private markCurrentStepAsTouched(): void { + if (this.currentStep() === 0 && this.basicStep) { + this.basicStep.markAllAsTouched(); + } + // step 1 and 2 missing + } } diff --git a/frontend/src/app/pages/event-root/create-event/steps/basic-step.component.html b/frontend/src/app/pages/event-root/create-event/steps/basic-step.component.html index 819fb1b..c32aad6 100644 --- a/frontend/src/app/pages/event-root/create-event/steps/basic-step.component.html +++ b/frontend/src/app/pages/event-root/create-event/steps/basic-step.component.html @@ -14,11 +14,13 @@ class="input input-bordered w-full" />
    - + + {{ getErrorMessage('eventTitle') }} +
    -
    +
    @@ -45,7 +49,9 @@ class="input input-bordered w-full" />
    - + + {{ getErrorMessage('eventDate') }} +
    @@ -62,7 +68,9 @@ class="input input-bordered w-full" />
    - + + {{ getErrorMessage('eventTime') }} +
    diff --git a/frontend/src/app/pages/event-root/create-event/steps/basic-step.component.ts b/frontend/src/app/pages/event-root/create-event/steps/basic-step.component.ts index 0528008..3f95edc 100644 --- a/frontend/src/app/pages/event-root/create-event/steps/basic-step.component.ts +++ b/frontend/src/app/pages/event-root/create-event/steps/basic-step.component.ts @@ -6,7 +6,12 @@ import { ViewChild, input, } from '@angular/core'; -import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { + FormGroup, + FormsModule, + ReactiveFormsModule, + ValidationErrors, +} from '@angular/forms'; import { DropdownComponent } from '../../../../shared/components/dropdown/dropdown.component'; import { @@ -31,6 +36,8 @@ import { export class BasicStepComponent { @ViewChild(LocationDialogComponent) public locationModal!: LocationDialogComponent; + @ViewChild(DropdownComponent) + public dropdownComponent!: DropdownComponent; public items: string[] = ['Nachtigal Köln']; public form: InputSignal = input.required(); @@ -53,6 +60,71 @@ export class BasicStepComponent { //TODO: save location } + public markAllAsTouched(): void { + Object.values(this.form().controls).forEach((control) => { + control.markAsTouched(); + }); + if (this.dropdownComponent) { + this.dropdownComponent.markAsTouched(); + } + } + + public getErrorMessage(controlName: string): string { + const control = this.form().get(controlName); + + if (control && control.touched && control.errors) { + return this.getErrorMessageFromValidationErrors( + control.errors, + controlName + ); + } + return ''; + } + + private getErrorMessageFromValidationErrors( + errors: ValidationErrors, + controlName: string + ): string { + switch (controlName) { + case 'eventTitle': + if (errors['required']) { + return 'Event title is required to uniquely identify your event in our system and for attendees.'; + } + if (errors['minlength']) { + return `Please provide a more descriptive title (minimum ${errors['minlength'].requiredLength} characters) to improve discoverability.`; + } + break; + case 'eventLocation': + if (errors['required']) { + return 'Location is crucial for attendees and for our logistics planning. Please select or add a venue.'; + } + if (errors['invalidOption']) { + return 'Please select a valid location from the list or add a new one to ensure accurate event information.'; + } + break; + case 'eventDate': + if (errors['required']) { + return 'Event date is essential for scheduling and marketing. Please select a date for your event.'; + } + // You might add a custom validator for dates in the past + if (errors['pastDate']) { + return 'Please select a future date. Our platform is designed for upcoming events.'; + } + break; + case 'eventTime': + if (errors['required']) { + return 'Start time helps attendees plan their schedule. Please specify when your event begins.'; + } + break; + } + + // Generic messages for any other errors + if (errors['required']) { + return 'This information is required to ensure a complete and accurate event listing.'; + } + return 'Please check this field to ensure all event details are correct and complete.'; + } + private openLocationModal(): void { this.locationModal.openModal(); } diff --git a/frontend/src/app/shared/components/dropdown/dropdown.component.html b/frontend/src/app/shared/components/dropdown/dropdown.component.html index 2d840d3..5a0523f 100644 --- a/frontend/src/app/shared/components/dropdown/dropdown.component.html +++ b/frontend/src/app/shared/components/dropdown/dropdown.component.html @@ -114,6 +114,12 @@ }
    +
    + + + {{ errorMessage() }} + +
    {{ hintBottomLeft() }} {{ hintBottomRight() }} diff --git a/frontend/src/app/shared/components/dropdown/dropdown.component.ts b/frontend/src/app/shared/components/dropdown/dropdown.component.ts index d82b31d..c766f77 100644 --- a/frontend/src/app/shared/components/dropdown/dropdown.component.ts +++ b/frontend/src/app/shared/components/dropdown/dropdown.component.ts @@ -41,6 +41,7 @@ import { }) export class DropdownComponent implements ControlValueAccessor, Validator { public label: InputSignal = input.required(); + public errorMessage: InputSignal = input.required(); public placeholder: InputSignal = input.required(); public items: InputSignal = input.required(); public emptyStateText: InputSignal = input(''); @@ -59,6 +60,7 @@ export class DropdownComponent implements ControlValueAccessor, Validator { public itemSelected: OutputEmitterRef = output(); public submitNewItems: OutputEmitterRef = output(); public itemEdit: OutputEmitterRef = output(); + public touched: WritableSignal = signal(false); private isMouseInDropdown: WritableSignal = signal(false); public constructor(private readonly elementRef: ElementRef) {} @@ -78,6 +80,7 @@ export class DropdownComponent implements ControlValueAccessor, Validator { this.searchTerm.set(''); this.selectedItem.set(null); } + this.touched.set(false); } public registerOnChange(fn: (value: string) => void): void { @@ -85,7 +88,10 @@ export class DropdownComponent implements ControlValueAccessor, Validator { } public registerOnTouched(fn: () => void): void { - this.onTouched = fn; + this.onTouched = (): void => { + this.touched.set(true); + fn(); + }; } public validate(control: AbstractControl): ValidationErrors | null { @@ -148,19 +154,28 @@ export class DropdownComponent implements ControlValueAccessor, Validator { } public getInputClass(): string { - if (this.selectedItem()) { - return 'input-success'; - } else if ( - this.searchTerm().trim() !== '' && - !this.isValidOption(this.searchTerm()) - ) { - return 'input-error'; - } else if (this.required() && this.searchTerm().trim() === '') { - return 'input-error'; + if (this.touched()) { + if (this.selectedItem()) { + return 'input-success'; + } else if ( + this.searchTerm().trim() !== '' && + !this.isValidOption(this.searchTerm()) + ) { + return 'input-error'; + } else if (this.required() && this.searchTerm().trim() === '') { + return 'input-error'; + } } return ''; } + public markAsTouched(): void { + if (!this.touched()) { + this.touched.set(true); + this.onTouched(); + } + } + public editItem(item: string, event: MouseEvent): void { event.preventDefault(); event.stopPropagation(); -- 2.40.1 From 70f413f453b5900bd6b19dcff8ff030a1ec174e1 Mon Sep 17 00:00:00 2001 From: Igor Propisnov Date: Thu, 22 Aug 2024 13:18:31 +0200 Subject: [PATCH 15/20] move stepper logic to own component --- .../create-event/create-event.component.html | 58 +++++++--------- .../create-event/create-event.component.ts | 41 +++++++++-- .../stepper-indicator.component.html | 69 +++++++++++++++++++ .../stepper-indicator.component.ts | 24 +++++++ 4 files changed, 154 insertions(+), 38 deletions(-) create mode 100644 frontend/src/app/shared/components/stepper-indicator/stepper-indicator.component.html create mode 100644 frontend/src/app/shared/components/stepper-indicator/stepper-indicator.component.ts diff --git a/frontend/src/app/pages/event-root/create-event/create-event.component.html b/frontend/src/app/pages/event-root/create-event/create-event.component.html index e280ad0..be43da3 100644 --- a/frontend/src/app/pages/event-root/create-event/create-event.component.html +++ b/frontend/src/app/pages/event-root/create-event/create-event.component.html @@ -1,23 +1,18 @@
    -
    -
      - @for (step of steps; track $index) { -
    • - {{ step }} -
    • - } -
    +
    +
    + +
    + +
    @if (currentStep() === 0) { @@ -33,29 +28,24 @@ class="w-full bg-base-100 max-w-full sticky bottom-0 z-10 px-4 sm:px-8 py-4">
    - @if (currentStep() > 0) { - - } +
    @if (currentStep() < steps.length - 1) { }
    diff --git a/frontend/src/app/pages/event-root/create-event/create-event.component.ts b/frontend/src/app/pages/event-root/create-event/create-event.component.ts index 6780d54..e6461c3 100644 --- a/frontend/src/app/pages/event-root/create-event/create-event.component.ts +++ b/frontend/src/app/pages/event-root/create-event/create-event.component.ts @@ -9,6 +9,8 @@ import { } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { StepperIndicatorComponent } from '../../../shared/components/stepper-indicator/stepper-indicator.component'; + import { BasicStepComponent } from './steps/basic-step.component'; import { TicketsStepComponent } from './steps/tickets-step.component'; @@ -16,7 +18,12 @@ import { TicketsStepComponent } from './steps/tickets-step.component'; selector: 'app-create-event', standalone: true, providers: [], - imports: [CommonModule, BasicStepComponent, TicketsStepComponent], + imports: [ + CommonModule, + BasicStepComponent, + TicketsStepComponent, + StepperIndicatorComponent, + ], templateUrl: './create-event.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) @@ -114,9 +121,35 @@ export class CreateEventComponent { } public getNextButtonText(): string { - return this.hasAttemptedNextStep() && !this.isCurrentStepValid() - ? 'Required Fields Need Attention' - : `Next to ${this.steps[this.currentStep() + 1]}`; + if (this.hasAttemptedNextStep() && !this.isCurrentStepValid()) { + return 'Required Fields Need Attention'; + } else { + return `Next to ${this.steps[this.currentStep() + 1]}`; + } + } + + public getNextButtonAriaLabel(): string { + if (this.hasAttemptedNextStep() && !this.isCurrentStepValid()) { + return 'Required Fields Need Attention'; + } else { + return `Next to ${this.steps[this.currentStep() + 1]}`; + } + } + + public getBackButtonText(): string { + if (this.currentStep() === 0) { + return 'Back'; + } else { + return `Back to ${this.steps[this.currentStep() - 1]}`; + } + } + + public getBackButtonAriaLabel(): string { + if (this.currentStep() === 0) { + return 'Back (disabled)'; + } else { + return `Back to ${this.steps[this.currentStep() - 1]}`; + } } private markCurrentStepAsTouched(): void { diff --git a/frontend/src/app/shared/components/stepper-indicator/stepper-indicator.component.html b/frontend/src/app/shared/components/stepper-indicator/stepper-indicator.component.html new file mode 100644 index 0000000..0f854a2 --- /dev/null +++ b/frontend/src/app/shared/components/stepper-indicator/stepper-indicator.component.html @@ -0,0 +1,69 @@ +
    +
    + +
    + @for (step of steps; track $index) { + @if ($index < steps.length - 1) { +
    + } + } +
    + + + @for (step of steps; track $index) { +
    + + + {{ step }} + +
    + } +
    +
    diff --git a/frontend/src/app/shared/components/stepper-indicator/stepper-indicator.component.ts b/frontend/src/app/shared/components/stepper-indicator/stepper-indicator.component.ts new file mode 100644 index 0000000..b1a9498 --- /dev/null +++ b/frontend/src/app/shared/components/stepper-indicator/stepper-indicator.component.ts @@ -0,0 +1,24 @@ +import { CommonModule } from '@angular/common'; +import { Component, Input, Output, EventEmitter } from '@angular/core'; + +@Component({ + selector: 'app-stepper-indicator', + standalone: true, + imports: [CommonModule], + templateUrl: './stepper-indicator.component.html', + styles: [], +}) +export class StepperIndicatorComponent { + @Output() public stepChange: EventEmitter = + new EventEmitter(); + @Input() public steps: string[] = []; + @Input() public currentStep: number = 0; + @Input() public isStepValid: (index: number) => boolean = () => true; + @Input() public canAdvanceToStep: (index: number) => boolean = () => true; + + public goToStep(index: number): void { + if (index <= this.currentStep || this.canAdvanceToStep(index)) { + this.stepChange.emit(index); + } + } +} -- 2.40.1 From 68c21f3675815458c3a7e022bd7da7f98da1115d Mon Sep 17 00:00:00 2001 From: Igor Propisnov Date: Thu, 22 Aug 2024 13:25:23 +0200 Subject: [PATCH 16/20] update import --- .../app/pages/event-root/create-event/create-event.component.ts | 2 +- frontend/src/app/shared/components/index.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/pages/event-root/create-event/create-event.component.ts b/frontend/src/app/pages/event-root/create-event/create-event.component.ts index e6461c3..85d6e32 100644 --- a/frontend/src/app/pages/event-root/create-event/create-event.component.ts +++ b/frontend/src/app/pages/event-root/create-event/create-event.component.ts @@ -9,7 +9,7 @@ import { } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { StepperIndicatorComponent } from '../../../shared/components/stepper-indicator/stepper-indicator.component'; +import { StepperIndicatorComponent } from '../../../shared/components'; import { BasicStepComponent } from './steps/basic-step.component'; import { TicketsStepComponent } from './steps/tickets-step.component'; diff --git a/frontend/src/app/shared/components/index.ts b/frontend/src/app/shared/components/index.ts index c35cdb5..3990296 100644 --- a/frontend/src/app/shared/components/index.ts +++ b/frontend/src/app/shared/components/index.ts @@ -1 +1,2 @@ export * from './dropdown/dropdown.component'; +export * from './stepper-indicator/stepper-indicator.component'; -- 2.40.1 From 235edd247cbe5c980b2b2810fa3659de09bd8011 Mon Sep 17 00:00:00 2001 From: Igor Propisnov Date: Thu, 22 Aug 2024 13:29:23 +0200 Subject: [PATCH 17/20] validate field on touch --- .../src/app/shared/components/dropdown/dropdown.component.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/shared/components/dropdown/dropdown.component.ts b/frontend/src/app/shared/components/dropdown/dropdown.component.ts index c766f77..2609bde 100644 --- a/frontend/src/app/shared/components/dropdown/dropdown.component.ts +++ b/frontend/src/app/shared/components/dropdown/dropdown.component.ts @@ -208,12 +208,13 @@ export class DropdownComponent implements ControlValueAccessor, Validator { public onFocus(): void { this.filteredItems.set(this.items()); + this.markAsTouched(); } public onBlur(): void { - if (!this.isMouseInDropdown) { + if (!this.isMouseInDropdown()) { this.closeDropdown(); - this.onTouched(); + this.markAsTouched(); } } -- 2.40.1 From ce60fbf9062d842b061ca4b4c354aadc802a55ab Mon Sep 17 00:00:00 2001 From: Igor Propisnov Date: Thu, 22 Aug 2024 13:56:33 +0200 Subject: [PATCH 18/20] change colors --- .../stepper-indicator/stepper-indicator.component.html | 6 ------ 1 file changed, 6 deletions(-) diff --git a/frontend/src/app/shared/components/stepper-indicator/stepper-indicator.component.html b/frontend/src/app/shared/components/stepper-indicator/stepper-indicator.component.html index 0f854a2..ab9e7a7 100644 --- a/frontend/src/app/shared/components/stepper-indicator/stepper-indicator.component.html +++ b/frontend/src/app/shared/components/stepper-indicator/stepper-indicator.component.html @@ -7,11 +7,6 @@