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(false); protected form: FormGroup; private emails = signal([]); protected verifiedEmails = computed(() => this.emails() .filter(email => email.verified) .map(email => email.email.toLowerCase())); protected emailIsValid = signal(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(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; age: FormControl; discordUsername: FormControl; meetsRequirements: FormControl; pronouns: FormControl; joinDate: FormControl; weeklyPlaytime: FormControl; availableDays: FormControl; availableTimes: FormControl; previousExperience: FormControl; pluginExperience: FormControl; moderatorExpectations: FormControl; additionalInfo: FormControl; }