feat: Multiple Features, Accessibility, Security, and UX Improvements #59

Merged
naomi merged 27 commits from feat/polish into main 2026-02-20 01:51:25 -08:00
9 changed files with 169 additions and 0 deletions
Showing only changes of commit 5a59afaa5e - Show all commits
+4
View File
@@ -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[]
+11
View File
@@ -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).
*/ */
+9
View File
@@ -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",
+22
View File
@@ -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.
*/ */
+23
View File
@@ -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
+4
View File
@@ -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> {
+4
View File
@@ -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> {