Add punishment details and history retrieval functionality
This commit introduces a new `DetailsComponent` for displaying detailed punishment data and establishes a route to view punishment history by ID and type. It also updates the API to support fetching individual punishment records and refines database mappings for improved data handling.
This commit is contained in:
@@ -45,6 +45,10 @@ export const routes: Routes = [
|
||||
path: 'bans',
|
||||
loadComponent: () => import('./bans/bans.component').then(m => m.BansComponent)
|
||||
},
|
||||
{
|
||||
path: 'bans/:type/:id',
|
||||
loadComponent: () => import('./bans/details/details.component').then(m => m.DetailsComponent)
|
||||
},
|
||||
{
|
||||
path: 'economy',
|
||||
loadComponent: () => import('./economy/economy.component').then(m => m.EconomyComponent)
|
||||
|
||||
@@ -57,7 +57,8 @@
|
||||
</div>
|
||||
<div class="historyTable">
|
||||
<app-history [userType]="userType" [punishmentType]="punishmentType"
|
||||
[page]="page" [searchTerm]="finalSearchTerm" (pageChange)="updatePageSize($event)">
|
||||
[page]="page" [searchTerm]="finalSearchTerm" (pageChange)="updatePageSize($event)"
|
||||
(selectItem)="setSearch($event)">
|
||||
</app-history>
|
||||
</div>
|
||||
<div class="changePageButtons">
|
||||
|
||||
@@ -5,6 +5,7 @@ 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',
|
||||
@@ -210,4 +211,11 @@ export class BansComponent implements OnInit {
|
||||
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,62 @@
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</ng-container>
|
||||
</tbody>
|
||||
</div>
|
||||
</table>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
@@ -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,69 @@
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {HistoryService, PunishmentHistory} from '../../../api';
|
||||
import {NgIf, NgOptimizedImage} from '@angular/common';
|
||||
import {RemoveTrailingPeriodPipe} from '../../util/RemoveTrailingPeriodPipe';
|
||||
import {HistoryFormatService} from '../history-format.service';
|
||||
import {ActivatedRoute} from '@angular/router';
|
||||
import {catchError, map} from 'rxjs';
|
||||
import {HeaderComponent} from '../../header/header.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-details',
|
||||
imports: [
|
||||
NgIf,
|
||||
NgOptimizedImage,
|
||||
RemoveTrailingPeriodPipe,
|
||||
HeaderComponent
|
||||
],
|
||||
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,58 @@
|
||||
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 {
|
||||
return entry.expiryTime > Date.now();
|
||||
}
|
||||
|
||||
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 = '(' + 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): string {
|
||||
let uuid = entry.replace('-', '');
|
||||
if (uuid === 'C') {
|
||||
uuid = "f78a4d8dd51b4b3998a3230f2de0c670"
|
||||
}
|
||||
return `https://crafatar.com/avatars/${uuid}?size=25&overlay`;
|
||||
}
|
||||
}
|
||||
@@ -19,24 +19,32 @@
|
||||
<div>
|
||||
<tbody>
|
||||
<tr class="historyPlayerRow" *ngFor="let entry of history">
|
||||
<td class="historyType">{{ getType(entry) }}</td>
|
||||
<td class="historyPlayer">
|
||||
<td class="historyType" (click)="showDetailedPunishment(entry)">
|
||||
{{ this.historyFormat.getType(entry) }}
|
||||
</td>
|
||||
<td class="historyPlayer" (click)="setSearch(entry.type, entry.username, 'player')">
|
||||
<div class="playerContainer">
|
||||
<img class="avatar" [ngSrc]="getAvatarUrl(entry.uuid)" width="25" height="25"
|
||||
<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">
|
||||
<td class="historyPlayer" (click)="setSearch(entry.type, entry.punishedBy, 'staff')">
|
||||
<div class="playerContainer">
|
||||
<img class="avatar" [ngSrc]="getAvatarUrl(entry.punishedByUuid)" width="25" height="25"
|
||||
<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">{{ entry.reason | removeTrailingPeriod }}</td>
|
||||
<td class="historyDate">{{ getPunishmentTime(entry) }}</td>
|
||||
<td class="historyDate">{{ getExpiredTime(entry) }}</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>
|
||||
|
||||
@@ -62,6 +62,7 @@ table tr td {
|
||||
|
||||
.historyPlayerRow:hover {
|
||||
background-color: var(--history-table-row-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.historyTableHead {
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import {Component, EventEmitter, Input, OnChanges, OnInit, Output} from '@angular/core';
|
||||
import {BASE_PATH, HistoryService, PunishmentHistoryInner} from '../../../api';
|
||||
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 '../../util/RemoveTrailingPeriodPipe';
|
||||
import {HttpErrorResponse} from '@angular/common/http';
|
||||
import {environment} from '../../../environments/environment';
|
||||
import {HistoryFormatService} from '../history-format.service';
|
||||
import {SearchParams} from '../search-terms';
|
||||
import {Router} from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-history',
|
||||
@@ -30,12 +33,13 @@ export class HistoryComponent implements OnInit, OnChanges {
|
||||
@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: PunishmentHistoryInner[] = []
|
||||
public history: PunishmentHistory[] = []
|
||||
|
||||
constructor(private historyApi: HistoryService) {
|
||||
constructor(private historyApi: HistoryService, public historyFormat: HistoryFormatService, private router: Router) {
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
@@ -47,7 +51,7 @@ export class HistoryComponent implements OnInit, OnChanges {
|
||||
}
|
||||
|
||||
private reloadHistory(): void {
|
||||
let historyObservable: Observable<PunishmentHistoryInner[]>;
|
||||
let historyObservable: Observable<PunishmentHistory[]>;
|
||||
if (this.searchTerm.length === 0) {
|
||||
historyObservable = this.historyApi.getHistoryForAll(this.userType, this.punishmentType, this.page);
|
||||
} else {
|
||||
@@ -89,42 +93,31 @@ export class HistoryComponent implements OnInit, OnChanges {
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
public getPunishmentTime(entry: PunishmentHistoryInner) {
|
||||
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 getType(entry: PunishmentHistoryInner) {
|
||||
return entry.type.charAt(0).toUpperCase() + entry.type.slice(1);
|
||||
}
|
||||
|
||||
public getExpiredTime(entry: PunishmentHistoryInner) {
|
||||
if (entry.expiryTime <= 0) {
|
||||
return "Permanent " + this.getType(entry);
|
||||
setSearch(type: PunishmentHistory.TypeEnum, name: string, userType: 'player' | 'staff') {
|
||||
let punishmentType: 'all' | 'ban' | 'mute' | 'kick' | 'warn' = 'all';
|
||||
switch (type) {
|
||||
case 'ban':
|
||||
punishmentType = 'ban';
|
||||
break;
|
||||
case 'mute':
|
||||
punishmentType = 'mute';
|
||||
break;
|
||||
case 'kick':
|
||||
punishmentType = 'kick';
|
||||
break;
|
||||
case 'warn':
|
||||
punishmentType = 'warn';
|
||||
break;
|
||||
}
|
||||
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
|
||||
});
|
||||
let searchParams: SearchParams = {
|
||||
userType: userType,
|
||||
punishmentType: punishmentType,
|
||||
searchTerm: name
|
||||
}
|
||||
this.selectItem.emit(searchParams);
|
||||
}
|
||||
|
||||
public getAvatarUrl(entry: string): string {
|
||||
let uuid = entry.replace('-', '');
|
||||
if (uuid === 'C') {
|
||||
uuid = "f78a4d8dd51b4b3998a3230f2de0c670"
|
||||
}
|
||||
return `https://crafatar.com/avatars/${uuid}?size=25&overlay`;
|
||||
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