Compare commits

..

4 Commits

Author SHA1 Message Date
Teriuihi 643545a18a Add appeal and login functionality structure
Introduces initial structure for appeal and login forms in both the frontend and backend. New controllers, APIs, and components were created, but functionality has not been fully implemented yet. This serves as a foundation for future development of these features.
2025-04-26 20:58:47 +02:00
Teriuihi b922487d76 Fix punishment tag colors and avatar image size 2025-04-20 17:29:51 +02:00
Teriuihi 54eb1ea735 Fixes #15 2025-04-20 17:19:50 +02:00
Peter 9043c774f7 Refactor ban details layout and enhanced formatting.
Updated the HTML structure and styling for a clearer, responsive layout and better user experience.
2025-04-20 17:00:31 +02:00
17 changed files with 478 additions and 57 deletions

View File

@ -0,0 +1,28 @@
package com.alttd.altitudeweb.controllers.application;
import com.alttd.altitudeweb.api.AppealsApi;
import com.alttd.altitudeweb.model.AppealResponseDto;
import com.alttd.altitudeweb.model.DiscordAppealDto;
import com.alttd.altitudeweb.model.MinecraftAppealDto;
import com.alttd.altitudeweb.model.UpdateMailDto;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.server.ResponseStatusException;
public class AppealController implements AppealsApi {
@Override
public ResponseEntity<MinecraftAppealDto> submitDiscordAppeal(DiscordAppealDto discordAppealDto) {
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Discord appeals are not yet supported");
}
@Override
public ResponseEntity<AppealResponseDto> submitMinecraftAppeal(MinecraftAppealDto minecraftAppealDto) {
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Minecraft appeals are not yet supported");
}
@Override
public ResponseEntity<AppealResponseDto> updateMail(UpdateMailDto updateMailDto) {
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Updating mail is not yet supported");
}
}

View File

@ -0,0 +1,22 @@
package com.alttd.altitudeweb.controllers.login;
import com.alttd.altitudeweb.api.LoginApi;
import com.alttd.altitudeweb.model.AddLoginDto;
import com.alttd.altitudeweb.model.LoginDataDto;
import com.alttd.altitudeweb.model.LoginResultDto;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.server.ResponseStatusException;
public class LoginController implements LoginApi {
@Override
public ResponseEntity<Void> addLogin(AddLoginDto addLoginDto) {
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Adding login is not yet supported");
}
@Override
public ResponseEntity<LoginResultDto> login(LoginDataDto loginDataDto) {
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Logging in is not yet supported");
}
}

View File

