From 072d6870fee83219ab29d06063fd9dd833a2746c Mon Sep 17 00:00:00 2001 From: Froidefond Julien Date: Thu, 26 Mar 2026 22:45:48 +0100 Subject: [PATCH] feat: persistance des filtres server-side via cookies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remplace localStorage par des cookies pour la persistance des filtres. Le proxy restaure les filtres sauvegardés côté serveur, éliminant le flash au chargement. Co-Authored-By: Claude Opus 4.6 --- .../app/components/LiveSearchForm.tsx | 54 ++++++++----------- apps/backoffice/proxy.ts | 42 +++++++++++++-- 2 files changed, 60 insertions(+), 36 deletions(-) diff --git a/apps/backoffice/app/components/LiveSearchForm.tsx b/apps/backoffice/app/components/LiveSearchForm.tsx index 4dcf9de..4d6bd58 100644 --- a/apps/backoffice/app/components/LiveSearchForm.tsx +++ b/apps/backoffice/app/components/LiveSearchForm.tsx @@ -39,7 +39,19 @@ interface LiveSearchFormProps { debounceMs?: number; } -const STORAGE_KEY_PREFIX = "filters:"; +/** Convert a basePath to a cookie name: /series → filters_series */ +function filterCookieName(basePath: string): string { + return `filters_${basePath.replace(/^\//, "").replace(/\//g, "_")}`; +} + +function setCookie(name: string, value: string, days = 365) { + const expires = new Date(Date.now() + days * 864e5).toUTCString(); + document.cookie = `${name}=${encodeURIComponent(value)};path=/;expires=${expires};SameSite=Lax`; +} + +function deleteCookie(name: string) { + document.cookie = `${name}=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT`; +} export function LiveSearchForm({ fields, basePath, debounceMs = 300 }: LiveSearchFormProps) { const router = useRouter(); @@ -47,9 +59,8 @@ export function LiveSearchForm({ fields, basePath, debounceMs = 300 }: LiveSearc const { t } = useTranslation(); const timerRef = useRef | null>(null); const formRef = useRef(null); - const restoredRef = useRef(false); - const storageKey = `${STORAGE_KEY_PREFIX}${basePath}`; + const cookieName = filterCookieName(basePath); const buildUrl = useCallback((): string => { if (!formRef.current) return basePath; @@ -72,9 +83,13 @@ export function LiveSearchForm({ fields, basePath, debounceMs = 300 }: LiveSearc if (str) filters[key] = str; } try { - localStorage.setItem(storageKey, JSON.stringify(filters)); + if (Object.keys(filters).length > 0) { + setCookie(cookieName, JSON.stringify(filters)); + } else { + deleteCookie(cookieName); + } } catch {} - }, [storageKey]); + }, [cookieName]); const navigate = useCallback((immediate: boolean) => { if (timerRef.current) clearTimeout(timerRef.current); @@ -89,33 +104,6 @@ export function LiveSearchForm({ fields, basePath, debounceMs = 300 }: LiveSearc } }, [router, buildUrl, debounceMs, saveFilters]); - // Restore filters from localStorage on mount if URL has no filters - useEffect(() => { - if (restoredRef.current) return; - restoredRef.current = true; - - const hasUrlFilters = fields.some((f) => { - const val = searchParams.get(f.name); - return val && val.trim() !== ""; - }); - if (hasUrlFilters) return; - - try { - const saved = localStorage.getItem(storageKey); - if (!saved) return; - const filters: Record = JSON.parse(saved); - const fieldNames = new Set(fields.map((f) => f.name)); - const params = new URLSearchParams(); - for (const [key, value] of Object.entries(filters)) { - if (fieldNames.has(key) && value) params.set(key, value); - } - const qs = params.toString(); - if (qs) { - router.replace(`${basePath}?${qs}` as any); - } - } catch {} - }, []); // eslint-disable-line react-hooks/exhaustive-deps - useEffect(() => { return () => { if (timerRef.current) clearTimeout(timerRef.current); @@ -199,7 +187,7 @@ export function LiveSearchForm({ fields, basePath, debounceMs = 300 }: LiveSearc type="button" onClick={() => { formRef.current?.reset(); - try { localStorage.removeItem(storageKey); } catch {} + try { deleteCookie(cookieName); } catch {} router.replace(basePath as any); }} className=" diff --git a/apps/backoffice/proxy.ts b/apps/backoffice/proxy.ts index aa44278..5a93d61 100644 --- a/apps/backoffice/proxy.ts +++ b/apps/backoffice/proxy.ts @@ -8,8 +8,16 @@ function getSecret(): Uint8Array { return new TextEncoder().encode(secret); } +/** Paths where filter persistence is active */ +const FILTER_PATHS = ["/series", "/books", "/authors"]; + +/** Convert a basePath to a cookie name: /series → filters_series */ +function filterCookieName(basePath: string): string { + return `filters_${basePath.replace(/^\//, "").replace(/\//g, "_")}`; +} + export async function proxy(req: NextRequest) { - const { pathname } = req.nextUrl; + const { pathname, searchParams } = req.nextUrl; // Skip auth for login page and auth API routes if (pathname.startsWith("/login") || pathname.startsWith("/api/auth")) { @@ -20,10 +28,38 @@ export async function proxy(req: NextRequest) { if (token) { try { await jwtVerify(token, getSecret()); - return NextResponse.next(); } catch { - // Token invalid or expired + // Token invalid or expired — redirect to login + const loginUrl = new URL("/login", req.url); + loginUrl.searchParams.set("from", pathname); + return NextResponse.redirect(loginUrl); } + + // Restore saved filters from cookie for filter pages + if (FILTER_PATHS.includes(pathname)) { + const nonPaginationParams = Array.from(searchParams.entries()).filter( + ([key]) => key !== "page" && key !== "limit" + ); + if (nonPaginationParams.length === 0) { + const cookieName = filterCookieName(pathname); + const cookie = req.cookies.get(cookieName); + if (cookie?.value) { + try { + const filters: Record = JSON.parse(cookie.value); + const entries = Object.entries(filters).filter(([, v]) => v && v.trim()); + if (entries.length > 0) { + const url = req.nextUrl.clone(); + for (const [key, value] of entries) { + url.searchParams.set(key, value); + } + return NextResponse.redirect(url); + } + } catch {} + } + } + } + + return NextResponse.next(); } const loginUrl = new URL("/login", req.url);