diff --git a/src/app/api/client.service.ts b/src/app/api/client.service.ts
index f2177286..8293963d 100644
--- a/src/app/api/client.service.ts
+++ b/src/app/api/client.service.ts
@@ -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;
@@ -263,4 +264,16 @@ export class ClientService extends ApiImplementation {
deleteReviewsByUserByUuid(uuid: string) {
return this.http.delete(`/admin/users/uuid/${uuid}/reviews`);
}
+
+ getAllAnnouncements() {
+ return this.http.get(`/announcements`);
+ }
+
+ postAnnouncement(announcement: Announcement) {
+ return this.http.post(`/admin/announcements`, announcement);
+ }
+
+ deleteAnnouncementByUuid(uuid: string) {
+ return this.http.delete(`/admin/announcements/${uuid}`);
+ }
}
diff --git a/src/app/api/types/announcement.ts b/src/app/api/types/announcement.ts
index 4fb55daf..b52d42e5 100644
--- a/src/app/api/types/announcement.ts
+++ b/src/app/api/types/announcement.ts
@@ -2,4 +2,5 @@ export interface Announcement {
announcementId: string;
title: string;
text: string;
+ createdAt: Date | undefined;
}
diff --git a/src/app/api/types/instance.ts b/src/app/api/types/instance.ts
index a94a11a2..450f0c4f 100644
--- a/src/app/api/types/instance.ts
+++ b/src/app/api/types/instance.ts
@@ -19,7 +19,6 @@ export interface Instance {
blockedAssetFlags: AssetConfigFlags;
blockedAssetFlagsForTrustedUsers: AssetConfigFlags;
- announcements: Announcement[];
maintenanceModeEnabled: boolean;
grafanaDashboardUrl: string | null;
diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts
index d0793d9d..8c6fe264 100644
--- a/src/app/app.routes.ts
+++ b/src/app/app.routes.ts
@@ -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.
{
diff --git a/src/app/components/items/announcement.component.ts b/src/app/components/items/announcement.component.ts
index 51e00764..689830ac 100644
--- a/src/app/components/items/announcement.component.ts
+++ b/src/app/components/items/announcement.component.ts
@@ -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: `
-
-
{{data.title}}
-
{{data.text}}
+
+
+
+ {{data.title}}
+
+
+ @if (showDeleteButton) {
+
+ }
+
+
+
{{data.text}}
+
+ @if (data.createdAt != null) {
+
+ }
+
+ @defer (when showDeletionDialog) { @if (showDeletionDialog) {
+
+
+
+
+ }}
`
})
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;
}
diff --git a/src/app/components/ui/header/header-me-menu.component.ts b/src/app/components/ui/header/header-me-menu.component.ts
index da8ea5fb..9b48e89d 100644
--- a/src/app/components/ui/header/header-me-menu.component.ts
+++ b/src/app/components/ui/header/header-me-menu.component.ts
@@ -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({
@@ -40,6 +41,14 @@ import { UserRoleComponent } from "../info/user-role.component";
@for (item of topItems; track item.route) {
}
+
+ @if (isModerator) {
+
+ @for (item of specialItems; track item.route) {
+
+ }
+ }
+
@for (item of bottomItems; track item.route) {
@@ -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[] = [
{
@@ -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',
diff --git a/src/app/pages/instance-info/instance-info.component.html b/src/app/pages/instance-info/instance-info.component.html
index 464c9d27..021b6245 100644
--- a/src/app/pages/instance-info/instance-info.component.html
+++ b/src/app/pages/instance-info/instance-info.component.html
@@ -1,84 +1,112 @@
-@if(instance != null) {
-
-
- {{ instance.instanceName }}
-
- {{instance.instanceDescription}}
-
+
- Server Information
- Software: {{instance.softwareName}} ({{instance.softwareType}})
- Version: v{{instance.softwareVersion}}
- License:
-
- {{ instance.softwareLicenseName }}
-
-
- Source repository:
-
- {{ instance.softwareSourceUrl }}
-
-
- Blocked assets for regular users: {{ blockedAssetFlags }}
- Blocked assets for trusted users: {{ blockedAssetFlagsForTrustedUsers }}
-
+
+
+
+
+ Instance
+
+
+
+
+
+
+
+
+ Statistics
-
- }
- @else {
- No Discord server invite
- }
-
- Email address:
-
- {{instance.contactInfo.emailAddress }}
-
-
-
-}
+
+
+
+
+
+
Website
Source repository:
{{ websiteRepoUrl }}
-
-
-
-@if(statistics != null) {
- Things!
- Registered users: {{statistics.totalUsers}}
- Published levels: {{statistics.totalLevels}}
- Modded levels: {{statistics.moddedLevels}}
- Uploaded photos: {{statistics.totalPhotos}}
- Events occurred: {{statistics.totalEvents}}
-
-
- Activity
- Active users: {{statistics.activeUsers}}
- People online now: {{statistics.currentIngamePlayersCount}}
- Active rooms: {{statistics.currentRoomCount}}
-
-
- Requests ({{statistics.requestStatistics.totalRequests}} in total)
- API requests: {{statistics.requestStatistics.apiRequests}}
- Game API requests: {{statistics.requestStatistics.gameRequests}}
-}
+
\ No newline at end of file
diff --git a/src/app/pages/instance-info/instance-info.component.ts b/src/app/pages/instance-info/instance-info.component.ts
index 7e36fb93..0af214e2 100644
--- a/src/app/pages/instance-info/instance-info.component.ts
+++ b/src/app/pages/instance-info/instance-info.component.ts
@@ -9,19 +9,30 @@ import { NgOptimizedImage } from "@angular/common";
import { getWebsiteRepoUrl } from '../../helpers/data-fetching';
import { RouterLink } from "@angular/router";
import { blockedFlagsAsString } from '../../api/types/asset-config-flags';
+import { TwoPaneLayoutComponent } from "../../components/ui/layouts/two-pane-layout.component";
+import { ContainerComponent } from "../../components/ui/container.component";
+import { PaneTitleComponent } from "../../components/ui/text/pane-title.component";
+import { DividerComponent } from "../../components/ui/divider.component";
@Component({
selector: 'app-instance-info',
imports: [
PageTitleComponent,
NgOptimizedImage,
- RouterLink
+ RouterLink,
+ TwoPaneLayoutComponent,
+ ContainerComponent,
+ PaneTitleComponent,
+ DividerComponent
],
templateUrl: './instance-info.component.html'
})
export class InstanceInfoComponent {
protected instance: Instance | undefined;
+ protected instanceDownloadFailed: boolean = false;
+
protected statistics: Statistics | undefined;
+ protected statisticsDownloadFailed: boolean = false;
protected websiteRepoUrl: String = getWebsiteRepoUrl();
protected iconError: boolean = false;
@@ -32,6 +43,7 @@ export class InstanceInfoComponent {
constructor(private client: ClientService, protected banner: BannerService) {
client.getInstance().subscribe({
error: error => {
+ this.instanceDownloadFailed = true;
const apiError: RefreshApiError | undefined = error.error?.error;
this.banner.error("Failed to retrieve instance data", apiError == null ? error.message : apiError.message);
},
@@ -44,6 +56,7 @@ export class InstanceInfoComponent {
client.getStatistics(true).subscribe({
error: error => {
+ this.statisticsDownloadFailed = true;
const apiError: RefreshApiError | undefined = error.error?.error;
this.banner.error("Failed to retrieve instance statistics", apiError == null ? error.message : apiError.message);
},
diff --git a/src/app/pages/landing/landing.component.html b/src/app/pages/landing/landing.component.html
index e9142586..51480705 100644
--- a/src/app/pages/landing/landing.component.html
+++ b/src/app/pages/landing/landing.component.html
@@ -13,9 +13,9 @@ {{this.instance?.instanceName ?? 'Refresh'}}
{{this.instance?.instanceDescription ?? 'Loading...'}}
-@if (instance && instance.announcements.length > 0) {
+@if (announcements && announcements.length > 0) {
- @for (a of instance.announcements; track a.announcementId) {
+ @for (a of announcements; track a.announcementId) {
}
diff --git a/src/app/pages/landing/landing.component.ts b/src/app/pages/landing/landing.component.ts
index ea91657d..71005a5d 100644
--- a/src/app/pages/landing/landing.component.ts
+++ b/src/app/pages/landing/landing.component.ts
@@ -29,6 +29,11 @@ import {ActivityPage} from "../../api/types/activity/activity-page";
import {EventPageComponent} from "../../components/items/event-page.component";
import {repeat, Subscription} from "rxjs";
import {ContestBannerComponent} from "../../components/items/contest-banner.component";
+import { ExtendedUser } from '../../api/types/users/extended-user';
+import { AuthenticationService } from '../../api/authentication.service';
+import { UserRoles } from '../../api/types/users/user-roles';
+import { Announcement } from '../../api/types/announcement';
+import { RefreshApiError } from '../../api/refresh-api-error';
@Component({
selector: 'app-landing',
@@ -52,33 +57,58 @@ export class LandingComponent implements OnDestroy {
protected instance: Instance | undefined;
protected rooms: Room[] | undefined;
protected activity: ActivityPage | undefined;
+ protected announcements: Announcement[] | undefined;
private activitySubscription: Subscription | undefined;
private roomsSubscription: Subscription | undefined;
- constructor(private client: ClientService, @Inject(PLATFORM_ID) platformId: Object, changeDetector: ChangeDetectorRef) {
- client.getInstance().subscribe(data => this.instance = data);
-
- if(isPlatformBrowser(platformId)) {
- inject(NgZone).runOutsideAngular(() => {
- this.activitySubscription = this.fetchActivity()
- .pipe(repeat({delay: 5000}))
- .subscribe(data => {
- this.activity = data;
- changeDetector.detectChanges();
- });
-
- this.roomsSubscription = this.fetchRooms()
- .pipe(repeat({delay: 15000}))
- .subscribe(data => {
- this.rooms = data.data;
- changeDetector.detectChanges();
- });
- })
- }
-
- this.fetchActivity().subscribe(data => this.activity = data);
- this.fetchRooms().subscribe(data => this.rooms = data.data);
+ protected ownUser: ExtendedUser | null | undefined;
+ protected showAnnouncementDeleteButton: boolean = false;
+
+ constructor(private client: ClientService, @Inject(PLATFORM_ID) platformId: Object, changeDetector: ChangeDetectorRef, protected auth: AuthenticationService) {
+ client.getInstance().subscribe(data => this.instance = data);
+
+ if(isPlatformBrowser(platformId)) {
+ inject(NgZone).runOutsideAngular(() => {
+ this.activitySubscription = this.fetchActivity()
+ .pipe(repeat({delay: 5000}))
+ .subscribe(data => {
+ this.activity = data;
+ changeDetector.detectChanges();
+ });
+
+ this.roomsSubscription = this.fetchRooms()
+ .pipe(repeat({delay: 15000}))
+ .subscribe(data => {
+ this.rooms = data.data;
+ changeDetector.detectChanges();
+ });
+ })
+ }
+
+ this.fetchActivity().subscribe(data => this.activity = data);
+ this.fetchRooms().subscribe(data => this.rooms = data.data);
+
+ this.auth.user.subscribe(user => {
+ if (user) {
+ this.ownUser = user;
+
+ if (user.role >= UserRoles.Moderator) {
+ this.showAnnouncementDeleteButton = true;
+ }
+ }
+ });
+
+ client.getAllAnnouncements().subscribe({
+ error: error => {
+ // don't log, just print to console, as this isn't that important
+ const apiError: RefreshApiError | undefined = error.error?.error;
+ console.warn("Failed to retrieve announcements: " + (apiError == null ? error.message : apiError.message));
+ },
+ next: response => {
+ this.announcements = response;
+ }
+ });
}
ngOnDestroy(): void {
diff --git a/src/app/pages/mod-panel/mod-panel.component.html b/src/app/pages/mod-panel/mod-panel.component.html
new file mode 100644
index 00000000..476ec53d
--- /dev/null
+++ b/src/app/pages/mod-panel/mod-panel.component.html
@@ -0,0 +1,125 @@
+@if (ownUser) {
+
+ Hi, {{ ownUser.username }}! This is where you manage and view information about {{instance?.instanceName ?? 'this Refresh server'}}.
+
+
+
+
+
+
+ Instance
+
+
+
+
+ @if(instance != null) {
+
Software: {{instance.softwareName}} ({{instance.softwareType}})
+
Version: v{{instance.softwareVersion}}
+
+
Blocked assets for regular users: {{ blockedAssetFlags }}
+
Blocked assets for trusted users: {{ blockedAssetFlagsForTrustedUsers }}
+ }
+ @else if (instanceDownloadFailed) {
+
Failed to download instance metadata.
+ }
+ @else {
+
Downloading instance metadata...
+ }
+
+
+
+
+
+ Statistics
+
+
+
+
+
+
+
+
+
+ Announcements
+
+
+
+
+
+
Preview
+
+
+
Live Announcements
+ @if (announcements == null) {
+ @if (announcementsDownloadFailed) {
+
Failed to download announcements.
+ }
+ @else {
+
Loading instance data...
+ }
+ }
+ @else if (announcements.length <= 0) {
+ @if (announcementsDownloadFailed) {
+
There might be current announcements, reload the page to download them.
+ }
+ @else {
+
There are no current announcements.
+ }
+ }
+ @else {
+
+ @for (announcement of announcements; track announcement.announcementId) {
+
+ }
+
+ }
+
+
+
+ @if (this.grafanaSafeUrl != null) {
+
+ }
+
+
+
+ Other Actions
+ To manage users, levels, and other data, you can access administrative actions on their respective pages, usually on the page headers.
+
+ @defer (when showAnnouncementPostDialog) { @if (showAnnouncementPostDialog) {
+
+
+
+
+ }}
+}
\ No newline at end of file
diff --git a/src/app/pages/mod-panel/mod-panel.component.ts b/src/app/pages/mod-panel/mod-panel.component.ts
new file mode 100644
index 00000000..a456d007
--- /dev/null
+++ b/src/app/pages/mod-panel/mod-panel.component.ts
@@ -0,0 +1,195 @@
+import { Component } from '@angular/core';
+import {ClientService} from "../../api/client.service";
+import { PageTitleComponent } from "../../components/ui/text/page-title.component";
+import { ExtendedUser } from '../../api/types/users/extended-user';
+import { AuthenticationService } from '../../api/authentication.service';
+import { UserRoles } from '../../api/types/users/user-roles';
+import { BannerService } from '../../banners/banner.service';
+import { Instance } from '../../api/types/instance';
+import { Statistics } from '../../api/types/statistics';
+import { RefreshApiError } from '../../api/refresh-api-error';
+import { blockedFlagsAsString } from '../../api/types/asset-config-flags';
+import { DividerComponent } from "../../components/ui/divider.component";
+import { FormControl, FormGroup } from '@angular/forms';
+import { AnnouncementComponent } from "../../components/items/announcement.component";
+import { Announcement } from '../../api/types/announcement';
+import { faBullhorn, faPaperPlane, faPencil, faSignOutAlt, faTrash } from '@fortawesome/free-solid-svg-icons';
+import { TextboxComponent } from "../../components/ui/form/textbox.component";
+import { TextAreaComponent } from "../../components/ui/form/textarea.component";
+import { ButtonComponent } from "../../components/ui/form/button.component";
+import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
+import { RouterLink } from "@angular/router";
+import { TwoPaneLayoutComponent } from "../../components/ui/layouts/two-pane-layout.component";
+import { ContainerComponent } from "../../components/ui/container.component";
+import { PaneTitleComponent } from "../../components/ui/text/pane-title.component";
+import { ConfirmationDialogComponent } from "../../components/ui/confirmation-dialog.component";
+
+@Component({
+ selector: 'app-mod-panel',
+ imports: [
+ PageTitleComponent,
+ DividerComponent,
+ AnnouncementComponent,
+ TextboxComponent,
+ TextAreaComponent,
+ ButtonComponent,
+ RouterLink,
+ TwoPaneLayoutComponent,
+ ContainerComponent,
+ PaneTitleComponent,
+ ConfirmationDialogComponent
+],
+ templateUrl: './mod-panel.component.html'
+})
+export class ModPanelComponent {
+ protected ownUser: ExtendedUser | null | undefined;
+
+ protected instance: Instance | undefined;
+ protected instanceDownloadFailed: boolean = false;
+ protected announcements: Announcement[] | undefined;
+ protected announcementsDownloadFailed: boolean = false;
+ protected blockedAssetFlags: String = "None";
+ protected blockedAssetFlagsForTrustedUsers: String = "None";
+
+ protected statistics: Statistics | undefined;
+ protected statisticsDownloadFailed: boolean = false;
+
+ protected previewAnnouncement: Announcement = {
+ announcementId: "",
+ title: "",
+ text: "",
+ createdAt: undefined,
+ };
+ protected announcementForm = new FormGroup({
+ title: new FormControl(),
+ text: new FormControl(),
+ });
+ protected isAnnouncementComplete: boolean = false;
+ protected showAnnouncementPostDialog: boolean = false;
+
+ protected grafanaSafeUrl: SafeUrl | undefined;
+
+ constructor(private auth: AuthenticationService, private client: ClientService, protected banner: BannerService, private sanitizer: DomSanitizer) {
+ this.auth.user.subscribe(user => {
+ if (user) {
+ if (user.role < UserRoles.Moderator) {
+ this.banner.error("You are not a moderator", "Get out!");
+ }
+ else {
+ this.ownUser = user;
+
+ client.getInstance().subscribe({
+ error: error => {
+ this.instanceDownloadFailed = true;
+ const apiError: RefreshApiError | undefined = error.error?.error;
+ this.banner.error("Failed to retrieve instance data", apiError == null ? error.message : apiError.message);
+ },
+ next: response => {
+ this.instance = response;
+ this.blockedAssetFlags = blockedFlagsAsString(response.blockedAssetFlags);
+ this.blockedAssetFlagsForTrustedUsers = blockedFlagsAsString(response.blockedAssetFlagsForTrustedUsers);
+
+ // initialize sanitized URL
+ let originalUrl = this.instance?.grafanaDashboardUrl;
+ if (originalUrl != null && originalUrl.length > 0) {
+ this.grafanaSafeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(originalUrl);
+ }
+ }
+ });
+
+ client.getStatistics().subscribe({
+ error: error => {
+ this.statisticsDownloadFailed = true;
+ const apiError: RefreshApiError | undefined = error.error?.error;
+ this.banner.error("Failed to retrieve instance statistics", apiError == null ? error.message : apiError.message);
+ },
+ next: response => {
+ this.statistics = response;
+ }
+ });
+
+ client.getAllAnnouncements().subscribe({
+ error: error => {
+ this.announcementsDownloadFailed = true;
+ const apiError: RefreshApiError | undefined = error.error?.error;
+ this.banner.error("Failed to retrieve announcements", apiError == null ? error.message : apiError.message);
+ },
+ next: response => {
+ this.announcements = response;
+ }
+ });
+ }
+ }
+ });
+ }
+
+ protected checkAnnouncementTitleChanges() {
+ let currentTitle: string = this.announcementForm.controls.title.getRawValue();
+ this.previewAnnouncement.title = currentTitle;
+ this.checkAnnouncementCompleteness();
+ }
+
+ protected checkAnnouncementTextChanges() {
+ let currentText: string = this.announcementForm.controls.text.getRawValue();
+ this.previewAnnouncement.text = currentText;
+ this.checkAnnouncementCompleteness();
+ }
+
+ protected checkAnnouncementCompleteness() {
+ this.isAnnouncementComplete = this.previewAnnouncement.title.length > 0 && this.previewAnnouncement.text.length > 0;
+ }
+
+ protected resetAnnouncementInputs() {
+ this.isAnnouncementComplete = false;
+ this.announcementForm.controls.title.setValue("");
+ this.announcementForm.controls.text.setValue("");
+
+ this.previewAnnouncement = {
+ announcementId: "",
+ title: "",
+ text: "",
+ createdAt: undefined,
+ };
+ }
+
+ protected toggleAnnouncementPostDialog(visibility: boolean) {
+ this.showAnnouncementPostDialog = visibility;
+ }
+
+ protected postAnnouncement() {
+ if (!this.isAnnouncementComplete) return;
+ this.toggleAnnouncementPostDialog(false);
+
+ this.client.postAnnouncement(this.previewAnnouncement).subscribe({
+ error: error => {
+ const apiError: RefreshApiError | undefined = error.error?.error;
+ this.banner.error("Failed to post the announcement", apiError == null ? error.message : apiError.message);
+ },
+ next: response => {
+ this.resetAnnouncementInputs();
+ let newList: Announcement[] = [];
+ newList.push(response);
+
+ if (!this.announcements) this.announcements = newList
+ else this.announcements = newList.concat(this.announcements);
+ }
+ });
+ }
+
+ protected removeAnnouncement(uuid: string) {
+ if (this.announcements == null) return;
+
+ let newList: Announcement[] = [];
+ for (let announcement of this.announcements) {
+ if (announcement.announcementId !== uuid) newList.push(announcement);
+ }
+
+ this.announcements = newList;
+ }
+
+ protected readonly faBullhorn = faBullhorn;
+ protected readonly faPencil = faPencil;
+ protected readonly faPaperPlane = faPaperPlane;
+ protected readonly faTrash = faTrash;
+ protected readonly faSignOutAlt = faSignOutAlt;
+}
diff --git a/tailwind.config.js b/tailwind.config.js
index f84760ab..1615dabd 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -26,7 +26,7 @@ const defaultColors = {
"dark-green": "#4a9e27ff",
"light-blue": "#2d92e5ff",
"blue": "#2D43E5",
- "yellow": "#F2AA00",
+ "yellow": "#DB9901",
"orange": "#f8640eff",
"dark-pink": "#f145a4ff",
"pink": "#ff68f4",