AltitudeWeb/frontend/src/app/pages/forms/staff-application/staff-application.component.ts

380 lines
12 KiB
TypeScript

import {
AfterViewInit,
Component,
computed,
ElementRef,
inject,
OnDestroy,
OnInit,
Renderer2,
signal
} from '@angular/core';
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
import {ApplicationsService, EmailEntry, MailService, StaffApplication} from '@api';
import {HeaderComponent} from '@header/header.component';
import {NgOptimizedImage} from '@angular/common';
import {MatButtonModule} from '@angular/material/button';
import {MatIconModule} from '@angular/material/icon';
import {AuthService} from '@services/auth.service';
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatSelectModule} from '@angular/material/select';
import {MatInputModule} from '@angular/material/input';
import {MatDialog} from '@angular/material/dialog';
import {VerifyMailDialogComponent} from '@pages/forms/verify-mail-dialog/verify-mail-dialog.component';
import {Router} from '@angular/router';
import {MatCheckboxModule} from '@angular/material/checkbox';
import {MatDatepickerModule} from '@angular/material/datepicker';
import {MatNativeDateModule} from '@angular/material/core';
import {MatChipsModule} from '@angular/material/chips';
import {MatSnackBar} from '@angular/material/snack-bar';
@Component({
selector: 'app-staff-application',
imports: [
HeaderComponent,
NgOptimizedImage,
MatButtonModule,
MatIconModule,
MatProgressSpinnerModule,
MatFormFieldModule,
MatSelectModule,
MatInputModule,
ReactiveFormsModule,
MatCheckboxModule,
MatDatepickerModule,
MatNativeDateModule,
MatChipsModule
],
templateUrl: './staff-application.component.html',
styleUrl: './staff-application.component.scss'
})
export class StaffApplicationComponent implements OnInit, OnDestroy, AfterViewInit {
private mailService = inject(MailService);
private matSnackBar = inject(MatSnackBar);
public authService = inject(AuthService);
public staffApplicationService = inject(ApplicationsService)
private resizeObserver: ResizeObserver | null = null;
private boundHandleResize: any;
protected isSubmitting = signal<boolean>(false);
protected form: FormGroup<StaffApplicationForm>;
private emails = signal<EmailEntry[]>([]);
protected verifiedEmails = computed(() => this.emails()
.filter(email => email.verified)
.map(email => email.email.toLowerCase()));
protected emailIsValid = signal<boolean>(false);
protected dialog = inject(MatDialog);
protected availableDays: string[] = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
protected selectedDays: string[] = [];
protected userTimezone: string = Intl.DateTimeFormat().resolvedOptions().timeZone;
protected staffApplicationsIsOpen = signal<boolean>(false)
constructor(
private elementRef: ElementRef,
private renderer: Renderer2
) {
const staffApplication: StaffApplicationForm = {
email: new FormControl('', {
nonNullable: true,
validators: [Validators.required, Validators.email, Validators.maxLength(320)]
}),
age: new FormControl('', {
nonNullable: true,
validators: [Validators.required, Validators.min(13), Validators.pattern('^[0-9]*$')]
}),
discordUsername: new FormControl('', {
nonNullable: true,
validators: [Validators.required, Validators.maxLength(32)]
}),
meetsRequirements: new FormControl(false, {
nonNullable: true,
validators: [Validators.requiredTrue]
}),
pronouns: new FormControl('', {
nonNullable: true,
validators: [Validators.maxLength(32)]
}),
joinDate: new FormControl('', {
nonNullable: true,
validators: [Validators.required]
}),
weeklyPlaytime: new FormControl('', {
nonNullable: true,
validators: [Validators.required, Validators.min(1), Validators.pattern('^[0-9]*$')]
}),
availableDays: new FormControl([], {
nonNullable: true,
validators: [Validators.required]
}),
availableTimes: new FormControl('', {
nonNullable: true,
validators: [Validators.required, Validators.maxLength(900)]
}),
previousExperience: new FormControl('', {
nonNullable: true,
validators: [Validators.required, Validators.minLength(10), Validators.maxLength(4000)]
}),
pluginExperience: new FormControl('', {
nonNullable: true,
validators: [Validators.required, Validators.minLength(10), Validators.maxLength(4000)]
}),
moderatorExpectations: new FormControl('', {
nonNullable: true,
validators: [Validators.required, Validators.minLength(10), Validators.maxLength(4000)]
}),
additionalInfo: new FormControl('', {
nonNullable: true,
validators: [Validators.maxLength(4000)]
})
}
this.form = new FormGroup(staffApplication);
this.mailService.getUserEmails().subscribe(emails => {
this.emails.set(emails);
});
this.form.valueChanges.subscribe(() => {
if (this.verifiedEmails().includes(this.form.getRawValue().email.toLowerCase())) {
this.emailIsValid.set(true);
} else {
this.emailIsValid.set(false);
}
});
computed(() => {
if (this.verifiedEmails().length > 0) {
this.form.get('email')?.setValue(this.verifiedEmails()[0]);
this.emailIsValid.set(true);
}
});
}
ngOnInit() {
this.staffApplicationService.getStaffApplicationsIsOpen().subscribe(isOpen => {
this.staffApplicationsIsOpen.set(isOpen)
})
const uuid = this.authService.getUuid();
if (uuid === null) {
alert('Error retrieving token, please relog on the website and try again')
throw new Error('JWT subject is null, are you logged in?');
}
}
ngAfterViewInit() {
this.setupResizeObserver();
this.updateContainerHeight();
this.boundHandleResize = this.handleResize.bind(this);
window.addEventListener('resize', this.boundHandleResize);
setTimeout(() => this.updateContainerHeight(), 0);
}
ngOnDestroy() {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
}
if (this.boundHandleResize) {
window.removeEventListener('resize', this.boundHandleResize);
}
}
private handleResize() {
this.updateContainerHeight();
}
private setupResizeObserver() {
this.resizeObserver = new ResizeObserver(() => {
this.updateContainerHeight();
});
const headerElement = document.querySelector('app-header');
if (headerElement) {
this.resizeObserver.observe(headerElement);
}
const footerElement = document.querySelector('footer');
if (footerElement) {
this.resizeObserver.observe(footerElement);
}
}
private updateContainerHeight() {
const headerElement = document.querySelector('app-header');
const footerElement = document.querySelector('footer');
const container = this.elementRef.nativeElement.querySelector('.staff-application-container');
if (headerElement && footerElement && container) {
const headerHeight = headerElement.getBoundingClientRect().height;
const footerHeight = footerElement.getBoundingClientRect().height;
const calculatedHeight = `calc(100vh - ${headerHeight}px - ${footerHeight}px)`;
this.renderer.setStyle(container, 'min-height', calculatedHeight);
}
}
public onSubmit() {
this.isSubmitting.set(true);
if (this.form === undefined) {
console.error('Form is undefined');
this.matSnackBar.open('An error occurred, please try again later')
this.isSubmitting.set(false);
return
}
if (this.form.valid) {
this.sendForm()
} else {
// Mark all fields as touched to show validation errors
Object.keys(this.form.controls).forEach(field => {
const control = this.form.get(field);
control?.markAsTouched();
});
this.matSnackBar.open('Please fill out all required fields')
this.isSubmitting.set(false);
}
}
isFormInvalid() {
return !this.form.valid
}
private router = inject(Router)
private sendForm() {
const staffApplication: StaffApplication = this.mapToStaffApplication(this.form.getRawValue());
this.staffApplicationService.submitStaffApplication(staffApplication).subscribe({
next: result => {
if (!result.verified_mail) {
this.isSubmitting.set(false);
this.matSnackBar.open('Your email has not been verified. Please verify your email before submitting.', 'Close', {
duration: 5000
});
return;
}
this.router.navigate(['/forms/sent'], {
state: {message: result.message}
}).then();
},
error: (error) => {
this.isSubmitting.set(false);
console.error('Error submitting application:', error);
this.matSnackBar.open('An error occurred while submitting your application. Please try again later.', 'Close', {
duration: 5000
});
}
});
}
public currentPageIndex: number = 0;
public totalPages: number[] = [0, 1, 2, 3, 4];
public goToPage(pageIndex: number): void {
if (pageIndex >= 0 && pageIndex < this.totalPages.length) {
this.currentPageIndex = pageIndex;
}
}
public previousPage() {
this.goToPage(this.currentPageIndex - 1);
}
public nextPage() {
this.goToPage(this.currentPageIndex + 1);
}
public isFirstPage(): boolean {
return this.currentPageIndex === 0;
}
public isLastPage(): boolean {
return this.currentPageIndex === this.totalPages.length - 1;
}
protected validateMailOrNextPage() {
if (this.emailIsValid()) {
this.nextPage();
return;
}
const dialogRef = this.dialog.open(VerifyMailDialogComponent, {
data: {email: this.form.getRawValue().email},
});
dialogRef.afterClosed().subscribe(result => {
if (result === true) {
this.emailIsValid.set(true);
this.nextPage();
}
});
}
toggleDay(day: string) {
const availableDaysControl = this.form.get('availableDays');
const currentDays = [...(availableDaysControl?.value || [])];
if (currentDays.includes(day)) {
const index = currentDays.indexOf(day);
currentDays.splice(index, 1);
} else {
currentDays.push(day);
}
availableDaysControl?.setValue(currentDays);
}
private mapToStaffApplication(formData: any): StaffApplication {
let joinDateString: string;
if (formData.joinDate instanceof Date) {
joinDateString = formData.joinDate.toISOString();
} else if (typeof formData.joinDate === 'string' && formData.joinDate.trim() !== '') {
const parsedDate = new Date(formData.joinDate);
if (isNaN(parsedDate.getTime())) {
throw new Error('Invalid date string');
}
joinDateString = parsedDate.toISOString();
} else {
throw new Error('Invalid date string');
}
return {
email: formData.email,
age: Number(formData.age),
discordUsername: formData.discordUsername,
meetsRequirements: formData.meetsRequirements,
pronouns: formData.pronouns || '',
joinDate: joinDateString,
weeklyPlaytime: Number(formData.weeklyPlaytime),
availableDays: formData.availableDays,
availableTimes: `Timezone: ${this.userTimezone}\nAvailable Times: ${formData.availableTimes}`,
previousExperience: formData.previousExperience,
pluginExperience: formData.pluginExperience,
moderatorExpectations: formData.moderatorExpectations,
additionalInfo: formData.additionalInfo || ''
};
}
}
interface StaffApplicationForm {
email: FormControl<string>;
age: FormControl<string>;
discordUsername: FormControl<string>;
meetsRequirements: FormControl<boolean>;
pronouns: FormControl<string>;
joinDate: FormControl<string>;
weeklyPlaytime: FormControl<string>;
availableDays: FormControl<string[]>;
availableTimes: FormControl<string>;
previousExperience: FormControl<string>;
pluginExperience: FormControl<string>;
moderatorExpectations: FormControl<string>;
additionalInfo: FormControl<string>;
}