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:
@@ -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:");
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
// 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);
|
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 (
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
// 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);
|
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 (
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user