feat: add authors page to backoffice with dedicated API endpoint

Add a new GET /authors endpoint that aggregates unique authors from books
with book/series counts, pagination and search. Add author filter to
GET /books. Backoffice gets a list page with search/sort and a detail
page showing the author's series and books.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-20 11:43:22 +01:00
parent fe5de3d5c1
commit 4ad6d57271
12 changed files with 511 additions and 6 deletions

View File

@@ -284,12 +284,14 @@ export async function fetchBooks(
limit: number = 50,
readingStatus?: string,
sort?: string,
author?: string,
): Promise<BooksPageDto> {
const params = new URLSearchParams();
if (libraryId) params.set("library_id", libraryId);
if (series) params.set("series", series);
if (readingStatus) params.set("reading_status", readingStatus);
if (sort) params.set("sort", sort);
if (author) params.set("author", author);
params.set("page", page.toString());
params.set("limit", limit.toString());
@@ -552,6 +554,38 @@ export async function fetchStats() {
return apiFetch<StatsResponse>("/stats");
}
// ---------------------------------------------------------------------------
// Authors
// ---------------------------------------------------------------------------
export type AuthorDto = {
name: string;
book_count: number;
series_count: number;
};
export type AuthorsPageDto = {
items: AuthorDto[];
total: number;
page: number;
limit: number;
};
export async function fetchAuthors(
q?: string,
page: number = 1,
limit: number = 20,
sort?: string,
): Promise<AuthorsPageDto> {
const params = new URLSearchParams();
if (q) params.set("q", q);
if (sort) params.set("sort", sort);
params.set("page", page.toString());
params.set("limit", limit.toString());
return apiFetch<AuthorsPageDto>(`/authors?${params.toString()}`);
}
export type UpdateBookRequest = {
title: string;
author: string | null;

View File

@@ -113,6 +113,20 @@ const en: Record<TranslationKey, string> = {
"series.missingCount": "{{count}} missing",
"series.readCount": "{{read}}/{{total}} read",
// Authors page
"nav.authors": "Authors",
"authors.title": "Authors",
"authors.searchPlaceholder": "Search by author name...",
"authors.bookCount": "{{count}} book{{plural}}",
"authors.seriesCount": "{{count}} serie{{plural}}",
"authors.noResults": "No authors found matching your filters",
"authors.noAuthors": "No authors available",
"authors.matchingQuery": "matching",
"authors.sortName": "Name",
"authors.sortBooks": "Book count",
"authors.booksBy": "Books by {{name}}",
"authors.seriesBy": "Series by {{name}}",
// Libraries page
"libraries.title": "Libraries",
"libraries.addLibrary": "Add a library",

View File

@@ -111,6 +111,20 @@ const fr = {
"series.missingCount": "{{count}} manquant{{plural}}",
"series.readCount": "{{read}}/{{total}} lu{{plural}}",
// Authors page
"nav.authors": "Auteurs",
"authors.title": "Auteurs",
"authors.searchPlaceholder": "Rechercher par nom d'auteur...",
"authors.bookCount": "{{count}} livre{{plural}}",
"authors.seriesCount": "{{count}} série{{plural}}",
"authors.noResults": "Aucun auteur trouvé correspondant à vos filtres",
"authors.noAuthors": "Aucun auteur disponible",
"authors.matchingQuery": "correspondant à",
"authors.sortName": "Nom",
"authors.sortBooks": "Nombre de livres",
"authors.booksBy": "Livres de {{name}}",
"authors.seriesBy": "Séries de {{name}}",
// Libraries page
"libraries.title": "Bibliothèques",
"libraries.addLibrary": "Ajouter une bibliothèque",