From 99d9f412996c97534a585eb7800da8c6f86743d2 Mon Sep 17 00:00:00 2001 From: Froidefond Julien Date: Mon, 2 Mar 2026 13:38:45 +0100 Subject: [PATCH] feat: refresh buttons invalidate cache and show spinner during refresh - Add revalidateForRefresh(scope, id) server action for home/library/series - Library/Series wrappers: revalidate cache then router.refresh(), 400ms delay for animation - Home: revalidate home-data + path before refresh - RefreshButton uses refreshLibrary from RefreshContext when not passed as prop - Library/Series pages pass id to wrapper for context and pull-to-refresh - read-progress: pass 'max' to revalidateTag for Next 16 types Made-with: Cursor --- src/app/actions/read-progress.ts | 8 ++-- src/app/actions/refresh.ts | 32 +++++++++++++ .../[libraryId]/LibraryClientWrapper.tsx | 47 +++++++++++++------ src/app/libraries/[libraryId]/page.tsx | 2 +- .../series/[seriesId]/SeriesClientWrapper.tsx | 41 +++++++++++----- src/app/series/[seriesId]/page.tsx | 2 +- src/components/home/HomeClientWrapper.tsx | 12 +++-- src/components/library/RefreshButton.tsx | 5 +- 8 files changed, 110 insertions(+), 39 deletions(-) create mode 100644 src/app/actions/refresh.ts diff --git a/src/app/actions/read-progress.ts b/src/app/actions/read-progress.ts index d5367ec..35a1508 100644 --- a/src/app/actions/read-progress.ts +++ b/src/app/actions/read-progress.ts @@ -20,8 +20,8 @@ export async function updateReadProgress( await BookService.updateReadProgress(bookId, page, completed); // Invalider le cache home et libraries (statut de lecture des séries) - revalidateTag(HOME_CACHE_TAG); - revalidateTag(LIBRARY_SERIES_CACHE_TAG); + revalidateTag(HOME_CACHE_TAG, "max"); + revalidateTag(LIBRARY_SERIES_CACHE_TAG, "max"); return { success: true, message: "Progression mise à jour" }; } catch (error) { @@ -42,8 +42,8 @@ export async function deleteReadProgress( await BookService.deleteReadProgress(bookId); // Invalider le cache home et libraries (statut de lecture des séries) - revalidateTag(HOME_CACHE_TAG); - revalidateTag(LIBRARY_SERIES_CACHE_TAG); + revalidateTag(HOME_CACHE_TAG, "max"); + revalidateTag(LIBRARY_SERIES_CACHE_TAG, "max"); return { success: true, message: "Progression supprimée" }; } catch (error) { diff --git a/src/app/actions/refresh.ts b/src/app/actions/refresh.ts new file mode 100644 index 0000000..be5b224 --- /dev/null +++ b/src/app/actions/refresh.ts @@ -0,0 +1,32 @@ +"use server"; + +import { revalidatePath, revalidateTag } from "next/cache"; +import { LIBRARY_SERIES_CACHE_TAG } from "@/lib/services/library.service"; + +const HOME_CACHE_TAG = "home-data"; + +export type RefreshScope = "home" | "library" | "series"; + +/** + * Invalide le cache Next.js pour forcer un re-fetch au prochain router.refresh(). + * À appeler côté client avant router.refresh() sur les boutons / pull-to-refresh. + */ +export async function revalidateForRefresh(scope: RefreshScope, id: string): Promise { + switch (scope) { + case "home": + revalidateTag(HOME_CACHE_TAG, "max"); + revalidatePath("/"); + break; + case "library": + revalidateTag(LIBRARY_SERIES_CACHE_TAG, "max"); + revalidatePath(`/libraries/${id}`); + revalidatePath("/libraries"); + break; + case "series": + revalidatePath(`/series/${id}`); + revalidatePath("/series"); + break; + default: + break; + } +} diff --git a/src/app/libraries/[libraryId]/LibraryClientWrapper.tsx b/src/app/libraries/[libraryId]/LibraryClientWrapper.tsx index fd29446..c2c4ceb 100644 --- a/src/app/libraries/[libraryId]/LibraryClientWrapper.tsx +++ b/src/app/libraries/[libraryId]/LibraryClientWrapper.tsx @@ -1,29 +1,44 @@ "use client"; -import { useState, type ReactNode } from "react"; +import { useState, useCallback, type ReactNode } from "react"; import { useRouter } from "next/navigation"; import { PullToRefreshIndicator } from "@/components/common/PullToRefreshIndicator"; import { usePullToRefresh } from "@/hooks/usePullToRefresh"; +import { RefreshProvider } from "@/contexts/RefreshContext"; +import { revalidateForRefresh } from "@/app/actions/refresh"; interface LibraryClientWrapperProps { children: ReactNode; + libraryId?: string; } -export function LibraryClientWrapper({ children }: LibraryClientWrapperProps) { +const REFRESH_ANIMATION_MS = 400; + +export function LibraryClientWrapper({ children, libraryId }: LibraryClientWrapperProps) { const router = useRouter(); const [isRefreshing, setIsRefreshing] = useState(false); - const handleRefresh = async () => { - try { - setIsRefreshing(true); - router.refresh(); - return { success: true }; - } catch { - return { success: false, error: "Error refreshing library" }; - } finally { - setIsRefreshing(false); - } - }; + const handleRefresh = useCallback( + async (libraryIdArg?: string) => { + const id = libraryIdArg ?? libraryId; + if (!id) { + router.refresh(); + return { success: true }; + } + try { + setIsRefreshing(true); + await revalidateForRefresh("library", id); + router.refresh(); + await new Promise((r) => setTimeout(r, REFRESH_ANIMATION_MS)); + return { success: true }; + } catch { + return { success: false, error: "Error refreshing library" }; + } finally { + setIsRefreshing(false); + } + }, + [libraryId, router] + ); const pullToRefresh = usePullToRefresh({ onRefresh: async () => { @@ -33,7 +48,9 @@ export function LibraryClientWrapper({ children }: LibraryClientWrapperProps) { }); return ( - <> + handleRefresh(id) : undefined} + > {children} - + ); } diff --git a/src/app/libraries/[libraryId]/page.tsx b/src/app/libraries/[libraryId]/page.tsx index 4dca9a5..49eb580 100644 --- a/src/app/libraries/[libraryId]/page.tsx +++ b/src/app/libraries/[libraryId]/page.tsx @@ -43,7 +43,7 @@ export default async function LibraryPage({ params, searchParams }: PageProps) { ]); return ( - + { - try { - setIsRefreshing(true); - router.refresh(); - return { success: true }; - } catch { - return { success: false, error: "Error refreshing series" }; - } finally { - setIsRefreshing(false); - } - }; + const handleRefresh = useCallback( + async (seriesIdArg?: string) => { + const id = seriesIdArg ?? seriesId; + try { + setIsRefreshing(true); + if (id) { + await revalidateForRefresh("series", id); + } + router.refresh(); + await new Promise((r) => setTimeout(r, REFRESH_ANIMATION_MS)); + return { success: true }; + } catch { + return { success: false, error: "Error refreshing series" }; + } finally { + setIsRefreshing(false); + } + }, + [seriesId, router] + ); const pullToRefresh = usePullToRefresh({ onRefresh: async () => { @@ -44,7 +57,9 @@ export function SeriesClientWrapper({ canRefresh={pullToRefresh.canRefresh} isHiding={pullToRefresh.isHiding} /> - {children} + handleRefresh(id) : undefined}> + {children} + ); } diff --git a/src/app/series/[seriesId]/page.tsx b/src/app/series/[seriesId]/page.tsx index 8841c10..fd99bb2 100644 --- a/src/app/series/[seriesId]/page.tsx +++ b/src/app/series/[seriesId]/page.tsx @@ -36,7 +36,7 @@ export default async function SeriesPage({ params, searchParams }: PageProps) { ]); return ( - + { + const handleRefresh = useCallback(async () => { try { setIsRefreshing(true); - // Re-fetch server-side data + await revalidateForRefresh("home", "home"); router.refresh(); + await new Promise((r) => setTimeout(r, REFRESH_ANIMATION_MS)); return { success: true }; } catch (_error) { return { success: false, error: "Erreur lors du rafraîchissement de la page d'accueil" }; } finally { setIsRefreshing(false); } - }; + }, [router]); const pullToRefresh = usePullToRefresh({ onRefresh: async () => { diff --git a/src/components/library/RefreshButton.tsx b/src/components/library/RefreshButton.tsx index 543dd1d..90be1bf 100644 --- a/src/components/library/RefreshButton.tsx +++ b/src/components/library/RefreshButton.tsx @@ -7,17 +7,20 @@ import { Button } from "@/components/ui/button"; import { useToast } from "@/components/ui/use-toast"; import { cn } from "@/lib/utils"; import { useTranslation } from "react-i18next"; +import { useRefresh } from "@/contexts/RefreshContext"; interface RefreshButtonProps { libraryId: string; refreshLibrary?: (libraryId: string) => Promise<{ success: boolean; error?: string }>; } -export function RefreshButton({ libraryId, refreshLibrary }: RefreshButtonProps) { +export function RefreshButton({ libraryId, refreshLibrary: refreshLibraryProp }: RefreshButtonProps) { const [isRefreshing, setIsRefreshing] = useState(false); const router = useRouter(); const { toast } = useToast(); const { t } = useTranslation(); + const { refreshLibrary: refreshLibraryFromContext } = useRefresh(); + const refreshLibrary = refreshLibraryProp ?? refreshLibraryFromContext; const handleRefresh = async () => { setIsRefreshing(true);