feat: refresh buttons invalidate cache and show spinner during refresh
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m56s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m56s
- 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
This commit is contained in:
@@ -20,8 +20,8 @@ export async function updateReadProgress(
|
|||||||
await BookService.updateReadProgress(bookId, page, completed);
|
await BookService.updateReadProgress(bookId, page, completed);
|
||||||
|
|
||||||
// Invalider le cache home et libraries (statut de lecture des séries)
|
// Invalider le cache home et libraries (statut de lecture des séries)
|
||||||
revalidateTag(HOME_CACHE_TAG);
|
revalidateTag(HOME_CACHE_TAG, "max");
|
||||||
revalidateTag(LIBRARY_SERIES_CACHE_TAG);
|
revalidateTag(LIBRARY_SERIES_CACHE_TAG, "max");
|
||||||
|
|
||||||
return { success: true, message: "Progression mise à jour" };
|
return { success: true, message: "Progression mise à jour" };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -42,8 +42,8 @@ export async function deleteReadProgress(
|
|||||||
await BookService.deleteReadProgress(bookId);
|
await BookService.deleteReadProgress(bookId);
|
||||||
|
|
||||||
// Invalider le cache home et libraries (statut de lecture des séries)
|
// Invalider le cache home et libraries (statut de lecture des séries)
|
||||||
revalidateTag(HOME_CACHE_TAG);
|
revalidateTag(HOME_CACHE_TAG, "max");
|
||||||
revalidateTag(LIBRARY_SERIES_CACHE_TAG);
|
revalidateTag(LIBRARY_SERIES_CACHE_TAG, "max");
|
||||||
|
|
||||||
return { success: true, message: "Progression supprimée" };
|
return { success: true, message: "Progression supprimée" };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
32
src/app/actions/refresh.ts
Normal file
32
src/app/actions/refresh.ts
Normal file
@@ -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<void> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,29 +1,44 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, type ReactNode } from "react";
|
import { useState, useCallback, type ReactNode } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { PullToRefreshIndicator } from "@/components/common/PullToRefreshIndicator";
|
import { PullToRefreshIndicator } from "@/components/common/PullToRefreshIndicator";
|
||||||
import { usePullToRefresh } from "@/hooks/usePullToRefresh";
|
import { usePullToRefresh } from "@/hooks/usePullToRefresh";
|
||||||
|
import { RefreshProvider } from "@/contexts/RefreshContext";
|
||||||
|
import { revalidateForRefresh } from "@/app/actions/refresh";
|
||||||
|
|
||||||
interface LibraryClientWrapperProps {
|
interface LibraryClientWrapperProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
libraryId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LibraryClientWrapper({ children }: LibraryClientWrapperProps) {
|
const REFRESH_ANIMATION_MS = 400;
|
||||||
|
|
||||||
|
export function LibraryClientWrapper({ children, libraryId }: LibraryClientWrapperProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = useCallback(
|
||||||
|
async (libraryIdArg?: string) => {
|
||||||
|
const id = libraryIdArg ?? libraryId;
|
||||||
|
if (!id) {
|
||||||
|
router.refresh();
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
setIsRefreshing(true);
|
setIsRefreshing(true);
|
||||||
|
await revalidateForRefresh("library", id);
|
||||||
router.refresh();
|
router.refresh();
|
||||||
|
await new Promise((r) => setTimeout(r, REFRESH_ANIMATION_MS));
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch {
|
} catch {
|
||||||
return { success: false, error: "Error refreshing library" };
|
return { success: false, error: "Error refreshing library" };
|
||||||
} finally {
|
} finally {
|
||||||
setIsRefreshing(false);
|
setIsRefreshing(false);
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
[libraryId, router]
|
||||||
|
);
|
||||||
|
|
||||||
const pullToRefresh = usePullToRefresh({
|
const pullToRefresh = usePullToRefresh({
|
||||||
onRefresh: async () => {
|
onRefresh: async () => {
|
||||||
@@ -33,7 +48,9 @@ export function LibraryClientWrapper({ children }: LibraryClientWrapperProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<RefreshProvider
|
||||||
|
refreshLibrary={libraryId ? (id) => handleRefresh(id) : undefined}
|
||||||
|
>
|
||||||
<PullToRefreshIndicator
|
<PullToRefreshIndicator
|
||||||
isPulling={pullToRefresh.isPulling}
|
isPulling={pullToRefresh.isPulling}
|
||||||
isRefreshing={pullToRefresh.isRefreshing || isRefreshing}
|
isRefreshing={pullToRefresh.isRefreshing || isRefreshing}
|
||||||
@@ -42,6 +59,6 @@ export function LibraryClientWrapper({ children }: LibraryClientWrapperProps) {
|
|||||||
isHiding={pullToRefresh.isHiding}
|
isHiding={pullToRefresh.isHiding}
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
</>
|
</RefreshProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export default async function LibraryPage({ params, searchParams }: PageProps) {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LibraryClientWrapper>
|
<LibraryClientWrapper libraryId={libraryId}>
|
||||||
<LibraryContent
|
<LibraryContent
|
||||||
library={library}
|
library={library}
|
||||||
series={series}
|
series={series}
|
||||||
|
|||||||
@@ -1,32 +1,45 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, type ReactNode } from "react";
|
import { useState, useCallback, type ReactNode } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { PullToRefreshIndicator } from "@/components/common/PullToRefreshIndicator";
|
import { PullToRefreshIndicator } from "@/components/common/PullToRefreshIndicator";
|
||||||
import { usePullToRefresh } from "@/hooks/usePullToRefresh";
|
import { usePullToRefresh } from "@/hooks/usePullToRefresh";
|
||||||
import { RefreshProvider } from "@/contexts/RefreshContext";
|
import { RefreshProvider } from "@/contexts/RefreshContext";
|
||||||
|
import { revalidateForRefresh } from "@/app/actions/refresh";
|
||||||
|
|
||||||
interface SeriesClientWrapperProps {
|
interface SeriesClientWrapperProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
seriesId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const REFRESH_ANIMATION_MS = 400;
|
||||||
|
|
||||||
export function SeriesClientWrapper({
|
export function SeriesClientWrapper({
|
||||||
children,
|
children,
|
||||||
|
seriesId,
|
||||||
}: SeriesClientWrapperProps) {
|
}: SeriesClientWrapperProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = useCallback(
|
||||||
|
async (seriesIdArg?: string) => {
|
||||||
|
const id = seriesIdArg ?? seriesId;
|
||||||
try {
|
try {
|
||||||
setIsRefreshing(true);
|
setIsRefreshing(true);
|
||||||
|
if (id) {
|
||||||
|
await revalidateForRefresh("series", id);
|
||||||
|
}
|
||||||
router.refresh();
|
router.refresh();
|
||||||
|
await new Promise((r) => setTimeout(r, REFRESH_ANIMATION_MS));
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch {
|
} catch {
|
||||||
return { success: false, error: "Error refreshing series" };
|
return { success: false, error: "Error refreshing series" };
|
||||||
} finally {
|
} finally {
|
||||||
setIsRefreshing(false);
|
setIsRefreshing(false);
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
[seriesId, router]
|
||||||
|
);
|
||||||
|
|
||||||
const pullToRefresh = usePullToRefresh({
|
const pullToRefresh = usePullToRefresh({
|
||||||
onRefresh: async () => {
|
onRefresh: async () => {
|
||||||
@@ -44,7 +57,9 @@ export function SeriesClientWrapper({
|
|||||||
canRefresh={pullToRefresh.canRefresh}
|
canRefresh={pullToRefresh.canRefresh}
|
||||||
isHiding={pullToRefresh.isHiding}
|
isHiding={pullToRefresh.isHiding}
|
||||||
/>
|
/>
|
||||||
<RefreshProvider refreshSeries={handleRefresh}>{children}</RefreshProvider>
|
<RefreshProvider refreshSeries={seriesId ? (id) => handleRefresh(id) : undefined}>
|
||||||
|
{children}
|
||||||
|
</RefreshProvider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export default async function SeriesPage({ params, searchParams }: PageProps) {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SeriesClientWrapper>
|
<SeriesClientWrapper seriesId={seriesId}>
|
||||||
<SeriesContent
|
<SeriesContent
|
||||||
series={series}
|
series={series}
|
||||||
books={books}
|
books={books}
|
||||||
|
|||||||
@@ -1,31 +1,35 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, type ReactNode } from "react";
|
import { useState, useCallback, type ReactNode } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { RefreshButton } from "@/components/library/RefreshButton";
|
import { RefreshButton } from "@/components/library/RefreshButton";
|
||||||
import { PullToRefreshIndicator } from "@/components/common/PullToRefreshIndicator";
|
import { PullToRefreshIndicator } from "@/components/common/PullToRefreshIndicator";
|
||||||
import { usePullToRefresh } from "@/hooks/usePullToRefresh";
|
import { usePullToRefresh } from "@/hooks/usePullToRefresh";
|
||||||
|
import { revalidateForRefresh } from "@/app/actions/refresh";
|
||||||
|
|
||||||
interface HomeClientWrapperProps {
|
interface HomeClientWrapperProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const REFRESH_ANIMATION_MS = 400;
|
||||||
|
|
||||||
export function HomeClientWrapper({ children }: HomeClientWrapperProps) {
|
export function HomeClientWrapper({ children }: HomeClientWrapperProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setIsRefreshing(true);
|
setIsRefreshing(true);
|
||||||
// Re-fetch server-side data
|
await revalidateForRefresh("home", "home");
|
||||||
router.refresh();
|
router.refresh();
|
||||||
|
await new Promise((r) => setTimeout(r, REFRESH_ANIMATION_MS));
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
return { success: false, error: "Erreur lors du rafraîchissement de la page d'accueil" };
|
return { success: false, error: "Erreur lors du rafraîchissement de la page d'accueil" };
|
||||||
} finally {
|
} finally {
|
||||||
setIsRefreshing(false);
|
setIsRefreshing(false);
|
||||||
}
|
}
|
||||||
};
|
}, [router]);
|
||||||
|
|
||||||
const pullToRefresh = usePullToRefresh({
|
const pullToRefresh = usePullToRefresh({
|
||||||
onRefresh: async () => {
|
onRefresh: async () => {
|
||||||
|
|||||||
@@ -7,17 +7,20 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useRefresh } from "@/contexts/RefreshContext";
|
||||||
|
|
||||||
interface RefreshButtonProps {
|
interface RefreshButtonProps {
|
||||||
libraryId: string;
|
libraryId: string;
|
||||||
refreshLibrary?: (libraryId: string) => Promise<{ success: boolean; error?: 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 [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { refreshLibrary: refreshLibraryFromContext } = useRefresh();
|
||||||
|
const refreshLibrary = refreshLibraryProp ?? refreshLibraryFromContext;
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
setIsRefreshing(true);
|
setIsRefreshing(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user