@ -2,13 +2,15 @@ import {Component} from '@angular/core';
import {ScrollService} from '../scroll/scroll.service';
import {CommonModule} from '@angular/common';
import {HeaderComponent} from '../header/header.component';
import {RemoveTrailingPeriodPipe} from "../util/RemoveTrailingPeriodPipe";
@Component({
selector: 'app-about',
standalone: true,
imports: [
CommonModule,
HeaderComponent
HeaderComponent,
RemoveTrailingPeriodPipe
],
templateUrl: './about.component.html',
styleUrl: './about.component.scss'

View File

@ -6,57 +6,104 @@
</div>
</app-header>
<ng-container *ngIf="punishment === undefined">
<p>Loading...</p>
</ng-container>
<main>
<section class="darkmodeSection">
<section class="columnSection">
<div class="detailsBackButton">
<ng-container *ngIf="punishment === undefined">
<p>Loading...</p>
</ng-container>
<ng-container *ngIf="punishment">
<table [cellSpacing]="0">
<div>
<p>type: {{ this.historyFormat.getType(punishment) }}</p>
<p>is active: {{ this.historyFormat.isActive(punishment) }}</p>
<tbody>
<tr>
<td>Player</td>
<td>
<div class="playerContainer">
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(punishment.uuid)" width="25" height="25"
alt="{{punishment.username}}'s Minecraft skin">
<span class="username">{{ punishment.username }}</span>
</div>
</td>
</tr>
<tr>
<td>Moderator</td>
<td>
<div class="playerContainer">
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(punishment.punishedByUuid)" width="25"
height="25"
alt="{{punishment.punishedBy}}'s Minecraft skin">
<span class="username">{{ punishment.punishedBy }}</span>
</div>
</td>
</tr>
<tr>
<td>Reason</td>
<td>{{ punishment.reason | removeTrailingPeriod }}</td>
</tr>
<tr>
<td>Date</td>
<td>{{ this.historyFormat.getPunishmentTime(punishment) }}</td>
</tr>
<tr>
<td>Expires</td>
<td>{{ this.historyFormat.getExpiredTime(punishment) }}</td>
</tr>
<ng-container *ngIf="punishment.removedBy !== undefined && punishment.removedBy.length > 0">
<tr>
<td>Un{{ this.historyFormat.getType(punishment).toLocaleLowerCase() }} reason</td>
<td>{{ punishment.removedReason == null ? 'No reason specified' : punishment.removedReason }}</td>
</tr>
<a [routerLink]="['/bans']">< Back</a>
</div>
</section>
<section class="columnSection center">
<ng-container *ngIf="punishment">
<div>
<span class="tag tagInfo"
[ngClass]="{
'tagPermanent': this.historyFormat.isPermanent(punishment),
'tagExpired': !this.historyFormat.isPermanent(punishment)
}">
{{ this.historyFormat.getType(punishment) }}
</span>
</div>
<div>
<span
class="tag tagInfo"
[ngClass]="{
'tagActive': this.historyFormat.isActive(punishment),
'tagInactive': !this.historyFormat.isActive(punishment)
}">
{{ this.historyFormat.isActive(punishment) ? 'Active' : 'Inactive' }}
</span>
</div>
</ng-container>
</tbody>
</div>
</table>
</ng-container>
</section>
<section class="columnSection">
<div class="columnContainer">
<div class="columnParagraph">
<ng-container *ngIf="punishment">
<div class="playerContainer">
<h2>Player</h2>
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(punishment.uuid, '150')"
width="150"
height="150"
alt="{{punishment.username}}'s Minecraft skin"
>
<h3 class="detailsUsername">{{ punishment.username }}</h3>
</div>
</ng-container>
</div>
</div>
<div class="columnContainer">
<div class="columnParagraph">
<ng-container *ngIf="punishment">
<div class="playerContainer">
<h2>Moderator</h2>
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(punishment.punishedByUuid, '150')"
width="150"
height="150"
alt="{{punishment.punishedBy}}'s Minecraft skin"
>
<h3 class="detailsUsername">{{ punishment.punishedBy }}</h3>
</div>
</ng-container>
</div>
</div>
<div class="columnContainer">
<div class="columnParagraph">
<ng-container *ngIf="punishment">
<div class="detailsInfo">
<h2>Reason</h2>
<p>{{ punishment.reason | removeTrailingPeriod }}</p>
</div>
</ng-container>
</div>
</div>
<div class="columnContainer">
<div class="columnParagraph">
<ng-container *ngIf="punishment">
<div class="detailsInfo">
<h2>Date</h2>
<p>{{ this.historyFormat.getPunishmentTime(punishment) }}</p>
</div>
</ng-container>
</div>
</div>
</section>
</section>
</main>
</ng-container>
<section class="columnSection">
<ng-container *ngIf="punishment">
<span>Expires</span>
<span>{{ this.historyFormat.getExpiredTime(punishment) }}</span>
<ng-container *ngIf="punishment.removedBy !== undefined && punishment.removedBy.length > 0">
<span>Un{{ this.historyFormat.getType(punishment).toLocaleLowerCase() }} reason</span>
<span>{{ punishment.removedReason == null ? 'No reason specified' : punishment.removedReason }}</span>
</ng-container>
</ng-container>
</section>

View File

