1(function () { 2 'use strict'; 3 4 const csrfToken = () => { 5 const meta = document.querySelector('meta[name="csrf-token"]'); 6 return meta ? meta.getAttribute('content') : ''; 7 }; 8 9 const formatMoney = (value) => { 10 const number = Number(value || 0); 11 return number.toLocaleString('en-US', { 12 minimumFractionDigits: 2, 13 maximumFractionDigits: 2, 14 }); 15 }; 16 17 const request = async (url, payload = {}, method = 'POST') => { 18 const response = await fetch(url, { 19 method: method, 20 headers: { 21 'Content-Type': 'application/json', 22 'Accept': 'application/json', 23 'X-CSRF-TOKEN': csrfToken(), 24 'X-Requested-With': 'XMLHttpRequest', 25 }, 26 credentials: 'same-origin', 27 body: method === 'GET' ? undefined : JSON.stringify(payload), 28 }); 29 30 const data = await response.json().catch(() => ({})); 31 if (! response.ok || data.result === false) { 32 throw new Error(data.message || 'Cart request failed'); 33 } 34 return data; 35 }; 36 37 const escapeHtml = (value) => String(value) 38 .replace(/&/g, '&') 39 .replace(/</g, '<') 40 .replace(/>/g, '>') 41 .replace(/"/g, '"') 42 .replace(/'/g, '''); 43 44 const renderDropdownList = (items) => { 45 const list = document.querySelector('[data-addon-cart-dropdown-list]'); 46 if (! list) return; 47 48 if (! items.length) { 49 list.innerHTML = '<div class="addon-cart-dropdown__empty">No addons yet. Browse activities to add items.</div>'; 50 return; 51 } 52 53 const visible = items.slice(0, 5); 54 let html = visible.map((item) => ( 55 '<a href="' + item.url + '" class="addon-cart-dropdown__item">' 56 + '<img src="' + (item.image || (window.AddonCartRoutes.noImage || '')) + '" alt="">' 57 + '<div class="addon-cart-dropdown__text">' 58 + '<div class="addon-cart-dropdown__name">' + escapeHtml(item.name) + '</div>' 59 + '<div class="addon-cart-dropdown__meta">' 60 + formatMoney(item.amount).replace(/\.00$/, '') + ' THB / person' 61 + '</div></div></a>' 62 )).join(''); 63 64 if (items.length > 5) { 65 html += '<div class="addon-cart-dropdown__more text-muted small px-3 pb-2">+' 66 + (items.length - 5) + ' more item(s)</div>'; 67 } 68 69 list.innerHTML = html; 70 }; 71 72 const updateRowFromSummary = (summary) => { 73 (summary.items || []).forEach((item) => { 74 const row = document.querySelector('[data-cart-key="' + CSS.escape(item.key) + '"]'); 75 if (! row) return; 76 77 const lineTotal = row.querySelector('[data-line-total]'); 78 if (lineTotal) { 79 lineTotal.textContent = formatMoney(Number(item.amount || 0)); 80 } 81 }); 82 }; 83 84 const updateUi = (summary) => { 85 const count = Number(summary.count || 0); 86 const total = formatMoney(summary.total || 0); 87 88 document.querySelectorAll('[data-addon-cart-badge]').forEach((el) => { 89 el.textContent = String(count); 90 el.classList.toggle('is-visible', count > 0); 91 }); 92 93 document.querySelectorAll('[data-addon-cart-total]').forEach((el) => { 94 el.textContent = total; 95 }); 96 97 document.querySelectorAll('[data-addon-cart-count-label]').forEach((el) => { 98 el.textContent = String(count); 99 }); 100 101 updateRowFromSummary(summary); 102 renderDropdownList(summary.items || []); 103 }; 104 105 const toast = (message, isError) => { 106 let el = document.querySelector('[data-addon-cart-toast]'); 107 if (! el) { 108 el = document.createElement('div'); 109 el.setAttribute('data-addon-cart-toast', ''); 110 el.style.cssText = [ 111 'position:fixed', 112 'right:16px', 113 'bottom:16px', 114 'z-index:9999', 115 'padding:10px 14px', 116 'border-radius:10px', 117 'color:#fff', 118 'font-weight:700', 119 'box-shadow:0 10px 24px rgba(0,0,0,.18)', 120 'transition:opacity .2s ease', 121 ].join(';'); 122 document.body.appendChild(el); 123 } 124 125 el.style.background = isError ? '#c0392b' : '#0a3961'; 126 el.textContent = message; 127 el.style.opacity = '1'; 128 window.clearTimeout(el._timer); 129 el._timer = window.setTimeout(() => { 130 el.style.opacity = '0'; 131 }, 2200); 132 }; 133 134 const routes = window.AddonCartRoutes || {}; 135 136 document.addEventListener('click', async (event) => { 137 const addBtn = event.target.closest('[data-addon-cart]'); 138 if (addBtn) { 139 event.preventDefault(); 140 addBtn.disabled = true; 141 try { 142 const payload = { 143 type: addBtn.getAttribute('data-addon-type'), 144 id: addBtn.getAttribute('data-addon-id'), 145 code: addBtn.getAttribute('data-addon-code') || '', 146 name: addBtn.getAttribute('data-addon-name') || '', 147 amount: addBtn.getAttribute('data-addon-amount') || 0, 148 image: addBtn.getAttribute('data-addon-image') || '', 149 qty: 1, 150 }; 151 const result = await request(routes.add, payload); 152 updateUi(result.data || {}); 153 toast('Added to addon cart'); 154 } catch (error) { 155 toast(error.message || 'Could not add to cart', true); 156 } finally { 157 addBtn.disabled = false;
158 } 159 return; 160 } 161 162 const removeBtn = event.target.closest('[data-addon-cart-remove]'); 163 if (removeBtn) { 164 const row = removeBtn.closest('[data-cart-key]'); 165 if (! row) return; 166 try { 167 const result = await request(routes.remove, { key: row.getAttribute('data-cart-key') }); 168 updateUi(result.data || {}); 169 if ((result.data?.count || 0) === 0) { 170 window.location.reload(); 171 return; 172 } 173 row.remove(); 174 toast('Removed from cart'); 175 } catch (error) { 176 toast(error.message || 'Could not remove item', true); 177 } 178 return; 179 } 180 181 const clearBtn = event.target.closest('[data-addon-cart-clear]'); 182 if (clearBtn) { 183 if (! window.confirm('Clear all addon items from cart?')) return; 184 try { 185 await request(routes.clear, {}); 186 window.location.reload(); 187 } catch (error) { 188 toast(error.message || 'Could not clear cart', true); 189 } 190 } 191 }); 192})();
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.