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,13 +1,15 @@
import { FormEvent, useEffect, useState } from "react";
import { Search } from "lucide-react";
import type { BookDto } from "@readabook/shared";
import type { BookDto, JobDto } from "@readabook/shared";
import { api, getApiFallback } from "../api/client";
import { hasActiveCoverWork, isBookCoverUpdating } from "../api/types";
import { BookCard } from "../components/BookCard";
import { EmptyState, LoadingState, Panel } from "../components/ui";
export function SearchPage() {
const [query, setQuery] = useState("");
const [books, setBooks] = useState<BookDto[]>([]);
const [jobs, setJobs] = useState<JobDto[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string>();
@ -15,7 +17,9 @@ export function SearchPage() {
setLoading(true);
setError(undefined);
try {
setBooks(nextQuery.trim() ? await api.search(nextQuery.trim()) : await api.books());
const [nextBooks, nextJobs] = await Promise.all([nextQuery.trim() ? api.search(nextQuery.trim()) : api.books(), api.jobs().catch(() => [])]);
setBooks(nextBooks);
setJobs(nextJobs);
} catch (loadError) {
const fallback = getApiFallback<BookDto[]>(loadError);
setBooks(fallback ?? []);
@ -25,6 +29,8 @@ export function SearchPage() {
}
}
const fallbackCoverLoading = hasActiveCoverWork(jobs);
useEffect(() => {
void loadBooks("");
}, []);
@ -58,7 +64,7 @@ export function SearchPage() {
) : 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>
) : (