chore: initial commit — monorepo ReadaBook (API NestJS, web PWA, Docker)
This commit is contained in:
87
apps/web/src/pages/HomePage.tsx
Normal file
87
apps/web/src/pages/HomePage.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { LibraryBig, ScanLine } from "lucide-react";
|
||||
import { api } from "../api/client";
|
||||
import type { DashboardData } from "../api/types";
|
||||
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 />;
|
||||
|
||||
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) => (
|
||||
<button key={item.book.id} className="continue-tile" onClick={() => navigate(`/reader/${item.book.id}`)}>
|
||||
<strong>{item.book.title}</strong>
|
||||
<span>{item.book.author ?? "Auteur inconnu"}</span>
|
||||
<Meter value={item.progress.percent} />
|
||||
</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} />
|
||||
))}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user