merge: feature/45-home-redesign dans develop (refonte sobre de l'accueil : cartes simplifiées et livres en cours limités à 3 visibles — évolution #45)
This commit is contained in:
@ -1,10 +1,9 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { BookOpen, LibraryBig, ScanLine } from "lucide-react";
|
import { BookOpen, LibraryBig, ScanLine } from "lucide-react";
|
||||||
import { api } from "../api/client";
|
import { api } from "../api/client";
|
||||||
import { hasActiveCoverWork, isBookCoverUpdating, type DashboardData } from "../api/types";
|
import type { DashboardData } from "../api/types";
|
||||||
import { bookDisplayTitle, bookMetadataState, bookMetadataStateLabel, bookVolumeLabel, displayPublishedDate } from "../book/metadata";
|
import { bookDisplayTitle, bookVolumeLabel } from "../book/metadata";
|
||||||
import { BookCard } from "../components/BookCard";
|
import { EmptyState, LoadingState, Panel } from "../components/ui";
|
||||||
import { EmptyState, LoadingState, Meter, Panel } from "../components/ui";
|
|
||||||
import { navigate } from "../router";
|
import { navigate } from "../router";
|
||||||
|
|
||||||
export function HomePage() {
|
export function HomePage() {
|
||||||
@ -28,14 +27,29 @@ export function HomePage() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!state) return <LoadingState />;
|
if (!state) return <LoadingState />;
|
||||||
const fallbackCoverLoading = hasActiveCoverWork(state.jobs);
|
|
||||||
|
const renderHomeBook = (item: DashboardData["books"][number], className = "home-book-card") => {
|
||||||
|
const volumeLabel = bookVolumeLabel(item);
|
||||||
|
return (
|
||||||
|
<button key={item.id} className={className} onClick={() => navigate(`/book/${item.id}`)}>
|
||||||
|
<span className="home-book-cover" aria-hidden="true">
|
||||||
|
{item.coverPath ? <img src={api.bookCoverUrl(item.id)} alt="" /> : <BookOpen size={24} />}
|
||||||
|
</span>
|
||||||
|
<span className="home-book-copy">
|
||||||
|
{volumeLabel && <span className="home-book-volume">{volumeLabel}</span>}
|
||||||
|
<strong>{bookDisplayTitle(item)}</strong>
|
||||||
|
<span>{item.author ?? "Auteur inconnu"}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-grid">
|
<div className="page-grid home-page">
|
||||||
<section className="hero-band">
|
<section className="hero-band home-hero">
|
||||||
<div>
|
<div>
|
||||||
<p>Cabinet de curiosites numerique</p>
|
<p>Bibliotheque personnelle</p>
|
||||||
<h1>Ouvrir, classer, reprendre.</h1>
|
<h1>Reprendre la lecture.</h1>
|
||||||
<span>{fallback ? "API absente ou incomplete : specimens de demonstration actifs." : "Catalogue branche sur le serveur local."}</span>
|
<span>{fallback ? "API absente ou incomplete : specimens de demonstration actifs." : "Catalogue branche sur le serveur local."}</span>
|
||||||
</div>
|
</div>
|
||||||
<button className="primary-button" onClick={() => navigate("/search")}>
|
<button className="primary-button" onClick={() => navigate("/search")}>
|
||||||
@ -46,30 +60,12 @@ export function HomePage() {
|
|||||||
|
|
||||||
<Panel className="span-2">
|
<Panel className="span-2">
|
||||||
<div className="section-heading">
|
<div className="section-heading">
|
||||||
<h2>Reprise de lecture</h2>
|
<h2>Livres en cours</h2>
|
||||||
<span>{state.continueReading.length} traces</span>
|
<span>{state.continueReading.length} lectures</span>
|
||||||
</div>
|
</div>
|
||||||
{state.continueReading.length ? (
|
{state.continueReading.length ? (
|
||||||
<div className="continue-grid">
|
<div className="continue-grid">
|
||||||
{state.continueReading.map((item) => {
|
{state.continueReading.map((item) => renderHomeBook(item.book, "home-book-card continue-tile"))}
|
||||||
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>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<EmptyState title="Aucune trace" detail="Les lectures reprises apparaitront ici." />
|
<EmptyState title="Aucune trace" detail="Les lectures reprises apparaitront ici." />
|
||||||
@ -91,10 +87,8 @@ export function HomePage() {
|
|||||||
</div>
|
</div>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
<section className="book-grid span-3">
|
<section className="home-book-grid span-3">
|
||||||
{state.books.map((book) => (
|
{state.books.map((book) => renderHomeBook(book))}
|
||||||
<BookCard key={book.id} book={book} coverLoading={isBookCoverUpdating(book, fallbackCoverLoading)} />
|
|
||||||
))}
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,17 +2,31 @@ import { readFileSync } from "node:fs";
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
describe("home progress list layout", () => {
|
describe("home progress list layout", () => {
|
||||||
it("keeps the continue reading list scrollable after six visible books", () => {
|
it("keeps the continue reading list scrollable after three visible books", () => {
|
||||||
const styles = readFileSync(new URL("../styles/app.css", import.meta.url), "utf8");
|
const styles = readFileSync(new URL("../styles/app.css", import.meta.url), "utf8");
|
||||||
const continueGrid = styles.match(/\.continue-grid\s*\{[^}]+\}/)?.[0] ?? "";
|
const continueGrid = styles.match(/\.continue-grid\s*\{[^}]+\}/)?.[0] ?? "";
|
||||||
const continueTile = styles.match(/\.continue-tile\s*\{[^}]+\}/)?.[0] ?? "";
|
const continueTile = styles.match(/\.continue-tile\s*\{[^}]+\}/)?.[0] ?? "";
|
||||||
|
|
||||||
expect(continueGrid).toContain("--continue-visible-rows: 6");
|
expect(continueGrid).toContain("--continue-visible-rows: 3");
|
||||||
expect(continueGrid).toContain("--continue-tile-block-size: 118px");
|
expect(continueGrid).toContain("--continue-tile-block-size: 112px");
|
||||||
expect(continueGrid).toContain("max-height: calc((var(--continue-tile-block-size) * var(--continue-visible-rows)) + (10px * (var(--continue-visible-rows) - 1)))");
|
expect(continueGrid).toContain("max-height: calc((var(--continue-tile-block-size) * var(--continue-visible-rows)) + (10px * (var(--continue-visible-rows) - 1)))");
|
||||||
expect(continueGrid).toContain("overflow-y: auto");
|
expect(continueGrid).toContain("overflow-y: auto");
|
||||||
expect(continueGrid).toContain("scrollbar-gutter: stable");
|
expect(continueGrid).toContain("scrollbar-gutter: stable");
|
||||||
expect(continueTile).toContain("block-size: var(--continue-tile-block-size)");
|
expect(continueTile).toContain("block-size: var(--continue-tile-block-size)");
|
||||||
expect(continueTile).toContain("overflow: hidden");
|
expect(continueTile).toContain("overflow: hidden");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps home book cards scoped and simplified", () => {
|
||||||
|
const source = readFileSync(new URL("./HomePage.tsx", import.meta.url), "utf8");
|
||||||
|
|
||||||
|
expect(source).toContain("renderHomeBook");
|
||||||
|
expect(source).toContain("home-book-card");
|
||||||
|
expect(source).not.toContain("<BookCard");
|
||||||
|
expect(source).not.toContain("FormatPill");
|
||||||
|
expect(source).not.toContain("bookMetadataState");
|
||||||
|
expect(source).not.toContain("bookMetadataStateLabel");
|
||||||
|
expect(source).not.toContain("displayPublishedDate");
|
||||||
|
expect(source).not.toContain("book.description");
|
||||||
|
expect(source).not.toContain("book.language");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -356,6 +356,24 @@ h2 {
|
|||||||
background: rgba(169, 72, 52, 0.12);
|
background: rgba(169, 72, 52, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.home-page {
|
||||||
|
align-items: start;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-hero {
|
||||||
|
min-height: 176px;
|
||||||
|
border-color: rgba(247, 240, 223, 0.13);
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(247, 240, 223, 0.08), rgba(247, 240, 223, 0.025)),
|
||||||
|
rgba(23, 17, 13, 0.74);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-hero h1 {
|
||||||
|
max-width: 740px;
|
||||||
|
font-size: clamp(2.2rem, 5vw, 4.1rem);
|
||||||
|
}
|
||||||
|
|
||||||
.continue-grid,
|
.continue-grid,
|
||||||
.library-list,
|
.library-list,
|
||||||
.job-list {
|
.job-list {
|
||||||
@ -364,8 +382,8 @@ h2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.continue-grid {
|
.continue-grid {
|
||||||
--continue-visible-rows: 6;
|
--continue-visible-rows: 3;
|
||||||
--continue-tile-block-size: 118px;
|
--continue-tile-block-size: 112px;
|
||||||
max-height: calc((var(--continue-tile-block-size) * var(--continue-visible-rows)) + (10px * (var(--continue-visible-rows) - 1)));
|
max-height: calc((var(--continue-tile-block-size) * var(--continue-visible-rows)) + (10px * (var(--continue-visible-rows) - 1)));
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
@ -416,12 +434,89 @@ h2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.continue-tile {
|
.continue-tile {
|
||||||
grid-template-columns: 52px minmax(0, 1fr);
|
|
||||||
align-items: start;
|
|
||||||
block-size: var(--continue-tile-block-size);
|
block-size: var(--continue-tile-block-size);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.home-book-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-book-card {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 62px minmax(0, 1fr);
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid rgba(247, 240, 223, 0.12);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: rgba(255, 255, 255, 0.035);
|
||||||
|
color: var(--ink);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-book-card:hover {
|
||||||
|
border-color: rgba(213, 168, 77, 0.42);
|
||||||
|
background: rgba(255, 255, 255, 0.055);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-book-cover {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 2 / 3;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(247, 240, 223, 0.14);
|
||||||
|
border-radius: 5px;
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, rgba(213, 168, 77, 0.16), rgba(45, 111, 99, 0.16)),
|
||||||
|
var(--paper-soft);
|
||||||
|
color: var(--brass);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-book-cover img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-book-copy {
|
||||||
|
display: grid;
|
||||||
|
gap: 5px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-book-copy strong,
|
||||||
|
.home-book-copy span {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-book-copy strong {
|
||||||
|
line-height: 1.15;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-book-copy > span {
|
||||||
|
color: var(--ink-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-book-volume {
|
||||||
|
width: max-content;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 3px 6px;
|
||||||
|
border: 1px solid rgba(213, 168, 77, 0.42);
|
||||||
|
border-radius: 999px;
|
||||||
|
color: #f5dfaa;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 900;
|
||||||
|
background: rgba(213, 168, 77, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
.continue-cover {
|
.continue-cover {
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
|
|||||||
Reference in New Issue
Block a user