Skip to content

Commit 039f4a3

Browse files
authored
Merge pull request #205 from Toastbrot236/mod-pan
Basic moderation panel page
2 parents d6aa606 + e966efb commit 039f4a3

File tree

13 files changed

+604
-107
lines changed

13 files changed

+604
-107
lines changed

src/app/api/client.service.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { AdminUserUpdateRequest } from './types/users/admin-user-update-request'
2323
import { ExtendedUser } from './types/users/extended-user';
2424
import { PunishUserRequest } from './types/moderation/punish-user-request';
2525
import { PlanetInfo } from './types/users/planet-info';
26+
import { Announcement } from './types/announcement';
2627

2728
export const defaultPageSize: number = 40;
2829

@@ -263,4 +264,16 @@ export class ClientService extends ApiImplementation {
263264
deleteReviewsByUserByUuid(uuid: string) {
264265
return this.http.delete<Response>(`/admin/users/uuid/${uuid}/reviews`);
265266
}
267+
268+
getAllAnnouncements() {
269+
return this.http.get<Announcement[]>(`/announcements`);
270+
}
271+
272+
postAnnouncement(announcement: Announcement) {
273+
return this.http.post<Announcement>(`/admin/announcements`, announcement);
274+
}
275+
276+
deleteAnnouncementByUuid(uuid: string) {
277+
return this.http.delete<Response>(`/admin/announcements/${uuid}`);
278+
}
266279
}

src/app/api/types/announcement.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ export interface Announcement {
22
announcementId: string;
33
title: string;
44
text: string;
5+
createdAt: Date | undefined;
56
}

