generated from nhcarrigan/template
feat: implement user profiles with achievements and primary badge system #58
@@ -176,6 +176,13 @@ enum MangaStatus {
|
|||||||
RETIRED
|
RETIRED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum PrimaryBadge {
|
||||||
|
STAFF
|
||||||
|
MOD
|
||||||
|
VIP
|
||||||
|
DISCORD
|
||||||
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(auto()) @map("_id") @db.ObjectId
|
id String @id @default(auto()) @map("_id") @db.ObjectId
|
||||||
discordId String @unique
|
discordId String @unique
|
||||||
@@ -186,6 +193,7 @@ model User {
|
|||||||
displayName String?
|
displayName String?
|
||||||
bio String?
|
bio String?
|
||||||
profilePublic Boolean @default(true)
|
profilePublic Boolean @default(true)
|
||||||
|
primaryBadge PrimaryBadge?
|
||||||
website String?
|
website String?
|
||||||
discordServer String?
|
discordServer String?
|
||||||
bluesky String?
|
bluesky String?
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { FastifyPluginAsync } from "fastify";
|
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 { UserService } from "../../services/user.service";
|
||||||
import { AuditService } from "../../services/audit.service";
|
import { AuditService } from "../../services/audit.service";
|
||||||
import { adminGuard } from "../../middleware/admin-guard";
|
import { adminGuard } from "../../middleware/admin-guard";
|
||||||
@@ -15,6 +15,7 @@ interface UpdateUserSettingsBody {
|
|||||||
displayName?: string;
|
displayName?: string;
|
||||||
bio?: string;
|
bio?: string;
|
||||||
profilePublic?: boolean;
|
profilePublic?: boolean;
|
||||||
|
primaryBadge?: PrimaryBadge;
|
||||||
website?: string;
|
website?: string;
|
||||||
discordServer?: string;
|
discordServer?: string;
|
||||||
bluesky?: string;
|
bluesky?: string;
|
||||||
@@ -31,6 +32,7 @@ interface UserProfileResponse {
|
|||||||
avatar?: string;
|
avatar?: string;
|
||||||
bio?: string;
|
bio?: string;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
|
primaryBadge?: PrimaryBadge;
|
||||||
website?: string;
|
website?: string;
|
||||||
discordServer?: string;
|
discordServer?: string;
|
||||||
bluesky?: string;
|
bluesky?: string;
|
||||||
@@ -144,6 +146,7 @@ const usersRoutes: FastifyPluginAsync = async (app) => {
|
|||||||
avatar: profile.avatar,
|
avatar: profile.avatar,
|
||||||
bio: profile.bio,
|
bio: profile.bio,
|
||||||
slug: profile.slug,
|
slug: profile.slug,
|
||||||
|
primaryBadge: profile.primaryBadge,
|
||||||
website: profile.website,
|
website: profile.website,
|
||||||
discordServer: profile.discordServer,
|
discordServer: profile.discordServer,
|
||||||
bluesky: profile.bluesky,
|
bluesky: profile.bluesky,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* @author Naomi Carrigan
|
* @author Naomi Carrigan
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { User } from "@library/shared-types";
|
import { User, PrimaryBadge } from "@library/shared-types";
|
||||||
import { prisma } from "../lib/prisma";
|
import { prisma } from "../lib/prisma";
|
||||||
import { SuggestionStatus } from "@prisma/client";
|
import { SuggestionStatus } from "@prisma/client";
|
||||||
|
|
||||||
@@ -26,6 +26,7 @@ export class UserService {
|
|||||||
displayName: user.displayName || undefined,
|
displayName: user.displayName || undefined,
|
||||||
bio: user.bio || undefined,
|
bio: user.bio || undefined,
|
||||||
profilePublic: user.profilePublic,
|
profilePublic: user.profilePublic,
|
||||||
|
primaryBadge: (user.primaryBadge as PrimaryBadge) || undefined,
|
||||||
website: user.website || undefined,
|
website: user.website || undefined,
|
||||||
discordServer: user.discordServer || undefined,
|
discordServer: user.discordServer || undefined,
|
||||||
bluesky: user.bluesky || undefined,
|
bluesky: user.bluesky || undefined,
|
||||||
@@ -61,6 +62,7 @@ export class UserService {
|
|||||||
displayName: user.displayName || undefined,
|
displayName: user.displayName || undefined,
|
||||||
bio: user.bio || undefined,
|
bio: user.bio || undefined,
|
||||||
profilePublic: user.profilePublic,
|
profilePublic: user.profilePublic,
|
||||||
|
primaryBadge: (user.primaryBadge as PrimaryBadge) || undefined,
|
||||||
website: user.website || undefined,
|
website: user.website || undefined,
|
||||||
discordServer: user.discordServer || undefined,
|
discordServer: user.discordServer || undefined,
|
||||||
bluesky: user.bluesky || undefined,
|
bluesky: user.bluesky || undefined,
|
||||||
@@ -93,6 +95,7 @@ export class UserService {
|
|||||||
displayName: user.displayName || undefined,
|
displayName: user.displayName || undefined,
|
||||||
bio: user.bio || undefined,
|
bio: user.bio || undefined,
|
||||||
profilePublic: user.profilePublic,
|
profilePublic: user.profilePublic,
|
||||||
|
primaryBadge: (user.primaryBadge as PrimaryBadge) || undefined,
|
||||||
website: user.website || undefined,
|
website: user.website || undefined,
|
||||||
discordServer: user.discordServer || undefined,
|
discordServer: user.discordServer || undefined,
|
||||||
bluesky: user.bluesky || undefined,
|
bluesky: user.bluesky || undefined,
|
||||||
@@ -125,6 +128,7 @@ export class UserService {
|
|||||||
displayName: user.displayName || undefined,
|
displayName: user.displayName || undefined,
|
||||||
bio: user.bio || undefined,
|
bio: user.bio || undefined,
|
||||||
profilePublic: user.profilePublic,
|
profilePublic: user.profilePublic,
|
||||||
|
primaryBadge: (user.primaryBadge as PrimaryBadge) || undefined,
|
||||||
website: user.website || undefined,
|
website: user.website || undefined,
|
||||||
discordServer: user.discordServer || undefined,
|
discordServer: user.discordServer || undefined,
|
||||||
bluesky: user.bluesky || undefined,
|
bluesky: user.bluesky || undefined,
|
||||||
@@ -169,6 +173,7 @@ export class UserService {
|
|||||||
displayName: user.displayName || undefined,
|
displayName: user.displayName || undefined,
|
||||||
bio: user.bio || undefined,
|
bio: user.bio || undefined,
|
||||||
profilePublic: user.profilePublic,
|
profilePublic: user.profilePublic,
|
||||||
|
primaryBadge: (user.primaryBadge as PrimaryBadge) || undefined,
|
||||||
website: user.website || undefined,
|
website: user.website || undefined,
|
||||||
discordServer: user.discordServer || undefined,
|
discordServer: user.discordServer || undefined,
|
||||||
bluesky: user.bluesky || undefined,
|
bluesky: user.bluesky || undefined,
|
||||||
@@ -192,6 +197,7 @@ export class UserService {
|
|||||||
displayName?: string;
|
displayName?: string;
|
||||||
bio?: string;
|
bio?: string;
|
||||||
profilePublic?: boolean;
|
profilePublic?: boolean;
|
||||||
|
primaryBadge?: PrimaryBadge;
|
||||||
website?: string;
|
website?: string;
|
||||||
discordServer?: string;
|
discordServer?: string;
|
||||||
bluesky?: string;
|
bluesky?: string;
|
||||||
@@ -216,6 +222,7 @@ export class UserService {
|
|||||||
displayName: user.displayName || undefined,
|
displayName: user.displayName || undefined,
|
||||||
bio: user.bio || undefined,
|
bio: user.bio || undefined,
|
||||||
profilePublic: user.profilePublic,
|
profilePublic: user.profilePublic,
|
||||||
|
primaryBadge: (user.primaryBadge as PrimaryBadge) || undefined,
|
||||||
website: user.website || undefined,
|
website: user.website || undefined,
|
||||||
discordServer: user.discordServer || undefined,
|
discordServer: user.discordServer || undefined,
|
||||||
bluesky: user.bluesky || undefined,
|
bluesky: user.bluesky || undefined,
|
||||||
@@ -239,6 +246,7 @@ export class UserService {
|
|||||||
avatar?: string | null;
|
avatar?: string | null;
|
||||||
bio?: string | null;
|
bio?: string | null;
|
||||||
slug?: string | null;
|
slug?: string | null;
|
||||||
|
primaryBadge?: PrimaryBadge | null;
|
||||||
website?: string | null;
|
website?: string | null;
|
||||||
discordServer?: string | null;
|
discordServer?: string | null;
|
||||||
bluesky?: string | null;
|
bluesky?: string | null;
|
||||||
@@ -294,6 +302,7 @@ export class UserService {
|
|||||||
avatar: user.avatar,
|
avatar: user.avatar,
|
||||||
bio: user.bio,
|
bio: user.bio,
|
||||||
slug: user.slug,
|
slug: user.slug,
|
||||||
|
primaryBadge: user.primaryBadge as PrimaryBadge,
|
||||||
website: user.website,
|
website: user.website,
|
||||||
discordServer: user.discordServer,
|
discordServer: user.discordServer,
|
||||||
bluesky: user.bluesky,
|
bluesky: user.bluesky,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { CommentsService } from '../../services/comments.service';
|
|||||||
import { UserService } from '../../services/user.service';
|
import { UserService } from '../../services/user.service';
|
||||||
import { AuthService } from '../../services/auth.service';
|
import { AuthService } from '../../services/auth.service';
|
||||||
import { ToastService } from '../../services/toast.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({
|
@Component({
|
||||||
selector: 'app-admin-reports',
|
selector: 'app-admin-reports',
|
||||||
@@ -674,6 +674,31 @@ import { ProfileReportWithUsers, CommentReportWithDetails, ReportStatus, ReportR
|
|||||||
></textarea>
|
></textarea>
|
||||||
<small class="form-help">{{ (profileEditForm.bio.length || 0) }} / 500 characters</small>
|
<small class="form-help">{{ (profileEditForm.bio.length || 0) }} / 500 characters</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="primaryBadge">Primary Badge</label>
|
||||||
|
<select
|
||||||
|
id="primaryBadge"
|
||||||
|
name="primaryBadge"
|
||||||
|
[(ngModel)]="profileEditForm.primaryBadge"
|
||||||
|
class="form-control"
|
||||||
|
>
|
||||||
|
<option [ngValue]="undefined">None (show all badges)</option>
|
||||||
|
@if (editingProfile()?.profile?.badges.isStaff) {
|
||||||
|
<option [ngValue]="PrimaryBadge.STAFF">Staff</option>
|
||||||
|
}
|
||||||
|
@if (editingProfile()?.profile?.badges.isMod) {
|
||||||
|
<option [ngValue]="PrimaryBadge.MOD">Moderator</option>
|
||||||
|
}
|
||||||
|
@if (editingProfile()?.profile?.badges.isVip) {
|
||||||
|
<option [ngValue]="PrimaryBadge.VIP">VIP</option>
|
||||||
|
}
|
||||||
|
@if (editingProfile()?.profile?.badges.inDiscord) {
|
||||||
|
<option [ngValue]="PrimaryBadge.DISCORD">Discord Member</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
<small class="form-help">Choose one badge to display on profile, or show all</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
@@ -1462,8 +1487,9 @@ export class AdminReportsComponent implements OnInit {
|
|||||||
private toastService = inject(ToastService);
|
private toastService = inject(ToastService);
|
||||||
private router = inject(Router);
|
private router = inject(Router);
|
||||||
|
|
||||||
// Make ReportStatus accessible in template
|
// Make ReportStatus and PrimaryBadge accessible in template
|
||||||
protected readonly ReportStatus = ReportStatus;
|
protected readonly ReportStatus = ReportStatus;
|
||||||
|
protected readonly PrimaryBadge = PrimaryBadge;
|
||||||
|
|
||||||
reportType = signal<'profile' | 'comment'>('profile');
|
reportType = signal<'profile' | 'comment'>('profile');
|
||||||
allProfileReports = signal<ProfileReportWithUsers[]>([]);
|
allProfileReports = signal<ProfileReportWithUsers[]>([]);
|
||||||
@@ -1487,6 +1513,7 @@ export class AdminReportsComponent implements OnInit {
|
|||||||
slug: '',
|
slug: '',
|
||||||
bio: '',
|
bio: '',
|
||||||
profilePublic: true,
|
profilePublic: true,
|
||||||
|
primaryBadge: undefined as PrimaryBadge | undefined,
|
||||||
website: '',
|
website: '',
|
||||||
discordServer: '',
|
discordServer: '',
|
||||||
bluesky: '',
|
bluesky: '',
|
||||||
@@ -1773,6 +1800,7 @@ export class AdminReportsComponent implements OnInit {
|
|||||||
slug: profile.slug || '',
|
slug: profile.slug || '',
|
||||||
bio: profile.bio || '',
|
bio: profile.bio || '',
|
||||||
profilePublic: true, // We'll get this from the full user object if needed
|
profilePublic: true, // We'll get this from the full user object if needed
|
||||||
|
primaryBadge: profile.primaryBadge || undefined,
|
||||||
website: profile.website || '',
|
website: profile.website || '',
|
||||||
discordServer: profile.discordServer || '',
|
discordServer: profile.discordServer || '',
|
||||||
bluesky: profile.bluesky || '',
|
bluesky: profile.bluesky || '',
|
||||||
@@ -1798,6 +1826,7 @@ export class AdminReportsComponent implements OnInit {
|
|||||||
slug: '',
|
slug: '',
|
||||||
bio: '',
|
bio: '',
|
||||||
profilePublic: true,
|
profilePublic: true,
|
||||||
|
primaryBadge: undefined as PrimaryBadge | undefined,
|
||||||
website: '',
|
website: '',
|
||||||
discordServer: '',
|
discordServer: '',
|
||||||
bluesky: '',
|
bluesky: '',
|
||||||
|
|||||||
@@ -471,8 +471,8 @@ import { Art, CreateArtDto, UpdateArtDto, Comment, SuggestionEntity, Link } from
|
|||||||
|
|
||||||
<app-comment-display
|
<app-comment-display
|
||||||
[comments]="getCommentsSignal(art.id)"
|
[comments]="getCommentsSignal(art.id)"
|
||||||
(onEdit)="handleCommentEdit(art.id, $event)"
|
(edit)="handleCommentEdit(art.id, $event)"
|
||||||
(onDelete)="deleteComment(art.id, $event)"
|
(delete)="deleteComment(art.id, $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -658,8 +658,8 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEnti
|
|||||||
} @else {
|
} @else {
|
||||||
<app-comment-display
|
<app-comment-display
|
||||||
[comments]="getCommentsSignal(book.id)"
|
[comments]="getCommentsSignal(book.id)"
|
||||||
(onEdit)="handleCommentEdit(book.id, $event)"
|
(edit)="handleCommentEdit(book.id, $event)"
|
||||||
(onDelete)="deleteComment(book.id, $event)"
|
(delete)="deleteComment(book.id, $event)"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import { ReportModalComponent } from '../report-modal/report-modal.component';
|
|||||||
<button (click)="startEdit(comment)" class="btn btn-secondary btn-xs">Edit</button>
|
<button (click)="startEdit(comment)" class="btn btn-secondary btn-xs">Edit</button>
|
||||||
}
|
}
|
||||||
@if (canDeleteComment(comment)) {
|
@if (canDeleteComment(comment)) {
|
||||||
<button (click)="onDelete.emit(comment.id)" class="btn btn-danger btn-xs">Delete</button>
|
<button (click)="delete.emit(comment.id)" class="btn btn-danger btn-xs">Delete</button>
|
||||||
}
|
}
|
||||||
@if (canReportComment(comment)) {
|
@if (canReportComment(comment)) {
|
||||||
<button (click)="openReportModal(comment)" class="btn btn-warning btn-xs">Report</button>
|
<button (click)="openReportModal(comment)" class="btn btn-warning btn-xs">Report</button>
|
||||||
@@ -245,8 +245,8 @@ export class CommentDisplayComponent {
|
|||||||
readonly sanitizeService = inject(SanitizeService);
|
readonly sanitizeService = inject(SanitizeService);
|
||||||
|
|
||||||
@Input({ required: true }) comments = signal<Comment[]>([]);
|
@Input({ required: true }) comments = signal<Comment[]>([]);
|
||||||
@Output() onEdit = new EventEmitter<{ commentId: string; content: string }>();
|
@Output() edit = new EventEmitter<{ commentId: string; content: string }>();
|
||||||
@Output() onDelete = new EventEmitter<string>();
|
@Output() delete = new EventEmitter<string>();
|
||||||
|
|
||||||
editingCommentId = signal<string | null>(null);
|
editingCommentId = signal<string | null>(null);
|
||||||
editCommentContent = '';
|
editCommentContent = '';
|
||||||
@@ -289,7 +289,7 @@ export class CommentDisplayComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
saveEdit(commentId: string): void {
|
saveEdit(commentId: string): void {
|
||||||
this.onEdit.emit({ commentId, content: this.editCommentContent });
|
this.edit.emit({ commentId, content: this.editCommentContent });
|
||||||
this.cancelEdit();
|
this.cancelEdit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -611,8 +611,8 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEnti
|
|||||||
} @else {
|
} @else {
|
||||||
<app-comment-display
|
<app-comment-display
|
||||||
[comments]="getCommentsSignal(game.id)"
|
[comments]="getCommentsSignal(game.id)"
|
||||||
(onEdit)="handleCommentEdit(game.id, $event)"
|
(edit)="handleCommentEdit(game.id, $event)"
|
||||||
(onDelete)="deleteComment(game.id, $event)"
|
(delete)="deleteComment(game.id, $event)"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -613,8 +613,8 @@ import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto, Comment, Suggestion
|
|||||||
|
|
||||||
<app-comment-display
|
<app-comment-display
|
||||||
[comments]="getCommentsSignal(manga.id)"
|
[comments]="getCommentsSignal(manga.id)"
|
||||||
(onEdit)="handleCommentEdit(manga.id, $event)"
|
(edit)="handleCommentEdit(manga.id, $event)"
|
||||||
(onDelete)="deleteComment(manga.id, $event)"
|
(delete)="deleteComment(manga.id, $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -689,8 +689,8 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment,
|
|||||||
|
|
||||||
<app-comment-display
|
<app-comment-display
|
||||||
[comments]="getCommentsSignal(music.id)"
|
[comments]="getCommentsSignal(music.id)"
|
||||||
(onEdit)="handleCommentEdit(music.id, $event)"
|
(edit)="handleCommentEdit(music.id, $event)"
|
||||||
(onDelete)="deleteComment(music.id, $event)"
|
(delete)="deleteComment(music.id, $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { UserService, UserProfileResponse } from '../../services/user.service';
|
|||||||
import { ToastService } from '../../services/toast.service';
|
import { ToastService } from '../../services/toast.service';
|
||||||
import { AuthService } from '../../services/auth.service';
|
import { AuthService } from '../../services/auth.service';
|
||||||
import { ReportModalComponent } from '../report-modal/report-modal.component';
|
import { ReportModalComponent } from '../report-modal/report-modal.component';
|
||||||
|
import { PrimaryBadge } from '@library/shared-types';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-profile',
|
selector: 'app-profile',
|
||||||
@@ -58,6 +59,22 @@ import { ReportModalComponent } from '../report-modal/report-modal.component';
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="badges-section">
|
<div class="badges-section">
|
||||||
|
@if (profile()!.primaryBadge) {
|
||||||
|
<!-- Show only the selected primary badge -->
|
||||||
|
@if (profile()!.primaryBadge === PrimaryBadge.STAFF && profile()!.badges.isStaff) {
|
||||||
|
<span class="badge badge-staff">Staff</span>
|
||||||
|
}
|
||||||
|
@if (profile()!.primaryBadge === PrimaryBadge.MOD && profile()!.badges.isMod) {
|
||||||
|
<span class="badge badge-mod">Moderator</span>
|
||||||
|
}
|
||||||
|
@if (profile()!.primaryBadge === PrimaryBadge.VIP && profile()!.badges.isVip) {
|
||||||
|
<span class="badge badge-vip">VIP</span>
|
||||||
|
}
|
||||||
|
@if (profile()!.primaryBadge === PrimaryBadge.DISCORD && profile()!.badges.inDiscord) {
|
||||||
|
<span class="badge badge-member">Discord Member</span>
|
||||||
|
}
|
||||||
|
} @else {
|
||||||
|
<!-- Show all badges if no primary badge is selected -->
|
||||||
@if (profile()!.badges.isStaff) {
|
@if (profile()!.badges.isStaff) {
|
||||||
<span class="badge badge-staff">Staff</span>
|
<span class="badge badge-staff">Staff</span>
|
||||||
}
|
}
|
||||||
@@ -70,6 +87,7 @@ import { ReportModalComponent } from '../report-modal/report-modal.component';
|
|||||||
@if (profile()!.badges.inDiscord) {
|
@if (profile()!.badges.inDiscord) {
|
||||||
<span class="badge badge-member">Discord Member</span>
|
<span class="badge badge-member">Discord Member</span>
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (profile()!.bio) {
|
@if (profile()!.bio) {
|
||||||
@@ -426,6 +444,9 @@ export class ProfileComponent implements OnInit {
|
|||||||
error = signal<string | null>(null);
|
error = signal<string | null>(null);
|
||||||
reportModalOpen = signal(false);
|
reportModalOpen = signal(false);
|
||||||
|
|
||||||
|
// Expose PrimaryBadge enum for template
|
||||||
|
readonly PrimaryBadge = PrimaryBadge;
|
||||||
|
|
||||||
// Font Awesome icons
|
// Font Awesome icons
|
||||||
faGlobe = faGlobe;
|
faGlobe = faGlobe;
|
||||||
faGithub = faGithub;
|
faGithub = faGithub;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { FormsModule } from '@angular/forms';
|
|||||||
import { UserService, UpdateUserSettingsRequest } from '../../services/user.service';
|
import { UserService, UpdateUserSettingsRequest } from '../../services/user.service';
|
||||||
import { AuthService } from '../../services/auth.service';
|
import { AuthService } from '../../services/auth.service';
|
||||||
import { ToastService } from '../../services/toast.service';
|
import { ToastService } from '../../services/toast.service';
|
||||||
import { User } from '@library/shared-types';
|
import { User, PrimaryBadge } from '@library/shared-types';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-settings',
|
selector: 'app-settings',
|
||||||
@@ -69,6 +69,30 @@ import { User } from '@library/shared-types';
|
|||||||
></textarea>
|
></textarea>
|
||||||
<small class="form-help">{{ (formData.bio?.length || 0) }} / 500 characters</small>
|
<small class="form-help">{{ (formData.bio?.length || 0) }} / 500 characters</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="primaryBadge">Primary Badge</label>
|
||||||
|
<select
|
||||||
|
id="primaryBadge"
|
||||||
|
name="primaryBadge"
|
||||||
|
[(ngModel)]="formData.primaryBadge"
|
||||||
|
>
|
||||||
|
<option [ngValue]="undefined">None (show all badges)</option>
|
||||||
|
@if (user()!.isStaff) {
|
||||||
|
<option [ngValue]="PrimaryBadge.STAFF">Staff</option>
|
||||||
|
}
|
||||||
|
@if (user()!.isMod) {
|
||||||
|
<option [ngValue]="PrimaryBadge.MOD">Moderator</option>
|
||||||
|
}
|
||||||
|
@if (user()!.isVip) {
|
||||||
|
<option [ngValue]="PrimaryBadge.VIP">VIP</option>
|
||||||
|
}
|
||||||
|
@if (user()!.inDiscord) {
|
||||||
|
<option [ngValue]="PrimaryBadge.DISCORD">Discord Member</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
<small class="form-help">Choose one badge to display on your profile, or show all</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
@@ -264,7 +288,8 @@ import { User } from '@library/shared-types';
|
|||||||
}
|
}
|
||||||
|
|
||||||
.form-group input[type="text"],
|
.form-group input[type="text"],
|
||||||
.form-group textarea {
|
.form-group textarea,
|
||||||
|
.form-group select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
border: 1px solid rgba(155, 89, 182, 0.5);
|
border: 1px solid rgba(155, 89, 182, 0.5);
|
||||||
@@ -275,8 +300,13 @@ import { User } from '@library/shared-types';
|
|||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-group select {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.form-group input[type="text"]:focus,
|
.form-group input[type="text"]:focus,
|
||||||
.form-group textarea:focus {
|
.form-group textarea:focus,
|
||||||
|
.form-group select:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--accent-colour, #9b59b6);
|
border-color: var(--accent-colour, #9b59b6);
|
||||||
box-shadow: 0 0 0 2px rgba(155, 89, 182, 0.3);
|
box-shadow: 0 0 0 2px rgba(155, 89, 182, 0.3);
|
||||||
@@ -375,11 +405,15 @@ export class SettingsComponent implements OnInit {
|
|||||||
loading = signal(true);
|
loading = signal(true);
|
||||||
saving = signal(false);
|
saving = signal(false);
|
||||||
|
|
||||||
|
// Expose PrimaryBadge enum for template
|
||||||
|
readonly PrimaryBadge = PrimaryBadge;
|
||||||
|
|
||||||
formData: UpdateUserSettingsRequest & { bio?: string } = {
|
formData: UpdateUserSettingsRequest & { bio?: string } = {
|
||||||
displayName: '',
|
displayName: '',
|
||||||
slug: '',
|
slug: '',
|
||||||
bio: '',
|
bio: '',
|
||||||
profilePublic: true,
|
profilePublic: true,
|
||||||
|
primaryBadge: undefined,
|
||||||
website: '',
|
website: '',
|
||||||
discordServer: '',
|
discordServer: '',
|
||||||
bluesky: '',
|
bluesky: '',
|
||||||
@@ -398,6 +432,7 @@ export class SettingsComponent implements OnInit {
|
|||||||
slug: userData.slug || '',
|
slug: userData.slug || '',
|
||||||
bio: userData.bio || '',
|
bio: userData.bio || '',
|
||||||
profilePublic: userData.profilePublic ?? true,
|
profilePublic: userData.profilePublic ?? true,
|
||||||
|
primaryBadge: userData.primaryBadge || undefined,
|
||||||
website: userData.website || '',
|
website: userData.website || '',
|
||||||
discordServer: userData.discordServer || '',
|
discordServer: userData.discordServer || '',
|
||||||
bluesky: userData.bluesky || '',
|
bluesky: userData.bluesky || '',
|
||||||
@@ -424,6 +459,7 @@ export class SettingsComponent implements OnInit {
|
|||||||
slug: this.formData.slug || undefined,
|
slug: this.formData.slug || undefined,
|
||||||
bio: this.formData.bio || undefined,
|
bio: this.formData.bio || undefined,
|
||||||
profilePublic: this.formData.profilePublic,
|
profilePublic: this.formData.profilePublic,
|
||||||
|
primaryBadge: this.formData.primaryBadge || undefined,
|
||||||
website: this.formData.website || undefined,
|
website: this.formData.website || undefined,
|
||||||
discordServer: this.formData.discordServer || undefined,
|
discordServer: this.formData.discordServer || undefined,
|
||||||
bluesky: this.formData.bluesky || undefined,
|
bluesky: this.formData.bluesky || undefined,
|
||||||
|
|||||||
@@ -607,8 +607,8 @@ import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto, Comment, Sugg
|
|||||||
|
|
||||||
<app-comment-display
|
<app-comment-display
|
||||||
[comments]="getCommentsSignal(show.id)"
|
[comments]="getCommentsSignal(show.id)"
|
||||||
(onEdit)="handleCommentEdit(show.id, $event)"
|
(edit)="handleCommentEdit(show.id, $event)"
|
||||||
(onDelete)="deleteComment(show.id, $event)"
|
(delete)="deleteComment(show.id, $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { ApiService } from './api.service';
|
import { ApiService } from './api.service';
|
||||||
import { User } from '@library/shared-types';
|
import { User, PrimaryBadge } from '@library/shared-types';
|
||||||
|
|
||||||
export interface UserProfileResponse {
|
export interface UserProfileResponse {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -16,6 +16,7 @@ export interface UserProfileResponse {
|
|||||||
avatar?: string;
|
avatar?: string;
|
||||||
bio?: string;
|
bio?: string;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
|
primaryBadge?: PrimaryBadge;
|
||||||
website?: string;
|
website?: string;
|
||||||
discordServer?: string;
|
discordServer?: string;
|
||||||
bluesky?: string;
|
bluesky?: string;
|
||||||
@@ -43,6 +44,7 @@ export interface UpdateUserSettingsRequest {
|
|||||||
displayName?: string;
|
displayName?: string;
|
||||||
bio?: string;
|
bio?: string;
|
||||||
profilePublic?: boolean;
|
profilePublic?: boolean;
|
||||||
|
primaryBadge?: PrimaryBadge;
|
||||||
website?: string;
|
website?: string;
|
||||||
discordServer?: string;
|
discordServer?: string;
|
||||||
bluesky?: string;
|
bluesky?: string;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
export type * from "./lib/art.types";
|
export type * from "./lib/art.types";
|
||||||
export * from "./lib/audit.types";
|
export * from "./lib/audit.types";
|
||||||
export type * from "./lib/auth.types";
|
export * from "./lib/auth.types";
|
||||||
export * from "./lib/book.types";
|
export * from "./lib/book.types";
|
||||||
export type * from "./lib/comment.types";
|
export type * from "./lib/comment.types";
|
||||||
export type * from "./lib/common.types";
|
export type * from "./lib/common.types";
|
||||||
|
|||||||
@@ -4,6 +4,15 @@
|
|||||||
* @author Naomi Carrigan
|
* @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 {
|
interface User {
|
||||||
id: string;
|
id: string;
|
||||||
email: string;
|
email: string;
|
||||||
@@ -13,6 +22,7 @@ interface User {
|
|||||||
displayName?: string;
|
displayName?: string;
|
||||||
bio?: string;
|
bio?: string;
|
||||||
profilePublic: boolean;
|
profilePublic: boolean;
|
||||||
|
primaryBadge?: PrimaryBadge;
|
||||||
website?: string;
|
website?: string;
|
||||||
discordServer?: string;
|
discordServer?: string;
|
||||||
bluesky?: string;
|
bluesky?: string;
|
||||||
@@ -47,4 +57,5 @@ interface AuthResponse {
|
|||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { PrimaryBadge };
|
||||||
export type { AuthResponse, JwtPayload, User };
|
export type { AuthResponse, JwtPayload, User };
|
||||||
|
|||||||
Reference in New Issue
Block a user