Harden onion boot flow and deepen site surfaces
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
This commit is contained in:
149
contexts/CartContext.tsx
Normal file
149
contexts/CartContext.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
"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;
|
||||
}
|
||||
Reference in New Issue
Block a user