PageSourceSearch

https://tigerlineferry.com/assets/js/app/addon-cart.js?v=20261010075554

js tigerlineferry.com collected 2026-10-10 00:56:07 UTC 6,976 bytes, 192 lines download raw bytes

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, '&lt;')
40        .replace(/>/g, '&gt;')
41        .replace(/"/g, '&quot;')
42        .replace(/'/g, '&#39;');
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.