feat: suivi de la progression de lecture par livre

- API : nouvelle table book_reading_progress (migration 0016) et module
  reading_progress.rs avec GET/PATCH /books/:id/progress (token read)
- API : GET /books/:id enrichi avec reading_status, reading_current_page,
  reading_last_read_at via LEFT JOIN
- Backoffice : badge de statut (Non lu / En cours · p.N / Lu) sur la page
  de détail et overlay sur les BookCards
- OpenSpec : change reading-progress avec proposal/design/specs/tasks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 21:53:52 +01:00
parent 278f422206
commit 648d86970f
16 changed files with 516 additions and 11 deletions

View File

@@ -3,10 +3,17 @@
import { useState } from "react";
import Image from "next/image";
import Link from "next/link";
import { BookDto } from "../../lib/api";
import { BookDto, ReadingStatus } from "../../lib/api";
const readingStatusOverlay: Record<ReadingStatus, { label: string; className: string } | null> = {
unread: null,
reading: { label: "En cours", className: "bg-amber-500/90 text-white" },
read: { label: "Lu", className: "bg-green-600/90 text-white" },
};
interface BookCardProps {
book: BookDto & { coverUrl?: string };
readingStatus?: ReadingStatus;
}
function BookImage({ src, alt }: { src: string; alt: string }) {
@@ -37,18 +44,27 @@ function BookImage({ src, alt }: { src: string; alt: string }) {
);
}
export function BookCard({ book }: BookCardProps) {
export function BookCard({ book, readingStatus }: BookCardProps) {
const coverUrl = book.coverUrl || `/api/books/${book.id}/thumbnail`;
const status = readingStatus ?? book.reading_status;
const overlay = status ? readingStatusOverlay[status] : null;
return (
<Link
href={`/books/${book.id}`}
<Link
href={`/books/${book.id}`}
className="group block bg-card rounded-xl border border-border/60 shadow-sm hover:shadow-md hover:-translate-y-1 transition-all duration-200 overflow-hidden"
>
<BookImage
src={coverUrl}
alt={`Cover of ${book.title}`}
/>
<div className="relative">
<BookImage
src={coverUrl}
alt={`Cover of ${book.title}`}
/>
{overlay && (
<span className={`absolute bottom-2 left-2 px-2 py-0.5 rounded-full text-[10px] font-bold tracking-wide ${overlay.className}`}>
{overlay.label}
</span>
)}
</div>
{/* Book Info */}
<div className="p-4">