// store.jsx — products, filament color palette, cart + orders (localStorage). // Exports to window: PRODUCTS, FILAMENTS, fmtPrice, useCart, useOrders, genOrderId, findFilament // 3 keychain packs. `count` = number of personalisable keychains in the pack. const PRODUCTS = [ { id: "solo", slotId: "photo-solo", count: 1, price: 9.99, badge: null, name_fr: "1 Porte-clé QR Code", name_en: "1 QR Code keychain", desc_fr: "\nPorte-clé QR Code personnalisé imprimé en 3D.\n\n", desc_en: "Custom 3D-printed QR Code keychain.", dims_fr: "≈ 35 mm · anneau inclus", dims_en: "≈ 35 mm · ring included", }, { id: "duo", slotId: "photo-duo", count: 2, price: 17.99, badge: "popular", name_fr: "Pack Duo - 2 Porte-clés QR Code", name_en: "Duo Pack - 2 QR Code keychains", desc_fr: "Deux porte-clés QR Code personnalisés imprimés en 3D. Idéal pour un couple ou pour avoir un double.", desc_en: "Two custom 3D-printed QR Code keychains. Perfect for a couple or to keep a spare.", dims_fr: "2 × ≈ 35 mm · anneaux inclus", dims_en: "2 × ≈ 35 mm · rings included", }, { id: "famille", slotId: "photo-famille", count: 3, price: 24.99, badge: "best", name_fr: "Pack Famille - 3 Porte-clés QR Code", name_en: "Family Pack - 3 QR Code keychains", desc_fr: "Trois porte-clés QR Code personnalisés imprimés en 3D. Parfait pour toute la famille.\n\n", desc_en: "Three custom 3D-printed QR Code keychains. Perfect for the whole family.", dims_fr: "3 × ≈ 35 mm · anneaux inclus", dims_en: "3 × ≈ 35 mm · rings included", }, ]; // The real filament colors (matching the product band photos). const FILAMENTS = [ { id: "green", hex: "#24963A", fr: "Vert", en: "Green" }, { id: "red", hex: "#EF090A", fr: "Rouge", en: "Red" }, { id: "blue", hex: "#1E3CD0", fr: "Bleu", en: "Blue" }, { id: "teal", hex: "#0499A4", fr: "Bleu turquoise", en: "Turquoise" }, { id: "yellow", hex: "#F3B604", fr: "Jaune", en: "Yellow" }, { id: "orange", hex: "#FE4C02", fr: "Orange", en: "Orange" }, { id: "black", hex: "#121313", fr: "Noir", en: "Black" }, { id: "white", hex: "#E5E5E9", fr: "Blanc", en: "White" }, { id: "clear", hex: "#CED6DC", fr: "Transparent", en: "Clear" }, { id: "pink", hex: "#E96E9E", fr: "Rose", en: "Pink" }, { id: "gold", hex: "#C59955", fr: "Gold silk", en: "Gold silk" }, { id: "applegreen", hex: "#5FBB03", fr: "Vert pomme", en: "Apple green" }, ]; function findFilament(id) { return FILAMENTS.find((f) => f.id === id) || FILAMENTS[0]; } // --- Auto contrast for QR scannability ------------------------------------ // A colored QR code (anything but strong dark/light) is unreadable by phone // cameras: the code MUST contrast strongly with the plate. So the client only // picks the PLATE color; the code color is derived here — whichever of the two // print filaments (black or white) gives the higher contrast ratio with the // chosen plate. Light plate -> black code, dark plate -> white code. function _relLum(hex) { const c = String(hex).replace("#", ""); const ch = (i) => { const v = parseInt(c.substr(i, 2), 16) / 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); }; return 0.2126 * ch(0) + 0.7152 * ch(2) + 0.0722 * ch(4); } function _contrast(a, b) { const L1 = _relLum(a), L2 = _relLum(b); const hi = Math.max(L1, L2), lo = Math.min(L1, L2); return (hi + 0.05) / (lo + 0.05); } function autoCodeColor(plateId) { const plate = findFilament(plateId).hex; const black = findFilament("black").hex; const white = findFilament("white").hex; return _contrast(plate, black) >= _contrast(plate, white) ? "black" : "white"; } // Plates that offer a manual code choice (both colors scan fine) with a // recommended default. Everything else is fully automatic (autoCodeColor). const CODE_CHOICE = { pink: { options: ["white", "black"], default: "white" }, teal: { options: ["white", "black"], default: "white" }, orange: { options: ["white", "black"], default: "white" }, green: { options: ["white", "black"], default: "white" }, red: { options: ["white", "black"], default: "white" }, applegreen: { options: ["white", "black"], default: "black" }, yellow: { options: ["white", "black"], default: "black" }, gold: { options: ["white", "black"], default: "black" } }; // Resolve the code color actually used for a unit: honor a valid manual choice // on a choice-enabled plate, otherwise fall back to the automatic contrast pick. function codeForUnit(u) { const choice = CODE_CHOICE[u.plate]; if (choice) { return choice.options.includes(u.code) ? u.code : choice.default; } return autoCodeColor(u.plate); } function fmtPrice(n, lang) { const v = (Math.round(n * 100) / 100).toFixed(2); return lang === "en" ? `€${v}` : `${v.replace(".", ",")} €`; } function genOrderId() { const n = Math.floor(1000 + Math.random() * 9000); return `QBT-${n}`; } const CART_KEY = "qbt_cart_v1"; const ORDERS_KEY = "qbt_orders_v1"; function readLS(key, fallback) { try { const raw = localStorage.getItem(key); return raw ? JSON.parse(raw) : fallback; } catch (e) { return fallback; } } function writeLS(key, val) { try { localStorage.setItem(key, JSON.stringify(val)); } catch (e) {} } function useCart() { const [items, setItems] = React.useState(() => readLS(CART_KEY, [])); React.useEffect(() => { writeLS(CART_KEY, items); }, [items]); const add = (item) => setItems((prev) => [...prev, { ...item, lineId: Date.now() + "-" + Math.random().toString(36).slice(2, 7) }]); const remove = (lineId) => setItems((prev) => prev.filter((i) => i.lineId !== lineId)); const update = (lineId, patch) => setItems((prev) => prev.map((i) => (i.lineId === lineId ? { ...i, ...patch } : i))); const clear = () => setItems([]); const count = items.reduce((s, i) => s + (i.qty || 1), 0); const subtotal = items.reduce((s, i) => s + (i.price || 0) * (i.qty || 1), 0); return { items, add, remove, update, clear, count, subtotal }; } function useOrders() { const [orders, setOrders] = React.useState(() => readLS(ORDERS_KEY, [])); React.useEffect(() => { writeLS(ORDERS_KEY, orders); }, [orders]); const place = (order) => setOrders((prev) => [order, ...prev]); const setStatus = (id, status) => setOrders((prev) => prev.map((o) => (o.id === id ? { ...o, status } : o))); const clearAll = () => setOrders([]); return { orders, place, setStatus, clearAll }; } Object.assign(window, { PRODUCTS, FILAMENTS, fmtPrice, useCart, useOrders, genOrderId, findFilament, autoCodeColor, codeForUnit, CODE_CHOICE, });