feat: add cache invalidation for series after updating or deleting read progress, and enhance BookGrid and BookList components with refresh functionality

This commit is contained in:
Julien Froidefond
2025-12-07 18:49:16 +01:00
parent 6b6fed34fb
commit 181240cd5f
6 changed files with 62 additions and 8 deletions

View File

@@ -1,6 +1,7 @@
import type { NextRequest } from "next/server"; import type { NextRequest } from "next/server";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { BookService } from "@/lib/services/book.service"; import { BookService } from "@/lib/services/book.service";
import { SeriesService } from "@/lib/services/series.service";
import { ERROR_CODES } from "@/constants/errorCodes"; import { ERROR_CODES } from "@/constants/errorCodes";
import { getErrorMessage } from "@/utils/errors"; import { getErrorMessage } from "@/utils/errors";
import { AppError } from "@/utils/errors"; import { AppError } from "@/utils/errors";
@@ -28,6 +29,17 @@ export async function PATCH(
} }
await BookService.updateReadProgress(bookId, page, completed); await BookService.updateReadProgress(bookId, page, completed);
// Invalider le cache de la série après avoir mis à jour la progression
try {
const seriesId = await BookService.getBookSeriesId(bookId);
await SeriesService.invalidateSeriesBooksCache(seriesId);
await SeriesService.invalidateSeriesCache(seriesId);
} catch (cacheError) {
// Ne pas faire échouer la requête si l'invalidation du cache échoue
logger.error({ err: cacheError }, "Erreur lors de l'invalidation du cache de la série:");
}
return NextResponse.json({ message: "📖 Progression mise à jour avec succès" }); return NextResponse.json({ message: "📖 Progression mise à jour avec succès" });
} catch (error) { } catch (error) {
logger.error({ err: error }, "Erreur lors de la mise à jour de la progression:"); logger.error({ err: error }, "Erreur lors de la mise à jour de la progression:");
@@ -64,6 +76,17 @@ export async function DELETE(
const bookId: string = (await params).bookId; const bookId: string = (await params).bookId;
await BookService.deleteReadProgress(bookId); await BookService.deleteReadProgress(bookId);
// Invalider le cache de la série après avoir supprimé la progression
try {
const seriesId = await BookService.getBookSeriesId(bookId);
await SeriesService.invalidateSeriesBooksCache(seriesId);
await SeriesService.invalidateSeriesCache(seriesId);
} catch (cacheError) {
// Ne pas faire échouer la requête si l'invalidation du cache échoue
logger.error({ err: cacheError }, "Erreur lors de l'invalidation du cache de la série:");
}
return NextResponse.json({ message: "🗑️ Progression supprimée avec succès" }); return NextResponse.json({ message: "🗑️ Progression supprimée avec succès" });
} catch (error) { } catch (error) {
logger.error({ err: error }, "Erreur lors de la suppression de la progression:"); logger.error({ err: error }, "Erreur lors de la suppression de la progression:");

View File

@@ -199,6 +199,7 @@ export function ClientSeriesPage({
totalElements={books.totalElements} totalElements={books.totalElements}
defaultShowOnlyUnread={preferences.showOnlyUnread} defaultShowOnlyUnread={preferences.showOnlyUnread}
showOnlyUnread={unreadOnly} showOnlyUnread={unreadOnly}
onRefresh={() => handleRefresh(seriesId)}
/> />
</div> </div>
</> </>

View File

@@ -2,7 +2,7 @@
import type { KomgaBook } from "@/types/komga"; import type { KomgaBook } from "@/types/komga";
import { BookCover } from "@/components/ui/book-cover"; import { BookCover } from "@/components/ui/book-cover";
import { useState, useEffect } from "react"; import { useState, useEffect, useRef } from "react";
import { useTranslate } from "@/hooks/useTranslate"; import { useTranslate } from "@/hooks/useTranslate";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useBookOfflineStatus } from "@/hooks/useBookOfflineStatus"; import { useBookOfflineStatus } from "@/hooks/useBookOfflineStatus";
@@ -11,6 +11,7 @@ interface BookGridProps {
books: KomgaBook[]; books: KomgaBook[];
onBookClick: (book: KomgaBook) => void; onBookClick: (book: KomgaBook) => void;
isCompact?: boolean; isCompact?: boolean;
onRefresh?: () => void;
} }
interface BookCardProps { interface BookCardProps {
@@ -61,12 +62,18 @@ function BookCard({ book, onBookClick, onSuccess, isCompact }: BookCardProps) {
); );
} }
export function BookGrid({ books, onBookClick, isCompact = false }: BookGridProps) { export function BookGrid({ books, onBookClick, isCompact = false, onRefresh }: BookGridProps) {
const [localBooks, setLocalBooks] = useState(books); const [localBooks, setLocalBooks] = useState(books);
const { t } = useTranslate(); const { t } = useTranslate();
const previousBookIdsRef = useRef<string>(books.map((b) => b.id).join(","));
useEffect(() => { useEffect(() => {
setLocalBooks(books); // Ne réinitialiser que si les IDs des livres ont changé (nouvelle page, nouveau filtre, etc.)
const newIds = books.map((b) => b.id).join(",");
if (previousBookIdsRef.current !== newIds) {
setLocalBooks(books);
previousBookIdsRef.current = newIds;
}
}, [books]); }, [books]);
if (!localBooks.length) { if (!localBooks.length) {
@@ -107,6 +114,8 @@ export function BookGrid({ books, onBookClick, isCompact = false }: BookGridProp
) )
); );
} }
// Rafraîchir les données après avoir marqué comme lu/non lu
onRefresh?.();
}; };
return ( return (

View File

@@ -2,7 +2,7 @@
import type { KomgaBook } from "@/types/komga"; import type { KomgaBook } from "@/types/komga";
import { BookCover } from "@/components/ui/book-cover"; import { BookCover } from "@/components/ui/book-cover";
import { useState, useEffect } from "react"; import { useState, useEffect, useRef } from "react";
import { useTranslate } from "@/hooks/useTranslate"; import { useTranslate } from "@/hooks/useTranslate";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useBookOfflineStatus } from "@/hooks/useBookOfflineStatus"; import { useBookOfflineStatus } from "@/hooks/useBookOfflineStatus";
@@ -18,6 +18,7 @@ interface BookListProps {
books: KomgaBook[]; books: KomgaBook[];
onBookClick: (book: KomgaBook) => void; onBookClick: (book: KomgaBook) => void;
isCompact?: boolean; isCompact?: boolean;
onRefresh?: () => void;
} }
interface BookListItemProps { interface BookListItemProps {
@@ -288,12 +289,18 @@ function BookListItem({ book, onBookClick, onSuccess, isCompact = false }: BookL
); );
} }
export function BookList({ books, onBookClick, isCompact = false }: BookListProps) { export function BookList({ books, onBookClick, isCompact = false, onRefresh }: BookListProps) {
const [localBooks, setLocalBooks] = useState(books); const [localBooks, setLocalBooks] = useState(books);
const { t } = useTranslate(); const { t } = useTranslate();
const previousBookIdsRef = useRef<string>(books.map((b) => b.id).join(","));
useEffect(() => { useEffect(() => {
setLocalBooks(books); // Ne réinitialiser que si les IDs des livres ont changé (nouvelle page, nouveau filtre, etc.)
const newIds = books.map((b) => b.id).join(",");
if (previousBookIdsRef.current !== newIds) {
setLocalBooks(books);
previousBookIdsRef.current = newIds;
}
}, [books]); }, [books]);
if (!localBooks.length) { if (!localBooks.length) {
@@ -334,6 +341,8 @@ export function BookList({ books, onBookClick, isCompact = false }: BookListProp
) )
); );
} }
// Rafraîchir les données après avoir marqué comme lu/non lu
onRefresh?.();
}; };
return ( return (

View File

@@ -21,6 +21,7 @@ interface PaginatedBookGridProps {
totalElements: number; totalElements: number;
defaultShowOnlyUnread: boolean; defaultShowOnlyUnread: boolean;
showOnlyUnread: boolean; showOnlyUnread: boolean;
onRefresh?: () => void;
} }
export function PaginatedBookGrid({ export function PaginatedBookGrid({
@@ -30,6 +31,7 @@ export function PaginatedBookGrid({
totalElements, totalElements,
defaultShowOnlyUnread, defaultShowOnlyUnread,
showOnlyUnread: initialShowOnlyUnread, showOnlyUnread: initialShowOnlyUnread,
onRefresh,
}: PaginatedBookGridProps) { }: PaginatedBookGridProps) {
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
@@ -130,9 +132,9 @@ export function PaginatedBookGrid({
</div> </div>
{viewMode === "grid" ? ( {viewMode === "grid" ? (
<BookGrid books={books} onBookClick={handleBookClick} isCompact={isCompact} /> <BookGrid books={books} onBookClick={handleBookClick} isCompact={isCompact} onRefresh={onRefresh} />
) : ( ) : (
<BookList books={books} onBookClick={handleBookClick} isCompact={isCompact} /> <BookList books={books} onBookClick={handleBookClick} isCompact={isCompact} onRefresh={onRefresh} />
)} )}
<div className="flex flex-col items-center gap-4 sm:flex-row sm:justify-between"> <div className="flex flex-col items-center gap-4 sm:flex-row sm:justify-between">

View File

@@ -61,6 +61,16 @@ export class BookService extends BaseApiService {
} }
} }
static async getBookSeriesId(bookId: string): Promise<string> {
try {
// Récupérer le livre sans cache pour éviter les données obsolètes
const book = await this.fetchFromApi<KomgaBook>({ path: `books/${bookId}` });
return book.seriesId;
} catch (error) {
throw new AppError(ERROR_CODES.BOOK.NOT_FOUND, {}, error);
}
}
static async updateReadProgress( static async updateReadProgress(
bookId: string, bookId: string,
page: number, page: number,