refacto(thumbnails): revew thumbnails and caching

This commit is contained in:
Julien Froidefond
2025-02-16 16:02:07 +01:00
parent 683a4821f3
commit 938e0461ae
2 changed files with 57 additions and 11 deletions

View File

@@ -2,7 +2,7 @@ import { ThumbnailProps } from "../types";
import { ImageLoader } from "@/components/ui/image-loader"; import { ImageLoader } from "@/components/ui/image-loader";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import Image from "next/image"; import Image from "next/image";
import { forwardRef, useEffect, useState } from "react"; import { forwardRef, useEffect, useState, useCallback, useRef } from "react";
export const Thumbnail = forwardRef<HTMLButtonElement, ThumbnailProps>( export const Thumbnail = forwardRef<HTMLButtonElement, ThumbnailProps>(
( (
@@ -19,23 +19,60 @@ export const Thumbnail = forwardRef<HTMLButtonElement, ThumbnailProps>(
) => { ) => {
const [imageUrl, setImageUrl] = useState<string | null>(null); const [imageUrl, setImageUrl] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);
const loadAttempts = useRef(0);
const maxAttempts = 3;
const resetLoadingState = useCallback(() => {
setIsLoading(true);
setHasError(false);
loadAttempts.current = 0;
}, []);
useEffect(() => { useEffect(() => {
if (isVisible) { try {
const url = getThumbnailUrl(pageNumber); const url = getThumbnailUrl(pageNumber);
setImageUrl(url); setImageUrl(url);
// On réinitialise le loading uniquement si la miniature n'est pas déjà chargée
if (!loadedThumbnails[pageNumber]) { if (!loadedThumbnails[pageNumber]) {
setIsLoading(true); resetLoadingState();
} else {
setIsLoading(false);
} }
} catch (error) {
console.error(`Erreur lors du chargement de la miniature ${pageNumber}:`, error);
setHasError(true);
setIsLoading(false);
} }
}, [isVisible, pageNumber, getThumbnailUrl, loadedThumbnails]); }, [pageNumber, getThumbnailUrl, loadedThumbnails, resetLoadingState]);
const handleImageLoad = () => { const handleImageLoad = useCallback(() => {
setIsLoading(false); setIsLoading(false);
setHasError(false);
if (!loadedThumbnails[pageNumber]) { if (!loadedThumbnails[pageNumber]) {
onThumbnailLoad(pageNumber); onThumbnailLoad(pageNumber);
} }
}; }, [loadedThumbnails, pageNumber, onThumbnailLoad]);
const handleImageError = useCallback(() => {
loadAttempts.current += 1;
if (loadAttempts.current < maxAttempts) {
// Réessayer avec un délai croissant
const delay = Math.min(1000 * Math.pow(2, loadAttempts.current - 1), 5000);
console.log(
`Réessai ${loadAttempts.current}/${maxAttempts} dans ${delay}ms pour la page ${pageNumber}`
);
setTimeout(() => {
setImageUrl((prev) => (prev ? `${prev}?retry=${loadAttempts.current}` : null));
}, delay);
} else {
console.error(
`Échec du chargement de l'image pour la page ${pageNumber} après ${maxAttempts} tentatives`
);
setHasError(true);
setIsLoading(false);
}
}, [pageNumber]);
return ( return (
<button <button
@@ -43,14 +80,16 @@ export const Thumbnail = forwardRef<HTMLButtonElement, ThumbnailProps>(
data-page={pageNumber} data-page={pageNumber}
id={`thumbnail-${pageNumber}`} id={`thumbnail-${pageNumber}`}
onClick={() => onPageChange(pageNumber)} onClick={() => onPageChange(pageNumber)}
className={`relative h-56 w-40 flex-shrink-0 rounded-md overflow-hidden transition-all cursor-pointer snap-center ${ className={cn(
"relative h-56 w-40 flex-shrink-0 rounded-md overflow-hidden transition-all cursor-pointer snap-center",
currentPage === pageNumber currentPage === pageNumber
? "ring-2 ring-primary scale-110 z-10" ? "ring-2 ring-primary scale-110 z-10"
: "opacity-80 hover:opacity-100 hover:scale-105" : "opacity-80 hover:opacity-100 hover:scale-105",
}`} hasError && "bg-muted"
)}
> >
<ImageLoader isLoading={isLoading} /> <ImageLoader isLoading={isLoading} />
{isVisible && imageUrl && ( {imageUrl && !hasError && (
<Image <Image
src={imageUrl} src={imageUrl}
alt={`Miniature page ${pageNumber}`} alt={`Miniature page ${pageNumber}`}
@@ -63,8 +102,14 @@ export const Thumbnail = forwardRef<HTMLButtonElement, ThumbnailProps>(
loading="lazy" loading="lazy"
quality={50} quality={50}
onLoad={handleImageLoad} onLoad={handleImageLoad}
onError={handleImageError}
/> />
)} )}
{hasError && (
<div className="absolute inset-0 flex items-center justify-center bg-muted">
<span className="text-sm text-muted-foreground">Erreur</span>
</div>
)}
<div className="absolute bottom-0 inset-x-0 h-8 bg-gradient-to-t from-black/60 to-transparent flex items-center justify-center"> <div className="absolute bottom-0 inset-x-0 h-8 bg-gradient-to-t from-black/60 to-transparent flex items-center justify-center">
<span className="text-sm text-white font-medium">{pageNumber}</span> <span className="text-sm text-white font-medium">{pageNumber}</span>
</div> </div>

View File

@@ -12,6 +12,7 @@ class ServerCacheService {
private static readonly tenMinutes = 10 * 60; private static readonly tenMinutes = 10 * 60;
private static readonly twentyFourHours = 24 * 60 * 60; private static readonly twentyFourHours = 24 * 60 * 60;
private static readonly oneMinute = 1 * 60; private static readonly oneMinute = 1 * 60;
private static readonly oneWeek = 7 * 24 * 60 * 60;
private static readonly noCache = 0; private static readonly noCache = 0;
// Configuration des temps de cache en secondes // Configuration des temps de cache en secondes
@@ -21,7 +22,7 @@ class ServerCacheService {
LIBRARIES: ServerCacheService.twentyFourHours, LIBRARIES: ServerCacheService.twentyFourHours,
SERIES: ServerCacheService.fiveMinutes, SERIES: ServerCacheService.fiveMinutes,
BOOKS: ServerCacheService.fiveMinutes, BOOKS: ServerCacheService.fiveMinutes,
IMAGES: ServerCacheService.twentyFourHours, IMAGES: ServerCacheService.oneWeek,
}; };
private constructor() { private constructor() {