- 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>
102 lines
4.1 KiB
TypeScript
102 lines
4.1 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { BookOpen, LibraryBig, ScanLine } from "lucide-react";
|
|
import { api } from "../api/client";
|
|
import { hasActiveCoverWork, isBookCoverUpdating, type DashboardData } from "../api/types";
|
|
import { bookDisplayTitle, bookMetadataState, bookMetadataStateLabel, bookVolumeLabel, displayPublishedDate } from "../book/metadata";
|
|
import { BookCard } from "../components/BookCard";
|
|
import { EmptyState, LoadingState, Meter, Panel } from "../components/ui";
|
|
import { navigate } from "../router";
|
|
|
|
export function HomePage() {
|
|
const [state, setState] = useState<DashboardData | null>(null);
|
|
const [fallback, setFallback] = useState(false);
|
|
|
|
useEffect(() => {
|
|
let alive = true;
|
|
Promise.all([api.books(), api.continueReading(), api.libraries(), api.jobs()])
|
|
.then(([books, continueReading, libraries, jobs]) => {
|
|
if (!alive) return;
|
|
setFallback(books.some((book) => book.filePath.startsWith("/library/")) && jobs.length === 1);
|
|
setState({ books, continueReading, libraries, jobs });
|
|
})
|
|
.catch(() => {
|
|
if (alive) setState({ books: [], continueReading: [], libraries: [], jobs: [] });
|
|
});
|
|
return () => {
|
|
alive = false;
|
|
};
|
|
}, []);
|
|
|
|
if (!state) return <LoadingState />;
|
|
const fallbackCoverLoading = hasActiveCoverWork(state.jobs);
|
|
|
|
return (
|
|
<div className="page-grid">
|
|
<section className="hero-band">
|
|
<div>
|
|
<p>Cabinet de curiosites numerique</p>
|
|
<h1>Ouvrir, classer, reprendre.</h1>
|
|
<span>{fallback ? "API absente ou incomplete : specimens de demonstration actifs." : "Catalogue branche sur le serveur local."}</span>
|
|
</div>
|
|
<button className="primary-button" onClick={() => navigate("/search")}>
|
|
<ScanLine size={18} />
|
|
Explorer
|
|
</button>
|
|
</section>
|
|
|
|
<Panel className="span-2">
|
|
<div className="section-heading">
|
|
<h2>Reprise de lecture</h2>
|
|
<span>{state.continueReading.length} traces</span>
|
|
</div>
|
|
{state.continueReading.length ? (
|
|
<div className="continue-grid">
|
|
{state.continueReading.map((item) => {
|
|
const metadataState = bookMetadataState(item.book);
|
|
const publishedDate = displayPublishedDate(item.book.publishedDate);
|
|
const volumeLabel = bookVolumeLabel(item.book);
|
|
return (
|
|
<button key={item.book.id} className="continue-tile" onClick={() => navigate(`/reader/${item.book.id}`)}>
|
|
<span className="continue-cover" aria-hidden="true">
|
|
{item.book.coverPath ? <img src={api.bookCoverUrl(item.book.id)} alt="" /> : <BookOpen size={22} />}
|
|
</span>
|
|
<span className="continue-copy">
|
|
<strong>{bookDisplayTitle(item.book)}</strong>
|
|
<span>{item.book.author ?? "Auteur inconnu"}</span>
|
|
{(volumeLabel || publishedDate) && <span>{[volumeLabel, publishedDate].filter(Boolean).join(" · ")}</span>}
|
|
<span className={`metadata-pill metadata-${metadataState}`}>{bookMetadataStateLabel(item.book)}</span>
|
|
<Meter value={item.progress.percent} />
|
|
</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
<EmptyState title="Aucune trace" detail="Les lectures reprises apparaitront ici." />
|
|
)}
|
|
</Panel>
|
|
|
|
<Panel>
|
|
<div className="section-heading">
|
|
<h2>Bibliotheques</h2>
|
|
<LibraryBig size={20} />
|
|
</div>
|
|
<div className="library-list">
|
|
{state.libraries.map((library) => (
|
|
<button key={library.id} onClick={() => navigate(`/library/${library.id}`)}>
|
|
<strong>{library.name}</strong>
|
|
<span>{library.path}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</Panel>
|
|
|
|
<section className="book-grid span-3">
|
|
{state.books.map((book) => (
|
|
<BookCard key={book.id} book={book} coverLoading={isBookCoverUpdating(book, fallbackCoverLoading)} />
|
|
))}
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|