Add persistent onion key backup and restore, improve startup resilience, and flesh out the major site verticals with richer navigation, search coverage, and operator documentation. Made-with: Cursor
150 lines
3.7 KiB
TypeScript
150 lines
3.7 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
createContext,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useState,
|
|
type ReactNode,
|
|
} from "react";
|
|
import type { ShopCurrency, ShopProduct } from "@/lib/shopCatalog";
|
|
|
|
const CART_KEY = "cyberlux-cart-v1";
|
|
|
|
export type CartLine = {
|
|
productId: string;
|
|
qty: number;
|
|
name: string;
|
|
price: number;
|
|
currency: ShopCurrency;
|
|
};
|
|
|
|
type CartContextValue = {
|
|
lines: CartLine[];
|
|
itemCount: number;
|
|
hydrated: boolean;
|
|
addToCart: (product: ShopProduct, qty?: number) => void;
|
|
removeLine: (productId: string) => void;
|
|
setLineQty: (productId: string, qty: number) => void;
|
|
clearCart: () => void;
|
|
};
|
|
|
|
const CartContext = createContext<CartContextValue | undefined>(undefined);
|
|
|
|
function loadRaw(): CartLine[] {
|
|
if (typeof window === "undefined") return [];
|
|
try {
|
|
const raw = localStorage.getItem(CART_KEY);
|
|
if (!raw) return [];
|
|
const v = JSON.parse(raw) as CartLine[];
|
|
if (!Array.isArray(v)) return [];
|
|
return v.filter(
|
|
(row) =>
|
|
row &&
|
|
typeof row.productId === "string" &&
|
|
typeof row.qty === "number" &&
|
|
row.qty > 0 &&
|
|
typeof row.name === "string" &&
|
|
typeof row.price === "number" &&
|
|
row.currency &&
|
|
["BTC", "ETH", "USD", "XMR"].includes(row.currency),
|
|
);
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function persist(lines: CartLine[]) {
|
|
if (typeof window === "undefined") return;
|
|
localStorage.setItem(CART_KEY, JSON.stringify(lines));
|
|
}
|
|
|
|
export function CartProvider({ children }: { children: ReactNode }) {
|
|
const [lines, setLines] = useState<CartLine[]>([]);
|
|
const [hydrated, setHydrated] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setLines(loadRaw());
|
|
setHydrated(true);
|
|
}, []);
|
|
|
|
const flush = useCallback((next: CartLine[]) => {
|
|
persist(next);
|
|
setLines(next);
|
|
}, []);
|
|
|
|
const addToCart = useCallback((product: ShopProduct, qty = 1) => {
|
|
const q = Math.max(1, Math.min(99, qty));
|
|
const cur = loadRaw();
|
|
const i = cur.findIndex((l) => l.productId === product.id);
|
|
let next: CartLine[];
|
|
if (i >= 0) {
|
|
next = [...cur];
|
|
next[i] = { ...next[i]!, qty: Math.min(99, next[i]!.qty + q) };
|
|
} else {
|
|
next = [
|
|
...cur,
|
|
{
|
|
productId: product.id,
|
|
qty: q,
|
|
name: product.name,
|
|
price: product.price,
|
|
currency: product.currency,
|
|
},
|
|
];
|
|
}
|
|
persist(next);
|
|
setLines(next);
|
|
}, []);
|
|
|
|
const removeLine = useCallback((productId: string) => {
|
|
const next = loadRaw().filter((l) => l.productId !== productId);
|
|
persist(next);
|
|
setLines(next);
|
|
}, []);
|
|
|
|
const setLineQty = useCallback((productId: string, qty: number) => {
|
|
const q = Math.floor(qty);
|
|
if (q < 1) {
|
|
const next = loadRaw().filter((l) => l.productId !== productId);
|
|
persist(next);
|
|
setLines(next);
|
|
return;
|
|
}
|
|
const capped = Math.min(99, q);
|
|
const next = loadRaw().map((l) => (l.productId === productId ? { ...l, qty: capped } : l));
|
|
persist(next);
|
|
setLines(next);
|
|
}, []);
|
|
|
|
const clearCart = useCallback(() => {
|
|
persist([]);
|
|
setLines([]);
|
|
}, []);
|
|
|
|
const itemCount = useMemo(() => lines.reduce((s, l) => s + l.qty, 0), [lines]);
|
|
|
|
const value = useMemo(
|
|
() => ({
|
|
lines,
|
|
itemCount,
|
|
hydrated,
|
|
addToCart,
|
|
removeLine,
|
|
setLineQty,
|
|
clearCart,
|
|
}),
|
|
[lines, itemCount, hydrated, addToCart, removeLine, setLineQty, clearCart],
|
|
);
|
|
|
|
return <CartContext.Provider value={value}>{children}</CartContext.Provider>;
|
|
}
|
|
|
|
export function useCart(): CartContextValue {
|
|
const ctx = useContext(CartContext);
|
|
if (!ctx) throw new Error("useCart must be used within CartProvider");
|
|
return ctx;
|
|
}
|