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-mail:3.1.5")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
|
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
|
//AOP
|
||||||
implementation("org.aspectj:aspectjrt:1.9.19")
|
implementation("org.aspectj:aspectjrt:1.9.19")
|
||||||
implementation("org.aspectj:aspectjweaver: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.EmailVerification;
|
||||||
import com.alttd.altitudeweb.database.web_db.mail.EmailVerificationMapper;
|
import com.alttd.altitudeweb.database.web_db.mail.EmailVerificationMapper;
|
||||||
import com.alttd.altitudeweb.mappers.AppealDataMapper;
|
import com.alttd.altitudeweb.mappers.AppealDataMapper;
|
||||||
import com.alttd.altitudeweb.model.AppealResponseDto;
|
|
||||||
import com.alttd.altitudeweb.model.DiscordAppealDto;
|
import com.alttd.altitudeweb.model.DiscordAppealDto;
|
||||||
|
import com.alttd.altitudeweb.model.FormResponseDto;
|
||||||
import com.alttd.altitudeweb.model.MinecraftAppealDto;
|
import com.alttd.altitudeweb.model.MinecraftAppealDto;
|
||||||
import com.alttd.altitudeweb.model.UpdateMailDto;
|
import com.alttd.altitudeweb.model.UpdateMailDto;
|
||||||
import com.alttd.altitudeweb.services.limits.RateLimit;
|
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 org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
|
@ -41,13 +40,13 @@ public class AppealController implements AppealsApi {
|
||||||
|
|
||||||
@RateLimit(limit = 3, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "discordAppeal")
|
@RateLimit(limit = 3, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "discordAppeal")
|
||||||
@Override
|
@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");
|
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Discord appeals are not yet supported");
|
||||||
}
|
}
|
||||||
|
|
||||||
@RateLimit(limit = 3, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "minecraftAppeal")
|
@RateLimit(limit = 3, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "minecraftAppeal")
|
||||||
@Override
|
@Override
|
||||||
public ResponseEntity<AppealResponseDto> submitMinecraftAppeal(MinecraftAppealDto minecraftAppealDto) {
|
public ResponseEntity<FormResponseDto> submitMinecraftAppeal(MinecraftAppealDto minecraftAppealDto) {
|
||||||
boolean success = true;
|
boolean success = true;
|
||||||
CompletableFuture<Appeal> appealCompletableFuture = new CompletableFuture<>();
|
CompletableFuture<Appeal> appealCompletableFuture = new CompletableFuture<>();
|
||||||
|
|
||||||
|
|
@ -105,7 +104,7 @@ public class AppealController implements AppealsApi {
|
||||||
sqlSession.getMapper(AppealMapper.class)
|
sqlSession.getMapper(AppealMapper.class)
|
||||||
.markAppealAsSent(appeal.id());
|
.markAppealAsSent(appeal.id());
|
||||||
});
|
});
|
||||||
AppealResponseDto appealResponseDto = new AppealResponseDto(
|
FormResponseDto appealResponseDto = new FormResponseDto(
|
||||||
appeal.id().toString(),
|
appeal.id().toString(),
|
||||||
"Your appeal has been submitted. You will be notified when it has been reviewed.",
|
"Your appeal has been submitted. You will be notified when it has been reviewed.",
|
||||||
true);
|
true);
|
||||||
|
|
@ -113,7 +112,8 @@ public class AppealController implements AppealsApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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");
|
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']
|
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',
|
path: 'community',
|
||||||
loadComponent: () => import('./pages/altitude/community/community.component').then(m => m.CommunityComponent)
|
loadComponent: () => import('./pages/altitude/community/community.component').then(m => m.CommunityComponent)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,14 @@
|
||||||
</p>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</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 {
|
dependencies {
|
||||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-validation")
|
implementation("org.springframework.boot:spring-boot-starter-validation")
|
||||||
implementation("io.swagger.core.v3:swagger-annotations:2.2.20")
|
implementation("io.swagger.core.v3:swagger-annotations:2.2.37")
|
||||||
implementation("io.swagger.core.v3:swagger-models:2.2.8")
|
implementation("io.swagger.core.v3:swagger-models:2.2.37")
|
||||||
implementation("io.swagger.core.v3:swagger-core:2.2.8")
|
implementation("io.swagger.core.v3:swagger-core:2.2.37")
|
||||||
|
|
||||||
implementation("org.openapitools:jackson-databind-nullable:0.2.6")
|
implementation("org.openapitools:jackson-databind-nullable:0.2.6")
|
||||||
implementation("org.springframework.hateoas:spring-hateoas:2.2.0")
|
implementation("org.springframework.hateoas:spring-hateoas:2.2.0")
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,8 @@ paths:
|
||||||
$ref: './schemas/forms/appeal/appeal.yml#/MinecraftAppeal'
|
$ref: './schemas/forms/appeal/appeal.yml#/MinecraftAppeal'
|
||||||
/api/appeal/discord-appeal:
|
/api/appeal/discord-appeal:
|
||||||
$ref: './schemas/forms/appeal/appeal.yml#/DiscordAppeal'
|
$ref: './schemas/forms/appeal/appeal.yml#/DiscordAppeal'
|
||||||
|
/api/apply/staff-application:
|
||||||
|
$ref: './schemas/forms/staff_apply/staff_apply.yml#/StaffApply'
|
||||||
/api/login/requestNewUserLogin/{uuid}:
|
/api/login/requestNewUserLogin/{uuid}:
|
||||||
$ref: './schemas/login/login.yml#/RequestNewUserLogin'
|
$ref: './schemas/login/login.yml#/RequestNewUserLogin'
|
||||||
/api/login/userLogin/{code}:
|
/api/login/userLogin/{code}:
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ UpdateMail:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/AppealResponse'
|
$ref: '../../generic/components.yml#/components/schemas/FormResponse'
|
||||||
default:
|
default:
|
||||||
description: Unexpected error
|
description: Unexpected error
|
||||||
content:
|
content:
|
||||||
|
|
@ -49,7 +49,7 @@ MinecraftAppeal:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/AppealResponse'
|
$ref: '../../generic/components.yml#/components/schemas/FormResponse'
|
||||||
default:
|
default:
|
||||||
description: Unexpected error
|
description: Unexpected error
|
||||||
content:
|
content:
|
||||||
|
|
@ -75,7 +75,7 @@ DiscordAppeal:
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/AppealResponse'
|
$ref: '../../generic/components.yml#/components/schemas/FormResponse'
|
||||||
default:
|
default:
|
||||||
description: Unexpected error
|
description: Unexpected error
|
||||||
content:
|
content:
|
||||||
|
|
@ -136,22 +136,6 @@ components:
|
||||||
appeal:
|
appeal:
|
||||||
type: string
|
type: string
|
||||||
description: Appeal text explaining why the punishment should be reconsidered
|
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:
|
UpdateMail:
|
||||||
type: object
|
type: object
|
||||||
required:
|
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