1/** 2 * Guest Cart Manager - localStorage based cart for guest users 3 * 4 * Manages shopping cart items in browser localStorage for users who are not logged in. 5 * Provides seamless transition to database when user logs in. 6 */ 7 8class GuestCart { 9 constructor() { 10 this.storageKey = 'plazait_guest_cart'; 11 this.apiBase = '/api/cart'; 12 13 // Initialize cart structure 14 this.cart = this.loadFromStorage(); 15 16 // Bind methods 17 this.add = this.add.bind(this); 18 this.remove = this.remove.bind(this); 19 this.update = this.update.bind(this); 20 this.clear = this.clear.bind(this); 21 this.getItems = this.getItems.bind(this); 22 this.mergeToDatabase = this.mergeToDatabase.bind(this); 23 } 24 25 /** 26 * Load cart from localStorage 27 */ 28 loadFromStorage() { 29 try { 30 const stored = localStorage.getItem(this.storageKey); 31 if (stored) { 32 const parsed = JSON.parse(stored); 33 if (parsed && typeof parsed === 'object' && parsed.items) { 34 return parsed; 35 } 36 } 37 } catch (error) { 38 // Silent error handling 39 } 40 41 return { 42 items: {}, 43 updated_at: Date.now(), 44 version: '1.0' 45 }; 46 } 47 48 /** 49 * Save cart to localStorage 50 */ 51 saveToStorage() { 52 try { 53 this.cart.updated_at = Date.now(); 54 localStorage.setItem(this.storageKey, JSON.stringify(this.cart)); 55 56 // Dispatch custom event for cart updates 57 window.dispatchEvent(new CustomEvent('guestCartUpdated', { 58 detail: { 59 itemsCount: this.getItemsCount(), 60 linesCount: this.getLinesCount(), 61 items: this.cart.items 62 } 63 })); 64 65 return true; 66 } catch (error) { 67 return false; 68 } 69 } 70 71 /** 72 * Generate cart item key 73 */ 74 generateKey(productId, variantId = 0) { 75 return `p${productId}v${variantId}`; 76 } 77 78 /** 79 * Add item to guest cart 80 */ 81 add(productId, qty = 1, variantId = 0, meta = {}) { 82 const key = this.generateKey(productId, variantId); 83 84 // Validate inputs 85 if (!productId || productId <= 0 || !qty || qty <= 0) { 86 throw new Error('Invalid product ID or quantity'); 87 } 88 89 qty = Math.max(1, Math.min(999, parseInt(qty))); 90 91 // Initialize item if not exists 92 if (!this.cart.items[key]) { 93 this.cart.items[key] = { 94 pid: parseInt(productId), 95 vid: parseInt(variantId) || 0, 96 qty: 0, 97 name: meta.name || '', 98 sku: meta.sku || '', 99 price: parseInt(meta.price) || 0, 100 image_url: meta.image_url || '', 101 product_url: meta.product_url || '/product/' + parseInt(productId), 102 added_at: Date.now() 103 }; 104 } 105 106 // Update quantity 107 this.cart.items[key].qty = Math.max(1, Math.min(999, this.cart.items[key].qty + qty)); 108 109 // Update metadata if provided 110 if (meta.name) this.cart.items[key].name = meta.name; 111 if (meta.sku) this.cart.items[key].sku = meta.sku; 112 if (meta.price) this.cart.items[key].price = parseInt(meta.price); 113 if (meta.image_url) this.cart.items[key].image_url = meta.image_url; 114 if (meta.product_url) this.cart.items[key].product_url = meta.product_url; 115 116 this.saveToStorage(); 117 118 return { 119 key, 120 item: this.cart.items[key], 121 itemsCount: this.getItemsCount(), 122 linesCount: this.getLinesCount() 123 }; 124 } 125 126 /** 127 * Remove item from guest cart 128 */ 129 remove(productId, variantId = 0) { 130 const key = this.generateKey(productId, variantId); 131 132 if (this.cart.items[key]) { 133 delete this.cart.items[key]; 134 this.saveToStorage(); 135 136 return true; 137 } 138 139 return false;
140 } 141 142 /** 143 * Update item quantity 144 */ 145 update(productId, qty, variantId = 0) { 146 const key = this.generateKey(productId, variantId); 147 148 if (!this.cart.items[key]) { 149 return false; 150 } 151 152 qty = parseInt(qty); 153 if (qty <= 0) { 154 return this.remove(productId, variantId); 155 } 156 157 this.cart.items[key].qty = Math.max(1, Math.min(999, qty)); 158 this.saveToStorage(); 159 160 return true; 161 } 162 163 /** 164 * Clear entire cart 165 */ 166 clear() { 167 this.cart = { 168 items: {}, 169 updated_at: Date.now(), 170 version: '1.0' 171 }; 172 173 this.saveToStorage(); 174 175 return true; 176 } 177 178 /** 179 * Get all cart items 180 */ 181 getItems() { 182 return this.cart.items; 183 } 184 185 /** 186 * Get cart items as array 187 */ 188 getItemsArray() { 189 return Object.values(this.cart.items); 190 } 191 192 /** 193 * Get total items count (sum of quantities) 194 */ 195 getItemsCount() { 196 return Object.values(this.cart.items).reduce((total, item) => total + (item.qty || 0), 0); 197 } 198 199 /** 200 * Get lines count (number of different items) 201 */ 202 getLinesCount() { 203 return Object.keys(this.cart.items).length; 204 } 205 206 /** 207 * Get cart summary 208 */ 209 getSummary() { 210 const itemsCount = this.getItemsCount(); 211 const linesCount = this.getLinesCount(); 212 const totalValue = Object.values(this.cart.items).reduce( 213 (total, item) => total + ((item.qty || 0) * (item.price || 0)), 0 214 ); 215 216 return { 217 itemsCount, 218 linesCount, 219 totalValue, 220 updatedAt: this.cart.updated_at 221 }; 222 } 223 224 /** 225 * Check if cart has items 226 */ 227 isEmpty() { 228 return this.getLinesCount() === 0; 229 } 230 231 /** 232 * Merge guest cart to database after user login 233 */ 234 async mergeToDatabase() { 235 const items = this.cart.items; 236 237 if (Object.keys(items).length === 0) { 238 return { ok: true, merged_count: 0, message: 'No items to merge' }; 239 } 240 241 try { 242 // Use new endpoint that handles localStorage format 243 const response = await fetch(`${this.apiBase}/merge-localStorage.php`, { 244 method: 'POST', 245 headers: { 246 'Content-Type': 'application/json', 247 }, 248 credentials: 'same-origin', 249 body: JSON.stringify({ 250 items: items // Send localStorage format directly 251 }) 252 }); 253 254 const result = await response.json(); 255 256 if (result.ok) { 257 // Clear localStorage cart after successful merge 258 this.clear(); 259 260 // Update cart counter 261 if (window.updateCartCounter) { 262 try { 263 await window.updateCartCounter(); 264 } catch (e) { 265 // Silent error handling 266 } 267 } 268 269 // Dispatch merge completion event 270 window.dispatchEvent(new CustomEvent('guestCartMerged', { 271 detail: result 272 })); 273 274 return result; 275 } else { 276 throw new Error(result.error || 'Failed to merge cart'); 277 } 278 279 } catch (error) { 280 throw error; 281 } 282 } 283 284 /** 285 * Export cart data (for debugging or backup) 286 */ 287 export() { 288 return { 289 ...this.cart, 290 summary: this.getSummary() 291 }; 292 } 293 294 /** 295 * Import cart data (for restore or testing) 296 */ 297 import(cartData) { 298 if (cartData && typeof cartData === 'object' && cartData.items) { 299 this.cart = { 300 items: cartData.items, 301 updated_at: cartData.updated_at || Date.now(), 302 version: cartData.version || '1.0' 303 }; 304 305 this.saveToStorage(); 306 return true; 307 } 308 309 return false;
310 } 311} 312 313// Create global instance 314window.guestCart = new GuestCart(); 315 316// Auto-merge on page load if user is logged in 317document.addEventListener('DOMContentLoaded', async function() { 318 // Multiple ways to check if user is logged in 319 const isLoggedIn = document.querySelector('meta[name="user-logged-in"]')?.content === 'true' || 320 document.body.classList.contains('logged-in') || 321 window.userLoggedIn === true || 322 document.body.dataset.userLoggedIn === 'true'; 323 324 if (isLoggedIn && !window.guestCart.isEmpty()) { 325 // Small delay to ensure page is fully loaded 326 setTimeout(async () => { 327 try { 328 const result = await window.guestCart.mergeToDatabase(); 329 } catch (error) { 330 // Silent error handling 331 } 332 }, 500); 333 } 334}); 335 336// Also listen for login events (if login happens via AJAX) 337window.addEventListener('userLoggedIn', async function() { 338 if (!window.guestCart.isEmpty()) { 339 try { 340 await window.guestCart.mergeToDatabase(); 341 } catch (error) { 342 // Silent error handling 343 } 344 } 345});
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.