generated from nhcarrigan/template
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 <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,8 @@ model Game {
|
|||||||
coverImage String?
|
coverImage String?
|
||||||
tags String[]
|
tags String[]
|
||||||
links Link[]
|
links Link[]
|
||||||
|
series String?
|
||||||
|
seriesOrder Int? @db.Int
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
comments Comment[]
|
comments Comment[]
|
||||||
@@ -58,6 +60,8 @@ model Book {
|
|||||||
coverImage String?
|
coverImage String?
|
||||||
tags String[]
|
tags String[]
|
||||||
links Link[]
|
links Link[]
|
||||||
|
series String?
|
||||||
|
seriesOrder Int? @db.Int
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
comments Comment[]
|
comments Comment[]
|
||||||
|
|||||||
@@ -24,6 +24,17 @@ const booksRoutes: FastifyPluginAsync = async (app) => {
|
|||||||
return bookService.getAllBooks();
|
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).
|
* Get single book by ID (public route).
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -22,6 +22,15 @@ const gamesRoutes: FastifyPluginAsync = async (app) => {
|
|||||||
return gameService.getAllGames();
|
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)
|
// Get single game (public route)
|
||||||
app.get<{ Params: { id: string }; Reply: Game | null }>(
|
app.get<{ Params: { id: string }; Reply: Game | null }>(
|
||||||
"/:id",
|
"/:id",
|
||||||
|
|||||||
@@ -56,6 +56,28 @@ export class BookService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all books in a series, ordered by seriesOrder.
|
||||||
|
*/
|
||||||
|
async getBooksBySeries(seriesName: string): Promise<Book[]> {
|
||||||
|
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.
|
* Create new book.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -58,6 +58,29 @@ export class GameService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all games in a series, ordered by seriesOrder.
|
||||||
|
*/
|
||||||
|
async getGamesBySeries(seriesName: string): Promise<Game[]> {
|
||||||
|
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.
|
* Create new game.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -127,6 +127,29 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEnti
|
|||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="series">Series (optional)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="series"
|
||||||
|
[(ngModel)]="newBook.series"
|
||||||
|
name="series"
|
||||||
|
placeholder="e.g., Harry Potter"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="seriesOrder">Series Order (optional)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="seriesOrder"
|
||||||
|
[(ngModel)]="newBook.seriesOrder"
|
||||||
|
name="seriesOrder"
|
||||||
|
min="1"
|
||||||
|
placeholder="Order in series"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="coverImage">Cover Image (max 500KB)</label>
|
<label for="coverImage">Cover Image (max 500KB)</label>
|
||||||
<input
|
<input
|
||||||
@@ -289,6 +312,29 @@ import { Book, BookStatus, CreateBookDto, UpdateBookDto, Comment, SuggestionEnti
|
|||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-series">Series (optional)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="edit-series"
|
||||||
|
[(ngModel)]="editBook.series"
|
||||||
|
name="series"
|
||||||
|
placeholder="e.g., Harry Potter"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-seriesOrder">Series Order (optional)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="edit-seriesOrder"
|
||||||
|
[(ngModel)]="editBook.seriesOrder"
|
||||||
|
name="seriesOrder"
|
||||||
|
min="1"
|
||||||
|
placeholder="Order in series"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="edit-coverImage">Cover Image (max 500KB)</label>
|
<label for="edit-coverImage">Cover Image (max 500KB)</label>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -115,6 +115,29 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEnti
|
|||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="series">Series (optional)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="series"
|
||||||
|
[(ngModel)]="newGame.series"
|
||||||
|
name="series"
|
||||||
|
placeholder="e.g., The Legend of Zelda"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="seriesOrder">Series Order (optional)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="seriesOrder"
|
||||||
|
[(ngModel)]="newGame.seriesOrder"
|
||||||
|
name="seriesOrder"
|
||||||
|
min="1"
|
||||||
|
placeholder="Order in series"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="coverImage">Box Art (max 500KB)</label>
|
<label for="coverImage">Box Art (max 500KB)</label>
|
||||||
<input
|
<input
|
||||||
@@ -265,6 +288,29 @@ import { Game, GameStatus, CreateGameDto, UpdateGameDto, Comment, SuggestionEnti
|
|||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-series">Series (optional)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="edit-series"
|
||||||
|
[(ngModel)]="editGame.series"
|
||||||
|
name="series"
|
||||||
|
placeholder="e.g., The Legend of Zelda"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-seriesOrder">Series Order (optional)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="edit-seriesOrder"
|
||||||
|
[(ngModel)]="editGame.seriesOrder"
|
||||||
|
name="seriesOrder"
|
||||||
|
min="1"
|
||||||
|
placeholder="Order in series"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="edit-coverImage">Box Art (max 500KB)</label>
|
<label for="edit-coverImage">Box Art (max 500KB)</label>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ interface Book {
|
|||||||
coverImage?: string;
|
coverImage?: string;
|
||||||
tags: Array<string>;
|
tags: Array<string>;
|
||||||
links: Array<Link>;
|
links: Array<Link>;
|
||||||
|
series?: string;
|
||||||
|
seriesOrder?: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
@@ -43,6 +45,8 @@ interface CreateBookDto {
|
|||||||
coverImage?: string;
|
coverImage?: string;
|
||||||
tags?: Array<string>;
|
tags?: Array<string>;
|
||||||
links?: Array<Link>;
|
links?: Array<Link>;
|
||||||
|
series?: string;
|
||||||
|
seriesOrder?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateBookDto extends Partial<CreateBookDto> {
|
interface UpdateBookDto extends Partial<CreateBookDto> {
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ interface Game {
|
|||||||
coverImage?: string;
|
coverImage?: string;
|
||||||
tags: Array<string>;
|
tags: Array<string>;
|
||||||
links: Array<Link>;
|
links: Array<Link>;
|
||||||
|
series?: string;
|
||||||
|
seriesOrder?: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
@@ -42,6 +44,8 @@ interface CreateGameDto {
|
|||||||
coverImage?: string;
|
coverImage?: string;
|
||||||
tags?: Array<string>;
|
tags?: Array<string>;
|
||||||
links?: Array<Link>;
|
links?: Array<Link>;
|
||||||
|
series?: string;
|
||||||
|
seriesOrder?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UpdateGameDto extends Partial<CreateGameDto> {
|
interface UpdateGameDto extends Partial<CreateGameDto> {
|
||||||
|
|||||||
Reference in New Issue
Block a user