generated from nhcarrigan/template
feat: bunch of work done here, got comments and edit and delete
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Comment[]> {
|
||||
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<Comment[]> {
|
||||
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<Comment[]> {
|
||||
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<Comment> {
|
||||
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<Comment> {
|
||||
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<Comment> {
|
||||
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<void> {
|
||||
await this.prisma.comment.delete({
|
||||
where: { id: commentId },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
<div class="form-group">
|
||||
<label for="status">Status</label>
|
||||
<select id="status" [(ngModel)]="newBook.status" name="status" required>
|
||||
<option value="reading">Currently Reading</option>
|
||||
<option value="finished">Finished</option>
|
||||
<option value="toRead">To Read</option>
|
||||
<option [value]="BookStatus.reading">Currently Reading</option>
|
||||
<option [value]="BookStatus.finished">Finished</option>
|
||||
<option [value]="BookStatus.toRead">To Read</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -103,6 +104,83 @@ import { Book, BookStatus, CreateBookDto } from '@library/shared-types';
|
||||
</form>
|
||||
}
|
||||
|
||||
@if (editingBook() && authService.isAdmin()) {
|
||||
<form (ngSubmit)="saveEdit()" class="add-form">
|
||||
<h3>Edit Book</h3>
|
||||
<div class="form-group">
|
||||
<label for="edit-title">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
id="edit-title"
|
||||
[(ngModel)]="editBook.title"
|
||||
name="title"
|
||||
required
|
||||
placeholder="Enter book title"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-author">Author</label>
|
||||
<input
|
||||
type="text"
|
||||
id="edit-author"
|
||||
[(ngModel)]="editBook.author"
|
||||
name="author"
|
||||
required
|
||||
placeholder="Enter author name"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-isbn">ISBN</label>
|
||||
<input
|
||||
type="text"
|
||||
id="edit-isbn"
|
||||
[(ngModel)]="editBook.isbn"
|
||||
name="isbn"
|
||||
placeholder="ISBN (optional)"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-status">Status</label>
|
||||
<select id="edit-status" [(ngModel)]="editBook.status" name="status" required>
|
||||
<option [value]="BookStatus.reading">Currently Reading</option>
|
||||
<option [value]="BookStatus.finished">Finished</option>
|
||||
<option [value]="BookStatus.toRead">To Read</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-rating">Rating (0-5)</label>
|
||||
<input
|
||||
type="number"
|
||||
id="edit-rating"
|
||||
[(ngModel)]="editBook.rating"
|
||||
name="rating"
|
||||
min="0"
|
||||
max="5"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-notes">Notes</label>
|
||||
<textarea
|
||||
id="edit-notes"
|
||||
[(ngModel)]="editBook.notes"
|
||||
name="notes"
|
||||
rows="3"
|
||||
placeholder="Your thoughts about the book..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
<button type="button" (click)="cancelEdit()" class="btn btn-secondary">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
|
||||
<div class="filters">
|
||||
<button
|
||||
(click)="setFilter('all')"
|
||||
@@ -116,21 +194,21 @@ import { Book, BookStatus, CreateBookDto } from '@library/shared-types';
|
||||
[class.active]="statusFilter() === BookStatus.reading"
|
||||
class="filter-btn"
|
||||
>
|
||||
Reading ({{ getCountByStatus(BookStatus.reading) }})
|
||||
Reading ({{ readingCount() }})
|
||||
</button>
|
||||
<button
|
||||
(click)="setFilter(BookStatus.finished)"
|
||||
[class.active]="statusFilter() === BookStatus.finished"
|
||||
class="filter-btn"
|
||||
>
|
||||
Finished ({{ getCountByStatus(BookStatus.finished) }})
|
||||
Finished ({{ finishedCount() }})
|
||||
</button>
|
||||
<button
|
||||
(click)="setFilter(BookStatus.toRead)"
|
||||
[class.active]="statusFilter() === BookStatus.toRead"
|
||||
class="filter-btn"
|
||||
>
|
||||
To Read ({{ getCountByStatus(BookStatus.toRead) }})
|
||||
To Read ({{ toReadCount() }})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -182,11 +260,58 @@ import { Book, BookStatus, CreateBookDto } from '@library/shared-types';
|
||||
|
||||
@if (authService.isAdmin()) {
|
||||
<div class="actions">
|
||||
<button (click)="startEdit(book)" class="btn btn-secondary btn-sm">
|
||||
Edit
|
||||
</button>
|
||||
<button (click)="deleteBook(book)" class="btn btn-danger btn-sm">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="comments-section">
|
||||
<button (click)="toggleComments(book.id)" class="btn btn-secondary btn-sm comments-toggle">
|
||||
{{ expandedComments()[book.id] ? 'Hide' : 'Show' }} Comments{{ comments()[book.id] ? ' (' + getCommentCount(book.id) + ')' : '' }}
|
||||
</button>
|
||||
|
||||
@if (expandedComments()[book.id]) {
|
||||
<div class="comments-container">
|
||||
@if (authService.isAuthenticated()) {
|
||||
<form (ngSubmit)="addComment(book.id)" class="comment-form">
|
||||
<textarea
|
||||
[(ngModel)]="newCommentContent[book.id]"
|
||||
name="comment"
|
||||
placeholder="Add a comment (Markdown supported)..."
|
||||
rows="2"
|
||||
></textarea>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Post Comment</button>
|
||||
</form>
|
||||
}
|
||||
|
||||
@if (commentsLoading()[book.id]) {
|
||||
<div class="comments-loading">Loading comments...</div>
|
||||
} @else {
|
||||
@for (comment of comments()[book.id] || []; track comment.id) {
|
||||
<div class="comment">
|
||||
<div class="comment-header">
|
||||
@if (comment.user.avatar) {
|
||||
<img [src]="comment.user.avatar" [alt]="comment.user.username" class="comment-avatar">
|
||||
}
|
||||
<span class="comment-author">{{ comment.user.username }}</span>
|
||||
<span class="comment-date">{{ formatDate(comment.createdAt) }}</span>
|
||||
@if (authService.isAdmin()) {
|
||||
<button (click)="deleteComment(book.id, comment.id)" class="btn btn-danger btn-xs">Delete</button>
|
||||
}
|
||||
</div>
|
||||
<div class="comment-content" [innerHTML]="comment.content"></div>
|
||||
</div>
|
||||
} @empty {
|
||||
<div class="no-comments">No comments yet. Be the first to comment!</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -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<Book[]>([]);
|
||||
loading = signal(true);
|
||||
showAddForm = signal(false);
|
||||
editingBook = signal<Book | null>(null);
|
||||
statusFilter = signal<'all' | BookStatus>('all');
|
||||
|
||||
// Comments state
|
||||
comments = signal<Record<string, Comment[]>>({});
|
||||
commentsLoading = signal<Record<string, boolean>>({});
|
||||
expandedComments = signal<Record<string, boolean>>({});
|
||||
newCommentContent: Record<string, string> = {};
|
||||
|
||||
// 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<CreateBookDto> = {
|
||||
title: '',
|
||||
author: '',
|
||||
@@ -475,6 +706,8 @@ export class BooksListComponent implements OnInit {
|
||||
notes: ''
|
||||
};
|
||||
|
||||
editBook: Partial<UpdateBookDto> = {};
|
||||
|
||||
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)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
<div class="form-group">
|
||||
<label for="status">Status</label>
|
||||
<select id="status" [(ngModel)]="newGame.status" name="status" required>
|
||||
<option value="playing">Currently Playing</option>
|
||||
<option value="completed">Completed</option>
|
||||
<option value="backlog">In Backlog</option>
|
||||
<option [value]="GameStatus.playing">Currently Playing</option>
|
||||
<option [value]="GameStatus.completed">Completed</option>
|
||||
<option [value]="GameStatus.backlog">In Backlog</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -91,6 +92,71 @@ import { Game, GameStatus, CreateGameDto } from '@library/shared-types';
|
||||
</form>
|
||||
}
|
||||
|
||||
@if (editingGame() && authService.isAdmin()) {
|
||||
<form (ngSubmit)="saveEdit()" class="add-form">
|
||||
<h3>Edit Game</h3>
|
||||
<div class="form-group">
|
||||
<label for="edit-title">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
id="edit-title"
|
||||
[(ngModel)]="editGame.title"
|
||||
name="title"
|
||||
required
|
||||
placeholder="Enter game title"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-platform">Platform</label>
|
||||
<input
|
||||
type="text"
|
||||
id="edit-platform"
|
||||
[(ngModel)]="editGame.platform"
|
||||
name="platform"
|
||||
placeholder="PC, PS5, Xbox, Switch, etc."
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-status">Status</label>
|
||||
<select id="edit-status" [(ngModel)]="editGame.status" name="status" required>
|
||||
<option [value]="GameStatus.playing">Currently Playing</option>
|
||||
<option [value]="GameStatus.completed">Completed</option>
|
||||
<option [value]="GameStatus.backlog">In Backlog</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-rating">Rating (0-10)</label>
|
||||
<input
|
||||
type="number"
|
||||
id="edit-rating"
|
||||
[(ngModel)]="editGame.rating"
|
||||
name="rating"
|
||||
min="0"
|
||||
max="10"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-notes">Notes</label>
|
||||
<textarea
|
||||
id="edit-notes"
|
||||
[(ngModel)]="editGame.notes"
|
||||
name="notes"
|
||||
rows="3"
|
||||
placeholder="Any thoughts about the game..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
<button type="button" (click)="cancelEdit()" class="btn btn-secondary">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
|
||||
<div class="filters">
|
||||
<button
|
||||
(click)="setFilter('all')"
|
||||
@@ -104,21 +170,21 @@ import { Game, GameStatus, CreateGameDto } from '@library/shared-types';
|
||||
[class.active]="statusFilter() === GameStatus.playing"
|
||||
class="filter-btn"
|
||||
>
|
||||
Playing ({{ getCountByStatus(GameStatus.playing) }})
|
||||
Playing ({{ playingCount() }})
|
||||
</button>
|
||||
<button
|
||||
(click)="setFilter(GameStatus.completed)"
|
||||
[class.active]="statusFilter() === GameStatus.completed"
|
||||
class="filter-btn"
|
||||
>
|
||||
Completed ({{ getCountByStatus(GameStatus.completed) }})
|
||||
Completed ({{ completedCount() }})
|
||||
</button>
|
||||
<button
|
||||
(click)="setFilter(GameStatus.backlog)"
|
||||
[class.active]="statusFilter() === GameStatus.backlog"
|
||||
class="filter-btn"
|
||||
>
|
||||
Backlog ({{ getCountByStatus(GameStatus.backlog) }})
|
||||
Backlog ({{ backlogCount() }})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -157,11 +223,58 @@ import { Game, GameStatus, CreateGameDto } from '@library/shared-types';
|
||||
|
||||
@if (authService.isAdmin()) {
|
||||
<div class="actions">
|
||||
<button (click)="startEdit(game)" class="btn btn-secondary btn-sm">
|
||||
Edit
|
||||
</button>
|
||||
<button (click)="deleteGame(game)" class="btn btn-danger btn-sm">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="comments-section">
|
||||
<button (click)="toggleComments(game.id)" class="btn btn-secondary btn-sm comments-toggle">
|
||||
{{ expandedComments()[game.id] ? 'Hide' : 'Show' }} Comments{{ comments()[game.id] ? ' (' + getCommentCount(game.id) + ')' : '' }}
|
||||
</button>
|
||||
|
||||
@if (expandedComments()[game.id]) {
|
||||
<div class="comments-container">
|
||||
@if (authService.isAuthenticated()) {
|
||||
<form (ngSubmit)="addComment(game.id)" class="comment-form">
|
||||
<textarea
|
||||
[(ngModel)]="newCommentContent[game.id]"
|
||||
name="comment"
|
||||
placeholder="Add a comment (Markdown supported)..."
|
||||
rows="2"
|
||||
></textarea>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Post Comment</button>
|
||||
</form>
|
||||
}
|
||||
|
||||
@if (commentsLoading()[game.id]) {
|
||||
<div class="comments-loading">Loading comments...</div>
|
||||
} @else {
|
||||
@for (comment of comments()[game.id] || []; track comment.id) {
|
||||
<div class="comment">
|
||||
<div class="comment-header">
|
||||
@if (comment.user.avatar) {
|
||||
<img [src]="comment.user.avatar" [alt]="comment.user.username" class="comment-avatar">
|
||||
}
|
||||
<span class="comment-author">{{ comment.user.username }}</span>
|
||||
<span class="comment-date">{{ formatDate(comment.createdAt) }}</span>
|
||||
@if (authService.isAdmin()) {
|
||||
<button (click)="deleteComment(game.id, comment.id)" class="btn btn-danger btn-xs">Delete</button>
|
||||
}
|
||||
</div>
|
||||
<div class="comment-content" [innerHTML]="comment.content"></div>
|
||||
</div>
|
||||
} @empty {
|
||||
<div class="no-comments">No comments yet. Be the first to comment!</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -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<Game[]>([]);
|
||||
loading = signal(true);
|
||||
showAddForm = signal(false);
|
||||
editingGame = signal<Game | null>(null);
|
||||
statusFilter = signal<'all' | GameStatus>('all');
|
||||
|
||||
// Comments state
|
||||
comments = signal<Record<string, Comment[]>>({});
|
||||
commentsLoading = signal<Record<string, boolean>>({});
|
||||
expandedComments = signal<Record<string, boolean>>({});
|
||||
newCommentContent: Record<string, string> = {};
|
||||
|
||||
// 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<CreateGameDto> = {
|
||||
title: '',
|
||||
platform: '',
|
||||
@@ -366,6 +574,8 @@ export class GamesListComponent implements OnInit {
|
||||
notes: ''
|
||||
};
|
||||
|
||||
editGame: Partial<UpdateGameDto> = {};
|
||||
|
||||
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)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
<div class="form-group">
|
||||
<label for="type">Type</label>
|
||||
<select id="type" [(ngModel)]="newMusic.type" name="type" required>
|
||||
<option value="album">Album</option>
|
||||
<option value="single">Single</option>
|
||||
<option value="ep">EP</option>
|
||||
<option [value]="MusicType.album">Album</option>
|
||||
<option [value]="MusicType.single">Single</option>
|
||||
<option [value]="MusicType.ep">EP</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="status">Status</label>
|
||||
<select id="status" [(ngModel)]="newMusic.status" name="status" required>
|
||||
<option value="listening">Currently Listening</option>
|
||||
<option value="completed">Completed</option>
|
||||
<option value="wantToListen">Want to Listen</option>
|
||||
<option [value]="MusicStatus.listening">Currently Listening</option>
|
||||
<option [value]="MusicStatus.completed">Completed</option>
|
||||
<option [value]="MusicStatus.wantToListen">Want to Listen</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -101,6 +102,81 @@ import { Music, MusicStatus, MusicType, CreateMusicDto } from '@library/shared-t
|
||||
</form>
|
||||
}
|
||||
|
||||
@if (editingMusic() && authService.isAdmin()) {
|
||||
<form (ngSubmit)="saveEdit()" class="add-form">
|
||||
<h3>Edit Music</h3>
|
||||
<div class="form-group">
|
||||
<label for="edit-title">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
id="edit-title"
|
||||
[(ngModel)]="editMusicData.title"
|
||||
name="title"
|
||||
required
|
||||
placeholder="Album/Single/EP title"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-artist">Artist</label>
|
||||
<input
|
||||
type="text"
|
||||
id="edit-artist"
|
||||
[(ngModel)]="editMusicData.artist"
|
||||
name="artist"
|
||||
required
|
||||
placeholder="Artist name"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-type">Type</label>
|
||||
<select id="edit-type" [(ngModel)]="editMusicData.type" name="type" required>
|
||||
<option [value]="MusicType.album">Album</option>
|
||||
<option [value]="MusicType.single">Single</option>
|
||||
<option [value]="MusicType.ep">EP</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-status">Status</label>
|
||||
<select id="edit-status" [(ngModel)]="editMusicData.status" name="status" required>
|
||||
<option [value]="MusicStatus.listening">Currently Listening</option>
|
||||
<option [value]="MusicStatus.completed">Completed</option>
|
||||
<option [value]="MusicStatus.wantToListen">Want to Listen</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-rating">Rating (0-5)</label>
|
||||
<input
|
||||
type="number"
|
||||
id="edit-rating"
|
||||
[(ngModel)]="editMusicData.rating"
|
||||
name="rating"
|
||||
min="0"
|
||||
max="5"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="edit-notes">Notes</label>
|
||||
<textarea
|
||||
id="edit-notes"
|
||||
[(ngModel)]="editMusicData.notes"
|
||||
name="notes"
|
||||
rows="3"
|
||||
placeholder="Your thoughts about this music..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary">Save Changes</button>
|
||||
<button type="button" (click)="cancelEdit()" class="btn btn-secondary">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
|
||||
<div class="filters">
|
||||
<div class="filter-group">
|
||||
<strong>Type:</strong>
|
||||
@@ -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() }})
|
||||
</button>
|
||||
<button
|
||||
(click)="setTypeFilter(MusicType.single)"
|
||||
[class.active]="typeFilter() === MusicType.single"
|
||||
class="filter-btn"
|
||||
>
|
||||
Singles ({{ getCountByType(MusicType.single) }})
|
||||
Singles ({{ singleCount() }})
|
||||
</button>
|
||||
<button
|
||||
(click)="setTypeFilter(MusicType.ep)"
|
||||
[class.active]="typeFilter() === MusicType.ep"
|
||||
class="filter-btn"
|
||||
>
|
||||
EPs ({{ getCountByType(MusicType.ep) }})
|
||||
EPs ({{ epCount() }})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -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() }})
|
||||
</button>
|
||||
<button
|
||||
(click)="setStatusFilter(MusicStatus.completed)"
|
||||
[class.active]="statusFilter() === MusicStatus.completed"
|
||||
class="filter-btn"
|
||||
>
|
||||
Completed ({{ getCountByStatus(MusicStatus.completed) }})
|
||||
Completed ({{ completedCount() }})
|
||||
</button>
|
||||
<button
|
||||
(click)="setStatusFilter(MusicStatus.wantToListen)"
|
||||
[class.active]="statusFilter() === MusicStatus.wantToListen"
|
||||
class="filter-btn"
|
||||
>
|
||||
Want to Listen ({{ getCountByStatus(MusicStatus.wantToListen) }})
|
||||
Want to Listen ({{ wantToListenCount() }})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -222,11 +298,58 @@ import { Music, MusicStatus, MusicType, CreateMusicDto } from '@library/shared-t
|
||||
|
||||
@if (authService.isAdmin()) {
|
||||
<div class="actions">
|
||||
<button (click)="startEdit(music)" class="btn btn-secondary btn-sm">
|
||||
Edit
|
||||
</button>
|
||||
<button (click)="deleteMusic(music)" class="btn btn-danger btn-sm">
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="comments-section">
|
||||
<button (click)="toggleComments(music.id)" class="btn btn-secondary btn-sm comments-toggle">
|
||||
{{ expandedComments()[music.id] ? 'Hide' : 'Show' }} Comments{{ comments()[music.id] ? ' (' + getCommentCount(music.id) + ')' : '' }}
|
||||
</button>
|
||||
|
||||
@if (expandedComments()[music.id]) {
|
||||
<div class="comments-container">
|
||||
@if (authService.isAuthenticated()) {
|
||||
<form (ngSubmit)="addComment(music.id)" class="comment-form">
|
||||
<textarea
|
||||
[(ngModel)]="newCommentContent[music.id]"
|
||||
name="comment"
|
||||
placeholder="Add a comment (Markdown supported)..."
|
||||
rows="2"
|
||||
></textarea>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Post Comment</button>
|
||||
</form>
|
||||
}
|
||||
|
||||
@if (commentsLoading()[music.id]) {
|
||||
<div class="comments-loading">Loading comments...</div>
|
||||
} @else {
|
||||
@for (comment of comments()[music.id] || []; track comment.id) {
|
||||
<div class="comment">
|
||||
<div class="comment-header">
|
||||
@if (comment.user.avatar) {
|
||||
<img [src]="comment.user.avatar" [alt]="comment.user.username" class="comment-avatar">
|
||||
}
|
||||
<span class="comment-author">{{ comment.user.username }}</span>
|
||||
<span class="comment-date">{{ formatDate(comment.createdAt) }}</span>
|
||||
@if (authService.isAdmin()) {
|
||||
<button (click)="deleteComment(music.id, comment.id)" class="btn btn-danger btn-xs">Delete</button>
|
||||
}
|
||||
</div>
|
||||
<div class="comment-content" [innerHTML]="comment.content"></div>
|
||||
</div>
|
||||
} @empty {
|
||||
<div class="no-comments">No comments yet. Be the first to comment!</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -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<Music[]>([]);
|
||||
loading = signal(true);
|
||||
showAddForm = signal(false);
|
||||
editingMusic = signal<Music | null>(null);
|
||||
typeFilter = signal<'all' | MusicType>('all');
|
||||
statusFilter = signal<'all' | MusicStatus>('all');
|
||||
|
||||
// Comments state
|
||||
comments = signal<Record<string, Comment[]>>({});
|
||||
commentsLoading = signal<Record<string, boolean>>({});
|
||||
expandedComments = signal<Record<string, boolean>>({});
|
||||
newCommentContent: Record<string, string> = {};
|
||||
|
||||
// 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<CreateMusicDto> = {
|
||||
title: '',
|
||||
artist: '',
|
||||
@@ -554,6 +793,8 @@ export class MusicListComponent implements OnInit {
|
||||
notes: ''
|
||||
};
|
||||
|
||||
editMusicData: Partial<UpdateMusicDto> = {};
|
||||
|
||||
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)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,6 @@ export class ApiService {
|
||||
|
||||
delete<T>(endpoint: string): Observable<T> {
|
||||
return this.http.delete<T>(`${this.apiUrl}${endpoint}`, {
|
||||
headers: this.getHeaders(),
|
||||
withCredentials: true
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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<Comment[]> {
|
||||
return this.api.get<Comment[]>(`/games/${gameId}/comments`);
|
||||
}
|
||||
|
||||
getCommentsForBook(bookId: string): Observable<Comment[]> {
|
||||
return this.api.get<Comment[]>(`/books/${bookId}/comments`);
|
||||
}
|
||||
|
||||
getCommentsForMusic(musicId: string): Observable<Comment[]> {
|
||||
return this.api.get<Comment[]>(`/music/${musicId}/comments`);
|
||||
}
|
||||
|
||||
addCommentToGame(gameId: string, comment: CreateCommentDto): Observable<Comment> {
|
||||
return this.api.post<Comment>(`/games/${gameId}/comments`, comment);
|
||||
}
|
||||
|
||||
addCommentToBook(bookId: string, comment: CreateCommentDto): Observable<Comment> {
|
||||
return this.api.post<Comment>(`/books/${bookId}/comments`, comment);
|
||||
}
|
||||
|
||||
addCommentToMusic(musicId: string, comment: CreateCommentDto): Observable<Comment> {
|
||||
return this.api.post<Comment>(`/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}`);
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
Generated
+368
-18
@@ -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: {}
|
||||
|
||||
@@ -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";
|
||||
export type * from "./lib/auth.types";
|
||||
export * from "./lib/comment.types";
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user