Add staff application feature with API integration and frontend form implementation
This commit is contained in:
parent
2a0f38aa28
commit
f886609a0e
|
|
@ -40,6 +40,10 @@ dependencies {
|
|||
implementation("org.springframework.boot:spring-boot-starter-mail:3.1.5")
|
||||
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
|
||||
|
||||
//Open API
|
||||
implementation("io.swagger.core.v3:swagger-annotations:2.2.37")
|
||||
implementation("io.swagger.core.v3:swagger-models:2.2.37")
|
||||
|
||||
//AOP
|
||||
implementation("org.aspectj:aspectjrt:1.9.19")
|
||||
implementation("org.aspectj:aspectjweaver:1.9.19")
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import com.alttd.altitudeweb.database.web_db.forms.AppealMapper;
|
|||
import com.alttd.altitudeweb.database.web_db.mail.EmailVerification;
|
||||
import com.alttd.altitudeweb.database.web_db.mail.EmailVerificationMapper;
|
||||
import com.alttd.altitudeweb.mappers.AppealDataMapper;
|
||||
import com.alttd.altitudeweb.model.AppealResponseDto;
|
||||
import com.alttd.altitudeweb.model.DiscordAppealDto;
|
||||
import com.alttd.altitudeweb.model.FormResponseDto;
|
||||
import com.alttd.altitudeweb.model.MinecraftAppealDto;
|
||||
import com.alttd.altitudeweb.model.UpdateMailDto;
|
||||
import com.alttd.altitudeweb.services.limits.RateLimit;
|
||||
|
|
@ -25,7 +25,6 @@ import org.springframework.web.bind.annotation.RestController;
|
|||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
|
|
@ -41,13 +40,13 @@ public class AppealController implements AppealsApi {
|
|||
|
||||
@RateLimit(limit = 3, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "discordAppeal")
|
||||
@Override
|
||||
public ResponseEntity<AppealResponseDto> submitDiscordAppeal(DiscordAppealDto discordAppealDto) {
|
||||
public ResponseEntity<FormResponseDto> submitDiscordAppeal(DiscordAppealDto discordAppealDto) {
|
||||
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Discord appeals are not yet supported");
|
||||
}
|
||||
|
||||
@RateLimit(limit = 3, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "minecraftAppeal")
|
||||
@Override
|
||||
public ResponseEntity<AppealResponseDto> submitMinecraftAppeal(MinecraftAppealDto minecraftAppealDto) {
|
||||
public ResponseEntity<FormResponseDto> submitMinecraftAppeal(MinecraftAppealDto minecraftAppealDto) {
|
||||
boolean success = true;
|
||||
CompletableFuture<Appeal> appealCompletableFuture = new CompletableFuture<>();
|
||||
|
||||
|
|
@ -105,7 +104,7 @@ public class AppealController implements AppealsApi {
|
|||
sqlSession.getMapper(AppealMapper.class)
|
||||
.markAppealAsSent(appeal.id());
|
||||
});
|
||||
AppealResponseDto appealResponseDto = new AppealResponseDto(
|
||||
FormResponseDto appealResponseDto = new FormResponseDto(
|
||||
appeal.id().toString(),
|
||||
"Your appeal has been submitted. You will be notified when it has been reviewed.",
|
||||
true);
|
||||
|
|
@ -113,7 +112,8 @@ public class AppealController implements AppealsApi {
|
|||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<AppealResponseDto> updateMail(UpdateMailDto updateMailDto) {
|
||||
public ResponseEntity<FormResponseDto> updateMail(UpdateMailDto updateMailDto) {
|
||||
//TODO move to its own endpoint
|
||||
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Updating mail is not yet supported");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
package com.alttd.altitudeweb.controllers.forms;
|
||||
|
||||
import com.alttd.altitudeweb.api.ApplicationsApi;
|
||||
import com.alttd.altitudeweb.model.FormResponseDto;
|
||||
import com.alttd.altitudeweb.model.StaffApplicationDto;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
public class ApplicationController implements ApplicationsApi {
|
||||
@Override
|
||||
public ResponseEntity<FormResponseDto> submitStaffApplication(StaffApplicationDto staffApplicationDto) {
|
||||
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Staff applications are not yet supported");
|
||||
}
|
||||
}
|
||||
|
|
@ -126,6 +126,14 @@ export const routes: Routes = [
|
|||
requiredAuthorizations: ['SCOPE_user']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'forms/staff-application',
|
||||
loadComponent: () => import('./pages/forms/staff-application/staff-application.component').then(m => m.StaffApplicationComponent),
|
||||
canActivate: [AuthGuard],
|
||||
data: {
|
||||
requiredAuthorizations: ['SCOPE_user']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'community',
|
||||
loadComponent: () => import('./pages/altitude/community/community.component').then(m => m.CommunityComponent)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,14 @@
|
|||
</p>
|
||||
</a>
|
||||
</div>
|
||||
<div class="columnParagraph">
|
||||
<a [routerLink]="['/forms/staff-application']">
|
||||
<h2>Staff Application</h2>
|
||||
<p>
|
||||
Interested in becoming a moderator on our server? Apply here.
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,309 @@
|
|||
<div>
|
||||
<app-header [current_page]="'staff-application'" height="200px" background_image="/public/img/backgrounds/staff.png"
|
||||
[overlay_gradient]="0.5">
|
||||
<div class="title" header-content>
|
||||
<h1>Staff Application</h1>
|
||||
</div>
|
||||
</app-header>
|
||||
<main>
|
||||
<section class="darkmodeSection staff-application-container">
|
||||
<div class="form-container">
|
||||
<div class="pages">
|
||||
<!-- Welcome Page -->
|
||||
@if (currentPageIndex === 0) {
|
||||
<section class="formPage">
|
||||
<img ngSrc="/public/img/logos/logo.png" alt="Logo" height="319" width="550"/>
|
||||
<h1>Moderator Application</h1>
|
||||
<p>Thank you for your interest in becoming a moderator on our Minecraft server.</p>
|
||||
<p>Please take your time to fill out this application thoroughly.</p>
|
||||
<button mat-raised-button (click)="nextPage()">
|
||||
Get Started
|
||||
</button>
|
||||
</section>
|
||||
}
|
||||
|
||||
<!-- Confirmation Page -->
|
||||
@if (currentPageIndex === 1) {
|
||||
<section class="formPage">
|
||||
<div class="description">
|
||||
<p>You are logged in as <strong>{{ authService.username() }}</strong>. If this is the correct account
|
||||
please continue</p>
|
||||
<br>
|
||||
<p><strong>Notice: </strong> Submitting a staff application is <strong>not</strong> an instant process.
|
||||
We will review your application carefully and get back to you if we think you're a good fit.</p>
|
||||
<p style="font-style: italic;">Applications that seem to have been made with
|
||||
little to no effort will be automatically rejected.</p>
|
||||
</div>
|
||||
<button mat-raised-button (click)="nextPage()" [disabled]="authService.username() == null">
|
||||
I, {{ authService.username() }}, understand and agree
|
||||
</button>
|
||||
</section>
|
||||
}
|
||||
|
||||
<form [formGroup]="form">
|
||||
<!-- Basic Information Page -->
|
||||
@if (currentPageIndex === 2) {
|
||||
<section class="formPage">
|
||||
<div class="description">
|
||||
<h2>Basic Information</h2>
|
||||
|
||||
<!-- Email -->
|
||||
<mat-form-field appearance="fill" style="width: 100%;">
|
||||
<mat-label>Email</mat-label>
|
||||
<input matInput
|
||||
formControlName="email"
|
||||
placeholder="Email">
|
||||
@if (form.controls.email.invalid && form.controls.email.touched) {
|
||||
<mat-error>
|
||||
@if (form.controls.email.errors?.['required']) {
|
||||
Email is required
|
||||
} @else if (form.controls.email.errors?.['email']) {
|
||||
Please enter a valid email address
|
||||
}
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
@if (emailIsValid()) {
|
||||
<div class="valid-email">
|
||||
<ng-container matSuffix>
|
||||
<mat-icon>check</mat-icon>
|
||||
<span>You have validated your email previously, and can continue to the next page!</span>
|
||||
</ng-container>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Age -->
|
||||
<mat-form-field appearance="fill" style="width: 100%;">
|
||||
<mat-label>Age</mat-label>
|
||||
<input matInput
|
||||
formControlName="age"
|
||||
placeholder="Age">
|
||||
@if (form.controls.age.invalid && form.controls.age.touched) {
|
||||
<mat-error>
|
||||
@if (form.controls.age.errors?.['required']) {
|
||||
Age is required
|
||||
} @else if (form.controls.age.errors?.['min']) {
|
||||
You must be at least 13 years old
|
||||
} @else if (form.controls.age.errors?.['pattern']) {
|
||||
Please enter a valid number
|
||||
}
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Discord Username -->
|
||||
<mat-form-field appearance="fill" style="width: 100%;">
|
||||
<mat-label>Discord Username</mat-label>
|
||||
<input matInput
|
||||
formControlName="discordUsername"
|
||||
placeholder="Discord Username">
|
||||
@if (form.controls.discordUsername.invalid && form.controls.discordUsername.touched) {
|
||||
<mat-error>
|
||||
Discord username is required
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<!-- PC Requirements -->
|
||||
<div class="checkbox-field">
|
||||
<mat-checkbox formControlName="meetsRequirements">
|
||||
I confirm that I meet the PC requirements (able to record video at 30fps 720p or higher, and able to talk in voice chat)
|
||||
</mat-checkbox>
|
||||
@if (form.controls.meetsRequirements.invalid && form.controls.meetsRequirements.touched) {
|
||||
<mat-error class="checkbox-error">
|
||||
You must meet the PC requirements to apply
|
||||
</mat-error>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Pronouns (Optional) -->
|
||||
<mat-form-field appearance="fill" style="width: 100%;">
|
||||
<mat-label>Pronouns (Optional)</mat-label>
|
||||
<input matInput
|
||||
formControlName="pronouns"
|
||||
placeholder="Pronouns">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<button mat-raised-button (click)="validateMailOrNextPage()"
|
||||
[disabled]="form.controls.email.invalid || form.controls.age.invalid || form.controls.discordUsername.invalid || !form.controls.meetsRequirements.value">
|
||||
Next
|
||||
</button>
|
||||
</section>
|
||||
}
|
||||
|
||||
<!-- Experience Page -->
|
||||
@if (currentPageIndex === 3) {
|
||||
<section class="formPage">
|
||||
<div class="description">
|
||||
<h2>Experience & Availability</h2>
|
||||
|
||||
<!-- Join Date -->
|
||||
<mat-form-field appearance="fill" style="width: 100%;">
|
||||
<mat-label>When did you join our server? (Estimate)</mat-label>
|
||||
<input matInput [matDatepicker]="picker" formControlName="joinDate">
|
||||
<mat-datepicker-toggle matIconSuffix [for]="picker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #picker></mat-datepicker>
|
||||
@if (form.controls.joinDate.invalid && form.controls.joinDate.touched) {
|
||||
<mat-error>
|
||||
Join date is required
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Weekly Playtime -->
|
||||
<mat-form-field appearance="fill" style="width: 100%;">
|
||||
<mat-label>Average expected playtime in a week (hours)</mat-label>
|
||||
<input matInput
|
||||
formControlName="weeklyPlaytime"
|
||||
placeholder="Hours per week">
|
||||
@if (form.controls.weeklyPlaytime.invalid && form.controls.weeklyPlaytime.touched) {
|
||||
<mat-error>
|
||||
@if (form.controls.weeklyPlaytime.errors?.['required']) {
|
||||
Weekly playtime is required
|
||||
} @else if (form.controls.weeklyPlaytime.errors?.['min']) {
|
||||
Weekly playtime must be at least 1 hour
|
||||
}
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Available Days -->
|
||||
<div class="field-container">
|
||||
<label class="field-label">Available days for moderating:</label>
|
||||
<div class="days-container">
|
||||
@for (day of availableDays; track day) {
|
||||
<div class="day-chip" [class.selected]="form.controls.availableDays.value.includes(day)" (click)="toggleDay(day)">
|
||||
{{ day }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (form.controls.availableDays.invalid && form.controls.availableDays.touched) {
|
||||
<mat-error>
|
||||
Please select at least one day
|
||||
</mat-error>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Available Times -->
|
||||
<mat-form-field appearance="fill" style="width: 100%;">
|
||||
<mat-label>Available times (Your timezone: {{ userTimezone }})</mat-label>
|
||||
<textarea matInput
|
||||
formControlName="availableTimes"
|
||||
placeholder="e.g., 6PM-10PM weekdays, 2PM-8PM weekends"
|
||||
rows="2"></textarea>
|
||||
@if (form.controls.availableTimes.invalid && form.controls.availableTimes.touched) {
|
||||
<mat-error>
|
||||
Available times are required
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<button mat-raised-button (click)="nextPage()"
|
||||
[disabled]="form.controls.joinDate.invalid || form.controls.weeklyPlaytime.invalid || form.controls.availableDays.invalid || form.controls.availableTimes.invalid">
|
||||
Next
|
||||
</button>
|
||||
</section>
|
||||
}
|
||||
|
||||
<!-- Qualifications Page -->
|
||||
@if (currentPageIndex === 4) {
|
||||
<section class="formPage">
|
||||
<div class="description">
|
||||
<h2>Qualifications & Expectations</h2>
|
||||
|
||||
<!-- Previous Experience -->
|
||||
<mat-form-field appearance="fill" style="width: 100%;">
|
||||
<mat-label>Previous experience (here or in other relevant places)</mat-label>
|
||||
<textarea matInput
|
||||
formControlName="previousExperience"
|
||||
placeholder="Describe your previous experience"
|
||||
rows="4"></textarea>
|
||||
@if (form.controls.previousExperience.invalid && form.controls.previousExperience.touched) {
|
||||
<mat-error>
|
||||
@if (form.controls.previousExperience.errors?.['required']) {
|
||||
Previous experience is required
|
||||
} @else if (form.controls.previousExperience.errors?.['minlength']) {
|
||||
Please provide more details (at least 10 characters)
|
||||
}
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Plugin Experience -->
|
||||
<mat-form-field appearance="fill" style="width: 100%;">
|
||||
<mat-label>Experience with plugins that players use on our server</mat-label>
|
||||
<textarea matInput
|
||||
formControlName="pluginExperience"
|
||||
placeholder="Describe your experience with our server plugins"
|
||||
rows="4"></textarea>
|
||||
@if (form.controls.pluginExperience.invalid && form.controls.pluginExperience.touched) {
|
||||
<mat-error>
|
||||
@if (form.controls.pluginExperience.errors?.['required']) {
|
||||
Plugin experience is required
|
||||
} @else if (form.controls.pluginExperience.errors?.['minlength']) {
|
||||
Please provide more details (at least 10 characters)
|
||||
}
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Moderator Expectations -->
|
||||
<mat-form-field appearance="fill" style="width: 100%;">
|
||||
<mat-label>What do you believe the expectations of a moderator are?</mat-label>
|
||||
<textarea matInput
|
||||
formControlName="moderatorExpectations"
|
||||
placeholder="Describe what you think a moderator should do"
|
||||
rows="4"></textarea>
|
||||
@if (form.controls.moderatorExpectations.invalid && form.controls.moderatorExpectations.touched) {
|
||||
<mat-error>
|
||||
@if (form.controls.moderatorExpectations.errors?.['required']) {
|
||||
Moderator expectations are required
|
||||
} @else if (form.controls.moderatorExpectations.errors?.['minlength']) {
|
||||
Please provide more details (at least 10 characters)
|
||||
}
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Additional Information -->
|
||||
<mat-form-field appearance="fill" style="width: 100%;">
|
||||
<mat-label>Additional Information (Optional)</mat-label>
|
||||
<textarea matInput
|
||||
formControlName="additionalInfo"
|
||||
placeholder="Any additional information you'd like to share"
|
||||
rows="4"></textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<button mat-raised-button (click)="onSubmit()"
|
||||
[disabled]="form.controls.previousExperience.invalid || form.controls.pluginExperience.invalid || form.controls.moderatorExpectations.invalid">
|
||||
Submit Application
|
||||
</button>
|
||||
</section>
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Navigation dots -->
|
||||
@if (totalPages.length > 1) {
|
||||
<div class="form-navigation">
|
||||
<button mat-icon-button class="nav-button" (click)="previousPage()" [disabled]="isFirstPage()">
|
||||
<mat-icon>navigate_before</mat-icon>
|
||||
</button>
|
||||
|
||||
@for (i of totalPages; track i) {
|
||||
<div
|
||||
class="nav-dot"
|
||||
[class.active]="i === currentPageIndex"
|
||||
(click)="goToPage(i)">
|
||||
</div>
|
||||
}
|
||||
|
||||
<button mat-icon-button class="nav-button" (click)="nextPage()" [disabled]="isLastPage()">
|
||||
<mat-icon>navigate_next</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.staff-application-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 80vh;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.formPage {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
animation: fadeIn 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.navigation-buttons {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.form-navigation {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.nav-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
|
||||
&.active {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
color: #1f9bde;
|
||||
}
|
||||
|
||||
.pages {
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
}
|
||||
|
||||
.description {
|
||||
max-width: 75ch;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.valid-email {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #4CAF50;
|
||||
margin: 10px 0;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
background-color: rgba(76, 175, 80, 0.1);
|
||||
}
|
||||
|
||||
.valid-email mat-icon {
|
||||
color: #4CAF50;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.valid-email span {
|
||||
color: #4CAF50;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.checkbox-field {
|
||||
margin: 16px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.checkbox-error {
|
||||
color: #f44336;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.field-container {
|
||||
margin: 16px 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
font-size: 16px;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.days-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.day-chip {
|
||||
padding: 8px 12px;
|
||||
border-radius: 16px;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.selected {
|
||||
background-color: #1f9bde;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
|
@ -0,0 +1,348 @@
|
|||
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';
|
||||
|
||||
@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);
|
||||
public authService = inject(AuthService);
|
||||
public staffApplicationService = inject(ApplicationsService)
|
||||
private resizeObserver: ResizeObserver | null = null;
|
||||
private boundHandleResize: any;
|
||||
|
||||
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;
|
||||
|
||||
|
||||
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)]
|
||||
}),
|
||||
availableDays: new FormControl([], {
|
||||
nonNullable: true,
|
||||
validators: [Validators.required]
|
||||
}),
|
||||
availableTimes: new FormControl('', {
|
||||
nonNullable: true,
|
||||
validators: [Validators.required, Validators.maxLength(1000)]
|
||||
}),
|
||||
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() {
|
||||
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() {
|
||||
if (this.form === undefined) {
|
||||
console.error('Form is undefined');
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private router = inject(Router)
|
||||
|
||||
private sendForm() {
|
||||
const staffApplication: StaffApplication = this.mapToStaffApplication(this.form.getRawValue());
|
||||
|
||||
this.staffApplicationService.submitStaffApplication(staffApplication).subscribe(result => {
|
||||
//TODO route to mail page
|
||||
// Navigate to the sent page
|
||||
this.router.navigate(['/forms/sent'], {
|
||||
state: {message: 'Your staff application has been submitted successfully. We will review your application and get back to you soon.'}
|
||||
}).then();
|
||||
})
|
||||
}
|
||||
|
||||
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: 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>;
|
||||
}
|
||||
|
|
@ -43,9 +43,9 @@ sourceSets {
|
|||
dependencies {
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
implementation("org.springframework.boot:spring-boot-starter-validation")
|
||||
implementation("io.swagger.core.v3:swagger-annotations:2.2.20")
|
||||
implementation("io.swagger.core.v3:swagger-models:2.2.8")
|
||||
implementation("io.swagger.core.v3:swagger-core:2.2.8")
|
||||
implementation("io.swagger.core.v3:swagger-annotations:2.2.37")
|
||||
implementation("io.swagger.core.v3:swagger-models:2.2.37")
|
||||
implementation("io.swagger.core.v3:swagger-core:2.2.37")
|
||||
|
||||
implementation("org.openapitools:jackson-databind-nullable:0.2.6")
|
||||
implementation("org.springframework.hateoas:spring-hateoas:2.2.0")
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ paths:
|
|||
$ref: './schemas/forms/appeal/appeal.yml#/MinecraftAppeal'
|
||||
/api/appeal/discord-appeal:
|
||||
$ref: './schemas/forms/appeal/appeal.yml#/DiscordAppeal'
|
||||
/api/apply/staff-application:
|
||||
$ref: './schemas/forms/staff_apply/staff_apply.yml#/StaffApply'
|
||||
/api/login/requestNewUserLogin/{uuid}:
|
||||
$ref: './schemas/login/login.yml#/RequestNewUserLogin'
|
||||
/api/login/userLogin/{code}:
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ UpdateMail:
|
|||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AppealResponse'
|
||||
$ref: '../../generic/components.yml#/components/schemas/FormResponse'
|
||||
default:
|
||||
description: Unexpected error
|
||||
content:
|
||||
|
|
@ -49,7 +49,7 @@ MinecraftAppeal:
|
|||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AppealResponse'
|
||||
$ref: '../../generic/components.yml#/components/schemas/FormResponse'
|
||||
default:
|
||||
description: Unexpected error
|
||||
content:
|
||||
|
|
@ -75,7 +75,7 @@ DiscordAppeal:
|
|||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AppealResponse'
|
||||
$ref: '../../generic/components.yml#/components/schemas/FormResponse'
|
||||
default:
|
||||
description: Unexpected error
|
||||
content:
|
||||
|
|
@ -136,22 +136,6 @@ components:
|
|||
appeal:
|
||||
type: string
|
||||
description: Appeal text explaining why the punishment should be reconsidered
|
||||
AppealResponse:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- message
|
||||
- verified_mail
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Unique identifier for the submitted appeal for referring to it later
|
||||
message:
|
||||
type: string
|
||||
description: Confirmation message
|
||||
verified_mail:
|
||||
type: boolean
|
||||
description: If this user has verified their mail already
|
||||
UpdateMail:
|
||||
type: object
|
||||
required:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,104 @@
|
|||
StaffApply:
|
||||
post:
|
||||
tags:
|
||||
- applications
|
||||
summary: Submit a Staff appeal
|
||||
description: Submit an staff application
|
||||
operationId: submitStaffApplication
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/StaffApplication'
|
||||
responses:
|
||||
'201':
|
||||
description: Application created please verify email
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../../generic/components.yml#/components/schemas/FormResponse'
|
||||
default:
|
||||
description: Unexpected error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '../../generic/errors.yml#/components/schemas/ApiError'
|
||||
components:
|
||||
schemas:
|
||||
StaffApplication:
|
||||
type: object
|
||||
description: Schema for staff application
|
||||
required:
|
||||
- email
|
||||
- age
|
||||
- discordUsername
|
||||
- meetsRequirements
|
||||
- pronouns
|
||||
- joinDate
|
||||
- weeklyPlaytime
|
||||
- availableDays
|
||||
- availableTimes
|
||||
- previousExperience
|
||||
- pluginExperience
|
||||
- moderatorExpectations
|
||||
- additionalInfo
|
||||
properties:
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
maxLength: 320
|
||||
description: Email address of the applicant
|
||||
age:
|
||||
type: integer
|
||||
minimum: 13
|
||||
description: Age of the applicant, must be 13 or older
|
||||
discordUsername:
|
||||
type: string
|
||||
maxLength: 32
|
||||
description: Discord username of the applicant
|
||||
meetsRequirements:
|
||||
type: boolean
|
||||
description: Confirmation that the applicant meets all requirements
|
||||
pronouns:
|
||||
type: string
|
||||
maxLength: 32
|
||||
description: Preferred pronouns of the applicant
|
||||
joinDate:
|
||||
type: string
|
||||
maxLength: 256
|
||||
format: date
|
||||
description: Date when the applicant joined the service
|
||||
weeklyPlaytime:
|
||||
type: integer
|
||||
minimum: 1
|
||||
description: Average weekly playtime in hours
|
||||
availableDays:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
maxLength: 256
|
||||
description: Days of the week when the applicant is available
|
||||
availableTimes:
|
||||
type: string
|
||||
maxLength: 1000
|
||||
description: Time ranges when the applicant is available
|
||||
previousExperience:
|
||||
type: string
|
||||
minLength: 10
|
||||
maxLength: 4000
|
||||
description: Description of previous relevant experience
|
||||
pluginExperience:
|
||||
type: string
|
||||
minLength: 10
|
||||
maxLength: 4000
|
||||
description: Description of experience with plugins
|
||||
moderatorExpectations:
|
||||
type: string
|
||||
minLength: 10
|
||||
maxLength: 4000
|
||||
description: Applicant's expectations and understanding of moderator responsibilities
|
||||
additionalInfo:
|
||||
type: string
|
||||
maxLength: 4000
|
||||
description: Any additional information the applicant wishes to provide
|
||||
18
open_api/src/main/resources/schemas/generic/components.yml
Normal file
18
open_api/src/main/resources/schemas/generic/components.yml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
components:
|
||||
schemas:
|
||||
FormResponse:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- message
|
||||
- verified_mail
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: Unique identifier for the submitted form for referring to it later
|
||||
message:
|
||||
type: string
|
||||
description: Confirmation message
|
||||
verified_mail:
|
||||
type: boolean
|
||||
description: If this user has verified their mail already
|
||||
Loading…
Reference in New Issue
Block a user