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) { +
+ + posted + + +
+ }
+ + @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) { -

- Server icon - {{ 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 + + + +
+ @if(instance != null) { +

+ Server icon + {{ 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 }}

+
+

Contact Us

+

Owner: {{ instance.contactInfo.adminName }}

-

Contact Us

-

Owner: {{ instance.contactInfo.adminName }}

+ @if (instance.contactInfo.adminDiscordUsername != null) { +

Owner Discord username: {{ instance.contactInfo.adminDiscordUsername }}

+ } + @else { +

No Discord username of the owner

+ } + + @if (instance.contactInfo.discordServerInvite != null) { +

Discord server invite: + + {{ instance.contactInfo.discordServerInvite }} + +

+ } + @else { +

No Discord server invite

+ } - @if (instance.contactInfo.adminDiscordUsername != null) { -

Owner Discord username: {{ instance.contactInfo.adminDiscordUsername }}

- } - @else { -

No Discord username of the owner

- } - - @if (instance.contactInfo.discordServerInvite != null) { -

Discord server invite: - - {{ instance.contactInfo.discordServerInvite }} +

Email address: + + {{instance.contactInfo.emailAddress }} + +

+
+ } + @else if (statisticsDownloadFailed) { +

Failed to download instance metadata.

+ } + @else { +

Downloading instance metadata...

+ } +
+
+ + + + Statistics -

- } - @else { -

No Discord server invite

- } - -

Email address: - - {{instance.contactInfo.emailAddress }} - -

-
-} +
+ +
+ @if(statistics != null) { +

Registered users: {{statistics.totalUsers}}

+

Published levels: {{statistics.totalLevels}}

+

Modded levels: {{statistics.moddedLevels}}

+

Uploaded photos: {{statistics.totalPhotos}}

+

Events occurred: {{statistics.totalEvents}}

+
+

Active users: {{statistics.activeUsers}}

+

People online now: {{statistics.currentIngamePlayersCount}}

+

Active rooms: {{statistics.currentRoomCount}}

+
+

API requests: {{statistics.requestStatistics.apiRequests}}

+

Game API requests: {{statistics.requestStatistics.gameRequests}}

+ } + @else if (statisticsDownloadFailed) { +

Failed to download instance statistics.

+ } + @else { +

Downloading instance statistics...

+ } +
+
+
+

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 + + + +
+ @if(statistics != null) { +

Registered users: {{statistics.totalUsers}}

+

Published levels: {{statistics.totalLevels}}

+

Uploaded photos: {{statistics.totalPhotos}}

+

Events occurred: {{statistics.totalEvents}}

+
+

Active users: {{statistics.activeUsers}}

+

People online now: {{statistics.currentIngamePlayersCount}}

+

Active rooms: {{statistics.currentRoomCount}}

+ } + @else if (statisticsDownloadFailed) { +

Failed to download instance statistics.

+ } + @else { +

Downloading instance statistics...

+ } +
+
+
+ + + +

Announcements

+ +
+
+

Create

+
+ + + +
+ + +
+
+
+ +
+

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",