Files
ReadaBook/apps/web/src/pages/SearchPage.tsx
Git Agent 93bb40a8cd 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>
2026-08-24 09:11:10 +02:00

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>
);
}