Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/app/api/client.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { AdminUserUpdateRequest } from './types/users/admin-user-update-request'
import { ExtendedUser } from './types/users/extended-user';
import { PunishUserRequest } from './types/moderation/punish-user-request';
import { PlanetInfo } from './types/users/planet-info';
import { Announcement } from './types/announcement';

export const defaultPageSize: number = 40;

Expand Down Expand Up @@ -263,4 +264,16 @@ export class ClientService extends ApiImplementation {
deleteReviewsByUserByUuid(uuid: string) {
return this.http.delete<Response>(`/admin/users/uuid/${uuid}/reviews`);
}

getAllAnnouncements() {
return this.http.get<Announcement[]>(`/announcements`);
}

postAnnouncement(announcement: Announcement) {
return this.http.post<Announcement>(`/admin/announcements`, announcement);
}

deleteAnnouncementByUuid(uuid: string) {
return this.http.delete<Response>(`/admin/announcements/${uuid}`);
}
}
1 change: 1 addition & 0 deletions src/app/api/types/announcement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export interface Announcement {
announcementId: string;
title: string;
text: string;
createdAt: Date | undefined;
}
1 change: 0 additions & 1 deletion src/app/api/types/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ export interface Instance {
blockedAssetFlags: AssetConfigFlags;
blockedAssetFlagsForTrustedUsers: AssetConfigFlags;

announcements: Announcement[];
maintenanceModeEnabled: boolean;
grafanaDashboardUrl: string | null;

Expand Down
5 changes: 5 additions & 0 deletions src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ export const routes: Routes = [
loadComponent: () => import('./pages/instance-info/instance-info.component').then(x => x.InstanceInfoComponent),
data: {title: "About Us"},
},
{
path: 'moderation',
loadComponent: () => import('./pages/mod-panel/mod-panel.component').then(x => x.ModPanelComponent),
data: {title: "Moderation Panel"},
},
...appendDebugRoutes(),
// KEEP THIS ROUTE LAST! It handles pages that do not exist.
{
Expand Down
77 changes: 71 additions & 6 deletions src/app/components/items/announcement.component.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,88 @@
import {Component, Input} from '@angular/core';
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {Announcement} from "../../api/types/announcement";

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

@Component({
selector: 'app-announcement',
imports: [
FaIconComponent
FaIconComponent,
ButtonComponent,
ConfirmationDialogComponent,
DateComponent
],
template: `
<div class="bg-yellow rounded px-5 py-2.5">
<fa-icon [icon]="faBullhorn" class="pr-1.5"></fa-icon>
<span class="text-xl font-bold">{{data.title}}</span>
<p>{{data.text}}</p>
<div class="flex flex-row gap-x-2 justify-between">
<div>
<fa-icon [icon]="faBullhorn" class="pr-1.5"></fa-icon>
<span class="text-xl font-bold word-wrap-and-break">{{data.title}}</span>
</div>

@if (showDeleteButton) {
<app-button color="bg-red text-[15px]" yPadding="" [icon]="faTrash" color="bg-red" (click)="toggleDeletionDialog(true)"></app-button>
}
</div>

<p class="word-wrap-and-break">{{data.text}}</p>

@if (data.createdAt != null) {
<div class="flex flex-row justify-end">
<span class="italic">
posted
<app-date [date]="data.createdAt"></app-date>
</span>
</div>
}
</div>

@defer (when showDeletionDialog) { @if (showDeletionDialog) {
<app-confirmation-dialog infoText="Do you really want to delete this announcement?" (closeDialog)="toggleDeletionDialog(false)">
<app-button text="Cancel" [icon]="faSignOutAlt" color="bg-secondary" (click)="toggleDeletionDialog(false)"></app-button>
<app-button text="Delete!" [icon]="faTrash" color="bg-red" (click)="delete()"></app-button>
</app-confirmation-dialog>
}}
`
})
export class AnnouncementComponent {
@Input({required: true}) data: Announcement = undefined!;
@Input() showDeleteButton: boolean = false;
@Output() deleted = new EventEmitter;

protected showDeletionDialog: boolean = false;

constructor(protected client: ClientService, protected banner: BannerService) {

}

protected toggleDeletionDialog(visibility: boolean) {
this.showDeletionDialog = visibility;
}

protected delete() {
if (this.data.announcementId.length == 0) return; // fake announcement which doesn't exist on the server
this.toggleDeletionDialog(false);

this.client.deleteAnnouncementByUuid(this.data.announcementId).subscribe({
error: error => {
const apiError: RefreshApiError | undefined = error.error?.error;
this.banner.error("Announcement deletion failed", apiError == null ? error.message : apiError.message);
},
next: _ => {
this.banner.success("Announcement successfully deleted!", "");
this.deleted.emit();
}
});
}

protected readonly faBullhorn = faBullhorn;
protected readonly faTrash = faTrash;
protected readonly faSignOutAlt = faSignOutAlt;
}
23 changes: 23 additions & 0 deletions src/app/components/ui/header/header-me-menu.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {UserStatisticsComponent} from "../../items/user-statistics.component";

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

@Component({
Expand Down Expand Up @@ -40,6 +41,14 @@ import { UserRoleComponent } from "../info/user-role.component";
@for (item of topItems; track item.route) {
<app-navbar-item [icon]="item.icon" [title]="item.name" [href]="item.route" iconClass="w-4 text-[1.1rem]" labelClass="text-lg"></app-navbar-item>
}

@if (isModerator) {
<app-divider></app-divider>
@for (item of specialItems; track item.route) {
<app-navbar-item [icon]="item.icon" [title]="item.name" [href]="item.route" iconClass="w-4 text-[1.1rem]" labelClass="text-lg"></app-navbar-item>
}
}

<app-divider></app-divider>
@for (item of bottomItems; track item.route) {
<app-navbar-item [icon]="item.icon" [title]="item.name" [href]="item.route" iconClass="w-4 text-[1.1rem]" labelClass="text-lg"></app-navbar-item>
Expand All @@ -49,6 +58,13 @@ import { UserRoleComponent } from "../info/user-role.component";
})
export class HeaderMeMenuComponent {
@Input({required: true}) user: User = undefined!;
isModerator: boolean = false;

ngOnInit() {
if (this.user.role >= UserRoles.Moderator) {
this.isModerator = true;
}
}

protected topItems: NavItem[] = [
{
Expand All @@ -67,6 +83,13 @@ export class HeaderMeMenuComponent {
route: '/settings/profile'
},
];
protected specialItems: NavItem[] = [
{
name: 'Mod Panel',
icon: 'tools',
route: '/moderation'
},
];
protected bottomItems: NavItem[] = [
{
name: 'Log out',
Expand Down
174 changes: 101 additions & 73 deletions src/app/pages/instance-info/instance-info.component.html
Original file line number Diff line number Diff line change
@@ -1,84 +1,112 @@
<app-page-title></app-page-title>
@if(instance != null) {
<br><p class="text-3xl">
<img [ngSrc]="instance.websiteLogoUrl" class="inline aspect-square object-cover rounded"
alt="Server icon" width="30" height="30"
(error)="iconErr($event.target)" loading="lazy">
{{ instance.instanceName }}
</p>
<p class="text-wrap"> {{instance.instanceDescription}}</p>
<br>
<br>

<h2 class="font-bold text-2xl">Server Information</h2>
<p>Software: <span class="italic">{{instance.softwareName}} ({{instance.softwareType}})</span></p>
<p>Version: <span class="word-wrap-and-break italic">v{{instance.softwareVersion}}</span></p>
<p>License:
<a [href]="instance.softwareLicenseUrl" class="text-link hover:text-link-hover hover:underline">
{{ instance.softwareLicenseName }}
</a>
</p>
<p>Source repository:
<a [href]="instance.softwareSourceUrl" class="text-link hover:text-link-hover hover:underline">
{{ instance.softwareSourceUrl }}
</a>
</p>
<p>Blocked assets for regular users: <span class="italic">{{ blockedAssetFlags }}</span></p>
<p>Blocked assets for trusted users: <span class="italic">{{ blockedAssetFlagsForTrustedUsers }}</span></p>
<br>
<app-two-pane-layout>
<app-container class="w-full">
<app-pane-title>
<a routerLink='/instance'>
Instance
</a>
</app-pane-title>
<app-divider></app-divider>
<div>
@if(instance != null) {
<p class="text-3xl">
<img [ngSrc]="instance.websiteLogoUrl" class="inline aspect-square object-cover rounded"
alt="Server icon" width="30" height="30"
(error)="iconErr($event.target)" loading="lazy">
{{ instance.instanceName }}
</p>
<p class="text-wrap"> {{instance.instanceDescription}}</p>
<br>
<h2 class="font-bold text-2xl">Server Information</h2>
<p>Software: <span class="italic">{{instance.softwareName}} ({{instance.softwareType}})</span></p>
<p>Version: <span class="word-wrap-and-break italic">v{{instance.softwareVersion}}</span></p>
<p>License:
<a [href]="instance.softwareLicenseUrl" class="text-link hover:text-link-hover hover:underline">
{{ instance.softwareLicenseName }}
</a>
</p>
<p>Source repository:
<a [href]="instance.softwareSourceUrl" class="text-link hover:text-link-hover hover:underline">
{{ instance.softwareSourceUrl }}
</a>
</p>
<p>Blocked assets for regular users: <span class="italic">{{ blockedAssetFlags }}</span></p>
<p>Blocked assets for trusted users: <span class="italic">{{ blockedAssetFlagsForTrustedUsers }}</span></p>
<br>
<h2 class="font-bold text-2xl">Contact Us</h2>
<p>Owner: <span class="italic">{{ instance.contactInfo.adminName }}</span></p>

<h2 class="font-bold text-2xl">Contact Us</h2>
<p>Owner: <span class="italic">{{ instance.contactInfo.adminName }}</span></p>
@if (instance.contactInfo.adminDiscordUsername != null) {
<p>Owner Discord username: <span class="italic">{{ instance.contactInfo.adminDiscordUsername }}</span></p>
}
@else {
<p>No Discord username of the owner</p>
}

@if (instance.contactInfo.discordServerInvite != null) {
<p>Discord server invite:
<a [href]="instance.contactInfo.discordServerInvite" class="text-link hover:text-link-hover hover:underline">
{{ instance.contactInfo.discordServerInvite }}
</a>
</p>
}
@else {
<p>No Discord server invite</p>
}

@if (instance.contactInfo.adminDiscordUsername != null) {
<p>Owner Discord username: <span class="italic">{{ instance.contactInfo.adminDiscordUsername }}</span></p>
}
@else {
<p>No Discord username of the owner</p>
}

@if (instance.contactInfo.discordServerInvite != null) {
<p>Discord server invite:
<a [href]="instance.contactInfo.discordServerInvite" class="text-link hover:text-link-hover hover:underline">
{{ instance.contactInfo.discordServerInvite }}
<p>Email address:
<a [href]="'mailto:' + instance.contactInfo.emailAddress" class="text-link hover:text-link-hover hover:underline word-wrap-and-break">
{{instance.contactInfo.emailAddress }}
</a>
</p>
<br>
}
@else if (statisticsDownloadFailed) {
<p>Failed to download instance metadata.</p>
}
@else {
<p>Downloading instance metadata...</p>
}
</div>
</app-container>
<app-container class="w-full">
<app-pane-title>
<a routerLink='/instance'>
Statistics
</a>
</p>
}
@else {
<p>No Discord server invite</p>
}

<p>Email address:
<a [href]="'mailto:' + instance.contactInfo.emailAddress" class="text-link hover:text-link-hover hover:underline word-wrap-and-break">
{{instance.contactInfo.emailAddress }}
</a>
</p>
<br>
}
</app-pane-title>
<app-divider></app-divider>
<div>
@if(statistics != null) {
<p><a class="text-link hover:text-link-hover hover:underline" routerLink="/users">Registered users: {{statistics.totalUsers}}</a></p>
<p><a class="text-link hover:text-link-hover hover:underline" routerLink="/levels">Published levels: {{statistics.totalLevels}}</a></p>
<p><a class="text-link hover:text-link-hover hover:underline" routerLink="/levels">Modded levels: {{statistics.moddedLevels}}</a></p>
<p><a class="text-link hover:text-link-hover hover:underline" routerLink="/photos">Uploaded photos: {{statistics.totalPhotos}}</a></p>
<p><a class="text-link hover:text-link-hover hover:underline" routerLink="/activity">Events occurred: {{statistics.totalEvents}}</a></p>
<br>
<p>Active users: {{statistics.activeUsers}}</p>
<p>People online now: {{statistics.currentIngamePlayersCount}}</p>
<p><a class="text-link hover:text-link-hover hover:underline" routerLink="/rooms">Active rooms: {{statistics.currentRoomCount}}</a></p>
<br>
<p>API requests: {{statistics.requestStatistics.apiRequests}}</p>
<p>Game API requests: {{statistics.requestStatistics.gameRequests}}</p>
}
@else if (statisticsDownloadFailed) {
<p>Failed to download instance statistics.</p>
}
@else {
<p>Downloading instance statistics...</p>
}
</div>
</app-container>
</app-two-pane-layout>

<br>
<h2 class="font-bold text-2xl">Website</h2>
<p>Source repository:
<a [href]="websiteRepoUrl" class="text-link hover:text-link-hover hover:underline">
{{ websiteRepoUrl }}
</a>
</p>
<br>

@if(statistics != null) {
<h2 class="font-bold text-2xl">Things!</h2>
<p><a routerLink="/users">Registered users: {{statistics.totalUsers}}</a></p>
<p><a routerLink="/levels">Published levels: {{statistics.totalLevels}}</a></p>
<p><a routerLink="/levels">Modded levels: {{statistics.moddedLevels}}</a></p>
<p><a routerLink="/photos">Uploaded photos: {{statistics.totalPhotos}}</a></p>
<p><a routerLink="/activity">Events occurred: {{statistics.totalEvents}}</a></p>
<br>

<h2 class="font-bold text-2xl">Activity</h2>
<p>Active users: {{statistics.activeUsers}}</p>
<p>People online now: {{statistics.currentIngamePlayersCount}}</p>
<p><a class="text-link hover:text-link-hover hover:underline" routerLink="/rooms">Active rooms: {{statistics.currentRoomCount}}</a></p>
<br>

<h2 class="font-bold text-2xl">Requests ({{statistics.requestStatistics.totalRequests}} in total)</h2>
<p>API requests: {{statistics.requestStatistics.apiRequests}}</p>
<p>Game API requests: {{statistics.requestStatistics.gameRequests}}</p>
}
</p>
Loading
Loading