From 318f3bc5008c58bbf629c4d18e3e62aea68902a4 Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Wed, 4 Feb 2026 13:00:16 -0800 Subject: [PATCH] feat: bunch of work done here, got comments and edit and delete --- PLAN.md | 58 +++ api/prisma/schema.prisma | 19 + api/src/app/plugins/auth.ts | 20 + api/src/app/routes/books/index.ts | 45 +- api/src/app/routes/games/index.ts | 39 +- api/src/app/routes/music/index.ts | 45 +- api/src/app/services/book.service.ts | 8 +- api/src/app/services/comment.service.ts | 154 +++++++ api/src/app/services/game.service.ts | 8 +- api/src/app/services/music.service.ts | 16 +- .../components/books/books-list.component.ts | 362 +++++++++++++++- .../components/games/games-list.component.ts | 342 ++++++++++++++- .../components/music/music-list.component.ts | 394 ++++++++++++++++-- apps/frontend/src/app/services/api.service.ts | 1 - .../src/app/services/comments.service.ts | 53 +++ package.json | 5 + pnpm-lock.yaml | 386 ++++++++++++++++- shared-types/src/index.ts | 3 +- shared-types/src/lib/comment.types.ts | 27 ++ 19 files changed, 1868 insertions(+), 117 deletions(-) create mode 100644 api/src/app/services/comment.service.ts create mode 100644 apps/frontend/src/app/services/comments.service.ts create mode 100644 shared-types/src/lib/comment.types.ts diff --git a/PLAN.md b/PLAN.md index 6ff3732..a79c26a 100644 --- a/PLAN.md +++ b/PLAN.md @@ -166,6 +166,64 @@ interface Music { 5. **Privacy**: Any items you'd want to keep private even from public view? +## Current Sprint: Edit, Filters & Comments + +### Feature 1: Edit Existing Library Entries (Admin Only) +**Backend:** +- Add PUT `/api/games/:id`, `/api/books/:id`, `/api/music/:id` endpoints +- Reuse existing admin authentication middleware +- Validate input against existing schemas + +**Frontend:** +- Add edit button to entry cards (visible for admins) +- Reuse existing "Add" modal/form components with pre-filled data +- Update state after successful edit + +### Feature 2: Fix Filter Tab State Updates +**Investigation needed:** +- Check how tab counts are calculated (likely not re-fetching after add) +- Ensure Angular state updates reactively when items are added/modified +- May need to refresh counts after mutations or use observables properly + +### Feature 3: Comments System +**Database Schema:** +```prisma +model Comment { + id String @id @default(auto()) @map("_id") @db.ObjectId + content String // Markdown content - sanitised on render + userId String @db.ObjectId + user User @relation(fields: [userId], references: [id]) + // Polymorphic relation - one of these will be set + gameId String? @db.ObjectId + game Game? @relation(fields: [gameId], references: [id]) + bookId String? @db.ObjectId + book Book? @relation(fields: [bookId], references: [id]) + musicId String? @db.ObjectId + music Music? @relation(fields: [musicId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} +``` + +**Backend:** +- POST `/api/games/:id/comments` (authenticated users) +- GET `/api/games/:id/comments` (public) +- DELETE `/api/games/:id/comments/:commentId` (admin only) +- Same for books and music +- Use a markdown sanitisation library (e.g., `sanitize-html` or `DOMPurify` on render) + +**Frontend:** +- Expandable comments section on each entry card +- Markdown rendering with sanitisation (use `marked` + `DOMPurify`) +- Add comment form for authenticated users +- Delete button for admins +- Show commenter's Discord username/avatar + +### Implementation Order +1. Fix filter tab bug (quick win) +2. Add edit functionality (builds on existing patterns) +3. Add comments system (new feature, more complex) + ## Next Steps 1. Choose technical stack diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index 74b12d8..df0e3bc 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -25,6 +25,7 @@ model Game { coverImage String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + comments Comment[] } enum GameStatus { @@ -46,6 +47,7 @@ model Book { coverImage String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + comments Comment[] } enum BookStatus { @@ -67,6 +69,7 @@ model Music { coverArt String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + comments Comment[] } enum MusicType { @@ -90,4 +93,20 @@ model User { isAdmin Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + comments Comment[] +} + +model Comment { + id String @id @default(auto()) @map("_id") @db.ObjectId + content String + userId String @db.ObjectId + user User @relation(fields: [userId], references: [id]) + gameId String? @db.ObjectId + game Game? @relation(fields: [gameId], references: [id]) + bookId String? @db.ObjectId + book Book? @relation(fields: [bookId], references: [id]) + musicId String? @db.ObjectId + music Music? @relation(fields: [musicId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } diff --git a/api/src/app/plugins/auth.ts b/api/src/app/plugins/auth.ts index 5c5f793..668cbbf 100644 --- a/api/src/app/plugins/auth.ts +++ b/api/src/app/plugins/auth.ts @@ -11,6 +11,18 @@ declare module "fastify" { } } +declare module "@fastify/jwt" { + interface FastifyJWT { + user: { + id: string; + username: string; + email?: string; + avatar?: string; + isAdmin: boolean; + }; + } +} + const authPlugin: FastifyPluginAsync = async (app) => { // Register JWT plugin app.register(fastifyJwt, { @@ -19,6 +31,14 @@ const authPlugin: FastifyPluginAsync = async (app) => { cookieName: "auth-token", signed: false, }, + formatUser: (payload: { sub: string; email?: string; username: string; isAdmin: boolean }) => { + return { + id: payload.sub, + email: payload.email, + username: payload.username, + isAdmin: payload.isAdmin, + }; + }, }); // Register cookie plugin diff --git a/api/src/app/routes/books/index.ts b/api/src/app/routes/books/index.ts index 395dfdd..07be569 100644 --- a/api/src/app/routes/books/index.ts +++ b/api/src/app/routes/books/index.ts @@ -5,12 +5,14 @@ */ import { FastifyPluginAsync } from "fastify"; -import { Book, CreateBookDto, UpdateBookDto } from "@library/shared-types"; +import { Book, CreateBookDto, UpdateBookDto, Comment, CreateCommentDto } from "@library/shared-types"; import { BookService } from "../../services/book.service"; +import { CommentService } from "../../services/comment.service"; import { adminGuard } from "../../middleware/admin-guard"; const booksRoutes: FastifyPluginAsync = async (app) => { const bookService = new BookService(); + const commentService = new CommentService(); /** * Get all books (public route). @@ -75,6 +77,47 @@ const booksRoutes: FastifyPluginAsync = async (app) => { return { success: true }; } ); + + /** + * Get comments for a book (public route). + */ + app.get<{ Params: { id: string }; Reply: Comment[] }>( + "/:id/comments", + async (request) => { + const { id } = request.params; + return commentService.getCommentsForBook(id); + } + ); + + /** + * Add comment to a book (authenticated users). + */ + app.post<{ Params: { id: string }; Body: CreateCommentDto; Reply: Comment }>( + "/:id/comments", + { + preValidation: [app.authenticate], + }, + async (request) => { + const { id } = request.params; + const userId = request.user.id; + return commentService.createCommentForBook(id, userId, request.body); + } + ); + + /** + * Delete comment (admin only). + */ + app.delete<{ Params: { id: string; commentId: string }; Reply: { success: boolean } }>( + "/:id/comments/:commentId", + { + preValidation: [app.authenticate, adminGuard], + }, + async (request) => { + const { commentId } = request.params; + await commentService.deleteComment(commentId); + return { success: true }; + } + ); }; export default booksRoutes; \ No newline at end of file diff --git a/api/src/app/routes/games/index.ts b/api/src/app/routes/games/index.ts index 9a3be45..cc6e2d8 100644 --- a/api/src/app/routes/games/index.ts +++ b/api/src/app/routes/games/index.ts @@ -5,12 +5,14 @@ */ import { FastifyPluginAsync } from "fastify"; -import { Game, CreateGameDto, UpdateGameDto } from "@library/shared-types"; +import { Game, CreateGameDto, UpdateGameDto, Comment, CreateCommentDto } from "@library/shared-types"; import { GameService } from "../../services/game.service"; +import { CommentService } from "../../services/comment.service"; import { adminGuard } from "../../middleware/admin-guard"; const gamesRoutes: FastifyPluginAsync = async (app) => { const gameService = new GameService(); + const commentService = new CommentService(); // Get all games (public route) app.get<{ Reply: Game[] }>("/", async () => { @@ -65,6 +67,41 @@ const gamesRoutes: FastifyPluginAsync = async (app) => { return { success: true }; } ); + + // Get comments for a game (public route) + app.get<{ Params: { id: string }; Reply: Comment[] }>( + "/:id/comments", + async (request) => { + const { id } = request.params; + return commentService.getCommentsForGame(id); + } + ); + + // Add comment to a game (authenticated users) + app.post<{ Params: { id: string }; Body: CreateCommentDto; Reply: Comment }>( + "/:id/comments", + { + preValidation: [app.authenticate], + }, + async (request) => { + const { id } = request.params; + const userId = request.user.id; + return commentService.createCommentForGame(id, userId, request.body); + } + ); + + // Delete comment (admin only) + app.delete<{ Params: { id: string; commentId: string }; Reply: { success: boolean } }>( + "/:id/comments/:commentId", + { + preValidation: [app.authenticate, adminGuard], + }, + async (request) => { + const { commentId } = request.params; + await commentService.deleteComment(commentId); + return { success: true }; + } + ); }; export default gamesRoutes; \ No newline at end of file diff --git a/api/src/app/routes/music/index.ts b/api/src/app/routes/music/index.ts index 3b2aa89..fe73704 100644 --- a/api/src/app/routes/music/index.ts +++ b/api/src/app/routes/music/index.ts @@ -5,12 +5,14 @@ */ import { FastifyPluginAsync } from "fastify"; -import { Music, CreateMusicDto, UpdateMusicDto } from "@library/shared-types"; +import { Music, CreateMusicDto, UpdateMusicDto, Comment, CreateCommentDto } from "@library/shared-types"; import { MusicService } from "../../services/music.service"; +import { CommentService } from "../../services/comment.service"; import { adminGuard } from "../../middleware/admin-guard"; const musicRoutes: FastifyPluginAsync = async (app) => { const musicService = new MusicService(); + const commentService = new CommentService(); /** * Get all music (public route). @@ -75,6 +77,47 @@ const musicRoutes: FastifyPluginAsync = async (app) => { return { success: true }; } ); + + /** + * Get comments for a music item (public route). + */ + app.get<{ Params: { id: string }; Reply: Comment[] }>( + "/:id/comments", + async (request) => { + const { id } = request.params; + return commentService.getCommentsForMusic(id); + } + ); + + /** + * Add comment to a music item (authenticated users). + */ + app.post<{ Params: { id: string }; Body: CreateCommentDto; Reply: Comment }>( + "/:id/comments", + { + preValidation: [app.authenticate], + }, + async (request) => { + const { id } = request.params; + const userId = request.user.id; + return commentService.createCommentForMusic(id, userId, request.body); + } + ); + + /** + * Delete comment (admin only). + */ + app.delete<{ Params: { id: string; commentId: string }; Reply: { success: boolean } }>( + "/:id/comments/:commentId", + { + preValidation: [app.authenticate, adminGuard], + }, + async (request) => { + const { commentId } = request.params; + await commentService.deleteComment(commentId); + return { success: true }; + } + ); }; export default musicRoutes; \ No newline at end of file diff --git a/api/src/app/services/book.service.ts b/api/src/app/services/book.service.ts index 8b7c910..fda295b 100644 --- a/api/src/app/services/book.service.ts +++ b/api/src/app/services/book.service.ts @@ -22,7 +22,7 @@ export class BookService { return books.map((book) => ({ ...book, - status: book.status.toLowerCase() as BookStatus, + status: book.status as unknown as BookStatus, dateAdded: book.dateAdded, dateFinished: book.dateFinished || undefined, createdAt: book.createdAt, @@ -42,7 +42,7 @@ export class BookService { return { ...book, - status: book.status.toLowerCase() as BookStatus, + status: book.status as unknown as BookStatus, dateAdded: book.dateAdded, dateFinished: book.dateFinished || undefined, createdAt: book.createdAt, @@ -63,7 +63,7 @@ export class BookService { return { ...book, - status: book.status.toLowerCase() as BookStatus, + status: book.status as unknown as BookStatus, dateAdded: book.dateAdded, dateFinished: book.dateFinished || undefined, createdAt: book.createdAt, @@ -87,7 +87,7 @@ export class BookService { return { ...book, - status: book.status.toLowerCase() as BookStatus, + status: book.status as unknown as BookStatus, dateAdded: book.dateAdded, dateFinished: book.dateFinished || undefined, createdAt: book.createdAt, diff --git a/api/src/app/services/comment.service.ts b/api/src/app/services/comment.service.ts new file mode 100644 index 0000000..70fae25 --- /dev/null +++ b/api/src/app/services/comment.service.ts @@ -0,0 +1,154 @@ +/** + * @copyright 2026 NHCarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ + +import { Comment, CreateCommentDto } from "@library/shared-types"; +import { prisma } from "../lib/prisma"; +import createDOMPurify from "dompurify"; +import { JSDOM } from "jsdom"; +import { marked } from "marked"; + +const window = new JSDOM("").window; +const DOMPurify = createDOMPurify(window); + +// Add hook to sanitise links - prevent javascript: URLs and add security attributes +DOMPurify.addHook("afterSanitizeAttributes", (node) => { + if (node.tagName === "A") { + const href = node.getAttribute("href") || ""; + // Block javascript:, data:, and vbscript: URLs + if (/^(javascript|data|vbscript):/i.test(href)) { + node.removeAttribute("href"); + } else { + // Add security attributes to external links + node.setAttribute("target", "_blank"); + node.setAttribute("rel", "noopener noreferrer nofollow"); + } + } +}); + +export class CommentService { + private prisma = prisma; + + constructor() {} + + private sanitizeMarkdown(content: string): string { + const html = marked.parse(content, { async: false }) as string; + return DOMPurify.sanitize(html, { + ALLOWED_TAGS: [ + "p", "br", "strong", "em", "b", "i", "u", "s", "strike", + "h1", "h2", "h3", "h4", "h5", "h6", + "ul", "ol", "li", + "blockquote", "code", "pre", + "a", "hr", + ], + ALLOWED_ATTR: ["href", "target", "rel"], + ALLOW_DATA_ATTR: false, + ADD_ATTR: ["target", "rel"], + FORCE_BODY: true, + }); + } + + private mapComment(comment: any): Comment { + return { + id: comment.id, + content: comment.content, + userId: comment.userId, + user: { + id: comment.user.id, + username: comment.user.username, + avatar: comment.user.avatar || undefined, + }, + gameId: comment.gameId || undefined, + bookId: comment.bookId || undefined, + musicId: comment.musicId || undefined, + createdAt: comment.createdAt, + updatedAt: comment.updatedAt, + }; + } + + async getCommentsForGame(gameId: string): Promise { + const comments = await this.prisma.comment.findMany({ + where: { gameId }, + include: { user: true }, + orderBy: { createdAt: "desc" }, + }); + return comments.map((c) => this.mapComment(c)); + } + + async getCommentsForBook(bookId: string): Promise { + const comments = await this.prisma.comment.findMany({ + where: { bookId }, + include: { user: true }, + orderBy: { createdAt: "desc" }, + }); + return comments.map((c) => this.mapComment(c)); + } + + async getCommentsForMusic(musicId: string): Promise { + const comments = await this.prisma.comment.findMany({ + where: { musicId }, + include: { user: true }, + orderBy: { createdAt: "desc" }, + }); + return comments.map((c) => this.mapComment(c)); + } + + async createCommentForGame( + gameId: string, + userId: string, + data: CreateCommentDto + ): Promise { + const sanitizedContent = this.sanitizeMarkdown(data.content); + const comment = await this.prisma.comment.create({ + data: { + content: sanitizedContent, + userId, + gameId, + }, + include: { user: true }, + }); + return this.mapComment(comment); + } + + async createCommentForBook( + bookId: string, + userId: string, + data: CreateCommentDto + ): Promise { + const sanitizedContent = this.sanitizeMarkdown(data.content); + const comment = await this.prisma.comment.create({ + data: { + content: sanitizedContent, + userId, + bookId, + }, + include: { user: true }, + }); + return this.mapComment(comment); + } + + async createCommentForMusic( + musicId: string, + userId: string, + data: CreateCommentDto + ): Promise { + const sanitizedContent = this.sanitizeMarkdown(data.content); + const comment = await this.prisma.comment.create({ + data: { + content: sanitizedContent, + userId, + musicId, + }, + include: { user: true }, + }); + return this.mapComment(comment); + } + + async deleteComment(commentId: string): Promise { + await this.prisma.comment.delete({ + where: { id: commentId }, + }); + } +} diff --git a/api/src/app/services/game.service.ts b/api/src/app/services/game.service.ts index bedadb6..e500b1e 100644 --- a/api/src/app/services/game.service.ts +++ b/api/src/app/services/game.service.ts @@ -22,7 +22,7 @@ export class GameService { return games.map((game) => ({ ...game, - status: game.status.toLowerCase() as GameStatus, + status: game.status as unknown as GameStatus, dateAdded: game.dateAdded, dateCompleted: game.dateCompleted || undefined, createdAt: game.createdAt, @@ -42,7 +42,7 @@ export class GameService { return { ...game, - status: game.status.toLowerCase() as GameStatus, + status: game.status as unknown as GameStatus, dateAdded: game.dateAdded, dateCompleted: game.dateCompleted || undefined, createdAt: game.createdAt, @@ -63,7 +63,7 @@ export class GameService { return { ...game, - status: game.status.toLowerCase() as GameStatus, + status: game.status as unknown as GameStatus, dateAdded: game.dateAdded, dateCompleted: game.dateCompleted || undefined, createdAt: game.createdAt, @@ -87,7 +87,7 @@ export class GameService { return { ...game, - status: game.status.toLowerCase() as GameStatus, + status: game.status as unknown as GameStatus, dateAdded: game.dateAdded, dateCompleted: game.dateCompleted || undefined, createdAt: game.createdAt, diff --git a/api/src/app/services/music.service.ts b/api/src/app/services/music.service.ts index 69182b2..22d8ce8 100644 --- a/api/src/app/services/music.service.ts +++ b/api/src/app/services/music.service.ts @@ -22,8 +22,8 @@ export class MusicService { return musicItems.map((music) => ({ ...music, - type: music.type.toLowerCase() as MusicType, - status: music.status.toLowerCase() as MusicStatus, + type: music.type as unknown as MusicType, + status: music.status as unknown as MusicStatus, dateAdded: music.dateAdded, dateCompleted: music.dateCompleted || undefined, createdAt: music.createdAt, @@ -43,8 +43,8 @@ export class MusicService { return { ...music, - type: music.type.toLowerCase() as MusicType, - status: music.status.toLowerCase() as MusicStatus, + type: music.type as unknown as MusicType, + status: music.status as unknown as MusicStatus, dateAdded: music.dateAdded, dateCompleted: music.dateCompleted || undefined, createdAt: music.createdAt, @@ -66,8 +66,8 @@ export class MusicService { return { ...music, - type: music.type.toLowerCase() as MusicType, - status: music.status.toLowerCase() as MusicStatus, + type: music.type as unknown as MusicType, + status: music.status as unknown as MusicStatus, dateAdded: music.dateAdded, dateCompleted: music.dateCompleted || undefined, createdAt: music.createdAt, @@ -94,8 +94,8 @@ export class MusicService { return { ...music, - type: music.type.toLowerCase() as MusicType, - status: music.status.toLowerCase() as MusicStatus, + type: music.type as unknown as MusicType, + status: music.status as unknown as MusicStatus, dateAdded: music.dateAdded, dateCompleted: music.dateCompleted || undefined, createdAt: music.createdAt, 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 345119c..48b6b7a 100644 --- a/apps/frontend/src/app/components/books/books-list.component.ts +++ b/apps/frontend/src/app/components/books/books-list.component.ts @@ -4,12 +4,13 @@ * @author Naomi Carrigan */ -import { Component, OnInit, inject, signal } from '@angular/core'; +import { Component, OnInit, inject, signal, computed } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { BooksService } from '../../services/books.service'; import { AuthService } from '../../services/auth.service'; -import { Book, BookStatus, CreateBookDto } from '@library/shared-types'; +import { CommentsService } from '../../services/comments.service'; +import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment } from '@library/shared-types'; @Component({ selector: 'app-books-list', @@ -67,9 +68,9 @@ import { Book, BookStatus, CreateBookDto } from '@library/shared-types';
@@ -103,6 +104,83 @@ import { Book, BookStatus, CreateBookDto } from '@library/shared-types'; } + @if (editingBook() && authService.isAdmin()) { +
+

Edit Book

+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ } +
@@ -182,11 +260,58 @@ import { Book, BookStatus, CreateBookDto } from '@library/shared-types'; @if (authService.isAdmin()) {
+
} + +
+ + + @if (expandedComments()[book.id]) { +
+ @if (authService.isAuthenticated()) { +
+ + +
+ } + + @if (commentsLoading()[book.id]) { +
Loading comments...
+ } @else { + @for (comment of comments()[book.id] || []; track comment.id) { +
+
+ @if (comment.user.avatar) { + + } + {{ comment.user.username }} + {{ formatDate(comment.createdAt) }} + @if (authService.isAdmin()) { + + } +
+
+
+ } @empty { +
No comments yet. Be the first to comment!
+ } + } +
+ } +
} @@ -452,20 +577,126 @@ import { Book, BookStatus, CreateBookDto } from '@library/shared-types'; } .btn-sm { padding: 0.25rem 0.75rem; font-size: 0.85rem; } + .btn-xs { padding: 0.15rem 0.5rem; font-size: 0.75rem; } + + .comments-section { + margin-top: 1rem; + border-top: 1px solid var(--witch-lavender); + padding-top: 1rem; + } + + .comments-toggle { + width: 100%; + } + + .comments-container { + margin-top: 1rem; + } + + .comment-form { + margin-bottom: 1rem; + } + + .comment-form textarea { + width: 100%; + padding: 0.5rem; + border: 2px solid var(--witch-lavender); + border-radius: 4px; + font-size: 0.9rem; + background-color: var(--witch-moon); + color: var(--witch-purple); + resize: vertical; + margin-bottom: 0.5rem; + } + + .comment-form textarea:focus { + outline: none; + border-color: var(--witch-rose); + } + + .comment { + background: var(--witch-moon); + border: 1px solid var(--witch-lavender); + border-radius: 4px; + padding: 0.75rem; + margin-bottom: 0.5rem; + } + + .comment-header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; + flex-wrap: wrap; + } + + .comment-avatar { + width: 24px; + height: 24px; + border-radius: 50%; + } + + .comment-author { + font-weight: 500; + color: var(--witch-plum); + } + + .comment-date { + font-size: 0.75rem; + color: var(--witch-mauve); + } + + .comment-content { + font-size: 0.9rem; + color: var(--witch-purple); + } + + .comment-content :deep(p) { + margin: 0.25rem 0; + } + + .comments-loading, + .no-comments { + text-align: center; + padding: 1rem; + color: var(--witch-mauve); + font-size: 0.9rem; + } `] }) export class BooksListComponent implements OnInit { booksService = inject(BooksService); authService = inject(AuthService); + commentsService = inject(CommentsService); books = signal([]); loading = signal(true); showAddForm = signal(false); + editingBook = signal(null); statusFilter = signal<'all' | BookStatus>('all'); + // Comments state + comments = signal>({}); + commentsLoading = signal>({}); + expandedComments = signal>({}); + newCommentContent: Record = {}; + // Expose BookStatus enum to template BookStatus = BookStatus; + // Computed signals for reactive count updates + readingCount = computed(() => this.books().filter(book => book.status === BookStatus.reading).length); + finishedCount = computed(() => this.books().filter(book => book.status === BookStatus.finished).length); + toReadCount = computed(() => this.books().filter(book => book.status === BookStatus.toRead).length); + + filteredBooks = computed(() => { + const filter = this.statusFilter(); + if (filter === 'all') { + return this.books(); + } + return this.books().filter(book => book.status === filter); + }); + newBook: Partial = { title: '', author: '', @@ -475,6 +706,8 @@ export class BooksListComponent implements OnInit { notes: '' }; + editBook: Partial = {}; + ngOnInit() { this.loadBooks(); } @@ -492,18 +725,6 @@ export class BooksListComponent implements OnInit { }); } - filteredBooks() { - const filter = this.statusFilter(); - if (filter === 'all') { - return this.books(); - } - return this.books().filter(book => book.status === filter); - } - - getCountByStatus(status: BookStatus): number { - return this.books().filter(book => book.status === status).length; - } - setFilter(filter: 'all' | BookStatus) { this.statusFilter.set(filter); } @@ -560,7 +781,108 @@ export class BooksListComponent implements OnInit { } } + startEdit(book: Book) { + this.editingBook.set(book); + this.editBook = { + title: book.title, + author: book.author, + isbn: book.isbn, + status: book.status, + rating: book.rating, + notes: book.notes + }; + this.showAddForm.set(false); + } + + cancelEdit() { + this.editingBook.set(null); + this.editBook = {}; + } + + saveEdit() { + const book = this.editingBook(); + if (!book || !this.editBook.title || !this.editBook.author || !this.editBook.status) return; + + this.booksService.updateBook(book.id, this.editBook).subscribe(() => { + this.loadBooks(); + this.cancelEdit(); + }); + } + formatDate(date: Date | string): string { return new Date(date).toLocaleDateString(); } + + // Comments methods + toggleComments(bookId: string) { + const expanded = this.expandedComments(); + const isCurrentlyExpanded = expanded[bookId]; + + this.expandedComments.set({ + ...expanded, + [bookId]: !isCurrentlyExpanded + }); + + if (!isCurrentlyExpanded && !this.comments()[bookId]) { + this.loadComments(bookId); + } + } + + loadComments(bookId: string) { + this.commentsLoading.set({ + ...this.commentsLoading(), + [bookId]: true + }); + + this.commentsService.getCommentsForBook(bookId).subscribe({ + next: (comments) => { + this.comments.set({ + ...this.comments(), + [bookId]: comments + }); + this.commentsLoading.set({ + ...this.commentsLoading(), + [bookId]: false + }); + }, + error: () => { + this.commentsLoading.set({ + ...this.commentsLoading(), + [bookId]: false + }); + } + }); + } + + getCommentCount(bookId: string): number { + return this.comments()[bookId]?.length || 0; + } + + addComment(bookId: string) { + const content = this.newCommentContent[bookId]; + if (!content?.trim()) return; + + this.commentsService.addCommentToBook(bookId, { content }).subscribe({ + next: (comment) => { + this.comments.set({ + ...this.comments(), + [bookId]: [comment, ...(this.comments()[bookId] || [])] + }); + this.newCommentContent[bookId] = ''; + } + }); + } + + deleteComment(bookId: string, commentId: string) { + if (!confirm('Are you sure you want to delete this comment?')) return; + + this.commentsService.deleteCommentFromBook(bookId, commentId).subscribe({ + next: () => { + this.comments.set({ + ...this.comments(), + [bookId]: (this.comments()[bookId] || []).filter(c => c.id !== commentId) + }); + } + }); + } } \ No newline at end of file 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 a286feb..7c12a1a 100644 --- a/apps/frontend/src/app/components/games/games-list.component.ts +++ b/apps/frontend/src/app/components/games/games-list.component.ts @@ -4,12 +4,13 @@ * @author Naomi Carrigan */ -import { Component, OnInit, inject, signal } from '@angular/core'; +import { Component, OnInit, inject, signal, computed } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { GamesService } from '../../services/games.service'; import { AuthService } from '../../services/auth.service'; -import { Game, GameStatus, CreateGameDto } from '@library/shared-types'; +import { CommentsService } from '../../services/comments.service'; +import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment } from '@library/shared-types'; @Component({ selector: 'app-games-list', @@ -55,9 +56,9 @@ import { Game, GameStatus, CreateGameDto } from '@library/shared-types';
@@ -91,6 +92,71 @@ import { Game, GameStatus, CreateGameDto } from '@library/shared-types'; } + @if (editingGame() && authService.isAdmin()) { +
+

Edit Game

+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ } +
@@ -157,11 +223,58 @@ import { Game, GameStatus, CreateGameDto } from '@library/shared-types'; @if (authService.isAdmin()) {
+
} + +
+ + + @if (expandedComments()[game.id]) { +
+ @if (authService.isAuthenticated()) { +
+ + +
+ } + + @if (commentsLoading()[game.id]) { +
Loading comments...
+ } @else { + @for (comment of comments()[game.id] || []; track comment.id) { +
+
+ @if (comment.user.avatar) { + + } + {{ comment.user.username }} + {{ formatDate(comment.createdAt) }} + @if (authService.isAdmin()) { + + } +
+
+
+ } @empty { +
No comments yet. Be the first to comment!
+ } + } +
+ } +
} @@ -344,20 +457,115 @@ import { Game, GameStatus, CreateGameDto } from '@library/shared-types'; .btn-secondary { background: #6b7280; color: white; } .btn-danger { background: #ef4444; color: white; } .btn-sm { padding: 0.25rem 0.75rem; font-size: 0.85rem; } + .btn-xs { padding: 0.15rem 0.5rem; font-size: 0.75rem; } + + .comments-section { + margin-top: 1rem; + border-top: 1px solid #e5e7eb; + padding-top: 1rem; + } + + .comments-toggle { + width: 100%; + } + + .comments-container { + margin-top: 1rem; + } + + .comment-form { + margin-bottom: 1rem; + } + + .comment-form textarea { + width: 100%; + padding: 0.5rem; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 0.9rem; + resize: vertical; + margin-bottom: 0.5rem; + } + + .comment { + background: #f8f9fa; + border: 1px solid #e5e7eb; + border-radius: 4px; + padding: 0.75rem; + margin-bottom: 0.5rem; + } + + .comment-header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; + flex-wrap: wrap; + } + + .comment-avatar { + width: 24px; + height: 24px; + border-radius: 50%; + } + + .comment-author { + font-weight: 500; + color: #374151; + } + + .comment-date { + font-size: 0.75rem; + color: #6b7280; + } + + .comment-content { + font-size: 0.9rem; + color: #4b5563; + } + + .comments-loading, + .no-comments { + text-align: center; + padding: 1rem; + color: #6b7280; + font-size: 0.9rem; + } `] }) export class GamesListComponent implements OnInit { gamesService = inject(GamesService); authService = inject(AuthService); + commentsService = inject(CommentsService); games = signal([]); loading = signal(true); showAddForm = signal(false); + editingGame = signal(null); statusFilter = signal<'all' | GameStatus>('all'); + // Comments state + comments = signal>({}); + commentsLoading = signal>({}); + expandedComments = signal>({}); + newCommentContent: Record = {}; + // Expose GameStatus enum to template GameStatus = GameStatus; + // Computed signals for reactive count updates + playingCount = computed(() => this.games().filter(game => game.status === GameStatus.playing).length); + completedCount = computed(() => this.games().filter(game => game.status === GameStatus.completed).length); + backlogCount = computed(() => this.games().filter(game => game.status === GameStatus.backlog).length); + + filteredGames = computed(() => { + const filter = this.statusFilter(); + if (filter === 'all') { + return this.games(); + } + return this.games().filter(game => game.status === filter); + }); + newGame: Partial = { title: '', platform: '', @@ -366,6 +574,8 @@ export class GamesListComponent implements OnInit { notes: '' }; + editGame: Partial = {}; + ngOnInit() { this.loadGames(); } @@ -383,18 +593,6 @@ export class GamesListComponent implements OnInit { }); } - filteredGames() { - const filter = this.statusFilter(); - if (filter === 'all') { - return this.games(); - } - return this.games().filter(game => game.status === filter); - } - - getCountByStatus(status: GameStatus): number { - return this.games().filter(game => game.status === status).length; - } - setFilter(filter: 'all' | GameStatus) { this.statusFilter.set(filter); } @@ -448,4 +646,108 @@ export class GamesListComponent implements OnInit { }); } } + + startEdit(game: Game) { + this.editingGame.set(game); + this.editGame = { + title: game.title, + platform: game.platform, + status: game.status, + rating: game.rating, + notes: game.notes + }; + this.showAddForm.set(false); + } + + cancelEdit() { + this.editingGame.set(null); + this.editGame = {}; + } + + saveEdit() { + const game = this.editingGame(); + if (!game || !this.editGame.title || !this.editGame.status) return; + + this.gamesService.updateGame(game.id, this.editGame).subscribe(() => { + this.loadGames(); + this.cancelEdit(); + }); + } + + // Comments methods + formatDate(date: Date | string): string { + return new Date(date).toLocaleDateString(); + } + + toggleComments(gameId: string) { + const expanded = this.expandedComments(); + const isCurrentlyExpanded = expanded[gameId]; + + this.expandedComments.set({ + ...expanded, + [gameId]: !isCurrentlyExpanded + }); + + if (!isCurrentlyExpanded && !this.comments()[gameId]) { + this.loadComments(gameId); + } + } + + loadComments(gameId: string) { + this.commentsLoading.set({ + ...this.commentsLoading(), + [gameId]: true + }); + + this.commentsService.getCommentsForGame(gameId).subscribe({ + next: (comments) => { + this.comments.set({ + ...this.comments(), + [gameId]: comments + }); + this.commentsLoading.set({ + ...this.commentsLoading(), + [gameId]: false + }); + }, + error: () => { + this.commentsLoading.set({ + ...this.commentsLoading(), + [gameId]: false + }); + } + }); + } + + getCommentCount(gameId: string): number { + return this.comments()[gameId]?.length || 0; + } + + addComment(gameId: string) { + const content = this.newCommentContent[gameId]; + if (!content?.trim()) return; + + this.commentsService.addCommentToGame(gameId, { content }).subscribe({ + next: (comment) => { + this.comments.set({ + ...this.comments(), + [gameId]: [comment, ...(this.comments()[gameId] || [])] + }); + this.newCommentContent[gameId] = ''; + } + }); + } + + deleteComment(gameId: string, commentId: string) { + if (!confirm('Are you sure you want to delete this comment?')) return; + + this.commentsService.deleteCommentFromGame(gameId, commentId).subscribe({ + next: () => { + this.comments.set({ + ...this.comments(), + [gameId]: (this.comments()[gameId] || []).filter(c => c.id !== commentId) + }); + } + }); + } } \ No newline at end of file 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 76015db..5fefee6 100644 --- a/apps/frontend/src/app/components/music/music-list.component.ts +++ b/apps/frontend/src/app/components/music/music-list.component.ts @@ -4,12 +4,13 @@ * @author Naomi Carrigan */ -import { Component, OnInit, inject, signal } from '@angular/core'; +import { Component, OnInit, inject, signal, computed } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { MusicService } from '../../services/music.service'; import { AuthService } from '../../services/auth.service'; -import { Music, MusicStatus, MusicType, CreateMusicDto } from '@library/shared-types'; +import { CommentsService } from '../../services/comments.service'; +import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto, Comment } from '@library/shared-types'; @Component({ selector: 'app-music-list', @@ -56,18 +57,18 @@ import { Music, MusicStatus, MusicType, CreateMusicDto } from '@library/shared-t
@@ -101,6 +102,81 @@ import { Music, MusicStatus, MusicType, CreateMusicDto } from '@library/shared-t } + @if (editingMusic() && authService.isAdmin()) { +
+

Edit Music

+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ } +
Type: @@ -116,21 +192,21 @@ import { Music, MusicStatus, MusicType, CreateMusicDto } from '@library/shared-t [class.active]="typeFilter() === MusicType.album" class="filter-btn" > - Albums ({{ getCountByType(MusicType.album) }}) + Albums ({{ albumCount() }})
@@ -148,21 +224,21 @@ import { Music, MusicStatus, MusicType, CreateMusicDto } from '@library/shared-t [class.active]="statusFilter() === MusicStatus.listening" class="filter-btn" > - Listening ({{ getCountByStatus(MusicStatus.listening) }}) + Listening ({{ listeningCount() }})
@@ -222,11 +298,58 @@ import { Music, MusicStatus, MusicType, CreateMusicDto } from '@library/shared-t @if (authService.isAdmin()) {
+
} + +
+ + + @if (expandedComments()[music.id]) { +
+ @if (authService.isAuthenticated()) { +
+ + +
+ } + + @if (commentsLoading()[music.id]) { +
Loading comments...
+ } @else { + @for (comment of comments()[music.id] || []; track comment.id) { +
+
+ @if (comment.user.avatar) { + + } + {{ comment.user.username }} + {{ formatDate(comment.createdAt) }} + @if (authService.isAdmin()) { + + } +
+
+
+ } @empty { +
No comments yet. Be the first to comment!
+ } + } +
+ } +
} @@ -529,22 +652,138 @@ import { Music, MusicStatus, MusicType, CreateMusicDto } from '@library/shared-t padding: 0.25rem 0.75rem; font-size: 0.85rem; } + + .btn-xs { padding: 0.15rem 0.5rem; font-size: 0.75rem; } + + .comments-section { + margin-top: 1rem; + border-top: 1px solid var(--witch-lavender); + padding-top: 1rem; + } + + .comments-toggle { + width: 100%; + } + + .comments-container { + margin-top: 1rem; + } + + .comment-form { + margin-bottom: 1rem; + } + + .comment-form textarea { + width: 100%; + padding: 0.5rem; + border: 2px solid var(--witch-lavender); + border-radius: 4px; + font-size: 0.9rem; + background-color: var(--witch-moon); + color: var(--witch-purple); + resize: vertical; + margin-bottom: 0.5rem; + } + + .comment-form textarea:focus { + outline: none; + border-color: var(--witch-rose); + } + + .comment { + background: var(--witch-moon); + border: 1px solid var(--witch-lavender); + border-radius: 4px; + padding: 0.75rem; + margin-bottom: 0.5rem; + } + + .comment-header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; + flex-wrap: wrap; + } + + .comment-avatar { + width: 24px; + height: 24px; + border-radius: 50%; + } + + .comment-author { + font-weight: 500; + color: var(--witch-plum); + } + + .comment-date { + font-size: 0.75rem; + color: var(--witch-mauve); + } + + .comment-content { + font-size: 0.9rem; + color: var(--witch-purple); + } + + .comments-loading, + .no-comments { + text-align: center; + padding: 1rem; + color: var(--witch-mauve); + font-size: 0.9rem; + } `] }) export class MusicListComponent implements OnInit { musicService = inject(MusicService); authService = inject(AuthService); + commentsService = inject(CommentsService); music = signal([]); loading = signal(true); showAddForm = signal(false); + editingMusic = signal(null); typeFilter = signal<'all' | MusicType>('all'); statusFilter = signal<'all' | MusicStatus>('all'); + // Comments state + comments = signal>({}); + commentsLoading = signal>({}); + expandedComments = signal>({}); + newCommentContent: Record = {}; + // Expose enums to template MusicType = MusicType; MusicStatus = MusicStatus; + // Computed signals for reactive count updates (type) + albumCount = computed(() => this.music().filter(m => m.type === MusicType.album).length); + singleCount = computed(() => this.music().filter(m => m.type === MusicType.single).length); + epCount = computed(() => this.music().filter(m => m.type === MusicType.ep).length); + + // Computed signals for reactive count updates (status) + listeningCount = computed(() => this.music().filter(m => m.status === MusicStatus.listening).length); + completedCount = computed(() => this.music().filter(m => m.status === MusicStatus.completed).length); + wantToListenCount = computed(() => this.music().filter(m => m.status === MusicStatus.wantToListen).length); + + filteredMusic = computed(() => { + let filtered = this.music(); + + const typeFilter = this.typeFilter(); + if (typeFilter !== 'all') { + filtered = filtered.filter(music => music.type === typeFilter); + } + + const statusFilter = this.statusFilter(); + if (statusFilter !== 'all') { + filtered = filtered.filter(music => music.status === statusFilter); + } + + return filtered; + }); + newMusic: Partial = { title: '', artist: '', @@ -554,6 +793,8 @@ export class MusicListComponent implements OnInit { notes: '' }; + editMusicData: Partial = {}; + ngOnInit() { this.loadMusic(); } @@ -571,30 +812,6 @@ export class MusicListComponent implements OnInit { }); } - filteredMusic() { - let filtered = this.music(); - - const typeFilter = this.typeFilter(); - if (typeFilter !== 'all') { - filtered = filtered.filter(music => music.type === typeFilter); - } - - const statusFilter = this.statusFilter(); - if (statusFilter !== 'all') { - filtered = filtered.filter(music => music.status === statusFilter); - } - - return filtered; - } - - getCountByType(type: MusicType): number { - return this.music().filter(music => music.type === type).length; - } - - getCountByStatus(status: MusicStatus): number { - return this.music().filter(music => music.status === status).length; - } - setTypeFilter(filter: 'all' | MusicType) { this.typeFilter.set(filter); } @@ -663,7 +880,108 @@ export class MusicListComponent implements OnInit { } } + startEdit(music: Music) { + this.editingMusic.set(music); + this.editMusicData = { + title: music.title, + artist: music.artist, + type: music.type, + status: music.status, + rating: music.rating, + notes: music.notes + }; + this.showAddForm.set(false); + } + + cancelEdit() { + this.editingMusic.set(null); + this.editMusicData = {}; + } + + saveEdit() { + const music = this.editingMusic(); + if (!music || !this.editMusicData.title || !this.editMusicData.artist || !this.editMusicData.type || !this.editMusicData.status) return; + + this.musicService.updateMusic(music.id, this.editMusicData).subscribe(() => { + this.loadMusic(); + this.cancelEdit(); + }); + } + formatDate(date: Date | string): string { return new Date(date).toLocaleDateString(); } + + // Comments methods + toggleComments(musicId: string) { + const expanded = this.expandedComments(); + const isCurrentlyExpanded = expanded[musicId]; + + this.expandedComments.set({ + ...expanded, + [musicId]: !isCurrentlyExpanded + }); + + if (!isCurrentlyExpanded && !this.comments()[musicId]) { + this.loadComments(musicId); + } + } + + loadComments(musicId: string) { + this.commentsLoading.set({ + ...this.commentsLoading(), + [musicId]: true + }); + + this.commentsService.getCommentsForMusic(musicId).subscribe({ + next: (comments) => { + this.comments.set({ + ...this.comments(), + [musicId]: comments + }); + this.commentsLoading.set({ + ...this.commentsLoading(), + [musicId]: false + }); + }, + error: () => { + this.commentsLoading.set({ + ...this.commentsLoading(), + [musicId]: false + }); + } + }); + } + + getCommentCount(musicId: string): number { + return this.comments()[musicId]?.length || 0; + } + + addComment(musicId: string) { + const content = this.newCommentContent[musicId]; + if (!content?.trim()) return; + + this.commentsService.addCommentToMusic(musicId, { content }).subscribe({ + next: (comment) => { + this.comments.set({ + ...this.comments(), + [musicId]: [comment, ...(this.comments()[musicId] || [])] + }); + this.newCommentContent[musicId] = ''; + } + }); + } + + deleteComment(musicId: string, commentId: string) { + if (!confirm('Are you sure you want to delete this comment?')) return; + + this.commentsService.deleteCommentFromMusic(musicId, commentId).subscribe({ + next: () => { + this.comments.set({ + ...this.comments(), + [musicId]: (this.comments()[musicId] || []).filter(c => c.id !== commentId) + }); + } + }); + } } \ No newline at end of file diff --git a/apps/frontend/src/app/services/api.service.ts b/apps/frontend/src/app/services/api.service.ts index 608df28..533ac62 100644 --- a/apps/frontend/src/app/services/api.service.ts +++ b/apps/frontend/src/app/services/api.service.ts @@ -47,7 +47,6 @@ export class ApiService { delete(endpoint: string): Observable { return this.http.delete(`${this.apiUrl}${endpoint}`, { - headers: this.getHeaders(), withCredentials: true }); } diff --git a/apps/frontend/src/app/services/comments.service.ts b/apps/frontend/src/app/services/comments.service.ts new file mode 100644 index 0000000..120378f --- /dev/null +++ b/apps/frontend/src/app/services/comments.service.ts @@ -0,0 +1,53 @@ +/** + * @copyright 2026 NHCarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ + +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { ApiService } from './api.service'; +import { Comment, CreateCommentDto } from '@library/shared-types'; + +@Injectable({ + providedIn: 'root' +}) +export class CommentsService { + constructor(private api: ApiService) {} + + getCommentsForGame(gameId: string): Observable { + return this.api.get(`/games/${gameId}/comments`); + } + + getCommentsForBook(bookId: string): Observable { + return this.api.get(`/books/${bookId}/comments`); + } + + getCommentsForMusic(musicId: string): Observable { + return this.api.get(`/music/${musicId}/comments`); + } + + addCommentToGame(gameId: string, comment: CreateCommentDto): Observable { + return this.api.post(`/games/${gameId}/comments`, comment); + } + + addCommentToBook(bookId: string, comment: CreateCommentDto): Observable { + return this.api.post(`/books/${bookId}/comments`, comment); + } + + addCommentToMusic(musicId: string, comment: CreateCommentDto): Observable { + return this.api.post(`/music/${musicId}/comments`, comment); + } + + deleteCommentFromGame(gameId: string, commentId: string): Observable<{ success: boolean }> { + return this.api.delete<{ success: boolean }>(`/games/${gameId}/comments/${commentId}`); + } + + deleteCommentFromBook(bookId: string, commentId: string): Observable<{ success: boolean }> { + return this.api.delete<{ success: boolean }>(`/books/${bookId}/comments/${commentId}`); + } + + deleteCommentFromMusic(musicId: string, commentId: string): Observable<{ success: boolean }> { + return this.api.delete<{ success: boolean }>(`/music/${musicId}/comments/${commentId}`); + } +} diff --git a/package.json b/package.json index 3702d63..76f1f20 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,11 @@ "@fastify/sensible": "6.0.4", "@fastify/static": "^9.0.0", "@prisma/client": "6.19.2", + "dompurify": "^3.3.1", "fastify": "5.2.2", "fastify-plugin": "5.0.1", + "jsdom": "^28.0.0", + "marked": "^17.0.1", "rxjs": "7.8.2" }, "devDependencies": { @@ -56,7 +59,9 @@ "@swc-node/register": "1.9.2", "@swc/core": "1.5.29", "@swc/helpers": "0.5.18", + "@types/dompurify": "^3.2.0", "@types/jest": "30.0.0", + "@types/jsdom": "^27.0.0", "@types/jsonwebtoken": "^9.0.10", "@types/node": "20.19.9", "@typescript-eslint/utils": "8.54.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2309463..ffa4009 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,19 +47,28 @@ importers: '@prisma/client': specifier: 6.19.2 version: 6.19.2(prisma@6.19.2(typescript@5.9.3))(typescript@5.9.3) + dompurify: + specifier: ^3.3.1 + version: 3.3.1 fastify: specifier: 5.2.2 version: 5.2.2 fastify-plugin: specifier: 5.0.1 version: 5.0.1 + jsdom: + specifier: ^28.0.0 + version: 28.0.0 + marked: + specifier: ^17.0.1 + version: 17.0.1 rxjs: specifier: 7.8.2 version: 7.8.2 devDependencies: '@angular-devkit/build-angular': specifier: 21.1.2 - version: 21.1.2(@angular/compiler-cli@21.1.2(@angular/compiler@21.1.2)(typescript@5.9.3))(@angular/compiler@21.1.2)(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2))(@angular/platform-browser@21.1.2(@angular/common@21.1.2(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2)))(@rspack/core@1.6.8(@swc/helpers@0.5.18))(@swc/core@1.5.29(@swc/helpers@0.5.18))(@types/node@20.19.9)(chokidar@5.0.0)(jest@30.2.0(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.5.29(@swc/helpers@0.5.18))(@types/node@20.19.9)(typescript@5.9.3)))(jiti@2.6.1)(sass-embedded@1.97.3)(typescript@5.9.3)(vitest@4.0.18(@types/node@20.19.9)(jiti@2.6.1)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2))(yaml@2.8.2) + version: 21.1.2(@angular/compiler-cli@21.1.2(@angular/compiler@21.1.2)(typescript@5.9.3))(@angular/compiler@21.1.2)(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2))(@angular/platform-browser@21.1.2(@angular/common@21.1.2(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2)))(@rspack/core@1.6.8(@swc/helpers@0.5.18))(@swc/core@1.5.29(@swc/helpers@0.5.18))(@types/node@20.19.9)(chokidar@5.0.0)(jest@30.2.0(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.5.29(@swc/helpers@0.5.18))(@types/node@20.19.9)(typescript@5.9.3)))(jiti@2.6.1)(sass-embedded@1.97.3)(typescript@5.9.3)(vitest@4.0.18(@types/node@20.19.9)(jiti@2.6.1)(jsdom@28.0.0)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2))(yaml@2.8.2) '@angular-devkit/core': specifier: 21.1.2 version: 21.1.2(chokidar@5.0.0) @@ -80,10 +89,10 @@ importers: version: 9.39.2 '@nhcarrigan/eslint-config': specifier: 5.2.0 - version: 5.2.0(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(playwright@1.58.1)(react@19.2.4)(typescript@5.9.3)(vitest@4.0.18(@types/node@20.19.9)(jiti@2.6.1)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2)) + version: 5.2.0(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(playwright@1.58.1)(react@19.2.4)(typescript@5.9.3)(vitest@4.0.18(@types/node@20.19.9)(jiti@2.6.1)(jsdom@28.0.0)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2)) '@nx/angular': specifier: 22.4.4 - version: 22.4.4(ec507f70e00b67864c7695431dce0b70) + version: 22.4.4(53644491fdd75e214444a711ff7c7d5c) '@nx/cypress': specifier: 22.4.4 version: 22.4.4(@babel/traverse@7.29.0)(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.18))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.18))(@zkochan/js-yaml@0.0.7)(cypress@15.9.0)(eslint@9.39.2(jiti@2.6.1))(nx@22.4.4(@swc-node/register@1.9.2(@swc/core@1.5.29(@swc/helpers@0.5.18))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.5.29(@swc/helpers@0.5.18)))(typescript@5.9.3) @@ -123,9 +132,15 @@ importers: '@swc/helpers': specifier: 0.5.18 version: 0.5.18 + '@types/dompurify': + specifier: ^3.2.0 + version: 3.2.0 '@types/jest': specifier: 30.0.0 version: 30.0.0 + '@types/jsdom': + specifier: ^27.0.0 + version: 27.0.0 '@types/jsonwebtoken': specifier: ^9.0.10 version: 9.0.10 @@ -183,6 +198,9 @@ importers: packages: + '@acemir/cssom@0.9.31': + resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==} + '@algolia/abtesting@1.12.2': resolution: {integrity: sha512-oWknd6wpfNrmRcH0vzed3UPX0i17o4kYLM5OMITyMVM2xLgaRbIafoxL0e8mcrNNb0iORCJA0evnNDKRYth5WQ==} engines: {node: '>= 14.0.0'} @@ -481,6 +499,15 @@ packages: '@angular/platform-browser': 21.1.2 rxjs: ^6.5.3 || ^7.4.0 + '@asamuzakjp/css-color@4.1.1': + resolution: {integrity: sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==} + + '@asamuzakjp/dom-selector@6.7.7': + resolution: {integrity: sha512-8CO/UQ4tzDd7ula+/CVimJIVWez99UJlbMyIgk8xOnhAVPKLnBZmUFYVgugS441v2ZqUq5EnSh6B0Ua0liSFAA==} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -1141,6 +1168,37 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-syntax-patches-for-csstree@1.0.26': + resolution: {integrity: sha512-6boXK0KkzT5u5xOgF6TKB+CLq9SOpEGmkZw0g5n9/7yg85wab3UzSxB8TxhLJ31L4SGJ6BCFRw/iftTha1CJXA==} + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + '@cypress/request@3.0.10': resolution: {integrity: sha512-hauBrOdvu08vOsagkZ/Aju5XuiZx6ldsLfByg1htFeldhex+PeMrYauANzFsMJeAA0+dyPLbDoX2OYuvVoLDkQ==} engines: {node: '>= 6'} @@ -1520,6 +1578,15 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@exodus/bytes@1.11.0': + resolution: {integrity: sha512-wO3vd8nsEHdumsXrjGO/v4p6irbg7hy9kvIeR6i2AwylZSk4HJdWgL0FNaVquW1+AweJcdvU1IEpuIWk/WaPnA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + '@fastify/accept-negotiator@2.0.1': resolution: {integrity: sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==} @@ -3257,6 +3324,10 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/dompurify@3.2.0': + resolution: {integrity: sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==} + deprecated: This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed. + '@types/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} @@ -3296,6 +3367,9 @@ packages: '@types/jest@30.0.0': resolution: {integrity: sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==} + '@types/jsdom@27.0.0': + resolution: {integrity: sha512-NZyFl/PViwKzdEkQg96gtnB8wm+1ljhdDay9ahn4hgb+SfVtPCbm3TlmDUFXTA+MGN3CijicnMhG18SI5H3rFw==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -3362,6 +3436,12 @@ packages: '@types/tmp@0.2.6': resolution: {integrity: sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==} + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} @@ -4098,6 +4178,9 @@ packages: resolution: {integrity: sha512-NaWu+f4YrJxEttJSm16AzMIFtVldCvaJ68b1L098KpqXmxt9xOLtKoLkKxb8ekhOrLqEJAbvT6n6SEvB/sac7A==} engines: {node: '>=14.0.0'} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + big.js@5.2.2: resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} @@ -4596,6 +4679,10 @@ packages: resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css-tree@3.1.0: + resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css-what@6.2.2: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} @@ -4631,6 +4718,10 @@ packages: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + cssstyle@5.3.7: + resolution: {integrity: sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==} + engines: {node: '>=20'} + cypress@15.9.0: resolution: {integrity: sha512-Ks6Bdilz3TtkLZtTQyqYaqtL/WT3X3APKaSLhTV96TmTyudzSjc6EJsJCHmBb7DxO+3R12q3Jkbjgm/iPgmwfg==} engines: {node: ^20.1.0 || ^22.0.0 || >=24.0.0} @@ -4640,6 +4731,10 @@ packages: resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} engines: {node: '>=0.10'} + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -4693,6 +4788,9 @@ packages: supports-color: optional: true + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + dedent@1.7.1: resolution: {integrity: sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==} peerDependencies: @@ -4813,6 +4911,9 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} + dompurify@3.3.1: + resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} + domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -5666,6 +5767,10 @@ packages: resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} engines: {node: '>=12'} + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + html-entities@2.6.0: resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} @@ -5980,6 +6085,9 @@ packages: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} @@ -6285,6 +6393,15 @@ packages: resolution: {integrity: sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==} engines: {node: '>=12.0.0'} + jsdom@28.0.0: + resolution: {integrity: sha512-KDYJgZ6T2TKdU8yBfYueq5EPG/EylMsBvCaenWMJb2OXmjgczzwveRCoJ+Hgj1lXPDyasvrgneSn4GBuR1hYyA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsesc@0.5.0: resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} hasBin: true @@ -6578,6 +6695,11 @@ packages: makeerror@1.0.12: resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + marked@17.0.1: + resolution: {integrity: sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==} + engines: {node: '>= 20'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -6588,6 +6710,9 @@ packages: mdn-data@2.0.30: resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + mdn-data@2.12.2: + resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==} + media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -7091,6 +7216,9 @@ packages: parse5@4.0.0: resolution: {integrity: sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==} + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parse5@8.0.0: resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} @@ -7944,6 +8072,10 @@ packages: resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==} engines: {node: '>=11.0.0'} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -8330,6 +8462,9 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + sync-child-process@1.0.2: resolution: {integrity: sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==} engines: {node: '>=16.0.0'} @@ -8433,10 +8568,17 @@ packages: tldts-core@6.1.86: resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + tldts-core@7.0.22: + resolution: {integrity: sha512-KgbTDC5wzlL6j/x6np6wCnDSMUq4kucHNm00KXPbfNzmllCmtmvtykJHfmgdHntwIeupW04y8s1N/43S1PkQDw==} + tldts@6.1.86: resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} hasBin: true + tldts@7.0.22: + resolution: {integrity: sha512-nqpKFC53CgopKPjT6Wfb6tpIcZXHcI6G37hesvikhx0EmUGPkZrujRyAjgnmp1SHNgpQfKVanZ+KfpANFt2Hxw==} + hasBin: true + tmp@0.2.5: resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} engines: {node: '>=14.14'} @@ -8460,9 +8602,17 @@ packages: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} + tough-cookie@6.0.0: + resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + engines: {node: '>=16'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + tree-dump@1.1.0: resolution: {integrity: sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==} engines: {node: '>=10.0'} @@ -8660,6 +8810,10 @@ packages: resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==} engines: {node: '>=20.18.1'} + undici@7.20.0: + resolution: {integrity: sha512-MJZrkjyd7DeC+uPZh+5/YaMDxFiiEEaDgbUSVMXayofAkDWF1088CDo+2RPg7B1BuS1qf1vgNE7xqwPxE0DuSQ==} + engines: {node: '>=20.18.1'} + unicode-canonical-property-names-ecmascript@2.0.1: resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} engines: {node: '>=4'} @@ -8873,6 +9027,10 @@ packages: jsdom: optional: true + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} @@ -8896,6 +9054,10 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + webpack-dev-middleware@7.4.5: resolution: {integrity: sha512-uxQ6YqGdE4hgDKNf7hUiPXOdtkXvBJXrfEGYSx7P7LC8hnUYGK70X6xQXUvXeNyBDDcsiQXpG2m3G9vxowaEuA==} engines: {node: '>= 18.12.0'} @@ -8990,6 +9152,14 @@ packages: engines: {node: '>=12'} deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@16.0.0: + resolution: {integrity: sha512-9CcxtEKsf53UFwkSUZjG+9vydAsFO4lFHBpJUtjBcoJOCJpKnSJNwCw813zrYJHpCJ7sgfbtOe0V5Ku7Pa1XMQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -9093,6 +9263,13 @@ packages: resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} engines: {node: '>=20'} + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -9169,6 +9346,8 @@ packages: snapshots: + '@acemir/cssom@0.9.31': {} + '@algolia/abtesting@1.12.2': dependencies: '@algolia/client-common': 5.46.2 @@ -9265,13 +9444,13 @@ snapshots: transitivePeerDependencies: - chokidar - '@angular-devkit/build-angular@21.1.2(@angular/compiler-cli@21.1.2(@angular/compiler@21.1.2)(typescript@5.9.3))(@angular/compiler@21.1.2)(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2))(@angular/platform-browser@21.1.2(@angular/common@21.1.2(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2)))(@rspack/core@1.6.8(@swc/helpers@0.5.18))(@swc/core@1.5.29(@swc/helpers@0.5.18))(@types/node@20.19.9)(chokidar@5.0.0)(jest@30.2.0(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.5.29(@swc/helpers@0.5.18))(@types/node@20.19.9)(typescript@5.9.3)))(jiti@2.6.1)(sass-embedded@1.97.3)(typescript@5.9.3)(vitest@4.0.18(@types/node@20.19.9)(jiti@2.6.1)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2))(yaml@2.8.2)': + '@angular-devkit/build-angular@21.1.2(@angular/compiler-cli@21.1.2(@angular/compiler@21.1.2)(typescript@5.9.3))(@angular/compiler@21.1.2)(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2))(@angular/platform-browser@21.1.2(@angular/common@21.1.2(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2)))(@rspack/core@1.6.8(@swc/helpers@0.5.18))(@swc/core@1.5.29(@swc/helpers@0.5.18))(@types/node@20.19.9)(chokidar@5.0.0)(jest@30.2.0(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.5.29(@swc/helpers@0.5.18))(@types/node@20.19.9)(typescript@5.9.3)))(jiti@2.6.1)(sass-embedded@1.97.3)(typescript@5.9.3)(vitest@4.0.18(@types/node@20.19.9)(jiti@2.6.1)(jsdom@28.0.0)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2))(yaml@2.8.2)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2101.2(chokidar@5.0.0) '@angular-devkit/build-webpack': 0.2101.2(chokidar@5.0.0)(webpack-dev-server@5.2.2(tslib@2.8.1)(webpack@5.104.1(@swc/core@1.5.29(@swc/helpers@0.5.18))(esbuild@0.27.2)))(webpack@5.104.1(@swc/core@1.5.29(@swc/helpers@0.5.18))(esbuild@0.27.2)) '@angular-devkit/core': 21.1.2(chokidar@5.0.0) - '@angular/build': 21.1.2(@angular/compiler-cli@21.1.2(@angular/compiler@21.1.2)(typescript@5.9.3))(@angular/compiler@21.1.2)(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2))(@angular/platform-browser@21.1.2(@angular/common@21.1.2(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2)))(@types/node@20.19.9)(chokidar@5.0.0)(jiti@2.6.1)(less@4.4.2)(postcss@8.5.6)(sass-embedded@1.97.3)(terser@5.44.1)(tslib@2.8.1)(typescript@5.9.3)(vitest@4.0.18(@types/node@20.19.9)(jiti@2.6.1)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2))(yaml@2.8.2) + '@angular/build': 21.1.2(@angular/compiler-cli@21.1.2(@angular/compiler@21.1.2)(typescript@5.9.3))(@angular/compiler@21.1.2)(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2))(@angular/platform-browser@21.1.2(@angular/common@21.1.2(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2)))(@types/node@20.19.9)(chokidar@5.0.0)(jiti@2.6.1)(less@4.4.2)(postcss@8.5.6)(sass-embedded@1.97.3)(terser@5.44.1)(tslib@2.8.1)(typescript@5.9.3)(vitest@4.0.18(@types/node@20.19.9)(jiti@2.6.1)(jsdom@28.0.0)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2))(yaml@2.8.2) '@angular/compiler-cli': 21.1.2(@angular/compiler@21.1.2)(typescript@5.9.3) '@babel/core': 7.28.5 '@babel/generator': 7.28.5 @@ -9446,7 +9625,7 @@ snapshots: eslint: 9.39.2(jiti@2.6.1) typescript: 5.9.3 - '@angular/build@21.1.2(@angular/compiler-cli@21.1.2(@angular/compiler@21.1.2)(typescript@5.9.3))(@angular/compiler@21.1.2)(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2))(@angular/platform-browser@21.1.2(@angular/common@21.1.2(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2)))(@types/node@20.19.9)(chokidar@5.0.0)(jiti@2.6.1)(less@4.4.2)(postcss@8.5.6)(sass-embedded@1.97.3)(terser@5.44.1)(tslib@2.8.1)(typescript@5.9.3)(vitest@4.0.18(@types/node@20.19.9)(jiti@2.6.1)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2))(yaml@2.8.2)': + '@angular/build@21.1.2(@angular/compiler-cli@21.1.2(@angular/compiler@21.1.2)(typescript@5.9.3))(@angular/compiler@21.1.2)(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2))(@angular/platform-browser@21.1.2(@angular/common@21.1.2(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2)))(@types/node@20.19.9)(chokidar@5.0.0)(jiti@2.6.1)(less@4.4.2)(postcss@8.5.6)(sass-embedded@1.97.3)(terser@5.44.1)(tslib@2.8.1)(typescript@5.9.3)(vitest@4.0.18(@types/node@20.19.9)(jiti@2.6.1)(jsdom@28.0.0)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2))(yaml@2.8.2)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2101.2(chokidar@5.0.0) @@ -9485,7 +9664,7 @@ snapshots: less: 4.4.2 lmdb: 3.4.4 postcss: 8.5.6 - vitest: 4.0.18(@types/node@20.19.9)(jiti@2.6.1)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2) + vitest: 4.0.18(@types/node@20.19.9)(jiti@2.6.1)(jsdom@28.0.0)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2) transitivePeerDependencies: - '@types/node' - chokidar @@ -9499,7 +9678,7 @@ snapshots: - tsx - yaml - '@angular/build@21.1.2(@angular/compiler-cli@21.1.2(@angular/compiler@21.1.2)(typescript@5.9.3))(@angular/compiler@21.1.2)(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2))(@angular/platform-browser@21.1.2(@angular/common@21.1.2(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2)))(@types/node@20.19.9)(chokidar@5.0.0)(jiti@2.6.1)(less@4.5.1)(postcss@8.5.6)(sass-embedded@1.97.3)(terser@5.44.1)(tslib@2.8.1)(typescript@5.9.3)(vitest@4.0.18(@types/node@20.19.9)(jiti@2.6.1)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2))(yaml@2.8.2)': + '@angular/build@21.1.2(@angular/compiler-cli@21.1.2(@angular/compiler@21.1.2)(typescript@5.9.3))(@angular/compiler@21.1.2)(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2))(@angular/platform-browser@21.1.2(@angular/common@21.1.2(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2)))(@types/node@20.19.9)(chokidar@5.0.0)(jiti@2.6.1)(less@4.5.1)(postcss@8.5.6)(sass-embedded@1.97.3)(terser@5.44.1)(tslib@2.8.1)(typescript@5.9.3)(vitest@4.0.18(@types/node@20.19.9)(jiti@2.6.1)(jsdom@28.0.0)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2))(yaml@2.8.2)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2101.2(chokidar@5.0.0) @@ -9538,7 +9717,7 @@ snapshots: less: 4.5.1 lmdb: 3.4.4 postcss: 8.5.6 - vitest: 4.0.18(@types/node@20.19.9)(jiti@2.6.1)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2) + vitest: 4.0.18(@types/node@20.19.9)(jiti@2.6.1)(jsdom@28.0.0)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2) transitivePeerDependencies: - '@types/node' - chokidar @@ -9639,6 +9818,24 @@ snapshots: rxjs: 7.8.2 tslib: 2.8.1 + '@asamuzakjp/css-color@4.1.1': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 11.2.5 + + '@asamuzakjp/dom-selector@6.7.7': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.1.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.2.5 + + '@asamuzakjp/nwsapi@2.3.9': {} + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -11022,6 +11219,28 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-syntax-patches-for-csstree@1.0.26': {} + + '@csstools/css-tokenizer@3.0.4': {} + '@cypress/request@3.0.10': dependencies: aws-sign2: 0.7.0 @@ -11290,6 +11509,8 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@exodus/bytes@1.11.0': {} + '@fastify/accept-negotiator@2.0.1': {} '@fastify/ajv-compiler@4.0.5': @@ -12416,7 +12637,7 @@ snapshots: typescript: 5.9.3 webpack: 5.104.1(@swc/core@1.5.29(@swc/helpers@0.5.18))(esbuild@0.27.2) - '@nhcarrigan/eslint-config@5.2.0(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(playwright@1.58.1)(react@19.2.4)(typescript@5.9.3)(vitest@4.0.18(@types/node@20.19.9)(jiti@2.6.1)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2))': + '@nhcarrigan/eslint-config@5.2.0(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(playwright@1.58.1)(react@19.2.4)(typescript@5.9.3)(vitest@4.0.18(@types/node@20.19.9)(jiti@2.6.1)(jsdom@28.0.0)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2))': dependencies: '@eslint-community/eslint-plugin-eslint-comments': 4.4.1(eslint@9.39.2(jiti@2.6.1)) '@eslint/compat': 1.2.4(eslint@9.39.2(jiti@2.6.1)) @@ -12425,7 +12646,7 @@ snapshots: '@stylistic/eslint-plugin': 2.12.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/eslint-plugin': 8.19.0(@typescript-eslint/parser@8.19.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/parser': 8.19.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@vitest/eslint-plugin': 1.1.24(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18(@types/node@20.19.9)(jiti@2.6.1)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2)) + '@vitest/eslint-plugin': 1.1.24(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18(@types/node@20.19.9)(jiti@2.6.1)(jsdom@28.0.0)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2)) eslint: 9.39.2(jiti@2.6.1) eslint-plugin-deprecation: 3.0.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.19.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)) @@ -12438,7 +12659,7 @@ snapshots: playwright: 1.58.1 react: 19.2.4 typescript: 5.9.3 - vitest: 4.0.18(@types/node@20.19.9)(jiti@2.6.1)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2) + vitest: 4.0.18(@types/node@20.19.9)(jiti@2.6.1)(jsdom@28.0.0)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2) transitivePeerDependencies: - '@typescript-eslint/utils' - eslint-import-resolver-typescript @@ -12518,7 +12739,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@nx/angular@22.4.4(ec507f70e00b67864c7695431dce0b70)': + '@nx/angular@22.4.4(53644491fdd75e214444a711ff7c7d5c)': dependencies: '@angular-devkit/core': 21.1.2(chokidar@5.0.0) '@angular-devkit/schematics': 21.1.2(chokidar@5.0.0) @@ -12542,8 +12763,8 @@ snapshots: tslib: 2.8.1 webpack-merge: 5.10.0 optionalDependencies: - '@angular-devkit/build-angular': 21.1.2(@angular/compiler-cli@21.1.2(@angular/compiler@21.1.2)(typescript@5.9.3))(@angular/compiler@21.1.2)(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2))(@angular/platform-browser@21.1.2(@angular/common@21.1.2(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2)))(@rspack/core@1.6.8(@swc/helpers@0.5.18))(@swc/core@1.5.29(@swc/helpers@0.5.18))(@types/node@20.19.9)(chokidar@5.0.0)(jest@30.2.0(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.5.29(@swc/helpers@0.5.18))(@types/node@20.19.9)(typescript@5.9.3)))(jiti@2.6.1)(sass-embedded@1.97.3)(typescript@5.9.3)(vitest@4.0.18(@types/node@20.19.9)(jiti@2.6.1)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2))(yaml@2.8.2) - '@angular/build': 21.1.2(@angular/compiler-cli@21.1.2(@angular/compiler@21.1.2)(typescript@5.9.3))(@angular/compiler@21.1.2)(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2))(@angular/platform-browser@21.1.2(@angular/common@21.1.2(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2)))(@types/node@20.19.9)(chokidar@5.0.0)(jiti@2.6.1)(less@4.5.1)(postcss@8.5.6)(sass-embedded@1.97.3)(terser@5.44.1)(tslib@2.8.1)(typescript@5.9.3)(vitest@4.0.18(@types/node@20.19.9)(jiti@2.6.1)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2))(yaml@2.8.2) + '@angular-devkit/build-angular': 21.1.2(@angular/compiler-cli@21.1.2(@angular/compiler@21.1.2)(typescript@5.9.3))(@angular/compiler@21.1.2)(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2))(@angular/platform-browser@21.1.2(@angular/common@21.1.2(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2)))(@rspack/core@1.6.8(@swc/helpers@0.5.18))(@swc/core@1.5.29(@swc/helpers@0.5.18))(@types/node@20.19.9)(chokidar@5.0.0)(jest@30.2.0(@types/node@20.19.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.1(@swc/core@1.5.29(@swc/helpers@0.5.18))(@types/node@20.19.9)(typescript@5.9.3)))(jiti@2.6.1)(sass-embedded@1.97.3)(typescript@5.9.3)(vitest@4.0.18(@types/node@20.19.9)(jiti@2.6.1)(jsdom@28.0.0)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2))(yaml@2.8.2) + '@angular/build': 21.1.2(@angular/compiler-cli@21.1.2(@angular/compiler@21.1.2)(typescript@5.9.3))(@angular/compiler@21.1.2)(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2))(@angular/platform-browser@21.1.2(@angular/common@21.1.2(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@21.1.2(@angular/compiler@21.1.2)(rxjs@7.8.2)))(@types/node@20.19.9)(chokidar@5.0.0)(jiti@2.6.1)(less@4.5.1)(postcss@8.5.6)(sass-embedded@1.97.3)(terser@5.44.1)(tslib@2.8.1)(typescript@5.9.3)(vitest@4.0.18(@types/node@20.19.9)(jiti@2.6.1)(jsdom@28.0.0)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2))(yaml@2.8.2) transitivePeerDependencies: - '@babel/traverse' - '@module-federation/enhanced' @@ -13648,6 +13869,10 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/dompurify@3.2.0': + dependencies: + dompurify: 3.3.1 + '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 @@ -13701,6 +13926,12 @@ snapshots: expect: 30.2.0 pretty-format: 30.2.0 + '@types/jsdom@27.0.0': + dependencies: + '@types/node': 20.19.9 + '@types/tough-cookie': 4.0.5 + parse5: 7.3.0 + '@types/json-schema@7.0.15': {} '@types/json5@0.0.29': {} @@ -13765,6 +13996,11 @@ snapshots: '@types/tmp@0.2.6': {} + '@types/tough-cookie@4.0.5': {} + + '@types/trusted-types@2.0.7': + optional: true + '@types/ws@8.18.1': dependencies: '@types/node': 20.19.9 @@ -14051,13 +14287,13 @@ snapshots: dependencies: vite: 7.3.0(@types/node@20.19.9)(jiti@2.6.1)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2) - '@vitest/eslint-plugin@1.1.24(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18(@types/node@20.19.9)(jiti@2.6.1)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2))': + '@vitest/eslint-plugin@1.1.24(@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)(vitest@4.0.18(@types/node@20.19.9)(jiti@2.6.1)(jsdom@28.0.0)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2))': dependencies: '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.2(jiti@2.6.1) optionalDependencies: typescript: 5.9.3 - vitest: 4.0.18(@types/node@20.19.9)(jiti@2.6.1)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2) + vitest: 4.0.18(@types/node@20.19.9)(jiti@2.6.1)(jsdom@28.0.0)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2) '@vitest/expect@4.0.18': dependencies: @@ -14666,6 +14902,10 @@ snapshots: postcss: 8.5.6 postcss-media-query-parser: 0.2.3 + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + big.js@5.2.2: {} binary-extensions@2.3.0: {} @@ -15175,6 +15415,11 @@ snapshots: mdn-data: 2.0.30 source-map-js: 1.2.1 + css-tree@3.1.0: + dependencies: + mdn-data: 2.12.2 + source-map-js: 1.2.1 + css-what@6.2.2: {} css-what@7.0.0: {} @@ -15229,6 +15474,13 @@ snapshots: dependencies: css-tree: 2.2.1 + cssstyle@5.3.7: + dependencies: + '@asamuzakjp/css-color': 4.1.1 + '@csstools/css-syntax-patches-for-csstree': 1.0.26 + css-tree: 3.1.0 + lru-cache: 11.2.5 + cypress@15.9.0: dependencies: '@cypress/request': 3.0.10 @@ -15278,6 +15530,13 @@ snapshots: dependencies: assert-plus: 1.0.0 + data-urls@7.0.0: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.0 + transitivePeerDependencies: + - '@noble/hashes' + data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -15320,6 +15579,8 @@ snapshots: optionalDependencies: supports-color: 8.1.1 + decimal.js@10.6.0: {} + dedent@1.7.1(babel-plugin-macros@3.1.0): optionalDependencies: babel-plugin-macros: 3.1.0 @@ -15415,6 +15676,10 @@ snapshots: dependencies: domelementtype: 2.3.0 + dompurify@3.3.1: + optionalDependencies: + '@types/trusted-types': 2.0.7 + domutils@3.2.2: dependencies: dom-serializer: 2.0.0 @@ -16572,6 +16837,12 @@ snapshots: dependencies: whatwg-encoding: 2.0.0 + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.11.0 + transitivePeerDependencies: + - '@noble/hashes' + html-entities@2.6.0: {} html-escaper@2.0.2: {} @@ -16880,6 +17151,8 @@ snapshots: is-plain-object@5.0.0: {} + is-potential-custom-element-name@1.0.1: {} + is-promise@4.0.0: {} is-regex@1.2.1: @@ -17380,6 +17653,32 @@ snapshots: jsdoc-type-pratt-parser@4.1.0: {} + jsdom@28.0.0: + dependencies: + '@acemir/cssom': 0.9.31 + '@asamuzakjp/dom-selector': 6.7.7 + '@exodus/bytes': 1.11.0 + cssstyle: 5.3.7 + data-urls: 7.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + parse5: 8.0.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.0 + undici: 7.20.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + - supports-color + jsesc@0.5.0: {} jsesc@3.1.0: {} @@ -17731,12 +18030,16 @@ snapshots: dependencies: tmpl: 1.0.5 + marked@17.0.1: {} + math-intrinsics@1.1.0: {} mdn-data@2.0.28: {} mdn-data@2.0.30: {} + mdn-data@2.12.2: {} + media-typer@0.3.0: {} media-typer@1.1.0: {} @@ -18327,6 +18630,10 @@ snapshots: parse5@4.0.0: {} + parse5@7.3.0: + dependencies: + entities: 6.0.1 + parse5@8.0.0: dependencies: entities: 6.0.1 @@ -19188,6 +19495,10 @@ snapshots: sax@1.4.4: optional: true + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.27.0: {} schema-utils@3.3.0: @@ -19699,6 +20010,8 @@ snapshots: csso: 5.0.5 picocolors: 1.1.1 + symbol-tree@3.2.4: {} + sync-child-process@1.0.2: dependencies: sync-message-port: 1.2.0 @@ -19812,10 +20125,16 @@ snapshots: tldts-core@6.1.86: {} + tldts-core@7.0.22: {} + tldts@6.1.86: dependencies: tldts-core: 6.1.86 + tldts@7.0.22: + dependencies: + tldts-core: 7.0.22 + tmp@0.2.5: {} tmpl@1.0.5: {} @@ -19832,8 +20151,16 @@ snapshots: dependencies: tldts: 6.1.86 + tough-cookie@6.0.0: + dependencies: + tldts: 7.0.22 + tr46@0.0.3: {} + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + tree-dump@1.1.0(tslib@2.8.1): dependencies: tslib: 2.8.1 @@ -20047,6 +20374,8 @@ snapshots: undici@7.18.2: {} + undici@7.20.0: {} + unicode-canonical-property-names-ecmascript@2.0.1: {} unicode-match-property-ecmascript@2.0.0: @@ -20202,7 +20531,7 @@ snapshots: terser: 5.44.1 yaml: 2.8.2 - vitest@4.0.18(@types/node@20.19.9)(jiti@2.6.1)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2): + vitest@4.0.18(@types/node@20.19.9)(jiti@2.6.1)(jsdom@28.0.0)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2): dependencies: '@vitest/expect': 4.0.18 '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@20.19.9)(jiti@2.6.1)(less@4.5.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.44.1)(yaml@2.8.2)) @@ -20226,6 +20555,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 20.19.9 + jsdom: 28.0.0 transitivePeerDependencies: - jiti - less @@ -20239,6 +20569,10 @@ snapshots: - tsx - yaml + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + walker@1.0.8: dependencies: makeerror: 1.0.12 @@ -20266,6 +20600,8 @@ snapshots: webidl-conversions@3.0.1: {} + webidl-conversions@8.0.1: {} + webpack-dev-middleware@7.4.5(tslib@2.8.1)(webpack@5.104.1(@swc/core@1.5.29(@swc/helpers@0.5.18))(esbuild@0.27.2)): dependencies: colorette: 2.0.20 @@ -20472,6 +20808,16 @@ snapshots: dependencies: iconv-lite: 0.6.3 + whatwg-mimetype@5.0.0: {} + + whatwg-url@16.0.0: + dependencies: + '@exodus/bytes': 1.11.0 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -20585,6 +20931,10 @@ snapshots: is-wsl: 3.1.0 powershell-utils: 0.1.0 + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + xtend@4.0.2: {} y18n@5.0.8: {} diff --git a/shared-types/src/index.ts b/shared-types/src/index.ts index 137ba74..7ba5b4f 100644 --- a/shared-types/src/index.ts +++ b/shared-types/src/index.ts @@ -6,4 +6,5 @@ export * from "./lib/game.types"; export * from "./lib/book.types"; export * from "./lib/music.types"; -export type * from "./lib/auth.types"; \ No newline at end of file +export type * from "./lib/auth.types"; +export * from "./lib/comment.types"; \ No newline at end of file diff --git a/shared-types/src/lib/comment.types.ts b/shared-types/src/lib/comment.types.ts new file mode 100644 index 0000000..ac21191 --- /dev/null +++ b/shared-types/src/lib/comment.types.ts @@ -0,0 +1,27 @@ +/** + * @copyright 2026 NHCarrigan + * @license Naomi's Public License + * @author Naomi Carrigan + */ + +export interface CommentUser { + id: string; + username: string; + avatar?: string; +} + +export interface Comment { + id: string; + content: string; + userId: string; + user: CommentUser; + gameId?: string; + bookId?: string; + musicId?: string; + createdAt: Date; + updatedAt: Date; +} + +export interface CreateCommentDto { + content: string; +}