Rework folder structure in frontend
Pages are now grouped per group they appear in on in the header (where possible) Utilities used by multiple pages in the project are grouped in folders such as services/pipes/etc
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
<ng-container>
|
||||
<app-header [current_page]="'bans'" height="200px" background_image="/public/img/backgrounds/staff.png"
|
||||
[overlay_gradient]="0.5">>
|
||||
<div class="title" header-content>
|
||||
<h1>Minecraft Punishments</h1>
|
||||
</div>
|
||||
</app-header>
|
||||
|
||||
<main>
|
||||
<section class="darkmodeSection">
|
||||
<div class="container">
|
||||
<div class="columnSection">
|
||||
<div class="historyButtonContainer">
|
||||
<div [id]="getCurrentButtonId('all')" class="button-outer" (click)="changeHistoryPunishment('all')">
|
||||
<span class="button-inner"
|
||||
[ngClass]="active">All</span>
|
||||
</div>
|
||||
<div [id]="getCurrentButtonId('ban')" class="button-outer" (click)="changeHistoryPunishment('ban')">
|
||||
<span class="button-inner"
|
||||
[ngClass]="active">Bans</span>
|
||||
</div>
|
||||
<div [id]="getCurrentButtonId('mute')" class="button-outer" (click)="changeHistoryPunishment('mute')">
|
||||
<span class="button-inner"
|
||||
[ngClass]="active">Mutes</span>
|
||||
</div>
|
||||
<div [id]="getCurrentButtonId('warn')" class="button-outer" (click)="changeHistoryPunishment('warn')">
|
||||
<span class="button-inner"
|
||||
[ngClass]="active">Warnings</span>
|
||||
</div>
|
||||
<div [id]="getCurrentUserTypeButtonId('player')" class="button-outer" (click)="changeUserType('player')"
|
||||
style="margin-left: 120px;">
|
||||
<span class="button-inner"
|
||||
[ngClass]="active">Player</span>
|
||||
</div>
|
||||
<div [id]="getCurrentUserTypeButtonId('staff')" class="button-outer" (click)="changeUserType('staff')">
|
||||
<span class="button-inner"
|
||||
[ngClass]="active">Staff</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="historySearchContainer">
|
||||
<input class="historySearch"
|
||||
type="search"
|
||||
placeholder="Search.."
|
||||
[(ngModel)]="searchTerm"
|
||||
(input)="filterNames()"
|
||||
(keyup.enter)="search()"
|
||||
>
|
||||
<div class="dropdown-results" *ngIf="filteredNames.length > 0 && searchTerm">
|
||||
<div
|
||||
class="dropdown-item"
|
||||
*ngFor="let name of filteredNames"
|
||||
(mousedown)="selectName(name)">
|
||||
{{ name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="historyTable">
|
||||
<app-history [userType]="userType" [punishmentType]="punishmentType"
|
||||
[page]="page" [searchTerm]="finalSearchTerm" (pageChange)="updatePageSize($event)"
|
||||
(selectItem)="setSearch($event)">
|
||||
</app-history>
|
||||
</div>
|
||||
<div class="changePageButtons">
|
||||
<button [ngClass]="{'active': buttonActive(0), 'disabled': !buttonActive(0)}"
|
||||
[disabled]="!buttonActive(0)"
|
||||
(click)="setPage(0)" class="historyPageButton">
|
||||
First page
|
||||
</button>
|
||||
<button [ngClass]="{'active': buttonActive(0), 'disabled': !buttonActive(0)}"
|
||||
[disabled]="!buttonActive(0)"
|
||||
(click)="previousPage()" class="historyPageButton">
|
||||
Previous page
|
||||
</button>
|
||||
<span class="pageNumber">{{ this.page }} / {{ getMaxPage() }}</span>
|
||||
<button [ngClass]="{'active': buttonActive(getMaxPage()), 'disabled': !buttonActive(getMaxPage())}"
|
||||
[disabled]="!buttonActive(getMaxPage())"
|
||||
(click)="nextPage()" class="historyPageButton">
|
||||
Next page
|
||||
</button>
|
||||
<button [ngClass]="{'active': buttonActive(getMaxPage()), 'disabled': !buttonActive(getMaxPage())}"
|
||||
[disabled]="!buttonActive(getMaxPage())"
|
||||
(click)="setPage(getMaxPage())" class="historyPageButton">
|
||||
Last page
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,171 @@
|
||||
main .container {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1300px;
|
||||
}
|
||||
|
||||
.columnSection {
|
||||
padding: 20px 0 20px 0;
|
||||
max-width: 1300px;
|
||||
}
|
||||
|
||||
.historyButtonContainer {
|
||||
width: 80%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.historySearchContainer {
|
||||
width: 20%;
|
||||
box-sizing: border-box;
|
||||
text-align: right;
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.button-outer {
|
||||
margin-right: 15px;
|
||||
background: var(--history-link-color);
|
||||
}
|
||||
|
||||
.button-outer.active {
|
||||
color: var(--navlink-color);
|
||||
}
|
||||
|
||||
#currentButton {
|
||||
text-decoration: none;
|
||||
color: var(--white);
|
||||
background: var(--link-color);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
input.historySearch {
|
||||
font-family: 'open-sans', sans-serif;
|
||||
width: 260px;
|
||||
border: 1px solid #DEDEDE;
|
||||
padding: 9px 4px 9px 40px;
|
||||
background: #DEDEDE url(/public/img/random/search.svg) no-repeat 10px;
|
||||
background-size: 18px 18px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.searchButton {
|
||||
vertical-align: -15px;
|
||||
}
|
||||
|
||||
.historyTable {
|
||||
font-family: 'open-sans', sans-serif;
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
||||
border-radius: 5px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.dropdown-results {
|
||||
font-family: 'open-sans', sans-serif;
|
||||
position: absolute;
|
||||
top: 35px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
background-color: #DEDEDE;
|
||||
border: 1px solid #ddd;
|
||||
border-top: none;
|
||||
z-index: 1050;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
border-bottom-right-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.changePageButtons {
|
||||
margin: 0 auto;
|
||||
padding: 10px 0 30px 0;
|
||||
}
|
||||
|
||||
.historyPageButton {
|
||||
align-items: center;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: .25rem;
|
||||
box-shadow: rgba(0, 0, 0, 0.02) 0 1px 3px 0;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--history-page-button-background);
|
||||
color: var(--font-color);
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
font-family: 'opensans', sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
justify-content: center;
|
||||
line-height: 1.25;
|
||||
margin: 0 5px 0 5px;
|
||||
min-height: 3rem;
|
||||
padding: calc(.875rem - 1px) calc(1.5rem - 1px);
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
transition: all 250ms;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
touch-action: manipulation;
|
||||
vertical-align: baseline;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.historyPageButton.active:hover,
|
||||
.historyPageButton.active:focus {
|
||||
border-color: rgba(0, 0, 0, 0.15);
|
||||
box-shadow: rgba(0, 0, 0, 0.1) 0 4px 12px;
|
||||
color: var(--font-color-hover);
|
||||
}
|
||||
|
||||
.historyPageButton.active:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.historyPageButton.active:active {
|
||||
border-color: rgba(0, 0, 0, 0.15);
|
||||
box-shadow: rgba(0, 0, 0, 0.06) 0 2px 4px;
|
||||
color: var(--font-color-hover);
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.historyPageButton.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
background-color: var(--color-quaternary);
|
||||
color: var(--font-color);
|
||||
box-shadow: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Prevent hover effects on disabled buttons */
|
||||
.historyPageButton.disabled:hover,
|
||||
.historyPageButton.disabled:focus {
|
||||
border-color: rgba(0, 0, 0, 0.1);
|
||||
box-shadow: none;
|
||||
transform: none;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.pageNumber {
|
||||
font-family: 'open-sans', sans-serif;
|
||||
margin: 0 25px 0 25px;
|
||||
color: var(--font-color);
|
||||
transition: 0.5s ease;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BansComponent } from './bans.component';
|
||||
|
||||
describe('BansComponent', () => {
|
||||
let component: BansComponent;
|
||||
let fixture: ComponentFixture<BansComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [BansComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(BansComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,221 @@
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {HeaderComponent} from "@header/header.component";
|
||||
import {HistoryComponent} from './history/history.component';
|
||||
import {HistoryCount, HistoryService} from '@api';
|
||||
import {NgClass, NgForOf, NgIf} from '@angular/common';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {catchError, map, Observable} from 'rxjs';
|
||||
import {SearchParams} from './search-terms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-bans',
|
||||
imports: [
|
||||
HeaderComponent,
|
||||
HistoryComponent,
|
||||
NgIf,
|
||||
FormsModule,
|
||||
NgForOf,
|
||||
NgClass
|
||||
],
|
||||
templateUrl: './bans.component.html',
|
||||
styleUrl: './bans.component.scss'
|
||||
})
|
||||
export class BansComponent implements OnInit {
|
||||
|
||||
constructor(public historyApi: HistoryService) {
|
||||
}
|
||||
|
||||
private PAGE_SIZE: number = 10;
|
||||
private uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
||||
private actualPage: number = 0;
|
||||
private totalSearchResults: number = -1;
|
||||
|
||||
public active: string = '';
|
||||
public userType: 'player' | 'staff' = "player";
|
||||
public punishmentType: 'all' | 'ban' | 'mute' | 'kick' | 'warn' = "all";
|
||||
public page: number = 0;
|
||||
public names: string[] = [];
|
||||
public finalSearchTerm: string = '';
|
||||
public searchTerm: string = '';
|
||||
public filteredNames: string[] = [];
|
||||
public pageSize = 10;
|
||||
public historyCount: HistoryCount = {
|
||||
bans: 0,
|
||||
mutes: 0,
|
||||
kicks: 0,
|
||||
warnings: 0
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.historyApi.getUserNames(this.userType, this.punishmentType).subscribe(names => {
|
||||
this.names = names;
|
||||
});
|
||||
|
||||
this.historyApi.getTotalPunishments().subscribe(totalPunishments => {
|
||||
this.historyCount = totalPunishments;
|
||||
})
|
||||
|
||||
this.pushState();
|
||||
|
||||
window.addEventListener('popstate', (event) => {
|
||||
if (event.state && event.state.searchTerm !== undefined) {
|
||||
this.searchTerm = event.state.searchTerm;
|
||||
this.finalSearchTerm = event.state.searchTerm;
|
||||
this.page = event.state.page;
|
||||
this.userType = event.state.userType;
|
||||
this.punishmentType = event.state.punishmentType;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getCurrentButtonId(options: 'all' | 'ban' | 'mute' | 'kick' | 'warn') {
|
||||
if (options == this.punishmentType) {
|
||||
return 'currentButton'
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public getCurrentUserTypeButtonId(options: 'player' | 'staff') {
|
||||
if (options == this.userType) {
|
||||
return 'currentButton'
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public filterNames() {
|
||||
if (!this.searchTerm) {
|
||||
this.filteredNames = [];
|
||||
return;
|
||||
}
|
||||
|
||||
this.filteredNames = this.names.filter(name =>
|
||||
name.toLowerCase().startsWith(this.searchTerm.toLowerCase())
|
||||
).slice(0, 10);
|
||||
}
|
||||
|
||||
public selectName(name: string) {
|
||||
this.searchTerm = name;
|
||||
this.filteredNames = [];
|
||||
this.search();
|
||||
}
|
||||
|
||||
public search() {
|
||||
this.pushState()
|
||||
this.finalSearchTerm = this.searchTerm;
|
||||
this.page = 0;
|
||||
if (this.finalSearchTerm.length === 0) {
|
||||
this.totalSearchResults = -1
|
||||
return
|
||||
}
|
||||
|
||||
let totalSearchResultsObservable: Observable<number>;
|
||||
if (this.uuidRegex.test(this.finalSearchTerm)) {
|
||||
totalSearchResultsObservable = this.historyApi.getTotalResultsForUuidSearch(this.userType, this.punishmentType, this.finalSearchTerm);
|
||||
} else {
|
||||
totalSearchResultsObservable = this.historyApi.getTotalResultsForUserSearch(this.userType, this.punishmentType, this.finalSearchTerm);
|
||||
}
|
||||
totalSearchResultsObservable.pipe(
|
||||
map(totalSearchResults => {
|
||||
this.totalSearchResults = totalSearchResults;
|
||||
}),
|
||||
catchError(err => {
|
||||
console.error(err);
|
||||
return [];
|
||||
})
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
public changeUserType(type: 'player' | 'staff') {
|
||||
this.pushState();
|
||||
this.userType = type;
|
||||
this.page = 0;
|
||||
this.finalSearchTerm = '';
|
||||
this.searchTerm = '';
|
||||
this.filteredNames = [];
|
||||
}
|
||||
|
||||
public changeHistoryPunishment(type: 'all' | 'ban' | 'mute' | 'kick' | 'warn') {
|
||||
this.pushState();
|
||||
this.punishmentType = type;
|
||||
this.page = 0;
|
||||
}
|
||||
|
||||
public nextPage() {
|
||||
if (this.page === this.getMaxPage()) {
|
||||
return;
|
||||
}
|
||||
this.setPage(this.page + 1)
|
||||
}
|
||||
|
||||
public previousPage() {
|
||||
if (this.page === 0) {
|
||||
return;
|
||||
}
|
||||
this.setPage(this.page - 1)
|
||||
}
|
||||
|
||||
public setPage(page: number) {
|
||||
if (this.actualPage !== this.page) {
|
||||
console.warn('Changing page too quickly, the previous page change has not loaded yet');
|
||||
return;
|
||||
}
|
||||
this.pushState();
|
||||
this.page = page
|
||||
}
|
||||
|
||||
private pushState() {
|
||||
const state = {
|
||||
searchTerm: this.searchTerm,
|
||||
page: this.page,
|
||||
userType: this.userType,
|
||||
punishmentType: this.punishmentType
|
||||
};
|
||||
const title = 'Altitude Bans';
|
||||
const url = window.location.href;
|
||||
window.history.pushState(state, title, url);
|
||||
}
|
||||
|
||||
public getMaxPage() {
|
||||
const all = this.historyCount.bans + this.historyCount.mutes + this.historyCount.warnings;
|
||||
if (this.finalSearchTerm.length !== 0) {
|
||||
return Math.floor(this.totalSearchResults / this.PAGE_SIZE);
|
||||
}
|
||||
switch (this.punishmentType) {
|
||||
case 'all':
|
||||
return Math.floor(all / this.PAGE_SIZE);
|
||||
case 'ban':
|
||||
return Math.floor(this.historyCount.bans / this.PAGE_SIZE);
|
||||
case 'mute':
|
||||
return Math.floor(this.historyCount.mutes / this.PAGE_SIZE);
|
||||
case 'kick':
|
||||
return Math.floor(this.historyCount.kicks / this.PAGE_SIZE);
|
||||
case 'warn':
|
||||
return Math.floor(this.historyCount.warnings / this.PAGE_SIZE);
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public buttonActive(compare: number): boolean {
|
||||
if (compare !== 0 && this.pageSize < this.PAGE_SIZE) {
|
||||
return false;
|
||||
}
|
||||
return this.page !== compare;
|
||||
}
|
||||
|
||||
public updatePageSize(pageSize: number) {
|
||||
if (pageSize < 0) {
|
||||
return;
|
||||
} else {
|
||||
this.pageSize = pageSize;
|
||||
this.actualPage = this.page;
|
||||
}
|
||||
}
|
||||
|
||||
public setSearch(searchParams: SearchParams) {
|
||||
this.userType = searchParams.userType
|
||||
this.searchTerm = searchParams.searchTerm
|
||||
this.punishmentType = searchParams.punishmentType
|
||||
this.search();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<ng-container>
|
||||
<app-header [current_page]="'bans'" height="200px" background_image="/public/img/backgrounds/staff.png"
|
||||
[overlay_gradient]="0.5">>
|
||||
<div class="title" header-content>
|
||||
<h1>Minecraft Punishments</h1>
|
||||
</div>
|
||||
</app-header>
|
||||
|
||||
<main>
|
||||
<section class="darkmodeSection">
|
||||
<section class="columnSection">
|
||||
<div class="detailsBackButton">
|
||||
<ng-container *ngIf="punishment === undefined">
|
||||
<p>Loading...</p>
|
||||
</ng-container>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DetailsComponent } from './details.component';
|
||||
|
||||
describe('DetailsComponent', () => {
|
||||
let component: DetailsComponent;
|
||||
let fixture: ComponentFixture<DetailsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [DetailsComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(DetailsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {HistoryService, PunishmentHistory} from '@api';
|
||||
import {NgClass, NgIf, NgOptimizedImage} from '@angular/common';
|
||||
import {RemoveTrailingPeriodPipe} from '@pipes/RemoveTrailingPeriodPipe';
|
||||
import {HistoryFormatService} from '../history-format.service';
|
||||
import {ActivatedRoute, RouterLink} from '@angular/router';
|
||||
import {catchError, map} from 'rxjs';
|
||||
import {HeaderComponent} from '@header/header.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-details',
|
||||
imports: [
|
||||
NgIf,
|
||||
NgOptimizedImage,
|
||||
RemoveTrailingPeriodPipe,
|
||||
HeaderComponent,
|
||||
RouterLink,
|
||||
NgClass
|
||||
],
|
||||
templateUrl: './details.component.html',
|
||||
styleUrl: './details.component.scss'
|
||||
})
|
||||
export class DetailsComponent implements OnInit {
|
||||
type: 'all' | 'ban' | 'mute' | 'kick' | 'warn' = 'all';
|
||||
id: number = -1;
|
||||
punishment: PunishmentHistory | undefined;
|
||||
|
||||
constructor(private route: ActivatedRoute,
|
||||
private historyApi: HistoryService,
|
||||
public historyFormat: HistoryFormatService) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.paramMap.subscribe(params => {
|
||||
switch (params.get('type')) {
|
||||
case 'ban':
|
||||
this.type = 'ban';
|
||||
break;
|
||||
case 'mute':
|
||||
this.type = 'mute';
|
||||
break;
|
||||
case 'kick':
|
||||
this.type = 'kick';
|
||||
break;
|
||||
case 'warn':
|
||||
this.type = 'warn';
|
||||
break;
|
||||
default:
|
||||
throw new Error("Invalid type");
|
||||
}
|
||||
this.id = Number(params.get('id') || '-1');
|
||||
this.loadPunishmentData();
|
||||
});
|
||||
}
|
||||
|
||||
private loadPunishmentData() {
|
||||
if (!this.type || this.type === 'all' || this.id < 0 || isNaN(this.id)) {
|
||||
console.error("Invalid type or id");
|
||||
return;
|
||||
}
|
||||
this.historyApi.getHistoryById(this.type, this.id).pipe(
|
||||
map(punishment => {
|
||||
this.punishment = punishment;
|
||||
}),
|
||||
catchError(err => {
|
||||
console.error(err);
|
||||
return [];
|
||||
})
|
||||
).subscribe();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { HistoryFormatService } from './history-format.service';
|
||||
|
||||
describe('HistoryFormatService', () => {
|
||||
let service: HistoryFormatService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(HistoryFormatService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {PunishmentHistory} from '@api';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class HistoryFormatService {
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
public getPunishmentTime(entry: PunishmentHistory) {
|
||||
const date = new Date(entry.punishmentTime);
|
||||
return date.toLocaleString(navigator.language, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public getExpiredTime(entry: PunishmentHistory): string {
|
||||
let suffix: string = '';
|
||||
if (entry.removedBy !== null && entry.removedBy !== undefined) {
|
||||
suffix = '(Unbanned by ' + entry.removedBy + ')';
|
||||
}
|
||||
if (entry.expiryTime <= 0) {
|
||||
return "Permanent " + this.getType(entry) + " " + suffix;
|
||||
}
|
||||
const date = new Date(entry.expiryTime);
|
||||
return date.toLocaleString(navigator.language, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
}) + " " + suffix;
|
||||
}
|
||||
|
||||
public getAvatarUrl(entry: string, size: string = '25'): string {
|
||||
let uuid = entry.replace('-', '');
|
||||
if (uuid === 'C') {
|
||||
uuid = "f78a4d8dd51b4b3998a3230f2de0c670"
|
||||
}
|
||||
return `https://crafatar.com/avatars/${uuid}?size=${size}&overlay`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<ng-container *ngIf="history.length === 0">
|
||||
<p>No history found</p>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="history.length > 0">
|
||||
<table [cellSpacing]="0">
|
||||
<div class="historyTableHead">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="historyType">Type</th>
|
||||
<th class="historyPlayer">Player</th>
|
||||
<th class="historyPlayer">Banned By</th>
|
||||
<th class="historyReason">Reason</th>
|
||||
<th class="historyDate">Date</th>
|
||||
<th class="historyDate">Expires</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</div>
|
||||
<div>
|
||||
<tbody>
|
||||
<tr class="historyPlayerRow" *ngFor="let entry of history">
|
||||
<td class="historyType" (click)="showDetailedPunishment(entry)">
|
||||
{{ this.historyFormat.getType(entry) }}
|
||||
</td>
|
||||
<td class="historyPlayer" (click)="setSearch(entry.username, 'player')">
|
||||
<div class="playerContainer">
|
||||
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(entry.uuid)" width="25" height="25"
|
||||
alt="{{entry.username}}'s Minecraft skin">
|
||||
<span class="username">{{ entry.username }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="historyPlayer" (click)="setSearch(entry.punishedBy, 'staff')">
|
||||
<div class="playerContainer">
|
||||
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(entry.punishedByUuid)" width="25" height="25"
|
||||
alt="{{entry.punishedBy}}'s Minecraft skin">
|
||||
<span>{{ entry.punishedBy }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="historyReason" (click)="showDetailedPunishment(entry)">
|
||||
{{ entry.reason | removeTrailingPeriod }}
|
||||
</td>
|
||||
<td class="historyDate" (click)="showDetailedPunishment(entry)">
|
||||
{{ this.historyFormat.getPunishmentTime(entry) }}
|
||||
</td>
|
||||
<td class="historyDate" (click)="showDetailedPunishment(entry)">
|
||||
{{ this.historyFormat.getExpiredTime(entry) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</div>
|
||||
</table>
|
||||
</ng-container>
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
thead {
|
||||
height: 25px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
tbody {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 5px 0 5px 0;
|
||||
height: 55px;
|
||||
}
|
||||
|
||||
th {
|
||||
padding: 5px 0 5px 0;
|
||||
}
|
||||
|
||||
th, td {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
padding: 5px 0 8px 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
th.historyType, td.historyType {
|
||||
width: 100px !important;
|
||||
min-width: 100px;
|
||||
max-width: 100px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
th.historyPlayer, td.historyPlayer {
|
||||
width: 180px !important;
|
||||
min-width: 180px;
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
th.historyReason, td.historyReason {
|
||||
width: 540px !important;
|
||||
min-width: 540px;
|
||||
max-width: 540px;
|
||||
}
|
||||
|
||||
th.historyDate, td.historyDate {
|
||||
width: 150px !important;
|
||||
min-width: 150px;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
table tr td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.historyPlayerRow:hover {
|
||||
background-color: var(--history-table-row-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.historyTableHead {
|
||||
width: 1300px;
|
||||
background-color: var(--history-table-head-color);
|
||||
transition: all 100ms cubic-bezier(0.55, 0.085, 0.68, 0.53), 0.5s background ease;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 4px 8px -4px rgba(0, 0, 0, 0.2), 0 6px 20px -8px rgba(0, 0, 0, 0.19);
|
||||
}
|
||||
|
||||
.historyType {
|
||||
max-width: 100%;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.historyPlayer {
|
||||
max-width: 100%;
|
||||
width: 200px;
|
||||
text-overflow: clip;
|
||||
}
|
||||
|
||||
.playerContainer {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.username {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
img {
|
||||
vertical-align: middle;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin-bottom: 5px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.historyReason {
|
||||
width: 650px;
|
||||
}
|
||||
|
||||
.historyDate {
|
||||
width: 170px;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { HistoryComponent } from './history.component';
|
||||
|
||||
describe('HistoryComponent', () => {
|
||||
let component: HistoryComponent;
|
||||
let fixture: ComponentFixture<HistoryComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [HistoryComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(HistoryComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import {Component, EventEmitter, Input, OnChanges, OnInit, Output} from '@angular/core';
|
||||
import {BASE_PATH, HistoryService, PunishmentHistory} from '@api';
|
||||
import {catchError, map, Observable, shareReplay} from 'rxjs';
|
||||
import {NgForOf, NgIf, NgOptimizedImage} from '@angular/common';
|
||||
import {CookieService} from 'ngx-cookie-service';
|
||||
import {RemoveTrailingPeriodPipe} from '@pipes/RemoveTrailingPeriodPipe';
|
||||
import {HttpErrorResponse} from '@angular/common/http';
|
||||
import {environment} from '@environment';
|
||||
import {HistoryFormatService} from '../history-format.service';
|
||||
import {SearchParams} from '../search-terms';
|
||||
import {Router} from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-history',
|
||||
imports: [
|
||||
NgIf,
|
||||
NgForOf,
|
||||
NgOptimizedImage,
|
||||
RemoveTrailingPeriodPipe
|
||||
],
|
||||
templateUrl: './history.component.html',
|
||||
styleUrl: './history.component.scss',
|
||||
providers: [
|
||||
CookieService,
|
||||
{provide: BASE_PATH, useValue: environment.apiUrl}
|
||||
],
|
||||
})
|
||||
export class HistoryComponent implements OnInit, OnChanges {
|
||||
|
||||
@Input() userType: 'player' | 'staff' = "player";
|
||||
@Input() punishmentType: 'all' | 'ban' | 'mute' | 'kick' | 'warn' = "all";
|
||||
@Input() page: number = 0;
|
||||
@Input() searchTerm: string = '';
|
||||
|
||||
@Output() pageChange = new EventEmitter<number>();
|
||||
@Output() selectItem = new EventEmitter<SearchParams>();
|
||||
|
||||
private uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
||||
|
||||
public history: PunishmentHistory[] = []
|
||||
|
||||
constructor(private historyApi: HistoryService, public historyFormat: HistoryFormatService, private router: Router) {
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.reloadHistory();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.reloadHistory();
|
||||
}
|
||||
|
||||
private reloadHistory(): void {
|
||||
let historyObservable: Observable<PunishmentHistory[]>;
|
||||
if (this.searchTerm.length === 0) {
|
||||
historyObservable = this.historyApi.getHistoryForAll(this.userType, this.punishmentType, this.page);
|
||||
} else {
|
||||
if (this.uuidRegex.test(this.searchTerm)) {
|
||||
historyObservable = this.historyApi.getHistoryForUuid(this.userType, this.punishmentType, this.searchTerm, this.page);
|
||||
} else {
|
||||
historyObservable = this.historyApi.getHistoryForUsers(this.userType, this.punishmentType, this.searchTerm, this.page);
|
||||
}
|
||||
}
|
||||
historyObservable.pipe(
|
||||
map(history => {
|
||||
this.history = history;
|
||||
this.pageChange.emit(this.history.length);
|
||||
}),
|
||||
catchError(err => {
|
||||
this.pageChange.emit(-1);
|
||||
let retrySeconds = 5;
|
||||
|
||||
if (err instanceof HttpErrorResponse) {
|
||||
if (err.status !== 429) {
|
||||
return this.history;
|
||||
}
|
||||
const headers = err.headers;
|
||||
const retryAfterHeader = headers.get('Retry-After');
|
||||
console.warn(err.error);
|
||||
if (retryAfterHeader) {
|
||||
const retryAfter = parseInt(retryAfterHeader || '0', 10);
|
||||
if (!isNaN(retryAfter) && retryAfter > 0) {
|
||||
retrySeconds = retryAfter + 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error(err);
|
||||
}
|
||||
setTimeout(() => this.reloadHistory(), retrySeconds * 1000);
|
||||
return this.history;
|
||||
}),
|
||||
shareReplay(1)
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
setSearch(name: string, userType: 'player' | 'staff') {
|
||||
let searchParams: SearchParams = {
|
||||
userType: userType,
|
||||
punishmentType: 'all',
|
||||
searchTerm: name
|
||||
}
|
||||
this.selectItem.emit(searchParams);
|
||||
}
|
||||
|
||||
public showDetailedPunishment(entry: PunishmentHistory) {
|
||||
this.router.navigate([`bans/${entry.type}/${entry.id}`]).then();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface SearchParams {
|
||||
userType: 'player' | 'staff';
|
||||
searchTerm: string;
|
||||
punishmentType: 'all' | 'ban' | 'mute' | 'kick' | 'warn';
|
||||
}
|
||||
Reference in New Issue
Block a user