@ -0,0 +1,68 @@
.detailsBackButton {
font-family: open-sans, sans-serif;
}
.columnSection {
padding-top: 30px;
}
h2 {
text-align: center;
}
h3 {
text-align: center;
}
.avatar {
padding: 10px 0;
}
.detailsUsername {
font-size: 1.2em;
font-family: 'opensans-bold', sans-serif;
}
.detailsInfo {
padding-top: 50px;
}
.detailsInfo p {
text-align: center;
}
.tag {
display: inline-block;
padding: 5px 10px;
line-height: 1;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 0.25rem;
font-family: 'opensans-bold', sans-serif;
}
.tagInfo {
margin: 0 20px;
font-size: 1.1em;
}
.tagActive {
color: #FFFFFF;
background-color: #EE5555;
}
.tagInactive {
color: #FFFFFF;
background-color: #F79720;
}
.tagPermanent {
color: #FFFFFF;
background-color: #EE5555;
}
.tagExpired {
color: #FFFFFF;
background-color: #777777
}

View File

@ -1,9 +1,9 @@
import {Component, OnInit} from '@angular/core';
import {HistoryService, PunishmentHistory} from '../../../api';
import {NgIf, NgOptimizedImage} from '@angular/common';
import {NgClass, NgIf, NgOptimizedImage} from '@angular/common';
import {RemoveTrailingPeriodPipe} from '../../util/RemoveTrailingPeriodPipe';
import {HistoryFormatService} from '../history-format.service';
import {ActivatedRoute} from '@angular/router';
import {ActivatedRoute, RouterLink} from '@angular/router';
import {catchError, map} from 'rxjs';
import {HeaderComponent} from '../../header/header.component';
@ -13,7 +13,9 @@ import {HeaderComponent} from '../../header/header.component';
NgIf,
NgOptimizedImage,
RemoveTrailingPeriodPipe,
HeaderComponent
HeaderComponent,
RouterLink,
NgClass
],
templateUrl: './details.component.html',
styleUrl: './details.component.scss'

View File

@ -22,9 +22,19 @@ export class HistoryFormatService {
}
public isActive(entry: PunishmentHistory): boolean {
if (entry.removedBy !== null) {
return false;
}
if (entry.expiryTime <= 0) {
return true;
}
return entry.expiryTime > Date.now();
}
public isPermanent(entry: PunishmentHistory): boolean {
return entry.expiryTime <= 0;
}
public getType(entry: PunishmentHistory): string {
return entry.type.charAt(0).toUpperCase() + entry.type.slice(1);
}
@ -48,11 +58,11 @@ export class HistoryFormatService {
}) + " " + suffix;
}
public getAvatarUrl(entry: string): string {
public getAvatarUrl(entry: string, size: string = '25'): string {
let uuid = entry.replace('-', '');
if (uuid === 'C') {
uuid = "f78a4d8dd51b4b3998a3230f2de0c670"
}
return `https://crafatar.com/avatars/${uuid}?size=25&overlay`;
return `https://crafatar.com/avatars/${uuid}?size=${size}&overlay`;
}
}

View File

