fix(web,api): UI fiche livre et lecteur — progression, locators EPUB/PDF, styles

- web: BookPage/ReaderPage enrichis, sauvegarde de progression lecteur
- web: locators EPUB/PDF normalisés (+ tests), EpubReader/PdfReader ajustés
- web: nginx et service worker ajustés
- api: progress — corrections contrôleur/service

Refs: #13
This commit is contained in:
Git Agent
2026-08-23 10:32:44 +02:00
parent e94524f119
commit 85b56ef3b2
12 changed files with 254 additions and 54 deletions

View File

@ -1,27 +1,55 @@
import { useEffect, useState } from "react";
import { BookOpen, LibraryBig } from "lucide-react";
import { BookOpen, LibraryBig, RotateCcw } from "lucide-react";
import type { BookDto, ProgressDto } from "@readabook/shared";
import { api } from "../api/client";
import { FormatPill, LoadingState, Meter, Panel } from "../components/ui";
import { api, getApiFallback } from "../api/client";
import { EmptyState, ErrorRibbon, FormatPill, LoadingState, Meter, Panel } from "../components/ui";
import { navigate } from "../router";
export function BookPage({ bookId }: { bookId: number }) {
const [book, setBook] = useState<BookDto | null>(null);
const [progress, setProgress] = useState<ProgressDto | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string>();
async function loadBook() {
setLoading(true);
setError(undefined);
try {
const [nextBook, nextProgress] = await Promise.all([api.book(bookId), api.progress(bookId)]);
setBook(nextBook);
setProgress(nextProgress);
} catch (loadError) {
const fallback = getApiFallback<BookDto>(loadError);
setBook(fallback ?? null);
setProgress(null);
setError(fallback ? "Fiche chargee en mode secours." : "Fiche livre indisponible.");
} finally {
setLoading(false);
}
}
useEffect(() => {
let alive = true;
Promise.all([api.book(bookId), api.progress(bookId)]).then(([nextBook, nextProgress]) => {
loadBook().finally(() => {
if (!alive) return;
setBook(nextBook);
setProgress(nextProgress);
});
return () => {
alive = false;
};
}, [bookId]);
if (!book) return <LoadingState />;
if (loading && !book) return <LoadingState />;
if (!book) {
return (
<Panel>
<EmptyState title="Fiche introuvable" detail={error ?? "Le catalogue n'a pas renvoye cet ouvrage."} />
<button className="ghost-button" onClick={() => void loadBook()}>
<RotateCcw size={17} />
Reessayer
</button>
</Panel>
);
}
return (
<div className="book-detail">
@ -29,6 +57,7 @@ export function BookPage({ bookId }: { bookId: number }) {
{book.coverPath ? <img src={api.bookCoverUrl(book.id)} alt="" /> : <BookOpen size={72} />}
</section>
<Panel className="book-facts">
<ErrorRibbon message={error} />
<div className="book-card-meta">
<FormatPill format={book.format} />
<span>{book.language ?? "langue inconnue"}</span>
@ -46,6 +75,12 @@ export function BookPage({ bookId }: { bookId: number }) {
<LibraryBig size={18} />
Rayon
</button>
{error && (
<button className="ghost-button" onClick={() => void loadBook()}>
<RotateCcw size={18} />
Reessayer
</button>
)}
</div>
</Panel>
</div>