refactor: make library rendering server-first and deterministic
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 4m7s

Move library header/covers to deterministic server-side rendering, split preference controls into controlled/uncontrolled modes, and remove client cover wrapper to eliminate hydration mismatches and provider coupling on library pages.
This commit is contained in:
2026-02-28 14:06:27 +01:00
parent 26021ea907
commit 01951c806d
14 changed files with 264 additions and 154 deletions

View File

@@ -1,6 +1,5 @@
"use client";
import { CoverClient } from "./cover-client";
import { ProgressBar } from "./progress-bar";
import type { BookCoverProps } from "./cover-utils";
import { getImageUrl } from "@/lib/utils/image-url";
@@ -70,8 +69,7 @@ export function BookCover({
const currentPage = ClientOfflineBookService.getCurrentPage(book);
const totalPages = book.media.pagesCount;
const showProgress =
showProgressUi && currentPage && totalPages && currentPage > 0 && !isCompleted;
const showProgress = Boolean(showProgressUi && totalPages > 0 && currentPage > 0 && !isCompleted);
const statusInfo = getReadingStatusInfo(book, t);
const isRead = book.readProgress?.completed || false;
@@ -91,11 +89,17 @@ export function BookCover({
return (
<>
<div className={`relative w-full h-full ${isUnavailable ? "opacity-40 grayscale" : ""}`}>
<CoverClient
imageUrl={imageUrl}
<img
src={imageUrl.trim()}
alt={alt || t("books.defaultCoverAlt")}
className={className}
isCompleted={isCompleted}
loading="lazy"
className={[
"absolute inset-0 w-full h-full object-cover rounded-lg",
isCompleted ? "opacity-50" : "",
className || "",
]
.filter(Boolean)
.join(" ")}
/>
{showProgress && <ProgressBar progress={currentPage} total={totalPages} type="book" />}
{/* Badge hors ligne si non accessible */}

View File

@@ -1,48 +0,0 @@
"use client";
import { useState, useRef, useEffect } from "react";
import { cn } from "@/lib/utils";
import { ImageLoader } from "@/components/ui/image-loader";
interface CoverClientProps {
imageUrl: string;
alt: string;
className?: string;
isCompleted?: boolean;
}
export const CoverClient = ({
imageUrl,
alt,
className,
isCompleted = false,
}: CoverClientProps) => {
const imgRef = useRef<HTMLImageElement>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const img = imgRef.current;
if (img?.complete && img.naturalWidth > 0) {
setIsLoading(false);
}
}, []);
return (
<div className="relative w-full h-full">
<ImageLoader isLoading={isLoading} />
<img
ref={imgRef}
src={imageUrl}
alt={alt}
loading="lazy"
className={cn(
"absolute inset-0 w-full h-full object-cover rounded-lg",
isCompleted && "opacity-50",
className
)}
onLoad={() => setIsLoading(false)}
onError={() => setIsLoading(false)}
/>
</div>
);
};

View File

@@ -1,6 +1,3 @@
"use client";
import { CoverClient } from "./cover-client";
import { ProgressBar } from "./progress-bar";
import type { SeriesCoverProps } from "./cover-utils";
import { getImageUrl } from "@/lib/utils/image-url";
@@ -16,12 +13,23 @@ export function SeriesCover({
const readBooks = series.booksReadCount;
const totalBooks = series.booksCount;
const showProgress = showProgressUi && readBooks && totalBooks && readBooks > 0 && !isCompleted;
const showProgress = Boolean(showProgressUi && totalBooks > 0 && readBooks > 0 && !isCompleted);
return (
<div className="relative w-full h-full">
<CoverClient imageUrl={imageUrl} alt={alt} className={className} isCompleted={isCompleted} />
{showProgress && <ProgressBar progress={readBooks} total={totalBooks} type="series" />}
<img
src={imageUrl}
alt={alt}
loading="lazy"
className={[
"absolute inset-0 w-full h-full object-cover rounded-lg",
isCompleted ? "opacity-50" : "",
className || "",
]
.filter(Boolean)
.join(" ")}
/>
{showProgress ? <ProgressBar progress={readBooks} total={totalBooks} type="series" /> : null}
</div>
);
}