All checks were successful
CI / test (pull_request) Successful in 2m21s
ESLint react-hooks/set-state-in-effect bloque le CI. On déplace la re-hydratation depuis le cookie dans le lazy initializer de useState (qui ne court qu'une fois côté client). Conserve la cohérence si un autre onglet a modifié le panier entre le render serveur et l'hydration, sans déclencher de re-render en cascade. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
110 lines
3 KiB
TypeScript
110 lines
3 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
createContext,
|
|
useCallback,
|
|
useContext,
|
|
useMemo,
|
|
useState,
|
|
type ReactNode,
|
|
} from "react";
|
|
|
|
import {
|
|
CART_COOKIE,
|
|
EMPTY_CART,
|
|
parseCart,
|
|
serializeCart,
|
|
type Cart,
|
|
type CartEntry,
|
|
} from "@/lib/rental-cart";
|
|
|
|
type CartContextValue = {
|
|
cart: Cart;
|
|
addEntry: (entry: CartEntry) => void;
|
|
removeEntry: (index: number) => void;
|
|
updateEntry: (index: number, patch: Partial<CartEntry>) => void;
|
|
clear: () => void;
|
|
totalItems: number;
|
|
};
|
|
|
|
const Ctx = createContext<CartContextValue | null>(null);
|
|
|
|
function readCookieClient(): Cart {
|
|
if (typeof document === "undefined") return EMPTY_CART;
|
|
const match = document.cookie.split(/;\s*/).find((c) => c.startsWith(`${CART_COOKIE}=`));
|
|
if (!match) return EMPTY_CART;
|
|
const value = decodeURIComponent(match.slice(CART_COOKIE.length + 1));
|
|
return parseCart(value);
|
|
}
|
|
|
|
function writeCookieClient(cart: Cart): void {
|
|
if (typeof document === "undefined") return;
|
|
document.cookie = `${CART_COOKIE}=${encodeURIComponent(serializeCart(cart))}; path=/; max-age=${60 * 60 * 24 * 30}; SameSite=Lax`;
|
|
}
|
|
|
|
export function RentalCartProvider({ children, initial }: { children: ReactNode; initial?: Cart }) {
|
|
// Initial vient du serveur (cookie lu côté serveur). Sur le client on relit le
|
|
// cookie une seule fois via lazy initializer pour rester cohérent si un autre
|
|
// onglet a modifié le panier entre le render serveur et l'hydration.
|
|
const [cart, setCart] = useState<Cart>(() => {
|
|
if (typeof document === "undefined") return initial ?? EMPTY_CART;
|
|
const fromCookie = readCookieClient();
|
|
return fromCookie.items.length > 0 ? fromCookie : initial ?? EMPTY_CART;
|
|
});
|
|
|
|
const persist = useCallback((next: Cart) => {
|
|
setCart(next);
|
|
writeCookieClient(next);
|
|
}, []);
|
|
|
|
const addEntry = useCallback(
|
|
(entry: CartEntry) => {
|
|
const next = { ...cart, items: [...cart.items, entry] };
|
|
persist(next);
|
|
},
|
|
[cart, persist],
|
|
);
|
|
|
|
const removeEntry = useCallback(
|
|
(index: number) => {
|
|
const next = { ...cart, items: cart.items.filter((_, i) => i !== index) };
|
|
persist(next);
|
|
},
|
|
[cart, persist],
|
|
);
|
|
|
|
const updateEntry = useCallback(
|
|
(index: number, patch: Partial<CartEntry>) => {
|
|
const next = {
|
|
...cart,
|
|
items: cart.items.map((e, i) => (i === index ? { ...e, ...patch } : e)),
|
|
};
|
|
persist(next);
|
|
},
|
|
[cart, persist],
|
|
);
|
|
|
|
const clear = useCallback(() => {
|
|
persist({ v: 1, items: [] });
|
|
}, [persist]);
|
|
|
|
const value = useMemo<CartContextValue>(
|
|
() => ({
|
|
cart,
|
|
addEntry,
|
|
removeEntry,
|
|
updateEntry,
|
|
clear,
|
|
totalItems: cart.items.reduce((acc, e) => acc + e.qty, 0),
|
|
}),
|
|
[cart, addEntry, removeEntry, updateEntry, clear],
|
|
);
|
|
|
|
return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
|
|
}
|
|
|
|
export function useCart(): CartContextValue {
|
|
const ctx = useContext(Ctx);
|
|
if (!ctx) throw new Error("useCart must be used inside <RentalCartProvider>");
|
|
return ctx;
|
|
}
|