Split the 100-line script into catalog, cart, format, dom and main modules. Money is integer cents, the bag is capped and sanitised and syncs across tabs, and the view (category, sort) lives in the URL. Templates escape every interpolation. Rewrite the minified stylesheet as cascade layers on design tokens. Fixes heading tracking that collapsed word gaps, a 2.6:1 focus ring on the hero, 1.3:1 control borders, misaligned cart quantities, uneven product cards and undersized touch targets. Adds a favicon and a resolvable dialog label.
65 lines
2.1 KiB
JavaScript
65 lines
2.1 KiB
JavaScript
/**
|
|
* Shopping bag logic. A cart is a plain `{ [productId]: quantity }` object.
|
|
*
|
|
* Everything here is pure (no DOM, no globals) except load/save, which take the storage object
|
|
* as an argument so they can be tested with a fake, including one that throws.
|
|
*/
|
|
|
|
export const MAX_QTY = 9;
|
|
export const STORAGE_KEY = "fieldwork-bag-v1"; // format unchanged, so existing bags carry over
|
|
|
|
const clamp = (n) => Math.min(MAX_QTY, Math.max(0, n));
|
|
|
|
/** Keep only known products with whole-number quantities, clamped to 1..MAX_QTY. */
|
|
export function sanitizeCart(raw, validIds) {
|
|
const clean = {};
|
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return clean;
|
|
for (const id of validIds) {
|
|
const qty = raw[id];
|
|
if (Number.isSafeInteger(qty) && qty > 0) clean[id] = clamp(qty);
|
|
}
|
|
return clean;
|
|
}
|
|
|
|
/** Return a new cart with `delta` added to one line. Lines at 0 are removed. */
|
|
export function changeQty(cart, id, delta) {
|
|
const next = { ...cart };
|
|
const qty = clamp((cart[id] || 0) + delta);
|
|
if (qty === 0) delete next[id];
|
|
else next[id] = qty;
|
|
return next;
|
|
}
|
|
|
|
export const cartCount = (cart) => Object.values(cart).reduce((sum, qty) => sum + qty, 0);
|
|
|
|
/** Lines in catalogue order: [{ product, qty, total }] with totals in cents. */
|
|
export function cartLines(cart, products) {
|
|
return products
|
|
.filter((product) => cart[product.id])
|
|
.map((product) => ({
|
|
product,
|
|
qty: cart[product.id],
|
|
total: product.price * cart[product.id],
|
|
}));
|
|
}
|
|
|
|
export const cartSubtotal = (cart, products) =>
|
|
cartLines(cart, products).reduce((sum, line) => sum + line.total, 0);
|
|
|
|
export function loadCart(storage, validIds) {
|
|
try {
|
|
return sanitizeCart(JSON.parse(storage.getItem(STORAGE_KEY)), validIds);
|
|
} catch {
|
|
return {}; // storage blocked, empty, or corrupted JSON
|
|
}
|
|
}
|
|
|
|
export function saveCart(storage, cart) {
|
|
try {
|
|
if (cartCount(cart) === 0) storage.removeItem(STORAGE_KEY);
|
|
else storage.setItem(STORAGE_KEY, JSON.stringify(cart));
|
|
} catch {
|
|
/* private mode or quota exceeded: the bag still works for this visit */
|
|
}
|
|
}
|