diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index 581605f..2e12dd0 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -176,6 +176,13 @@ enum MangaStatus { RETIRED } +enum PrimaryBadge { + STAFF + MOD + VIP + DISCORD +} + model User { id String @id @default(auto()) @map("_id") @db.ObjectId discordId String @unique @@ -186,6 +193,7 @@ model User { displayName String? bio String? profilePublic Boolean @default(true) + primaryBadge PrimaryBadge? website String? discordServer String? bluesky String? diff --git a/api/src/app/routes/users/index.ts b/api/src/app/routes/users/index.ts index c7283bc..fc94a67 100644 --- a/api/src/app/routes/users/index.ts +++ b/api/src/app/routes/users/index.ts @@ -5,7 +5,7 @@ */ import { FastifyPluginAsync } from "fastify"; -import { User, AuditAction, AuditCategory } from "@library/shared-types"; +import { User, AuditAction, AuditCategory, PrimaryBadge } from "@library/shared-types"; import { UserService } from "../../services/user.service"; import { AuditService } from "../../services/audit.service"; import { adminGuard } from "../../middleware/admin-guard"; @@ -15,6 +15,7 @@ interface UpdateUserSettingsBody { displayName?: string; bio?: string; profilePublic?: boolean; + primaryBadge?: PrimaryBadge; website?: string; discordServer?: string; bluesky?: string; @@ -31,6 +32,7 @@ interface UserProfileResponse { avatar?: string; bio?: string; slug?: string; + primaryBadge?: PrimaryBadge; website?: string; discordServer?: string; bluesky?: string; @@ -144,6 +146,7 @@ const usersRoutes: FastifyPluginAsync = async (app) => { avatar: profile.avatar, bio: profile.bio, slug: profile.slug, + primaryBadge: profile.primaryBadge, website: profile.website, discordServer: profile.discordServer, bluesky: profile.bluesky, diff --git a/api/src/app/services/user.service.ts b/api/src/app/services/user.service.ts index 7540c6f..68c3dcf 100644 --- a/api/src/app/services/user.service.ts +++ b/api/src/app/services/user.service.ts @@ -4,7 +4,7 @@ * @author Naomi Carrigan */ -import { User } from "@library/shared-types"; +import { User, PrimaryBadge } from "@library/shared-types"; import { prisma } from "../lib/prisma"; import { SuggestionStatus } from "@prisma/client"; @@ -26,6 +26,7 @@ export class UserService { displayName: user.displayName || undefined, bio: user.bio || undefined, profilePublic: user.profilePublic, + primaryBadge: (user.primaryBadge as PrimaryBadge) || undefined, website: user.website || undefined, discordServer: user.discordServer || undefined, bluesky: user.bluesky || undefined, @@ -61,6 +62,7 @@ export class UserService { displayName: user.displayName || undefined, bio: user.bio || undefined, profilePublic: user.profilePublic, + primaryBadge: (user.primaryBadge as PrimaryBadge) || undefined, website: user.website || undefined, discordServer: user.discordServer || undefined, bluesky: user.bluesky || undefined, @@ -93,6 +95,7 @@ export class UserService { displayName: user.displayName || undefined, bio: user.bio || undefined, profilePublic: user.profilePublic, + primaryBadge: (user.primaryBadge as PrimaryBadge) || undefined, website: user.website || undefined, discordServer: user.discordServer || undefined, bluesky: user.bluesky || undefined, @@ -125,6 +128,7 @@ export class UserService { displayName: user.displayName || undefined, bio: user.bio || undefined, profilePublic: user.profilePublic, + primaryBadge: (user.primaryBadge as PrimaryBadge) || undefined, website: user.website || undefined, discordServer: user.discordServer || undefined, bluesky: user.bluesky || undefined, @@ -169,6 +173,7 @@ export class UserService { displayName: user.displayName || undefined, bio: user.bio || undefined, profilePublic: user.profilePublic, + primaryBadge: (user.primaryBadge as PrimaryBadge) || undefined, website: user.website || undefined, discordServer: user.discordServer || undefined, bluesky: user.bluesky || undefined, @@ -192,6 +197,7 @@ export class UserService { displayName?: string; bio?: string; profilePublic?: boolean; + primaryBadge?: PrimaryBadge; website?: string; discordServer?: string; bluesky?: string; @@ -216,6 +222,7 @@ export class UserService { displayName: user.displayName || undefined, bio: user.bio || undefined, profilePublic: user.profilePublic, + primaryBadge: (user.primaryBadge as PrimaryBadge) || undefined, website: user.website || undefined, discordServer: user.discordServer || undefined, bluesky: user.bluesky || undefined, @@ -239,6 +246,7 @@ export class UserService { avatar?: string | null; bio?: string | null; slug?: string | null; + primaryBadge?: PrimaryBadge | null; website?: string | null; discordServer?: string | null; bluesky?: string | null; @@ -294,6 +302,7 @@ export class UserService { avatar: user.avatar, bio: user.bio, slug: user.slug, + primaryBadge: user.primaryBadge as PrimaryBadge, website: user.website, discordServer: user.discordServer, bluesky: user.bluesky, diff --git a/apps/frontend/src/app/components/admin-reports/admin-reports.component.ts b/apps/frontend/src/app/components/admin-reports/admin-reports.component.ts index 65d21a4..9edffab 100644 --- a/apps/frontend/src/app/components/admin-reports/admin-reports.component.ts +++ b/apps/frontend/src/app/components/admin-reports/admin-reports.component.ts @@ -14,7 +14,7 @@ import { CommentsService } from '../../services/comments.service'; import { UserService } from '../../services/user.service'; import { AuthService } from '../../services/auth.service'; import { ToastService } from '../../services/toast.service'; -import { ProfileReportWithUsers, CommentReportWithDetails, ReportStatus, ReportReason } from '@library/shared-types'; +import { ProfileReportWithUsers, CommentReportWithDetails, ReportStatus, ReportReason, PrimaryBadge } from '@library/shared-types'; @Component({ selector: 'app-admin-reports', @@ -674,6 +674,31 @@ import { ProfileReportWithUsers, CommentReportWithDetails, ReportStatus, ReportR > {{ (profileEditForm.bio.length || 0) }} / 500 characters + +
+ + + Choose one badge to display on profile, or show all +
@@ -1462,8 +1487,9 @@ export class AdminReportsComponent implements OnInit { private toastService = inject(ToastService); private router = inject(Router); - // Make ReportStatus accessible in template + // Make ReportStatus and PrimaryBadge accessible in template protected readonly ReportStatus = ReportStatus; + protected readonly PrimaryBadge = PrimaryBadge; reportType = signal<'profile' | 'comment'>('profile'); allProfileReports = signal([]); @@ -1487,6 +1513,7 @@ export class AdminReportsComponent implements OnInit { slug: '', bio: '', profilePublic: true, + primaryBadge: undefined as PrimaryBadge | undefined, website: '', discordServer: '', bluesky: '', @@ -1773,6 +1800,7 @@ export class AdminReportsComponent implements OnInit { slug: profile.slug || '', bio: profile.bio || '', profilePublic: true, // We'll get this from the full user object if needed + primaryBadge: profile.primaryBadge || undefined, website: profile.website || '', discordServer: profile.discordServer || '', bluesky: profile.bluesky || '', @@ -1798,6 +1826,7 @@ export class AdminReportsComponent implements OnInit { slug: '', bio: '', profilePublic: true, + primaryBadge: undefined as PrimaryBadge | undefined, website: '', discordServer: '', bluesky: '', diff --git a/apps/frontend/src/app/components/art/art-gallery.component.ts b/apps/frontend/src/app/components/art/art-gallery.component.ts index fbaefe0..cb015c9 100644 --- a/apps/frontend/src/app/components/art/art-gallery.component.ts +++ b/apps/frontend/src/app/components/art/art-gallery.component.ts @@ -471,8 +471,8 @@ import { Art, CreateArtDto, UpdateArtDto, Comment, SuggestionEntity, Link } from
} diff --git a/apps/frontend/src/app/components/books/books-list.component.ts b/apps/frontend/src/app/components/books/books-list.component.ts index 0af45ac..bcdbb50 100644 --- a/apps/frontend/src/app/components/books/books-list.component.ts +++ b/apps/frontend/src/app/components/books/books-list.component.ts @@ -658,8 +658,8 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEnti } @else { } diff --git a/apps/frontend/src/app/components/comment-display/comment-display.component.ts b/apps/frontend/src/app/components/comment-display/comment-display.component.ts index 879aa51..4a9f3dd 100644 --- a/apps/frontend/src/app/components/comment-display/comment-display.component.ts +++ b/apps/frontend/src/app/components/comment-display/comment-display.component.ts @@ -42,7 +42,7 @@ import { ReportModalComponent } from '../report-modal/report-modal.component'; } @if (canDeleteComment(comment)) { - + } @if (canReportComment(comment)) { @@ -245,8 +245,8 @@ export class CommentDisplayComponent { readonly sanitizeService = inject(SanitizeService); @Input({ required: true }) comments = signal([]); - @Output() onEdit = new EventEmitter<{ commentId: string; content: string }>(); - @Output() onDelete = new EventEmitter(); + @Output() edit = new EventEmitter<{ commentId: string; content: string }>(); + @Output() delete = new EventEmitter(); editingCommentId = signal(null); editCommentContent = ''; @@ -289,7 +289,7 @@ export class CommentDisplayComponent { } saveEdit(commentId: string): void { - this.onEdit.emit({ commentId, content: this.editCommentContent }); + this.edit.emit({ commentId, content: this.editCommentContent }); this.cancelEdit(); } diff --git a/apps/frontend/src/app/components/games/games-list.component.ts b/apps/frontend/src/app/components/games/games-list.component.ts index c592498..430c4f9 100644 --- a/apps/frontend/src/app/components/games/games-list.component.ts +++ b/apps/frontend/src/app/components/games/games-list.component.ts @@ -611,8 +611,8 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEnti } @else { } diff --git a/apps/frontend/src/app/components/manga/manga-list.component.ts b/apps/frontend/src/app/components/manga/manga-list.component.ts index 58c9ecf..a6eafe3 100644 --- a/apps/frontend/src/app/components/manga/manga-list.component.ts +++ b/apps/frontend/src/app/components/manga/manga-list.component.ts @@ -613,8 +613,8 @@ import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto, Comment, Suggestion } diff --git a/apps/frontend/src/app/components/music/music-list.component.ts b/apps/frontend/src/app/components/music/music-list.component.ts index 1c91be4..4ccad38 100644 --- a/apps/frontend/src/app/components/music/music-list.component.ts +++ b/apps/frontend/src/app/components/music/music-list.component.ts @@ -689,8 +689,8 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment, } diff --git a/apps/frontend/src/app/components/profile/profile.component.ts b/apps/frontend/src/app/components/profile/profile.component.ts index 62fb84e..3d3eeb5 100644 --- a/apps/frontend/src/app/components/profile/profile.component.ts +++ b/apps/frontend/src/app/components/profile/profile.component.ts @@ -14,6 +14,7 @@ import { UserService, UserProfileResponse } from '../../services/user.service'; import { ToastService } from '../../services/toast.service'; import { AuthService } from '../../services/auth.service'; import { ReportModalComponent } from '../report-modal/report-modal.component'; +import { PrimaryBadge } from '@library/shared-types'; @Component({ selector: 'app-profile', @@ -58,17 +59,34 @@ import { ReportModalComponent } from '../report-modal/report-modal.component';
- @if (profile()!.badges.isStaff) { - Staff - } - @if (profile()!.badges.isMod) { - Moderator - } - @if (profile()!.badges.isVip) { - VIP - } - @if (profile()!.badges.inDiscord) { - Discord Member + @if (profile()!.primaryBadge) { + + @if (profile()!.primaryBadge === PrimaryBadge.STAFF && profile()!.badges.isStaff) { + Staff + } + @if (profile()!.primaryBadge === PrimaryBadge.MOD && profile()!.badges.isMod) { + Moderator + } + @if (profile()!.primaryBadge === PrimaryBadge.VIP && profile()!.badges.isVip) { + VIP + } + @if (profile()!.primaryBadge === PrimaryBadge.DISCORD && profile()!.badges.inDiscord) { + Discord Member + } + } @else { + + @if (profile()!.badges.isStaff) { + Staff + } + @if (profile()!.badges.isMod) { + Moderator + } + @if (profile()!.badges.isVip) { + VIP + } + @if (profile()!.badges.inDiscord) { + Discord Member + } }
@@ -426,6 +444,9 @@ export class ProfileComponent implements OnInit { error = signal(null); reportModalOpen = signal(false); + // Expose PrimaryBadge enum for template + readonly PrimaryBadge = PrimaryBadge; + // Font Awesome icons faGlobe = faGlobe; faGithub = faGithub; diff --git a/apps/frontend/src/app/components/settings/settings.component.ts b/apps/frontend/src/app/components/settings/settings.component.ts index 49a6b22..1f6b490 100644 --- a/apps/frontend/src/app/components/settings/settings.component.ts +++ b/apps/frontend/src/app/components/settings/settings.component.ts @@ -10,7 +10,7 @@ import { FormsModule } from '@angular/forms'; import { UserService, UpdateUserSettingsRequest } from '../../services/user.service'; import { AuthService } from '../../services/auth.service'; import { ToastService } from '../../services/toast.service'; -import { User } from '@library/shared-types'; +import { User, PrimaryBadge } from '@library/shared-types'; @Component({ selector: 'app-settings', @@ -69,6 +69,30 @@ import { User } from '@library/shared-types'; > {{ (formData.bio?.length || 0) }} / 500 characters + +
+ + + Choose one badge to display on your profile, or show all +
@@ -264,7 +288,8 @@ import { User } from '@library/shared-types'; } .form-group input[type="text"], - .form-group textarea { + .form-group textarea, + .form-group select { width: 100%; padding: 0.75rem; border: 1px solid rgba(155, 89, 182, 0.5); @@ -275,8 +300,13 @@ import { User } from '@library/shared-types'; font-family: inherit; } + .form-group select { + cursor: pointer; + } + .form-group input[type="text"]:focus, - .form-group textarea:focus { + .form-group textarea:focus, + .form-group select:focus { outline: none; border-color: var(--accent-colour, #9b59b6); box-shadow: 0 0 0 2px rgba(155, 89, 182, 0.3); @@ -375,11 +405,15 @@ export class SettingsComponent implements OnInit { loading = signal(true); saving = signal(false); + // Expose PrimaryBadge enum for template + readonly PrimaryBadge = PrimaryBadge; + formData: UpdateUserSettingsRequest & { bio?: string } = { displayName: '', slug: '', bio: '', profilePublic: true, + primaryBadge: undefined, website: '', discordServer: '', bluesky: '', @@ -398,6 +432,7 @@ export class SettingsComponent implements OnInit { slug: userData.slug || '', bio: userData.bio || '', profilePublic: userData.profilePublic ?? true, + primaryBadge: userData.primaryBadge || undefined, website: userData.website || '', discordServer: userData.discordServer || '', bluesky: userData.bluesky || '', @@ -424,6 +459,7 @@ export class SettingsComponent implements OnInit { slug: this.formData.slug || undefined, bio: this.formData.bio || undefined, profilePublic: this.formData.profilePublic, + primaryBadge: this.formData.primaryBadge || undefined, website: this.formData.website || undefined, discordServer: this.formData.discordServer || undefined, bluesky: this.formData.bluesky || undefined, diff --git a/apps/frontend/src/app/components/shows/shows-list.component.ts b/apps/frontend/src/app/components/shows/shows-list.component.ts index ce41b07..24910d1 100644 --- a/apps/frontend/src/app/components/shows/shows-list.component.ts +++ b/apps/frontend/src/app/components/shows/shows-list.component.ts @@ -607,8 +607,8 @@ import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto, Comment, Sugg
} diff --git a/apps/frontend/src/app/services/user.service.ts b/apps/frontend/src/app/services/user.service.ts index 45f67d9..8b4f75f 100644 --- a/apps/frontend/src/app/services/user.service.ts +++ b/apps/frontend/src/app/services/user.service.ts @@ -7,7 +7,7 @@ import { Injectable, inject } from '@angular/core'; import { Observable } from 'rxjs'; import { ApiService } from './api.service'; -import { User } from '@library/shared-types'; +import { User, PrimaryBadge } from '@library/shared-types'; export interface UserProfileResponse { id: string; @@ -16,6 +16,7 @@ export interface UserProfileResponse { avatar?: string; bio?: string; slug?: string; + primaryBadge?: PrimaryBadge; website?: string; discordServer?: string; bluesky?: string; @@ -43,6 +44,7 @@ export interface UpdateUserSettingsRequest { displayName?: string; bio?: string; profilePublic?: boolean; + primaryBadge?: PrimaryBadge; website?: string; discordServer?: string; bluesky?: string; diff --git a/shared-types/src/index.ts b/shared-types/src/index.ts index 69b4d61..ec404b3 100644 --- a/shared-types/src/index.ts +++ b/shared-types/src/index.ts @@ -5,7 +5,7 @@ */ export type * from "./lib/art.types"; export * from "./lib/audit.types"; -export type * from "./lib/auth.types"; +export * from "./lib/auth.types"; export * from "./lib/book.types"; export type * from "./lib/comment.types"; export type * from "./lib/common.types"; diff --git a/shared-types/src/lib/auth.types.ts b/shared-types/src/lib/auth.types.ts index a80b044..8fda2c2 100644 --- a/shared-types/src/lib/auth.types.ts +++ b/shared-types/src/lib/auth.types.ts @@ -4,6 +4,15 @@ * @author Naomi Carrigan */ +/* eslint-disable @typescript-eslint/naming-convention -- Prisma enum values use UPPER_CASE */ +enum PrimaryBadge { + STAFF = "STAFF", + MOD = "MOD", + VIP = "VIP", + DISCORD = "DISCORD", +} +/* eslint-enable @typescript-eslint/naming-convention */ + interface User { id: string; email: string; @@ -13,6 +22,7 @@ interface User { displayName?: string; bio?: string; profilePublic: boolean; + primaryBadge?: PrimaryBadge; website?: string; discordServer?: string; bluesky?: string; @@ -47,4 +57,5 @@ interface AuthResponse { user: User; } +export { PrimaryBadge }; export type { AuthResponse, JwtPayload, User };