@ -0,0 +1,5 @@
<app-forms [currentPage]="'appeal'" [formTitle]="'Minecraft Appeal'">
<div form-content>
</div>
</app-forms>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AppealComponent } from './appeal.component';
describe('AppealComponent', () => {
let component: AppealComponent;
let fixture: ComponentFixture<AppealComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AppealComponent]
})
.compileComponents();
fixture = TestBed.createComponent(AppealComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,63 @@
import {Component, OnInit} from '@angular/core';
import {FormsComponent} from '../forms.component';
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
import {AppealsService} from '../../../api';
@Component({
selector: 'app-appeal',
imports: [
FormsComponent
],
templateUrl: './appeal.component.html',
styleUrl: './appeal.component.scss'
})
export class AppealComponent implements OnInit {
public form: FormGroup | undefined;
constructor(private fb: FormBuilder, private appealApi: AppealsService) {
}
ngOnInit() {
this.initForm()
}
private initForm() {
this.form = this.fb.group({
name: ['', [Validators.required]],
punishmentId: ['', [Validators.required]],
email: ['', [Validators.required, Validators.email]],
message: ['', [Validators.required, Validators.minLength(10)]]
});
}
public onSubmit() {
if (this.form === undefined) {
console.error('Form is undefined');
return
}
if (this.form.valid) {
console.log('Form submitted:', this.form.value);
// Process form submission here
} else {
// Mark all fields as touched to trigger validation display
Object.keys(this.form.controls).forEach(field => {
const control = this.form!.get(field);
if (!(control instanceof FormGroup)) {
console.error('Control [' + control + '] is not a FormGroup');
return;
}
control.markAsTouched({onlySelf: true});
});
}
}
private sendForm(validForm: FormGroup) {
// const appeal: MinecraftAppeal = {
//
// }
// this.appealApi.submitMinecraftAppeal()
}
}

View File

@ -0,0 +1,12 @@
<ng-container>
<app-header [current_page]="currentPage" height="200px" background_image="/public/img/backgrounds/staff.png"
[overlay_gradient]="0.5">
<div class="title" header-content>
<h1>{{ formTitle }}</h1>
</div>
</app-header>
<!-- TODO add form styling in this div-->
<div>
<ng-content select="[form-content]"></ng-content>
</div>
</ng-container>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsComponent } from './forms.component';
describe('FormsComponent', () => {
let component: FormsComponent;
let fixture: ComponentFixture<FormsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FormsComponent]
})
.compileComponents();
fixture = TestBed.createComponent(FormsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,15 @@
import {Component, Input} from '@angular/core';
import {HeaderComponent} from '../header/header.component';
@Component({
selector: 'app-forms',
imports: [
HeaderComponent
],
templateUrl: './forms.component.html',
styleUrl: './forms.component.scss'
})
export class FormsComponent {
@Input() formTitle: string = 'Form';
@Input() currentPage: string = 'forms';
}

View File

@ -36,3 +36,7 @@ paths:
$ref: './schemas/forms/appeal/appeal.yml#/MinecraftAppeal'
/appeal/discord-appeal:
$ref: './schemas/forms/appeal/appeal.yml#/DiscordAppeal'
/login/addUserLogin:
$ref: './schemas/login/login.yml#/AddUserLogin'
/login/userLogin:
$ref: './schemas/login/login.yml#/UserLogin'

View File

@ -0,0 +1,97 @@
UserLogin:
post:
tags:
- login
summary: Log in to the site
description: Log in to the site through a code from the server
operationId: login
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LoginData'
responses:
'200':
description: Logged in
content:
application/json:
schema:
$ref: '#/components/schemas/LoginResult'
'401':
description: Login failed - Invalid credentials
content:
application/json:
schema:
$ref: '../generic/errors.yml#/components/schemas/ApiError'
default:
description: Unexpected error
content:
application/json:
schema:
$ref: '../generic/errors.yml#/components/schemas/ApiError'
AddUserLogin:
post:
tags:
- login
summary: Add a login
description: Add a code, user combination that can be used to log in
operationId: addLogin
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AddLogin'
responses:
'200':
description: Success
default:
description: Unexpected error
content:
application/json:
schema:
$ref: '../generic/errors.yml#/components/schemas/ApiError'
components:
schemas:
LoginData:
type: object
required:
- loginCode
properties:
loginCode:
type: string
description: The code to log in
LoginResult:
type: object
required:
- uuid
- userName
- auth
properties:
uuid:
type: string
format: uuid
description: UUID of logged in user
userName:
type: string
description: Name of the logged in user
auth:
type: string
description: Token to use along side requests
AddLogin:
type: object
required:
- loginCode
- uuid
properties:
auth:
type: string
description: Token to verify the sender is allowed to add logins
loginCode:
type: string
description: The code that can be logged in with
uuid:
type: string
format: uuid
description: UUID of the user that will get logged in