1/** 2 * @file 3 * Frisbii Cart â client-side cart state via localStorage. 4 * 5 * Standalone ES6 object, no Drupal.behaviors. 6 * Consumed by frisbii-buy-button.js as a library dependency. 7 * 8 * localStorage key: frisbii_cart 9 * Item shape: { 10 * offerId: string, 11 * label: string, 12 * quantity: number, 13 * // optional product override fields (passed through to preparePurchase): 14 * productId?: string, // plenigoProductId 15 * itemId?: string, // productId + accessRightUniqueId (unique per issue) 16 * price?: number, // price override 17 * shippingCosts?: number, // shipping cost per unit 18 * maxQuantity?: number, // quantity cap (e.g. 1 for subscriptions) 19 * bonusId?: string, // selected Plenigo bonus (Prämie) ID 20 * bonusName?: string, // selected bonus label for cart display 21 * startDate?: string, // subscription start date (YYYY-MM-DD, from start issue select) 22 * startLabel?: string, // selected start issue label for cart display 23 * variant?: string, // 'print'|'digital' for issues â picks the skeleton-offer slot pool 24 * } 25 * 26 * Deduplication key: itemId when present, otherwise offerId. 27 * This allows multiple issues that share the same offerId (one offer template) 28 * to coexist in the cart as separate line items. 29 */ 30 31const FrisbiiCart = (() => { 32 const STORAGE_KEY = 'frisbii_cart'; 33 34 /** @returns {Array} */ 35 function getItems() { 36 try { 37 return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); 38 } 39 catch { 40 return []; 41 } 42 } 43 44 /** @param {Array} items */ 45 function setItems(items) { 46 localStorage.setItem(STORAGE_KEY, JSON.stringify(items)); 47 } 48 49 /** 50 * Returns the deduplication key for a cart item. 51 * Uses itemId when present so issues sharing an offerId stay separate. 52 * 53 * @param {string} offerId 54 * @param {string|undefined} itemId 55 * @returns {string} 56 */ 57 function itemKey(offerId, itemId) { 58 return itemId || offerId; 59 } 60 61 /** 62 * Clamps a quantity to an item's maxQuantity (if set). 63 * 64 * @param {number} quantity 65 * @param {number|undefined} maxQuantity 66 * @returns {number} 67 */ 68 function clampQuantity(quantity, maxQuantity) { 69 return maxQuantity ? Math.min(quantity, maxQuantity) : quantity; 70 } 71 72 /** 73 * Adds an item or increments its quantity if already in the cart. 74 * Quantities never exceed meta.maxQuantity when set. 75 * 76 * @param {string} offerId 77 * @param {string} label 78 * @param {number} [quantity=1] 79 * @param {{productId?: string, itemId?: string, price?: number, shippingCosts?: number, maxQuantity?: number, bonusId?: string, bonusName?: string, startDate?: string, startLabel?: string}} [meta={}] 80 */ 81 function add(offerId, label, quantity = 1, meta = {}) { 82 const items = getItems(); 83 const key = itemKey(offerId, meta.itemId); 84 const existing = items.find(item => itemKey(item.offerId, item.itemId) === key); 85 if (existing) { 86 // Backfill maxQuantity on items stored before it existed. 87 if (existing.maxQuantity === undefined && meta.maxQuantity !== undefined) { 88 existing.maxQuantity = meta.maxQuantity; 89 } 90 existing.quantity = clampQuantity(existing.quantity + quantity, existing.maxQuantity); 91 // Re-adding with a (new) start issue selection replaces the stored one; 92 // an empty startDate ("next possible issue") clears it. 93 if (meta.startDate !== undefined) { 94 if (meta.startDate) { 95 existing.startDate = meta.startDate; 96 existing.startLabel = meta.startLabel || ''; 97 } 98 else { 99 delete existing.startDate; 100 delete existing.startLabel; 101 } 102 } 103 } 104 else { 105 const item = { offerId, label, quantity: clampQuantity(quantity, meta.maxQuantity), ...meta }; 106 // Never store an empty start issue selection. 107 if (!item.startDate) { 108 delete item.startDate; 109 delete item.startLabel; 110 } 111 items.push(item); 112 } 113 setItems(items); 114 } 115 116 /** 117 * Marks an item as single-purchase: quantity 1, maxQuantity 1. 118 * Used to normalize legacy cart items (stored before maxQuantity existed). 119 * 120 * @param {string} key 121 */ 122 function limitToSingle(key) { 123 const items = getItems(); 124 const existing = items.find(item => itemKey(item.offerId, item.itemId) === key); 125 if (existing) { 126 existing.quantity = 1; 127 existing.maxQuantity = 1; 128 setItems(items); 129 } 130 } 131 132 /** 133 * Removes an item by its cart key (itemId if set, otherwise offerId). 134 * 135 * @param {string} key 136 */ 137 function remove(key) { 138 setItems(getItems().filter(item => itemKey(item.offerId, item.itemId) !== key)); 139 } 140 141 /** 142 * Sets the quantity for an item identified by its cart key. 143 * Removes the item when quantity drops to 0 or below; caps at maxQuantity. 144 * 145 * @param {string} key 146 * @param {number} quantity 147 */ 148 function updateQuantity(key, quantity) { 149 if (quantity <= 0) { 150 remove(key); 151 return; 152 } 153 const items = getItems(); 154 const existing = items.find(item => itemKey(item.offerId, item.itemId) === key); 155 if (existing) { 156 existing.quantity = clampQuantity(quantity, existing.maxQuantity); 157 setItems(items); 158 } 159 } 160 161 /** 162 * Clears the cart (call after successful checkout). 163 */ 164 function clear() { 165 localStorage.removeItem(STORAGE_KEY); 166 } 167 168 /** 169 * Returns the total item count (sum of all quantities). 170 * 171 * @returns {number} 172 */ 173 function count() { 174 return getItems().reduce((sum, item) => sum + item.quantity, 0); 175 } 176 177 return { add, remove, updateQuantity, limitToSingle, getItems, clear, count }; 178})(); 179 180// Expose globally so frisbii-buy-button.js and templates can access it. 181window.FrisbiiCart = FrisbiiCart;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.