feat(api,web): modélisation série + volume et parcours de série

- Table series + colonnes seriesId/volumeNumber/volumeLabel sur les livres,
  extraction souple des numéros (T03, 003, etc.) via extract-series-volume
- Contrôleur et page de série, badge volume sur les cartes de livre,
  styles associés (fond/clarté série, ajustements globaux)
- Statuts de scan/enrichissement et provenance des métadonnées en base

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Git Agent
2026-08-24 09:11:10 +02:00
parent 34159e5e9b
commit 93bb40a8cd
24 changed files with 1481 additions and 76 deletions

View File

@ -1,19 +1,22 @@
import { useEffect, useState } from "react";
import type { BookDto, LibraryDto } from "@readabook/shared";
import type { BookDto, JobDto, LibraryDto } from "@readabook/shared";
import { api } from "../api/client";
import { hasActiveCoverWork, isBookCoverUpdating } from "../api/types";
import { BookCard } from "../components/BookCard";
import { EmptyState, LoadingState, Panel } from "../components/ui";
export function LibraryPage({ libraryId }: { libraryId: number }) {
const [books, setBooks] = useState<BookDto[] | null>(null);
const [libraries, setLibraries] = useState<LibraryDto[]>([]);
const [jobs, setJobs] = useState<JobDto[]>([]);
useEffect(() => {
let alive = true;
Promise.all([api.books({ libraryId }), api.libraries()]).then(([nextBooks, nextLibraries]) => {
Promise.all([api.books({ libraryId }), api.libraries(), api.jobs().catch(() => [])]).then(([nextBooks, nextLibraries, nextJobs]) => {
if (!alive) return;
setBooks(nextBooks);
setLibraries(nextLibraries);
setJobs(nextJobs);
});
return () => {
alive = false;
@ -22,6 +25,7 @@ export function LibraryPage({ libraryId }: { libraryId: number }) {
if (!books) return <LoadingState />;
const library = libraries.find((item) => item.id === libraryId);
const fallbackCoverLoading = hasActiveCoverWork(jobs);
return (
<div className="page-grid">
@ -37,7 +41,7 @@ export function LibraryPage({ libraryId }: { libraryId: number }) {
{books.length ? (
<section className="book-grid span-3">
{books.map((book) => (
<BookCard key={book.id} book={book} />
<BookCard key={book.id} book={book} coverLoading={isBookCoverUpdating(book, fallbackCoverLoading)} />
))}
</section>
) : (