src/app/api/types/instance.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ export interface Instance {
1919
blockedAssetFlags: AssetConfigFlags;
2020
blockedAssetFlagsForTrustedUsers: AssetConfigFlags;
2121

22-
announcements: Announcement[];
2322
maintenanceModeEnabled: boolean;
2423
grafanaDashboardUrl: string | null;
2524

src/app/app.routes.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,11 @@ export const routes: Routes = [
120120
loadComponent: () => import('./pages/instance-info/instance-info.component').then(x => x.InstanceInfoComponent),
121121
data: {title: "About Us"},
122122
},
123+
{
124+
path: 'moderation',
125+
loadComponent: () => import('./pages/mod-panel/mod-panel.component').then(x => x.ModPanelComponent),
126+
data: {title: "Moderation Panel"},
127+
},
123128
...appendDebugRoutes(),
124129
// KEEP THIS ROUTE LAST! It handles pages that do not exist.
125130
{
Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,88 @@
1-
import {Component, Input} from '@angular/core';
1+
import {Component, EventEmitter, Input, Output} from '@angular/core';
22
import {Announcement} from "../../api/types/announcement";
33

44
import {FaIconComponent} from "@fortawesome/angular-fontawesome";
5-
import {faBullhorn} from "@fortawesome/free-solid-svg-icons";
5+
import {faBullhorn, faSignOutAlt, faTrash} from "@fortawesome/free-solid-svg-icons";
6+
import { ButtonComponent } from "../ui/form/button.component";
7+
import { ClientService } from '../../api/client.service';
8+
import { BannerService } from '../../banners/banner.service';
9+
import { RefreshApiError } from '../../api/refresh-api-error';
10+
import { ConfirmationDialogComponent } from "../ui/confirmation-dialog.component";
11+
import { DateComponent } from "../ui/info/date.component";
612

713
@Component({
814
selector: 'app-announcement',
915
imports: [
10-
FaIconComponent
16+
FaIconComponent,
17+
ButtonComponent,
18+
ConfirmationDialogComponent,
19+
DateComponent
1120
],
1221
template: `
1322
<div class="bg-yellow rounded px-5 py-2.5">
14-
<fa-icon [icon]="faBullhorn" class="pr-1.5"></fa-icon>
15-
<span class="text-xl font-bold">{{data.title}}</span>
16-
<p>{{data.text}}</p>
23+
<div class="flex flex-row gap-x-2 justify-between">
24+
<div>
25+
<fa-icon [icon]="faBullhorn" class="pr-1.5"></fa-icon>
26+
<span class="text-xl font-bold word-wrap-and-break">{{data.title}}</span>
27+
</div>
28+
29+
@if (showDeleteButton) {
30+
<app-button color="bg-red text-[15px]" yPadding="" [icon]="faTrash" color="bg-red" (click)="toggleDeletionDialog(true)"></app-button>
31+
}
32+
</div>
33+
34+
<p class="word-wrap-and-break">{{data.text}}</p>
35+
36+
@if (data.createdAt != null) {
37+
<div class="flex flex-row justify-end">
38+
<span class="italic">
39+
posted
40+
<app-date [date]="data.createdAt"></app-date>
41+
</span>
42+
</div>
43+
}
1744
</div>
45+
46+
@defer (when showDeletionDialog) { @if (showDeletionDialog) {
47+
<app-confirmation-dialog infoText="Do you really want to delete this announcement?" (closeDialog)="toggleDeletionDialog(false)">
48+
<app-button text="Cancel" [icon]="faSignOutAlt" color="bg-secondary" (click)="toggleDeletionDialog(false)"></app-button>
49+
<app-button text="Delete!" [icon]="faTrash" color="bg-red" (click)="delete()"></app-button>
50+
</app-confirmation-dialog>
51+
}}
1852
`
1953
})
2054
export class AnnouncementComponent {
2155
@Input({required: true}) data: Announcement = undefined!;
56+
@Input() showDeleteButton: boolean = false;
57+
@Output() deleted = new EventEmitter;
58+
59+
protected showDeletionDialog: boolean = false;
60+
61+
constructor(protected client: ClientService, protected banner: BannerService) {
62+
63+
}
64+
65+
protected toggleDeletionDialog(visibility: boolean) {
66+
this.showDeletionDialog = visibility;
67+
}
68+
69+
protected delete() {
70+
if (this.data.announcementId.length == 0) return; // fake announcement which doesn't exist on the server
71+
this.toggleDeletionDialog(false);
72+
73+
this.client.deleteAnnouncementByUuid(this.data.announcementId).subscribe({
74+
error: error => {
75+
const apiError: RefreshApiError | undefined = error.error?.error;
76+
this.banner.error("Announcement deletion failed", apiError == null ? error.message : apiError.message);
77+
},
78+
next: _ => {
79+
this.banner.success("Announcement successfully deleted!", "");
80+
this.deleted.emit();
81+
}
82+
});
83+
}
84+
2285
protected readonly faBullhorn = faBullhorn;
86+
protected readonly faTrash = faTrash;
87+
protected readonly faSignOutAlt = faSignOutAlt;
2388
}

src/app/components/ui/header/header-me-menu.component.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {UserStatisticsComponent} from "../../items/user-statistics.component";
88

99
import {DividerComponent} from "../divider.component";
1010
import {NavItem} from "./navtypes";
11+
import { UserRoles } from '../../../api/types/users/user-roles';
1112
import { UserRoleComponent } from "../info/user-role.component";
1213

1314
@Component({
@@ -40,6 +41,14 @@ import { UserRoleComponent } from "../info/user-role.component";
4041
@for (item of topItems; track item.route) {
4142
<app-navbar-item [icon]="item.icon" [title]="item.name" [href]="item.route" iconClass="w-4 text-[1.1rem]" labelClass="text-lg"></app-navbar-item>
4243
}
44+
45+
@if (isModerator) {
46+
<app-divider></app-divider>
47+
@for (item of specialItems; track item.route) {
48+
<app-navbar-item [icon]="item.icon" [title]="item.name" [href]="item.route" iconClass="w-4 text-[1.1rem]" labelClass="text-lg"></app-navbar-item>
49+
}
50+
}
51+
4352
<app-divider></app-divider>
4453
@for (item of bottomItems; track item.route) {
4554
<app-navbar-item [icon]="item.icon" [title]="item.name" [href]="item.route" iconClass="w-4 text-[1.1rem]" labelClass="text-lg"></app-navbar-item>
@@ -49,6 +58,13 @@ import { UserRoleComponent } from "../info/user-role.component";
4958
})
5059
export class HeaderMeMenuComponent {
5160
@Input({required: true}) user: User = undefined!;
61+
isModerator: boolean = false;
62+
63+
ngOnInit() {
64+
if (this.user.role >= UserRoles.Moderator) {
65+
this.isModerator = true;
66+
}
67+
}
5268

5369
protected topItems: NavItem[] = [
5470
{
@@ -67,6 +83,13 @@ export class HeaderMeMenuComponent {
6783
route: '/settings/profile'
6884
},
6985
];
86+
protected specialItems: NavItem[] = [
87+
{
88+
name: 'Mod Panel',
89+
icon: 'tools',
90+
route: '/moderation'
91+
},
92+
];
7093
protected bottomItems: NavItem[] = [
7194
{
7295
name: 'Log out',
Lines changed: 101 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,112 @@
11
<app-page-title></app-page-title>
2-
@if(instance != null) {
3-
<br><p class="text-3xl">
4-
<img [ngSrc]="instance.websiteLogoUrl" class="inline aspect-square object-cover rounded"
5-
alt="Server icon" width="30" height="30"
6-
(error)="iconErr($event.target)" loading="lazy">
7-
{{ instance.instanceName }}
8-
</p>
9-
<p class="text-wrap"> {{instance.instanceDescription}}</p>
10-
<br>
2+
<br>
113

12-
<h2 class="font-bold text-2xl">Server Information</h2>
13-
<p>Software: <span class="italic">{{instance.softwareName}} ({{instance.softwareType}})</span></p>
14-
<p>Version: <span class="word-wrap-and-break italic">v{{instance.softwareVersion}}</span></p>
15-
<p>License:
16-
<a [href]="instance.softwareLicenseUrl" class="text-link hover:text-link-hover hover:underline">
17-
{{ instance.softwareLicenseName }}
18-
</a>
19-
</p>
20-
<p>Source repository:
21-
<a [href]="instance.softwareSourceUrl" class="text-link hover:text-link-hover hover:underline">
22-
{{ instance.softwareSourceUrl }}
23-
</a>
24-
</p>
25-
<p>Blocked assets for regular users: <span class="italic">{{ blockedAssetFlags }}</span></p>
26-
<p>Blocked assets for trusted users: <span class="italic">{{ blockedAssetFlagsForTrustedUsers }}</span></p>
27-
<br>
4+
<app-two-pane-layout>
5+
<app-container class="w-full">
6+
<app-pane-title>
7+
<a routerLink='/instance'>
8+
Instance
9+
</a>
10+
</app-pane-title>
11+
<app-divider></app-divider>
12+
<div>
13+
@if(instance != null) {
14+
<p class="text-3xl">
15+
<img [ngSrc]="instance.websiteLogoUrl" class="inline aspect-square object-cover rounded"
16+
alt="Server icon" width="30" height="30"
17+
(error)="iconErr($event.target)" loading="lazy">
18+
{{ instance.instanceName }}
19+
</p>
20+
<p class="text-wrap"> {{instance.instanceDescription}}</p>
21+
<br>
22+
<h2 class="font-bold text-2xl">Server Information</h2>
23+
<p>Software: <span class="italic">{{instance.softwareName}} ({{instance.softwareType}})</span></p>
24+
<p>Version: <span class="word-wrap-and-break italic">v{{instance.softwareVersion}}</span></p>
25+
<p>License:
26+
<a [href]="instance.softwareLicenseUrl" class="text-link hover:text-link-hover hover:underline">
27+
{{ instance.softwareLicenseName }}
28+
</a>
29+
</p>
30+
<p>Source repository:
31+
<a [href]="instance.softwareSourceUrl" class="text-link hover:text-link-hover hover:underline">
32+
{{ instance.softwareSourceUrl }}
33+
</a>
34+
</p>
35+
<p>Blocked assets for regular users: <span class="italic">{{ blockedAssetFlags }}</span></p>
36+
<p>Blocked assets for trusted users: <span class="italic">{{ blockedAssetFlagsForTrustedUsers }}</span></p>
37+
<br>
38+
<h2 class="font-bold text-2xl">Contact Us</h2>
39+
<p>Owner: <span class="italic">{{ instance.contactInfo.adminName }}</span></p>
2840

29-
<h2 class="font-bold text-2xl">Contact Us</h2>
30-
<p>Owner: <span class="italic">{{ instance.contactInfo.adminName }}</span></p>
41+
@if (instance.contactInfo.adminDiscordUsername != null) {
42+
<p>Owner Discord username: <span class="italic">{{ instance.contactInfo.adminDiscordUsername }}</span></p>
43+
}
44+
@else {
45+
<p>No Discord username of the owner</p>
46+
}
47+
48+
@if (instance.contactInfo.discordServerInvite != null) {
49+
<p>Discord server invite:
50+
<a [href]="instance.contactInfo.discordServerInvite" class="text-link hover:text-link-hover hover:underline">
51+
{{ instance.contactInfo.discordServerInvite }}
52+
</a>
53+
</p>
54+
}
55+
@else {
56+
<p>No Discord server invite</p>
57+
}
3158

32-
@if (instance.contactInfo.adminDiscordUsername != null) {
33-
<p>Owner Discord username: <span class="italic">{{ instance.contactInfo.adminDiscordUsername }}</span></p>
34-
}
35-
@else {
36-
<p>No Discord username of the owner</p>
37-
}
38-
39-
@if (instance.contactInfo.discordServerInvite != null) {
40-
<p>Discord server invite:
41-
<a [href]="instance.contactInfo.discordServerInvite" class="text-link hover:text-link-hover hover:underline">
42-
{{ instance.contactInfo.discordServerInvite }}
59+
<p>Email address:
60+
<a [href]="'mailto:' + instance.contactInfo.emailAddress" class="text-link hover:text-link-hover hover:underline word-wrap-and-break">
61+
{{instance.contactInfo.emailAddress }}
62+
</a>
63+
</p>
64+
<br>
65+
}
66+
@else if (statisticsDownloadFailed) {
67+
<p>Failed to download instance metadata.</p>
68+
}
69+
@else {
70+
<p>Downloading instance metadata...</p>
71+
}
72+
</div>
73+
</app-container>
74+
<app-container class="w-full">
75+
<app-pane-title>
76+
<a routerLink='/instance'>
77+
Statistics
4378
</a>
44-
</p>
45-
}
46-
@else {
47-
<p>No Discord server invite</p>
48-
}
49-
50-
<p>Email address:
51-
<a [href]="'mailto:' + instance.contactInfo.emailAddress" class="text-link hover:text-link-hover hover:underline word-wrap-and-break">
52-
{{instance.contactInfo.emailAddress }}
53-
</a>
54-
</p>
55-
<br>
56-
}
79+
</app-pane-title>
80+
<app-divider></app-divider>
81+
<div>
82+
@if(statistics != null) {
83+
<p><a class="text-link hover:text-link-hover hover:underline" routerLink="/users">Registered users: {{statistics.totalUsers}}</a></p>
84+
<p><a class="text-link hover:text-link-hover hover:underline" routerLink="/levels">Published levels: {{statistics.totalLevels}}</a></p>
85+
<p><a class="text-link hover:text-link-hover hover:underline" routerLink="/levels">Modded levels: {{statistics.moddedLevels}}</a></p>
86+
<p><a class="text-link hover:text-link-hover hover:underline" routerLink="/photos">Uploaded photos: {{statistics.totalPhotos}}</a></p>
87+
<p><a class="text-link hover:text-link-hover hover:underline" routerLink="/activity">Events occurred: {{statistics.totalEvents}}</a></p>
88+
<br>
89+
<p>Active users: {{statistics.activeUsers}}</p>
90+
<p>People online now: {{statistics.currentIngamePlayersCount}}</p>
91+
<p><a class="text-link hover:text-link-hover hover:underline" routerLink="/rooms">Active rooms: {{statistics.currentRoomCount}}</a></p>
92+
<br>
93+
<p>API requests: {{statistics.requestStatistics.apiRequests}}</p>
94+
<p>Game API requests: {{statistics.requestStatistics.gameRequests}}</p>
95+
}
96+
@else if (statisticsDownloadFailed) {
97+
<p>Failed to download instance statistics.</p>
98+
}
99+
@else {
100+
<p>Downloading instance statistics...</p>
101+
}
102+
</div>
103+
</app-container>
104+
</app-two-pane-layout>
57105

106+
<br>
58107
<h2 class="font-bold text-2xl">Website</h2>
59108
<p>Source repository:
60109
<a [href]="websiteRepoUrl" class="text-link hover:text-link-hover hover:underline">
61110
{{ websiteRepoUrl }}
62111
</a>
63-
</p>
64-
<br>
65-
66-
@if(statistics != null) {
67-
<h2 class="font-bold text-2xl">Things!</h2>
68-
<p><a routerLink="/users">Registered users: {{statistics.totalUsers}}</a></p>
69-
<p><a routerLink="/levels">Published levels: {{statistics.totalLevels}}</a></p>
70-
<p><a routerLink="/levels">Modded levels: {{statistics.moddedLevels}}</a></p>
71-
<p><a routerLink="/photos">Uploaded photos: {{statistics.totalPhotos}}</a></p>
72-
<p><a routerLink="/activity">Events occurred: {{statistics.totalEvents}}</a></p>
73-
<br>
74-
75-
<h2 class="font-bold text-2xl">Activity</h2>
76-
<p>Active users: {{statistics.activeUsers}}</p>
77-
<p>People online now: {{statistics.currentIngamePlayersCount}}</p>
78-
<p><a class="text-link hover:text-link-hover hover:underline" routerLink="/rooms">Active rooms: {{statistics.currentRoomCount}}</a></p>
79-
<br>
80-
81-
<h2 class="font-bold text-2xl">Requests ({{statistics.requestStatistics.totalRequests}} in total)</h2>
82-
<p>API requests: {{statistics.requestStatistics.apiRequests}}</p>
83-
<p>Game API requests: {{statistics.requestStatistics.gameRequests}}</p>
84-
}
112+
</p>

0 commit comments

Comments
 (0)