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();