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?
|
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
|
## Next Steps
|
||||||
|
|
||||||
1. Choose technical stack
|
1. Choose technical stack
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ model Game {
|
|||||||
coverImage String?
|
coverImage String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
comments Comment[]
|
||||||
}
|
}
|
||||||
|
|
||||||
enum GameStatus {
|
enum GameStatus {
|
||||||
@@ -46,6 +47,7 @@ model Book {
|
|||||||
coverImage String?
|
coverImage String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
comments Comment[]
|
||||||
}
|
}
|
||||||
|
|
||||||
enum BookStatus {
|
enum BookStatus {
|
||||||
@@ -67,6 +69,7 @@ model Music {
|
|||||||
coverArt String?
|
coverArt String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
comments Comment[]
|
||||||
}
|
}
|
||||||
|
|
||||||
enum MusicType {
|
enum MusicType {
|
||||||
@@ -90,4 +93,20 @@ model User {
|
|||||||
isAdmin Boolean @default(false)
|
isAdmin Boolean @default(false)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
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) => {
|
const authPlugin: FastifyPluginAsync = async (app) => {
|
||||||
// Register JWT plugin
|
// Register JWT plugin
|
||||||
app.register(fastifyJwt, {
|
app.register(fastifyJwt, {
|
||||||
@@ -19,6 +31,14 @@ const authPlugin: FastifyPluginAsync = async (app) => {
|
|||||||
cookieName: "auth-token",
|
cookieName: "auth-token",
|
||||||
signed: false,
|
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
|
// Register cookie plugin
|
||||||
|
|||||||
@@ -5,12 +5,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { FastifyPluginAsync } from "fastify";
|
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 { BookService } from "../../services/book.service";
|
||||||
|
import { CommentService } from "../../services/comment.service";
|
||||||
import { adminGuard } from "../../middleware/admin-guard";
|
import { adminGuard } from "../../middleware/admin-guard";
|
||||||
|
|
||||||
const booksRoutes: FastifyPluginAsync = async (app) => {
|
const booksRoutes: FastifyPluginAsync = async (app) => {
|
||||||
const bookService = new BookService();
|
const bookService = new BookService();
|
||||||
|
const commentService = new CommentService();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all books (public route).
|
* Get all books (public route).
|
||||||
@@ -75,6 +77,47 @@ const booksRoutes: FastifyPluginAsync = async (app) => {
|
|||||||
return { success: true };
|
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;
|
export default booksRoutes;
|
||||||
@@ -5,12 +5,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { FastifyPluginAsync } from "fastify";
|
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 { GameService } from "../../services/game.service";
|
||||||
|
import { CommentService } from "../../services/comment.service";
|
||||||
import { adminGuard } from "../../middleware/admin-guard";
|
import { adminGuard } from "../../middleware/admin-guard";
|
||||||
|
|
||||||
const gamesRoutes: FastifyPluginAsync = async (app) => {
|
const gamesRoutes: FastifyPluginAsync = async (app) => {
|
||||||
const gameService = new GameService();
|
const gameService = new GameService();
|
||||||
|
const commentService = new CommentService();
|
||||||
|
|
||||||
// Get all games (public route)
|
// Get all games (public route)
|
||||||
app.get<{ Reply: Game[] }>("/", async () => {
|
app.get<{ Reply: Game[] }>("/", async () => {
|
||||||
@@ -65,6 +67,41 @@ const gamesRoutes: FastifyPluginAsync = async (app) => {
|
|||||||
return { success: true };
|
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;
|
export default gamesRoutes;
|
||||||
@@ -5,12 +5,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { FastifyPluginAsync } from "fastify";
|
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 { MusicService } from "../../services/music.service";
|
||||||
|
import { CommentService } from "../../services/comment.service";
|
||||||
import { adminGuard } from "../../middleware/admin-guard";
|
import { adminGuard } from "../../middleware/admin-guard";
|
||||||
|
|
||||||
const musicRoutes: FastifyPluginAsync = async (app) => {
|
const musicRoutes: FastifyPluginAsync = async (app) => {
|
||||||
const musicService = new MusicService();
|
const musicService = new MusicService();
|
||||||
|
const commentService = new CommentService();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all music (public route).
|
* Get all music (public route).
|
||||||
@@ -75,6 +77,47 @@ const musicRoutes: FastifyPluginAsync = async (app) => {
|
|||||||
return { success: true };
|
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;
|
export default musicRoutes;
|
||||||
@@ -22,7 +22,7 @@ export class BookService {
|
|||||||
|
|
||||||
return books.map((book) => ({
|
return books.map((book) => ({
|
||||||
...book,
|
...book,
|
||||||
status: book.status.toLowerCase() as BookStatus,
|
status: book.status as unknown as BookStatus,
|
||||||
dateAdded: book.dateAdded,
|
dateAdded: book.dateAdded,
|
||||||
dateFinished: book.dateFinished || undefined,
|
dateFinished: book.dateFinished || undefined,
|
||||||
createdAt: book.createdAt,
|
createdAt: book.createdAt,
|
||||||
@@ -42,7 +42,7 @@ export class BookService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...book,
|
...book,
|
||||||
status: book.status.toLowerCase() as BookStatus,
|
status: book.status as unknown as BookStatus,
|
||||||
dateAdded: book.dateAdded,
|
dateAdded: book.dateAdded,
|
||||||
dateFinished: book.dateFinished || undefined,
|
dateFinished: book.dateFinished || undefined,
|
||||||
createdAt: book.createdAt,
|
createdAt: book.createdAt,
|
||||||
@@ -63,7 +63,7 @@ export class BookService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...book,
|
...book,
|
||||||
status: book.status.toLowerCase() as BookStatus,
|
status: book.status as unknown as BookStatus,
|
||||||
dateAdded: book.dateAdded,
|
dateAdded: book.dateAdded,
|
||||||
dateFinished: book.dateFinished || undefined,
|
dateFinished: book.dateFinished || undefined,
|
||||||
createdAt: book.createdAt,
|
createdAt: book.createdAt,
|
||||||
@@ -87,7 +87,7 @@ export class BookService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...book,
|
...book,
|
||||||
status: book.status.toLowerCase() as BookStatus,
|
status: book.status as unknown as BookStatus,
|
||||||
dateAdded: book.dateAdded,
|
dateAdded: book.dateAdded,
|
||||||
dateFinished: book.dateFinished || undefined,
|
dateFinished: book.dateFinished || undefined,
|
||||||
createdAt: book.createdAt,
|
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) => ({
|
return games.map((game) => ({
|
||||||
...game,
|
...game,
|
||||||
status: game.status.toLowerCase() as GameStatus,
|
status: game.status as unknown as GameStatus,
|
||||||
dateAdded: game.dateAdded,
|
dateAdded: game.dateAdded,
|
||||||
dateCompleted: game.dateCompleted || undefined,
|
dateCompleted: game.dateCompleted || undefined,
|
||||||
createdAt: game.createdAt,
|
createdAt: game.createdAt,
|
||||||
@@ -42,7 +42,7 @@ export class GameService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...game,
|
...game,
|
||||||
status: game.status.toLowerCase() as GameStatus,
|
status: game.status as unknown as GameStatus,
|
||||||
dateAdded: game.dateAdded,
|
dateAdded: game.dateAdded,
|
||||||
dateCompleted: game.dateCompleted || undefined,
|
dateCompleted: game.dateCompleted || undefined,
|
||||||
createdAt: game.createdAt,
|
createdAt: game.createdAt,
|
||||||
@@ -63,7 +63,7 @@ export class GameService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...game,
|
...game,
|
||||||
status: game.status.toLowerCase() as GameStatus,
|
status: game.status as unknown as GameStatus,
|
||||||
dateAdded: game.dateAdded,
|
dateAdded: game.dateAdded,
|
||||||
dateCompleted: game.dateCompleted || undefined,
|
dateCompleted: game.dateCompleted || undefined,
|
||||||
createdAt: game.createdAt,
|
createdAt: game.createdAt,
|
||||||
@@ -87,7 +87,7 @@ export class GameService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...game,
|
...game,
|
||||||
status: game.status.toLowerCase() as GameStatus,
|
status: game.status as unknown as GameStatus,
|
||||||
dateAdded: game.dateAdded,
|
dateAdded: game.dateAdded,
|
||||||
dateCompleted: game.dateCompleted || undefined,
|
dateCompleted: game.dateCompleted || undefined,
|
||||||
createdAt: game.createdAt,
|
createdAt: game.createdAt,
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ export class MusicService {
|
|||||||
|
|
||||||
return musicItems.map((music) => ({
|
return musicItems.map((music) => ({
|
||||||
...music,
|
...music,
|
||||||
type: music.type.toLowerCase() as MusicType,
|
type: music.type as unknown as MusicType,
|
||||||
status: music.status.toLowerCase() as MusicStatus,
|
status: music.status as unknown as MusicStatus,
|
||||||
dateAdded: music.dateAdded,
|
dateAdded: music.dateAdded,
|
||||||
dateCompleted: music.dateCompleted || undefined,
|
dateCompleted: music.dateCompleted || undefined,
|
||||||
createdAt: music.createdAt,
|
createdAt: music.createdAt,
|
||||||
@@ -43,8 +43,8 @@ export class MusicService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...music,
|
...music,
|
||||||
type: music.type.toLowerCase() as MusicType,
|
type: music.type as unknown as MusicType,
|
||||||
status: music.status.toLowerCase() as MusicStatus,
|
status: music.status as unknown as MusicStatus,
|
||||||
dateAdded: music.dateAdded,
|
dateAdded: music.dateAdded,
|
||||||
dateCompleted: music.dateCompleted || undefined,
|
dateCompleted: music.dateCompleted || undefined,
|
||||||
createdAt: music.createdAt,
|
createdAt: music.createdAt,
|
||||||
@@ -66,8 +66,8 @@ export class MusicService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...music,
|
...music,
|
||||||
type: music.type.toLowerCase() as MusicType,
|
type: music.type as unknown as MusicType,
|
||||||
status: music.status.toLowerCase() as MusicStatus,
|
status: music.status as unknown as MusicStatus,
|
||||||
dateAdded: music.dateAdded,
|
dateAdded: music.dateAdded,
|
||||||
dateCompleted: music.dateCompleted || undefined,
|
dateCompleted: music.dateCompleted || undefined,
|
||||||
createdAt: music.createdAt,
|
createdAt: music.createdAt,
|
||||||
@@ -94,8 +94,8 @@ export class MusicService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...music,
|
...music,
|
||||||
type: music.type.toLowerCase() as MusicType,
|
type: music.type as unknown as MusicType,
|
||||||
status: music.status.toLowerCase() as MusicStatus,
|
status: music.status as unknown as MusicStatus,
|
||||||
dateAdded: music.dateAdded,
|
dateAdded: music.dateAdded,
|
||||||
dateCompleted: music.dateCompleted || undefined,
|
dateCompleted: music.dateCompleted || undefined,
|
||||||
createdAt: music.createdAt,
|
createdAt: music.createdAt,
|
||||||
|
|||||||
@@ -4,12 +4,13 @@
|
|||||||
* @author Naomi Carrigan
|
* @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 { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { BooksService } from '../../services/books.service';
|
import { BooksService } from '../../services/books.service';
|
||||||
import { AuthService } from '../../services/auth.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({
|
@Component({
|
||||||
selector: 'app-books-list',
|
selector: 'app-books-list',
|
||||||
@@ -67,9 +68,9 @@ import { Book, BookStatus, CreateBookDto } from '@library/shared-types';
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="status">Status</label>
|
<label for="status">Status</label>
|
||||||
<select id="status" [(ngModel)]="newBook.status" name="status" required>
|
<select id="status" [(ngModel)]="newBook.status" name="status" required>
|
||||||
<option value="reading">Currently Reading</option>
|
<option [value]="BookStatus.reading">Currently Reading</option>
|
||||||
<option value="finished">Finished</option>
|
<option [value]="BookStatus.finished">Finished</option>
|
||||||
<option value="toRead">To Read</option>
|
<option [value]="BookStatus.toRead">To Read</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -103,6 +104,83 @@ import { Book, BookStatus, CreateBookDto } from '@library/shared-types';
|
|||||||
</form>
|
</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">
|
<div class="filters">
|
||||||
<button
|
<button
|
||||||
(click)="setFilter('all')"
|
(click)="setFilter('all')"
|
||||||
@@ -116,21 +194,21 @@ import { Book, BookStatus, CreateBookDto } from '@library/shared-types';
|
|||||||
[class.active]="statusFilter() === BookStatus.reading"
|
[class.active]="statusFilter() === BookStatus.reading"
|
||||||
class="filter-btn"
|
class="filter-btn"
|
||||||
>
|
>
|
||||||
Reading ({{ getCountByStatus(BookStatus.reading) }})
|
Reading ({{ readingCount() }})
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="setFilter(BookStatus.finished)"
|
(click)="setFilter(BookStatus.finished)"
|
||||||
[class.active]="statusFilter() === BookStatus.finished"
|
[class.active]="statusFilter() === BookStatus.finished"
|
||||||
class="filter-btn"
|
class="filter-btn"
|
||||||
>
|
>
|
||||||
Finished ({{ getCountByStatus(BookStatus.finished) }})
|
Finished ({{ finishedCount() }})
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="setFilter(BookStatus.toRead)"
|
(click)="setFilter(BookStatus.toRead)"
|
||||||
[class.active]="statusFilter() === BookStatus.toRead"
|
[class.active]="statusFilter() === BookStatus.toRead"
|
||||||
class="filter-btn"
|
class="filter-btn"
|
||||||
>
|
>
|
||||||
To Read ({{ getCountByStatus(BookStatus.toRead) }})
|
To Read ({{ toReadCount() }})
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -182,11 +260,58 @@ import { Book, BookStatus, CreateBookDto } from '@library/shared-types';
|
|||||||
|
|
||||||
@if (authService.isAdmin()) {
|
@if (authService.isAdmin()) {
|
||||||
<div class="actions">
|
<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">
|
<button (click)="deleteBook(book)" class="btn btn-danger btn-sm">
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
||||||
</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-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 {
|
export class BooksListComponent implements OnInit {
|
||||||
booksService = inject(BooksService);
|
booksService = inject(BooksService);
|
||||||
authService = inject(AuthService);
|
authService = inject(AuthService);
|
||||||
|
commentsService = inject(CommentsService);
|
||||||
|
|
||||||
books = signal<Book[]>([]);
|
books = signal<Book[]>([]);
|
||||||
loading = signal(true);
|
loading = signal(true);
|
||||||
showAddForm = signal(false);
|
showAddForm = signal(false);
|
||||||
|
editingBook = signal<Book | null>(null);
|
||||||
statusFilter = signal<'all' | BookStatus>('all');
|
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
|
// Expose BookStatus enum to template
|
||||||
BookStatus = BookStatus;
|
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> = {
|
newBook: Partial<CreateBookDto> = {
|
||||||
title: '',
|
title: '',
|
||||||
author: '',
|
author: '',
|
||||||
@@ -475,6 +706,8 @@ export class BooksListComponent implements OnInit {
|
|||||||
notes: ''
|
notes: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
|
editBook: Partial<UpdateBookDto> = {};
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.loadBooks();
|
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) {
|
setFilter(filter: 'all' | BookStatus) {
|
||||||
this.statusFilter.set(filter);
|
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 {
|
formatDate(date: Date | string): string {
|
||||||
return new Date(date).toLocaleDateString();
|
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
|
* @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 { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { GamesService } from '../../services/games.service';
|
import { GamesService } from '../../services/games.service';
|
||||||
import { AuthService } from '../../services/auth.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({
|
@Component({
|
||||||
selector: 'app-games-list',
|
selector: 'app-games-list',
|
||||||
@@ -55,9 +56,9 @@ import { Game, GameStatus, CreateGameDto } from '@library/shared-types';
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="status">Status</label>
|
<label for="status">Status</label>
|
||||||
<select id="status" [(ngModel)]="newGame.status" name="status" required>
|
<select id="status" [(ngModel)]="newGame.status" name="status" required>
|
||||||
<option value="playing">Currently Playing</option>
|
<option [value]="GameStatus.playing">Currently Playing</option>
|
||||||
<option value="completed">Completed</option>
|
<option [value]="GameStatus.completed">Completed</option>
|
||||||
<option value="backlog">In Backlog</option>
|
<option [value]="GameStatus.backlog">In Backlog</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -91,6 +92,71 @@ import { Game, GameStatus, CreateGameDto } from '@library/shared-types';
|
|||||||
</form>
|
</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">
|
<div class="filters">
|
||||||
<button
|
<button
|
||||||
(click)="setFilter('all')"
|
(click)="setFilter('all')"
|
||||||
@@ -104,21 +170,21 @@ import { Game, GameStatus, CreateGameDto } from '@library/shared-types';
|
|||||||
[class.active]="statusFilter() === GameStatus.playing"
|
[class.active]="statusFilter() === GameStatus.playing"
|
||||||
class="filter-btn"
|
class="filter-btn"
|
||||||
>
|
>
|
||||||
Playing ({{ getCountByStatus(GameStatus.playing) }})
|
Playing ({{ playingCount() }})
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="setFilter(GameStatus.completed)"
|
(click)="setFilter(GameStatus.completed)"
|
||||||
[class.active]="statusFilter() === GameStatus.completed"
|
[class.active]="statusFilter() === GameStatus.completed"
|
||||||
class="filter-btn"
|
class="filter-btn"
|
||||||
>
|
>
|
||||||
Completed ({{ getCountByStatus(GameStatus.completed) }})
|
Completed ({{ completedCount() }})
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="setFilter(GameStatus.backlog)"
|
(click)="setFilter(GameStatus.backlog)"
|
||||||
[class.active]="statusFilter() === GameStatus.backlog"
|
[class.active]="statusFilter() === GameStatus.backlog"
|
||||||
class="filter-btn"
|
class="filter-btn"
|
||||||
>
|
>
|
||||||
Backlog ({{ getCountByStatus(GameStatus.backlog) }})
|
Backlog ({{ backlogCount() }})
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -157,11 +223,58 @@ import { Game, GameStatus, CreateGameDto } from '@library/shared-types';
|
|||||||
|
|
||||||
@if (authService.isAdmin()) {
|
@if (authService.isAdmin()) {
|
||||||
<div class="actions">
|
<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">
|
<button (click)="deleteGame(game)" class="btn btn-danger btn-sm">
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -344,20 +457,115 @@ import { Game, GameStatus, CreateGameDto } from '@library/shared-types';
|
|||||||
.btn-secondary { background: #6b7280; color: white; }
|
.btn-secondary { background: #6b7280; color: white; }
|
||||||
.btn-danger { background: #ef4444; color: white; }
|
.btn-danger { background: #ef4444; color: white; }
|
||||||
.btn-sm { padding: 0.25rem 0.75rem; font-size: 0.85rem; }
|
.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 {
|
export class GamesListComponent implements OnInit {
|
||||||
gamesService = inject(GamesService);
|
gamesService = inject(GamesService);
|
||||||
authService = inject(AuthService);
|
authService = inject(AuthService);
|
||||||
|
commentsService = inject(CommentsService);
|
||||||
|
|
||||||
games = signal<Game[]>([]);
|
games = signal<Game[]>([]);
|
||||||
loading = signal(true);
|
loading = signal(true);
|
||||||
showAddForm = signal(false);
|
showAddForm = signal(false);
|
||||||
|
editingGame = signal<Game | null>(null);
|
||||||
statusFilter = signal<'all' | GameStatus>('all');
|
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
|
// Expose GameStatus enum to template
|
||||||
GameStatus = GameStatus;
|
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> = {
|
newGame: Partial<CreateGameDto> = {
|
||||||
title: '',
|
title: '',
|
||||||
platform: '',
|
platform: '',
|
||||||
@@ -366,6 +574,8 @@ export class GamesListComponent implements OnInit {
|
|||||||
notes: ''
|
notes: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
|
editGame: Partial<UpdateGameDto> = {};
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.loadGames();
|
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) {
|
setFilter(filter: 'all' | GameStatus) {
|
||||||
this.statusFilter.set(filter);
|
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
|
* @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 { CommonModule } from '@angular/common';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { MusicService } from '../../services/music.service';
|
import { MusicService } from '../../services/music.service';
|
||||||
import { AuthService } from '../../services/auth.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({
|
@Component({
|
||||||
selector: 'app-music-list',
|
selector: 'app-music-list',
|
||||||
@@ -56,18 +57,18 @@ import { Music, MusicStatus, MusicType, CreateMusicDto } from '@library/shared-t
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="type">Type</label>
|
<label for="type">Type</label>
|
||||||
<select id="type" [(ngModel)]="newMusic.type" name="type" required>
|
<select id="type" [(ngModel)]="newMusic.type" name="type" required>
|
||||||
<option value="album">Album</option>
|
<option [value]="MusicType.album">Album</option>
|
||||||
<option value="single">Single</option>
|
<option [value]="MusicType.single">Single</option>
|
||||||
<option value="ep">EP</option>
|
<option [value]="MusicType.ep">EP</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="status">Status</label>
|
<label for="status">Status</label>
|
||||||
<select id="status" [(ngModel)]="newMusic.status" name="status" required>
|
<select id="status" [(ngModel)]="newMusic.status" name="status" required>
|
||||||
<option value="listening">Currently Listening</option>
|
<option [value]="MusicStatus.listening">Currently Listening</option>
|
||||||
<option value="completed">Completed</option>
|
<option [value]="MusicStatus.completed">Completed</option>
|
||||||
<option value="wantToListen">Want to Listen</option>
|
<option [value]="MusicStatus.wantToListen">Want to Listen</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -101,6 +102,81 @@ import { Music, MusicStatus, MusicType, CreateMusicDto } from '@library/shared-t
|
|||||||
</form>
|
</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="filters">
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<strong>Type:</strong>
|
<strong>Type:</strong>
|
||||||
@@ -116,21 +192,21 @@ import { Music, MusicStatus, MusicType, CreateMusicDto } from '@library/shared-t
|
|||||||
[class.active]="typeFilter() === MusicType.album"
|
[class.active]="typeFilter() === MusicType.album"
|
||||||
class="filter-btn"
|
class="filter-btn"
|
||||||
>
|
>
|
||||||
Albums ({{ getCountByType(MusicType.album) }})
|
Albums ({{ albumCount() }})
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="setTypeFilter(MusicType.single)"
|
(click)="setTypeFilter(MusicType.single)"
|
||||||
[class.active]="typeFilter() === MusicType.single"
|
[class.active]="typeFilter() === MusicType.single"
|
||||||
class="filter-btn"
|
class="filter-btn"
|
||||||
>
|
>
|
||||||
Singles ({{ getCountByType(MusicType.single) }})
|
Singles ({{ singleCount() }})
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="setTypeFilter(MusicType.ep)"
|
(click)="setTypeFilter(MusicType.ep)"
|
||||||
[class.active]="typeFilter() === MusicType.ep"
|
[class.active]="typeFilter() === MusicType.ep"
|
||||||
class="filter-btn"
|
class="filter-btn"
|
||||||
>
|
>
|
||||||
EPs ({{ getCountByType(MusicType.ep) }})
|
EPs ({{ epCount() }})
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -148,21 +224,21 @@ import { Music, MusicStatus, MusicType, CreateMusicDto } from '@library/shared-t
|
|||||||
[class.active]="statusFilter() === MusicStatus.listening"
|
[class.active]="statusFilter() === MusicStatus.listening"
|
||||||
class="filter-btn"
|
class="filter-btn"
|
||||||
>
|
>
|
||||||
Listening ({{ getCountByStatus(MusicStatus.listening) }})
|
Listening ({{ listeningCount() }})
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="setStatusFilter(MusicStatus.completed)"
|
(click)="setStatusFilter(MusicStatus.completed)"
|
||||||
[class.active]="statusFilter() === MusicStatus.completed"
|
[class.active]="statusFilter() === MusicStatus.completed"
|
||||||
class="filter-btn"
|
class="filter-btn"
|
||||||
>
|
>
|
||||||
Completed ({{ getCountByStatus(MusicStatus.completed) }})
|
Completed ({{ completedCount() }})
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="setStatusFilter(MusicStatus.wantToListen)"
|
(click)="setStatusFilter(MusicStatus.wantToListen)"
|
||||||
[class.active]="statusFilter() === MusicStatus.wantToListen"
|
[class.active]="statusFilter() === MusicStatus.wantToListen"
|
||||||
class="filter-btn"
|
class="filter-btn"
|
||||||
>
|
>
|
||||||
Want to Listen ({{ getCountByStatus(MusicStatus.wantToListen) }})
|
Want to Listen ({{ wantToListenCount() }})
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -222,11 +298,58 @@ import { Music, MusicStatus, MusicType, CreateMusicDto } from '@library/shared-t
|
|||||||
|
|
||||||
@if (authService.isAdmin()) {
|
@if (authService.isAdmin()) {
|
||||||
<div class="actions">
|
<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">
|
<button (click)="deleteMusic(music)" class="btn btn-danger btn-sm">
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -529,22 +652,138 @@ import { Music, MusicStatus, MusicType, CreateMusicDto } from '@library/shared-t
|
|||||||
padding: 0.25rem 0.75rem;
|
padding: 0.25rem 0.75rem;
|
||||||
font-size: 0.85rem;
|
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 {
|
export class MusicListComponent implements OnInit {
|
||||||
musicService = inject(MusicService);
|
musicService = inject(MusicService);
|
||||||
authService = inject(AuthService);
|
authService = inject(AuthService);
|
||||||
|
commentsService = inject(CommentsService);
|
||||||
|
|
||||||
music = signal<Music[]>([]);
|
music = signal<Music[]>([]);
|
||||||
loading = signal(true);
|
loading = signal(true);
|
||||||
showAddForm = signal(false);
|
showAddForm = signal(false);
|
||||||
|
editingMusic = signal<Music | null>(null);
|
||||||
typeFilter = signal<'all' | MusicType>('all');
|
typeFilter = signal<'all' | MusicType>('all');
|
||||||
statusFilter = signal<'all' | MusicStatus>('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
|
// Expose enums to template
|
||||||
MusicType = MusicType;
|
MusicType = MusicType;
|
||||||
MusicStatus = MusicStatus;
|
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> = {
|
newMusic: Partial<CreateMusicDto> = {
|
||||||
title: '',
|
title: '',
|
||||||
artist: '',
|
artist: '',
|
||||||
@@ -554,6 +793,8 @@ export class MusicListComponent implements OnInit {
|
|||||||
notes: ''
|
notes: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
|
editMusicData: Partial<UpdateMusicDto> = {};
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.loadMusic();
|
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) {
|
setTypeFilter(filter: 'all' | MusicType) {
|
||||||
this.typeFilter.set(filter);
|
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 {
|
formatDate(date: Date | string): string {
|
||||||
return new Date(date).toLocaleDateString();
|
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> {
|
delete<T>(endpoint: string): Observable<T> {
|
||||||
return this.http.delete<T>(`${this.apiUrl}${endpoint}`, {
|
return this.http.delete<T>(`${this.apiUrl}${endpoint}`, {
|
||||||
headers: this.getHeaders(),
|
|
||||||
withCredentials: true
|
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/sensible": "6.0.4",
|
||||||
"@fastify/static": "^9.0.0",
|
"@fastify/static": "^9.0.0",
|
||||||
"@prisma/client": "6.19.2",
|
"@prisma/client": "6.19.2",
|
||||||
|
"dompurify": "^3.3.1",
|
||||||
"fastify": "5.2.2",
|
"fastify": "5.2.2",
|
||||||
"fastify-plugin": "5.0.1",
|
"fastify-plugin": "5.0.1",
|
||||||
|
"jsdom": "^28.0.0",
|
||||||
|
"marked": "^17.0.1",
|
||||||
"rxjs": "7.8.2"
|
"rxjs": "7.8.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -56,7 +59,9 @@
|
|||||||
"@swc-node/register": "1.9.2",
|
"@swc-node/register": "1.9.2",
|
||||||
"@swc/core": "1.5.29",
|
"@swc/core": "1.5.29",
|
||||||
"@swc/helpers": "0.5.18",
|
"@swc/helpers": "0.5.18",
|
||||||
|
"@types/dompurify": "^3.2.0",
|
||||||
"@types/jest": "30.0.0",
|
"@types/jest": "30.0.0",
|
||||||
|
"@types/jsdom": "^27.0.0",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "20.19.9",
|
"@types/node": "20.19.9",
|
||||||
"@typescript-eslint/utils": "8.54.0",
|
"@typescript-eslint/utils": "8.54.0",
|
||||||
|
|||||||
Generated
+368
-18
@@ -47,19 +47,28 @@ importers:
|
|||||||
'@prisma/client':
|
'@prisma/client':
|
||||||
specifier: 6.19.2
|
specifier: 6.19.2
|
||||||
version: 6.19.2(prisma@6.19.2(typescript@5.9.3))(typescript@5.9.3)
|
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:
|
fastify:
|
||||||
specifier: 5.2.2
|
specifier: 5.2.2
|
||||||
version: 5.2.2
|
version: 5.2.2
|
||||||
fastify-plugin:
|
fastify-plugin:
|
||||||
specifier: 5.0.1
|
specifier: 5.0.1
|
||||||
version: 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:
|
rxjs:
|
||||||
specifier: 7.8.2
|
specifier: 7.8.2
|
||||||
version: 7.8.2
|
version: 7.8.2
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@angular-devkit/build-angular':
|
'@angular-devkit/build-angular':
|
||||||
specifier: 21.1.2
|
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':
|
'@angular-devkit/core':
|
||||||
specifier: 21.1.2
|
specifier: 21.1.2
|
||||||
version: 21.1.2(chokidar@5.0.0)
|
version: 21.1.2(chokidar@5.0.0)
|
||||||
@@ -80,10 +89,10 @@ importers:
|
|||||||
version: 9.39.2
|
version: 9.39.2
|
||||||
'@nhcarrigan/eslint-config':
|
'@nhcarrigan/eslint-config':
|
||||||
specifier: 5.2.0
|
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':
|
'@nx/angular':
|
||||||
specifier: 22.4.4
|
specifier: 22.4.4
|
||||||
version: 22.4.4(ec507f70e00b67864c7695431dce0b70)
|
version: 22.4.4(53644491fdd75e214444a711ff7c7d5c)
|
||||||
'@nx/cypress':
|
'@nx/cypress':
|
||||||
specifier: 22.4.4
|
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)
|
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':
|
'@swc/helpers':
|
||||||
specifier: 0.5.18
|
specifier: 0.5.18
|
||||||
version: 0.5.18
|
version: 0.5.18
|
||||||
|
'@types/dompurify':
|
||||||
|
specifier: ^3.2.0
|
||||||
|
version: 3.2.0
|
||||||
'@types/jest':
|
'@types/jest':
|
||||||
specifier: 30.0.0
|
specifier: 30.0.0
|
||||||
version: 30.0.0
|
version: 30.0.0
|
||||||
|
'@types/jsdom':
|
||||||
|
specifier: ^27.0.0
|
||||||
|
version: 27.0.0
|
||||||
'@types/jsonwebtoken':
|
'@types/jsonwebtoken':
|
||||||
specifier: ^9.0.10
|
specifier: ^9.0.10
|
||||||
version: 9.0.10
|
version: 9.0.10
|
||||||
@@ -183,6 +198,9 @@ importers:
|
|||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
|
'@acemir/cssom@0.9.31':
|
||||||
|
resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==}
|
||||||
|
|
||||||
'@algolia/abtesting@1.12.2':
|
'@algolia/abtesting@1.12.2':
|
||||||
resolution: {integrity: sha512-oWknd6wpfNrmRcH0vzed3UPX0i17o4kYLM5OMITyMVM2xLgaRbIafoxL0e8mcrNNb0iORCJA0evnNDKRYth5WQ==}
|
resolution: {integrity: sha512-oWknd6wpfNrmRcH0vzed3UPX0i17o4kYLM5OMITyMVM2xLgaRbIafoxL0e8mcrNNb0iORCJA0evnNDKRYth5WQ==}
|
||||||
engines: {node: '>= 14.0.0'}
|
engines: {node: '>= 14.0.0'}
|
||||||
@@ -481,6 +499,15 @@ packages:
|
|||||||
'@angular/platform-browser': 21.1.2
|
'@angular/platform-browser': 21.1.2
|
||||||
rxjs: ^6.5.3 || ^7.4.0
|
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':
|
'@babel/code-frame@7.29.0':
|
||||||
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
|
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@@ -1141,6 +1168,37 @@ packages:
|
|||||||
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||||
engines: {node: '>=12'}
|
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':
|
'@cypress/request@3.0.10':
|
||||||
resolution: {integrity: sha512-hauBrOdvu08vOsagkZ/Aju5XuiZx6ldsLfByg1htFeldhex+PeMrYauANzFsMJeAA0+dyPLbDoX2OYuvVoLDkQ==}
|
resolution: {integrity: sha512-hauBrOdvu08vOsagkZ/Aju5XuiZx6ldsLfByg1htFeldhex+PeMrYauANzFsMJeAA0+dyPLbDoX2OYuvVoLDkQ==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@@ -1520,6 +1578,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
|
resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
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':
|
'@fastify/accept-negotiator@2.0.1':
|
||||||
resolution: {integrity: sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==}
|
resolution: {integrity: sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==}
|
||||||
|
|
||||||
@@ -3257,6 +3324,10 @@ packages:
|
|||||||
'@types/deep-eql@4.0.2':
|
'@types/deep-eql@4.0.2':
|
||||||
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
|
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':
|
'@types/eslint-scope@3.7.7':
|
||||||
resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==}
|
resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==}
|
||||||
|
|
||||||
@@ -3296,6 +3367,9 @@ packages:
|
|||||||
'@types/jest@30.0.0':
|
'@types/jest@30.0.0':
|
||||||
resolution: {integrity: sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==}
|
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':
|
'@types/json-schema@7.0.15':
|
||||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||||
|
|
||||||
@@ -3362,6 +3436,12 @@ packages:
|
|||||||
'@types/tmp@0.2.6':
|
'@types/tmp@0.2.6':
|
||||||
resolution: {integrity: sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==}
|
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':
|
'@types/ws@8.18.1':
|
||||||
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
|
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
|
||||||
|
|
||||||
@@ -4098,6 +4178,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-NaWu+f4YrJxEttJSm16AzMIFtVldCvaJ68b1L098KpqXmxt9xOLtKoLkKxb8ekhOrLqEJAbvT6n6SEvB/sac7A==}
|
resolution: {integrity: sha512-NaWu+f4YrJxEttJSm16AzMIFtVldCvaJ68b1L098KpqXmxt9xOLtKoLkKxb8ekhOrLqEJAbvT6n6SEvB/sac7A==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
|
bidi-js@1.0.3:
|
||||||
|
resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
|
||||||
|
|
||||||
big.js@5.2.2:
|
big.js@5.2.2:
|
||||||
resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==}
|
resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==}
|
||||||
|
|
||||||
@@ -4596,6 +4679,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
|
resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
|
||||||
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
|
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:
|
css-what@6.2.2:
|
||||||
resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==}
|
resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@@ -4631,6 +4718,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==}
|
resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==}
|
||||||
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
|
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:
|
cypress@15.9.0:
|
||||||
resolution: {integrity: sha512-Ks6Bdilz3TtkLZtTQyqYaqtL/WT3X3APKaSLhTV96TmTyudzSjc6EJsJCHmBb7DxO+3R12q3Jkbjgm/iPgmwfg==}
|
resolution: {integrity: sha512-Ks6Bdilz3TtkLZtTQyqYaqtL/WT3X3APKaSLhTV96TmTyudzSjc6EJsJCHmBb7DxO+3R12q3Jkbjgm/iPgmwfg==}
|
||||||
engines: {node: ^20.1.0 || ^22.0.0 || >=24.0.0}
|
engines: {node: ^20.1.0 || ^22.0.0 || >=24.0.0}
|
||||||
@@ -4640,6 +4731,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==}
|
resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==}
|
||||||
engines: {node: '>=0.10'}
|
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:
|
data-view-buffer@1.0.2:
|
||||||
resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==}
|
resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -4693,6 +4788,9 @@ packages:
|
|||||||
supports-color:
|
supports-color:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
decimal.js@10.6.0:
|
||||||
|
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
|
||||||
|
|
||||||
dedent@1.7.1:
|
dedent@1.7.1:
|
||||||
resolution: {integrity: sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==}
|
resolution: {integrity: sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -4813,6 +4911,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
|
|
||||||
|
dompurify@3.3.1:
|
||||||
|
resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==}
|
||||||
|
|
||||||
domutils@3.2.2:
|
domutils@3.2.2:
|
||||||
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
|
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
|
||||||
|
|
||||||
@@ -5666,6 +5767,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==}
|
resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==}
|
||||||
engines: {node: '>=12'}
|
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:
|
html-entities@2.6.0:
|
||||||
resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==}
|
resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==}
|
||||||
|
|
||||||
@@ -5980,6 +6085,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
|
resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
is-potential-custom-element-name@1.0.1:
|
||||||
|
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
|
||||||
|
|
||||||
is-promise@4.0.0:
|
is-promise@4.0.0:
|
||||||
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
|
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
|
||||||
|
|
||||||
@@ -6285,6 +6393,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==}
|
resolution: {integrity: sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==}
|
||||||
engines: {node: '>=12.0.0'}
|
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:
|
jsesc@0.5.0:
|
||||||
resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==}
|
resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -6578,6 +6695,11 @@ packages:
|
|||||||
makeerror@1.0.12:
|
makeerror@1.0.12:
|
||||||
resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==}
|
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:
|
math-intrinsics@1.1.0:
|
||||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -6588,6 +6710,9 @@ packages:
|
|||||||
mdn-data@2.0.30:
|
mdn-data@2.0.30:
|
||||||
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
|
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:
|
media-typer@0.3.0:
|
||||||
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -7091,6 +7216,9 @@ packages:
|
|||||||
parse5@4.0.0:
|
parse5@4.0.0:
|
||||||
resolution: {integrity: sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==}
|
resolution: {integrity: sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==}
|
||||||
|
|
||||||
|
parse5@7.3.0:
|
||||||
|
resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==}
|
||||||
|
|
||||||
parse5@8.0.0:
|
parse5@8.0.0:
|
||||||
resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==}
|
resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==}
|
||||||
|
|
||||||
@@ -7944,6 +8072,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==}
|
resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==}
|
||||||
engines: {node: '>=11.0.0'}
|
engines: {node: '>=11.0.0'}
|
||||||
|
|
||||||
|
saxes@6.0.0:
|
||||||
|
resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==}
|
||||||
|
engines: {node: '>=v12.22.7'}
|
||||||
|
|
||||||
scheduler@0.27.0:
|
scheduler@0.27.0:
|
||||||
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||||
|
|
||||||
@@ -8330,6 +8462,9 @@ packages:
|
|||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
symbol-tree@3.2.4:
|
||||||
|
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
|
||||||
|
|
||||||
sync-child-process@1.0.2:
|
sync-child-process@1.0.2:
|
||||||
resolution: {integrity: sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==}
|
resolution: {integrity: sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==}
|
||||||
engines: {node: '>=16.0.0'}
|
engines: {node: '>=16.0.0'}
|
||||||
@@ -8433,10 +8568,17 @@ packages:
|
|||||||
tldts-core@6.1.86:
|
tldts-core@6.1.86:
|
||||||
resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==}
|
resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==}
|
||||||
|
|
||||||
|
tldts-core@7.0.22:
|
||||||
|
resolution: {integrity: sha512-KgbTDC5wzlL6j/x6np6wCnDSMUq4kucHNm00KXPbfNzmllCmtmvtykJHfmgdHntwIeupW04y8s1N/43S1PkQDw==}
|
||||||
|
|
||||||
tldts@6.1.86:
|
tldts@6.1.86:
|
||||||
resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==}
|
resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
tldts@7.0.22:
|
||||||
|
resolution: {integrity: sha512-nqpKFC53CgopKPjT6Wfb6tpIcZXHcI6G37hesvikhx0EmUGPkZrujRyAjgnmp1SHNgpQfKVanZ+KfpANFt2Hxw==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
tmp@0.2.5:
|
tmp@0.2.5:
|
||||||
resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==}
|
resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==}
|
||||||
engines: {node: '>=14.14'}
|
engines: {node: '>=14.14'}
|
||||||
@@ -8460,9 +8602,17 @@ packages:
|
|||||||
resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
|
resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
|
tough-cookie@6.0.0:
|
||||||
|
resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
tr46@0.0.3:
|
tr46@0.0.3:
|
||||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||||
|
|
||||||
|
tr46@6.0.0:
|
||||||
|
resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
tree-dump@1.1.0:
|
tree-dump@1.1.0:
|
||||||
resolution: {integrity: sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==}
|
resolution: {integrity: sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==}
|
||||||
engines: {node: '>=10.0'}
|
engines: {node: '>=10.0'}
|
||||||
@@ -8660,6 +8810,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==}
|
resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==}
|
||||||
engines: {node: '>=20.18.1'}
|
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:
|
unicode-canonical-property-names-ecmascript@2.0.1:
|
||||||
resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==}
|
resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -8873,6 +9027,10 @@ packages:
|
|||||||
jsdom:
|
jsdom:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
w3c-xmlserializer@5.0.0:
|
||||||
|
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
walker@1.0.8:
|
walker@1.0.8:
|
||||||
resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
|
resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
|
||||||
|
|
||||||
@@ -8896,6 +9054,10 @@ packages:
|
|||||||
webidl-conversions@3.0.1:
|
webidl-conversions@3.0.1:
|
||||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
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:
|
webpack-dev-middleware@7.4.5:
|
||||||
resolution: {integrity: sha512-uxQ6YqGdE4hgDKNf7hUiPXOdtkXvBJXrfEGYSx7P7LC8hnUYGK70X6xQXUvXeNyBDDcsiQXpG2m3G9vxowaEuA==}
|
resolution: {integrity: sha512-uxQ6YqGdE4hgDKNf7hUiPXOdtkXvBJXrfEGYSx7P7LC8hnUYGK70X6xQXUvXeNyBDDcsiQXpG2m3G9vxowaEuA==}
|
||||||
engines: {node: '>= 18.12.0'}
|
engines: {node: '>= 18.12.0'}
|
||||||
@@ -8990,6 +9152,14 @@ packages:
|
|||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
|
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:
|
whatwg-url@5.0.0:
|
||||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||||
|
|
||||||
@@ -9093,6 +9263,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==}
|
resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==}
|
||||||
engines: {node: '>=20'}
|
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:
|
xtend@4.0.2:
|
||||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||||
engines: {node: '>=0.4'}
|
engines: {node: '>=0.4'}
|
||||||
@@ -9169,6 +9346,8 @@ packages:
|
|||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
|
'@acemir/cssom@0.9.31': {}
|
||||||
|
|
||||||
'@algolia/abtesting@1.12.2':
|
'@algolia/abtesting@1.12.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@algolia/client-common': 5.46.2
|
'@algolia/client-common': 5.46.2
|
||||||
@@ -9265,13 +9444,13 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- chokidar
|
- 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:
|
dependencies:
|
||||||
'@ampproject/remapping': 2.3.0
|
'@ampproject/remapping': 2.3.0
|
||||||
'@angular-devkit/architect': 0.2101.2(chokidar@5.0.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/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-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)
|
'@angular/compiler-cli': 21.1.2(@angular/compiler@21.1.2)(typescript@5.9.3)
|
||||||
'@babel/core': 7.28.5
|
'@babel/core': 7.28.5
|
||||||
'@babel/generator': 7.28.5
|
'@babel/generator': 7.28.5
|
||||||
@@ -9446,7 +9625,7 @@ snapshots:
|
|||||||
eslint: 9.39.2(jiti@2.6.1)
|
eslint: 9.39.2(jiti@2.6.1)
|
||||||
typescript: 5.9.3
|
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:
|
dependencies:
|
||||||
'@ampproject/remapping': 2.3.0
|
'@ampproject/remapping': 2.3.0
|
||||||
'@angular-devkit/architect': 0.2101.2(chokidar@5.0.0)
|
'@angular-devkit/architect': 0.2101.2(chokidar@5.0.0)
|
||||||
@@ -9485,7 +9664,7 @@ snapshots:
|
|||||||
less: 4.4.2
|
less: 4.4.2
|
||||||
lmdb: 3.4.4
|
lmdb: 3.4.4
|
||||||
postcss: 8.5.6
|
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:
|
transitivePeerDependencies:
|
||||||
- '@types/node'
|
- '@types/node'
|
||||||
- chokidar
|
- chokidar
|
||||||
@@ -9499,7 +9678,7 @@ snapshots:
|
|||||||
- tsx
|
- tsx
|
||||||
- yaml
|
- 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:
|
dependencies:
|
||||||
'@ampproject/remapping': 2.3.0
|
'@ampproject/remapping': 2.3.0
|
||||||
'@angular-devkit/architect': 0.2101.2(chokidar@5.0.0)
|
'@angular-devkit/architect': 0.2101.2(chokidar@5.0.0)
|
||||||
@@ -9538,7 +9717,7 @@ snapshots:
|
|||||||
less: 4.5.1
|
less: 4.5.1
|
||||||
lmdb: 3.4.4
|
lmdb: 3.4.4
|
||||||
postcss: 8.5.6
|
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:
|
transitivePeerDependencies:
|
||||||
- '@types/node'
|
- '@types/node'
|
||||||
- chokidar
|
- chokidar
|
||||||
@@ -9639,6 +9818,24 @@ snapshots:
|
|||||||
rxjs: 7.8.2
|
rxjs: 7.8.2
|
||||||
tslib: 2.8.1
|
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':
|
'@babel/code-frame@7.29.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/helper-validator-identifier': 7.28.5
|
'@babel/helper-validator-identifier': 7.28.5
|
||||||
@@ -11022,6 +11219,28 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/trace-mapping': 0.3.9
|
'@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':
|
'@cypress/request@3.0.10':
|
||||||
dependencies:
|
dependencies:
|
||||||
aws-sign2: 0.7.0
|
aws-sign2: 0.7.0
|
||||||
@@ -11290,6 +11509,8 @@ snapshots:
|
|||||||
'@eslint/core': 0.17.0
|
'@eslint/core': 0.17.0
|
||||||
levn: 0.4.1
|
levn: 0.4.1
|
||||||
|
|
||||||
|
'@exodus/bytes@1.11.0': {}
|
||||||
|
|
||||||
'@fastify/accept-negotiator@2.0.1': {}
|
'@fastify/accept-negotiator@2.0.1': {}
|
||||||
|
|
||||||
'@fastify/ajv-compiler@4.0.5':
|
'@fastify/ajv-compiler@4.0.5':
|
||||||
@@ -12416,7 +12637,7 @@ snapshots:
|
|||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
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)
|
||||||
|
|
||||||
'@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:
|
dependencies:
|
||||||
'@eslint-community/eslint-plugin-eslint-comments': 4.4.1(eslint@9.39.2(jiti@2.6.1))
|
'@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))
|
'@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)
|
'@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/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)
|
'@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: 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-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))
|
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
|
playwright: 1.58.1
|
||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
typescript: 5.9.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)
|
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:
|
transitivePeerDependencies:
|
||||||
- '@typescript-eslint/utils'
|
- '@typescript-eslint/utils'
|
||||||
- eslint-import-resolver-typescript
|
- eslint-import-resolver-typescript
|
||||||
@@ -12518,7 +12739,7 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@nx/angular@22.4.4(ec507f70e00b67864c7695431dce0b70)':
|
'@nx/angular@22.4.4(53644491fdd75e214444a711ff7c7d5c)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@angular-devkit/core': 21.1.2(chokidar@5.0.0)
|
'@angular-devkit/core': 21.1.2(chokidar@5.0.0)
|
||||||
'@angular-devkit/schematics': 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
|
tslib: 2.8.1
|
||||||
webpack-merge: 5.10.0
|
webpack-merge: 5.10.0
|
||||||
optionalDependencies:
|
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-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)(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:
|
transitivePeerDependencies:
|
||||||
- '@babel/traverse'
|
- '@babel/traverse'
|
||||||
- '@module-federation/enhanced'
|
- '@module-federation/enhanced'
|
||||||
@@ -13648,6 +13869,10 @@ snapshots:
|
|||||||
|
|
||||||
'@types/deep-eql@4.0.2': {}
|
'@types/deep-eql@4.0.2': {}
|
||||||
|
|
||||||
|
'@types/dompurify@3.2.0':
|
||||||
|
dependencies:
|
||||||
|
dompurify: 3.3.1
|
||||||
|
|
||||||
'@types/eslint-scope@3.7.7':
|
'@types/eslint-scope@3.7.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/eslint': 9.6.1
|
'@types/eslint': 9.6.1
|
||||||
@@ -13701,6 +13926,12 @@ snapshots:
|
|||||||
expect: 30.2.0
|
expect: 30.2.0
|
||||||
pretty-format: 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/json-schema@7.0.15': {}
|
||||||
|
|
||||||
'@types/json5@0.0.29': {}
|
'@types/json5@0.0.29': {}
|
||||||
@@ -13765,6 +13996,11 @@ snapshots:
|
|||||||
|
|
||||||
'@types/tmp@0.2.6': {}
|
'@types/tmp@0.2.6': {}
|
||||||
|
|
||||||
|
'@types/tough-cookie@4.0.5': {}
|
||||||
|
|
||||||
|
'@types/trusted-types@2.0.7':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@types/ws@8.18.1':
|
'@types/ws@8.18.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.19.9
|
'@types/node': 20.19.9
|
||||||
@@ -14051,13 +14287,13 @@ snapshots:
|
|||||||
dependencies:
|
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)
|
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:
|
dependencies:
|
||||||
'@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
'@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)
|
eslint: 9.39.2(jiti@2.6.1)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.9.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)
|
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':
|
'@vitest/expect@4.0.18':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -14666,6 +14902,10 @@ snapshots:
|
|||||||
postcss: 8.5.6
|
postcss: 8.5.6
|
||||||
postcss-media-query-parser: 0.2.3
|
postcss-media-query-parser: 0.2.3
|
||||||
|
|
||||||
|
bidi-js@1.0.3:
|
||||||
|
dependencies:
|
||||||
|
require-from-string: 2.0.2
|
||||||
|
|
||||||
big.js@5.2.2: {}
|
big.js@5.2.2: {}
|
||||||
|
|
||||||
binary-extensions@2.3.0: {}
|
binary-extensions@2.3.0: {}
|
||||||
@@ -15175,6 +15415,11 @@ snapshots:
|
|||||||
mdn-data: 2.0.30
|
mdn-data: 2.0.30
|
||||||
source-map-js: 1.2.1
|
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@6.2.2: {}
|
||||||
|
|
||||||
css-what@7.0.0: {}
|
css-what@7.0.0: {}
|
||||||
@@ -15229,6 +15474,13 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
css-tree: 2.2.1
|
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:
|
cypress@15.9.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@cypress/request': 3.0.10
|
'@cypress/request': 3.0.10
|
||||||
@@ -15278,6 +15530,13 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
assert-plus: 1.0.0
|
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:
|
data-view-buffer@1.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bound: 1.0.4
|
call-bound: 1.0.4
|
||||||
@@ -15320,6 +15579,8 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
supports-color: 8.1.1
|
supports-color: 8.1.1
|
||||||
|
|
||||||
|
decimal.js@10.6.0: {}
|
||||||
|
|
||||||
dedent@1.7.1(babel-plugin-macros@3.1.0):
|
dedent@1.7.1(babel-plugin-macros@3.1.0):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
babel-plugin-macros: 3.1.0
|
babel-plugin-macros: 3.1.0
|
||||||
@@ -15415,6 +15676,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
domelementtype: 2.3.0
|
domelementtype: 2.3.0
|
||||||
|
|
||||||
|
dompurify@3.3.1:
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/trusted-types': 2.0.7
|
||||||
|
|
||||||
domutils@3.2.2:
|
domutils@3.2.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
dom-serializer: 2.0.0
|
dom-serializer: 2.0.0
|
||||||
@@ -16572,6 +16837,12 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
whatwg-encoding: 2.0.0
|
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-entities@2.6.0: {}
|
||||||
|
|
||||||
html-escaper@2.0.2: {}
|
html-escaper@2.0.2: {}
|
||||||
@@ -16880,6 +17151,8 @@ snapshots:
|
|||||||
|
|
||||||
is-plain-object@5.0.0: {}
|
is-plain-object@5.0.0: {}
|
||||||
|
|
||||||
|
is-potential-custom-element-name@1.0.1: {}
|
||||||
|
|
||||||
is-promise@4.0.0: {}
|
is-promise@4.0.0: {}
|
||||||
|
|
||||||
is-regex@1.2.1:
|
is-regex@1.2.1:
|
||||||
@@ -17380,6 +17653,32 @@ snapshots:
|
|||||||
|
|
||||||
jsdoc-type-pratt-parser@4.1.0: {}
|
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@0.5.0: {}
|
||||||
|
|
||||||
jsesc@3.1.0: {}
|
jsesc@3.1.0: {}
|
||||||
@@ -17731,12 +18030,16 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tmpl: 1.0.5
|
tmpl: 1.0.5
|
||||||
|
|
||||||
|
marked@17.0.1: {}
|
||||||
|
|
||||||
math-intrinsics@1.1.0: {}
|
math-intrinsics@1.1.0: {}
|
||||||
|
|
||||||
mdn-data@2.0.28: {}
|
mdn-data@2.0.28: {}
|
||||||
|
|
||||||
mdn-data@2.0.30: {}
|
mdn-data@2.0.30: {}
|
||||||
|
|
||||||
|
mdn-data@2.12.2: {}
|
||||||
|
|
||||||
media-typer@0.3.0: {}
|
media-typer@0.3.0: {}
|
||||||
|
|
||||||
media-typer@1.1.0: {}
|
media-typer@1.1.0: {}
|
||||||
@@ -18327,6 +18630,10 @@ snapshots:
|
|||||||
|
|
||||||
parse5@4.0.0: {}
|
parse5@4.0.0: {}
|
||||||
|
|
||||||
|
parse5@7.3.0:
|
||||||
|
dependencies:
|
||||||
|
entities: 6.0.1
|
||||||
|
|
||||||
parse5@8.0.0:
|
parse5@8.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
entities: 6.0.1
|
entities: 6.0.1
|
||||||
@@ -19188,6 +19495,10 @@ snapshots:
|
|||||||
sax@1.4.4:
|
sax@1.4.4:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
saxes@6.0.0:
|
||||||
|
dependencies:
|
||||||
|
xmlchars: 2.2.0
|
||||||
|
|
||||||
scheduler@0.27.0: {}
|
scheduler@0.27.0: {}
|
||||||
|
|
||||||
schema-utils@3.3.0:
|
schema-utils@3.3.0:
|
||||||
@@ -19699,6 +20010,8 @@ snapshots:
|
|||||||
csso: 5.0.5
|
csso: 5.0.5
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
|
|
||||||
|
symbol-tree@3.2.4: {}
|
||||||
|
|
||||||
sync-child-process@1.0.2:
|
sync-child-process@1.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
sync-message-port: 1.2.0
|
sync-message-port: 1.2.0
|
||||||
@@ -19812,10 +20125,16 @@ snapshots:
|
|||||||
|
|
||||||
tldts-core@6.1.86: {}
|
tldts-core@6.1.86: {}
|
||||||
|
|
||||||
|
tldts-core@7.0.22: {}
|
||||||
|
|
||||||
tldts@6.1.86:
|
tldts@6.1.86:
|
||||||
dependencies:
|
dependencies:
|
||||||
tldts-core: 6.1.86
|
tldts-core: 6.1.86
|
||||||
|
|
||||||
|
tldts@7.0.22:
|
||||||
|
dependencies:
|
||||||
|
tldts-core: 7.0.22
|
||||||
|
|
||||||
tmp@0.2.5: {}
|
tmp@0.2.5: {}
|
||||||
|
|
||||||
tmpl@1.0.5: {}
|
tmpl@1.0.5: {}
|
||||||
@@ -19832,8 +20151,16 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tldts: 6.1.86
|
tldts: 6.1.86
|
||||||
|
|
||||||
|
tough-cookie@6.0.0:
|
||||||
|
dependencies:
|
||||||
|
tldts: 7.0.22
|
||||||
|
|
||||||
tr46@0.0.3: {}
|
tr46@0.0.3: {}
|
||||||
|
|
||||||
|
tr46@6.0.0:
|
||||||
|
dependencies:
|
||||||
|
punycode: 2.3.1
|
||||||
|
|
||||||
tree-dump@1.1.0(tslib@2.8.1):
|
tree-dump@1.1.0(tslib@2.8.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
@@ -20047,6 +20374,8 @@ snapshots:
|
|||||||
|
|
||||||
undici@7.18.2: {}
|
undici@7.18.2: {}
|
||||||
|
|
||||||
|
undici@7.20.0: {}
|
||||||
|
|
||||||
unicode-canonical-property-names-ecmascript@2.0.1: {}
|
unicode-canonical-property-names-ecmascript@2.0.1: {}
|
||||||
|
|
||||||
unicode-match-property-ecmascript@2.0.0:
|
unicode-match-property-ecmascript@2.0.0:
|
||||||
@@ -20202,7 +20531,7 @@ snapshots:
|
|||||||
terser: 5.44.1
|
terser: 5.44.1
|
||||||
yaml: 2.8.2
|
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:
|
dependencies:
|
||||||
'@vitest/expect': 4.0.18
|
'@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))
|
'@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
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 20.19.9
|
'@types/node': 20.19.9
|
||||||
|
jsdom: 28.0.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- jiti
|
- jiti
|
||||||
- less
|
- less
|
||||||
@@ -20239,6 +20569,10 @@ snapshots:
|
|||||||
- tsx
|
- tsx
|
||||||
- yaml
|
- yaml
|
||||||
|
|
||||||
|
w3c-xmlserializer@5.0.0:
|
||||||
|
dependencies:
|
||||||
|
xml-name-validator: 5.0.0
|
||||||
|
|
||||||
walker@1.0.8:
|
walker@1.0.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
makeerror: 1.0.12
|
makeerror: 1.0.12
|
||||||
@@ -20266,6 +20600,8 @@ snapshots:
|
|||||||
|
|
||||||
webidl-conversions@3.0.1: {}
|
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)):
|
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:
|
dependencies:
|
||||||
colorette: 2.0.20
|
colorette: 2.0.20
|
||||||
@@ -20472,6 +20808,16 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
iconv-lite: 0.6.3
|
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:
|
whatwg-url@5.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
tr46: 0.0.3
|
tr46: 0.0.3
|
||||||
@@ -20585,6 +20931,10 @@ snapshots:
|
|||||||
is-wsl: 3.1.0
|
is-wsl: 3.1.0
|
||||||
powershell-utils: 0.1.0
|
powershell-utils: 0.1.0
|
||||||
|
|
||||||
|
xml-name-validator@5.0.0: {}
|
||||||
|
|
||||||
|
xmlchars@2.2.0: {}
|
||||||
|
|
||||||
xtend@4.0.2: {}
|
xtend@4.0.2: {}
|
||||||
|
|
||||||
y18n@5.0.8: {}
|
y18n@5.0.8: {}
|
||||||
|
|||||||
@@ -7,3 +7,4 @@ export * from "./lib/game.types";
|
|||||||
export * from "./lib/book.types";
|
export * from "./lib/book.types";
|
||||||
export * from "./lib/music.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