From 5a59afaa5e0ca85468a227d7a50571fd9a6b912e Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Thu, 19 Feb 2026 22:52:33 -0800 Subject: [PATCH] feat: add series support for books and games (#54) Implements series grouping functionality for books and games to allow tracking franchises and related items. Database changes: - Add series (optional string) and seriesOrder (optional number) fields to Book model - Add series (optional string) and seriesOrder (optional number) fields to Game model Backend changes: - Add GET /books/series/:seriesName endpoint to fetch all books in a series - Add GET /games/series/:seriesName endpoint to fetch all games in a series - Add getBooksBySeries() method to BookService (orders by seriesOrder asc) - Add getGamesBySeries() method to GameService (orders by seriesOrder asc) - Create/Update endpoints automatically handle series fields via DTOs Frontend changes: - Add series and seriesOrder input fields to "Add New Book" form - Add series and seriesOrder input fields to "Edit Book" form - Add series and seriesOrder input fields to "Add New Game" form - Add series and seriesOrder input fields to "Edit Game" form Type definitions: - Update Book and CreateBookDto interfaces with series fields - Update Game and CreateGameDto interfaces with series fields Closes #54 Co-Authored-By: Claude Sonnet 4.5 --- api/prisma/schema.prisma | 4 ++ api/src/app/routes/books/index.ts | 11 +++++ api/src/app/routes/games/index.ts | 9 ++++ api/src/app/services/book.service.ts | 22 +++++++++ api/src/app/services/game.service.ts | 23 ++++++++++ .../components/books/books-list.component.ts | 46 +++++++++++++++++++ .../components/games/games-list.component.ts | 46 +++++++++++++++++++ shared-types/src/lib/book.types.ts | 4 ++ shared-types/src/lib/game.types.ts | 4 ++ 9 files changed, 169 insertions(+) diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma index d7a77b8..7ecca71 100644 --- a/api/prisma/schema.prisma +++ b/api/prisma/schema.prisma @@ -32,6 +32,8 @@ model Game { coverImage String? tags String[] links Link[] + series String? + seriesOrder Int? @db.Int createdAt DateTime @default(now()) updatedAt DateTime @updatedAt comments Comment[] @@ -58,6 +60,8 @@ model Book { coverImage String? tags String[] links Link[] + series String? + seriesOrder Int? @db.Int createdAt DateTime @default(now()) updatedAt DateTime @updatedAt comments Comment[] diff --git a/api/src/app/routes/books/index.ts b/api/src/app/routes/books/index.ts index 2aea0fe..f6991d3 100644 --- a/api/src/app/routes/books/index.ts +++ b/api/src/app/routes/books/index.ts @@ -24,6 +24,17 @@ const booksRoutes: FastifyPluginAsync = async (app) => { return bookService.getAllBooks(); }); + /** + * Get all books in a series (public route). + */ + app.get<{ Params: { seriesName: string }; Reply: Book[] }>( + "/series/:seriesName", + async (request) => { + const { seriesName } = request.params; + return bookService.getBooksBySeries(seriesName); + } + ); + /** * Get single book by ID (public route). */ diff --git a/api/src/app/routes/games/index.ts b/api/src/app/routes/games/index.ts index 2d00886..eef98a0 100644 --- a/api/src/app/routes/games/index.ts +++ b/api/src/app/routes/games/index.ts @@ -22,6 +22,15 @@ const gamesRoutes: FastifyPluginAsync = async (app) => { return gameService.getAllGames(); }); + // Get all games in a series (public route) + app.get<{ Params: { seriesName: string }; Reply: Game[] }>( + "/series/:seriesName", + async (request) => { + const { seriesName } = request.params; + return gameService.getGamesBySeries(seriesName); + } + ); + // Get single game (public route) app.get<{ Params: { id: string }; Reply: Game | null }>( "/:id", diff --git a/api/src/app/services/book.service.ts b/api/src/app/services/book.service.ts index afb97bc..1e396af 100644 --- a/api/src/app/services/book.service.ts +++ b/api/src/app/services/book.service.ts @@ -56,6 +56,28 @@ export class BookService { }; } + /** + * Get all books in a series, ordered by seriesOrder. + */ + async getBooksBySeries(seriesName: string): Promise { + const books = await this.prisma.book.findMany({ + where: { series: seriesName }, + orderBy: { seriesOrder: "asc" }, + }); + + return books.map((book) => ({ + ...book, + status: book.status as unknown as BookStatus, + dateAdded: book.dateAdded, + dateStarted: book.dateStarted || undefined, + dateFinished: book.dateFinished || undefined, + tags: book.tags ?? [], + links: book.links ?? [], + createdAt: book.createdAt, + updatedAt: book.updatedAt, + })); + } + /** * Create new book. */ diff --git a/api/src/app/services/game.service.ts b/api/src/app/services/game.service.ts index d748c22..66a9cdf 100644 --- a/api/src/app/services/game.service.ts +++ b/api/src/app/services/game.service.ts @@ -58,6 +58,29 @@ export class GameService { }; } + /** + * Get all games in a series, ordered by seriesOrder. + */ + async getGamesBySeries(seriesName: string): Promise { + const games = await this.prisma.game.findMany({ + where: { series: seriesName }, + orderBy: { seriesOrder: "asc" }, + }); + + return games.map((game) => ({ + ...game, + status: game.status as unknown as GameStatus, + dateAdded: game.dateAdded, + dateStarted: game.dateStarted || undefined, + dateCompleted: game.dateCompleted || undefined, + dateFinished: game.dateFinished || undefined, + tags: game.tags ?? [], + links: game.links ?? [], + createdAt: game.createdAt, + updatedAt: game.updatedAt, + })); + } + /** * Create new game. */ diff --git a/apps/frontend/src/app/components/books/books-list.component.ts b/apps/frontend/src/app/components/books/books-list.component.ts index bcdbb50..ceda6ed 100644 --- a/apps/frontend/src/app/components/books/books-list.component.ts +++ b/apps/frontend/src/app/components/books/books-list.component.ts @@ -127,6 +127,29 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEnti > +
+ + +
+ +
+ + +
+
+
+ + +
+ +
+ + +
+
+
+ + +
+ +
+ + +
+
+
+ + +
+ +
+ + +
+
; links: Array; + series?: string; + seriesOrder?: number; createdAt: Date; updatedAt: Date; } @@ -43,6 +45,8 @@ interface CreateBookDto { coverImage?: string; tags?: Array; links?: Array; + series?: string; + seriesOrder?: number; } interface UpdateBookDto extends Partial { diff --git a/shared-types/src/lib/game.types.ts b/shared-types/src/lib/game.types.ts index 38949ac..4689816 100644 --- a/shared-types/src/lib/game.types.ts +++ b/shared-types/src/lib/game.types.ts @@ -27,6 +27,8 @@ interface Game { coverImage?: string; tags: Array; links: Array; + series?: string; + seriesOrder?: number; createdAt: Date; updatedAt: Date; } @@ -42,6 +44,8 @@ interface CreateGameDto { coverImage?: string; tags?: Array; links?: Array; + series?: string; + seriesOrder?: number; } interface UpdateGameDto extends Partial {