- 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>
81 lines
2.7 KiB
TypeScript
81 lines
2.7 KiB
TypeScript
import { FormEvent, useEffect, useState } from "react";
|
|
import { Search } from "lucide-react";
|
|
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>();
|
|
|
|
async function loadBooks(nextQuery = query) {
|
|
setLoading(true);
|
|
setError(undefined);
|
|
try {
|
|
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 ?? []);
|
|
setError(fallback ? "Catalogue indisponible, affichage de secours." : "Recherche indisponible.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
const fallbackCoverLoading = hasActiveCoverWork(jobs);
|
|
|
|
useEffect(() => {
|
|
void loadBooks("");
|
|
}, []);
|
|
|
|
async function submit(event: FormEvent) {
|
|
event.preventDefault();
|
|
await loadBooks(query);
|
|
}
|
|
|
|
return (
|
|
<div className="page-grid">
|
|
<Panel className="span-3">
|
|
<form className="search-form" onSubmit={submit}>
|
|
<Search size={20} />
|
|
<input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Titre, auteur, ISBN" />
|
|
<button className="primary-button" type="submit">
|
|
Chercher
|
|
</button>
|
|
</form>
|
|
{error && (
|
|
<div className="retry-row">
|
|
<span>{error}</span>
|
|
<button className="ghost-button" onClick={() => void loadBooks(query)}>
|
|
Reessayer
|
|
</button>
|
|
</div>
|
|
)}
|
|
</Panel>
|
|
{loading && !books.length ? (
|
|
<LoadingState />
|
|
) : books.length ? (
|
|
<section className="book-grid span-3">
|
|
{books.map((book) => (
|
|
<BookCard key={book.id} book={book} coverLoading={isBookCoverUpdating(book, fallbackCoverLoading)} />
|
|
))}
|
|
</section>
|
|
) : (
|
|
<Panel className="span-3">
|
|
<EmptyState
|
|
title={loading ? "Recherche en cours" : "Aucun specimen"}
|
|
detail={loading ? "Le formulaire reste disponible." : "Essaie un autre terme ou relance l'indexation."}
|
|
/>
|
|
</Panel>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|