PageSourceSearch

https://wikitree.org/js/marketplace-listing.js?v=1790286639

js wikitree.org collected 2026-09-24 21:50:45 UTC 23,450 bytes, 549 lines download raw bytes

1(function() {
2    'use strict';
3
4    // Theme toggle
5    var themeToggle = document.getElementById('themeToggle');
6    var iconLight = document.getElementById('themeIconLight');
7    var iconDark = document.getElementById('themeIconDark');
8    var html = document.documentElement;
9
10    function getEffectiveTheme() {
11        var theme = html.getAttribute('data-theme');
12        if (theme === 'dark') return 'dark';
13        if (theme === 'light') return 'light';
14        // auto — check system preference
15        return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
16    }
17
18    function updateThemeIcons() {
19        var effective = getEffectiveTheme();
20        // Show sun icon when dark (click to go light), moon when light (click to go dark)
21        iconLight.style.display = effective === 'dark' ? 'inline' : 'none';
22        iconDark.style.display = effective === 'light' ? 'inline' : 'none';
23    }
24
25    // Restore saved preference
26    var saved = localStorage.getItem('mkt_theme');
27    if (saved) {
28        html.setAttribute('data-theme', saved);
29    }
30    updateThemeIcons();
31
32    if (themeToggle) {
33        themeToggle.addEventListener('click', function() {
34            var effective = getEffectiveTheme();
35            var next = effective === 'dark' ? 'light' : 'dark';
36            html.setAttribute('data-theme', next);
37            localStorage.setItem('mkt_theme', next);
38            updateThemeIcons();
39        });
40    }
41
42    // Listen for system theme changes when in auto mode
43    window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function() {
44        if (html.getAttribute('data-theme') === 'auto') {
45            updateThemeIcons();
46        }
47    });
48
49    // Favorite / Save toggle — persists to the server (no login required; anon
50    // saves are attributed to a first-party cookie the endpoint sets).
51    var favBtn = document.getElementById('favoriteBtn');
52    if (favBtn) {
53        var favListingId = parseInt(favBtn.getAttribute('data-listing-id'), 10) || 0;
54
55        var paintFav = function(saved) {
56            favBtn.innerHTML = '<i class="fa' + (saved ? 's' : 'r') + ' fa-heart"></i> ' + (saved ? 'Saved' : 'Save');
57        };
58
59        favBtn.addEventListener('click', function() {
60            if (!favListingId || favBtn.dataset.busy === '1') return;
61            favBtn.dataset.busy = '1';
62            var wasSaved = favBtn.querySelector('i').classList.contains('fas');
63            paintFav(!wasSaved); // optimistic
64
65            fetch('/api/marketplace/favorite', {
66                method: 'POST',
67                headers: { 'Content-Type': 'application/json' },
68                credentials: 'same-origin',
69                body: JSON.stringify({ listing_id: favListingId })
70            })
71            .then(function(r) { return r.json(); })
72            .then(function(d) {
73                if (!d || !d.success) { paintFav(wasSaved); return; } // revert on failure
74                paintFav(d.saved);
75            })
76            .catch(function() { paintFav(wasSaved); })
77            .finally(function() { favBtn.dataset.busy = '0'; });
78        });
79    }
80
81    // Alert subscription
82    var alertBtn = document.getElementById('alertBtn');
83    if (alertBtn) {
84        alertBtn.addEventListener('click', function() {
85            // TODO: POST to /api/marketplace/price_alert.php
86            alert('Price alert subscription coming soon.');
87        });
88    }
89
90    // Buy Now — starts Stripe Checkout. Guest-friendly: no login required.
91    // If the buyer is on a parked seller domain (non-canonical host), that
92    // host doesn't serve the /api endpoints, so we redirect to the canonical
93    // listing URL with autobuy=1 and auto-fire the flow once it loads.
94    function startBuyNowCheckout(triggerBtn, channel) {
95        if (!priceCard) return;
96
97        if (priceCard.dataset.buyNowEnabled !== '1') {
98            alert('Buy Now is not available — the seller has not completed payout setup.');
99            return;
100        }
101
102        var canonicalUrl = priceCard.dataset.canonicalUrl;
103        try {
104            if (canonicalUrl && new URL(canonicalUrl).host !== window.location.host) {
105                window.location = canonicalUrl + (canonicalUrl.indexOf('?') === -1 ? '?' : '&') + 'autobuy=1';
106                return;
107            }
108        } catch (e) { /* ignore parse errors */ }
109
110        var listingId = parseInt(priceCard.dataset.listingId, 10);
111        if (!listingId) return;
112
113        var origLabel = triggerBtn ? triggerBtn.innerHTML : '';
114        if (triggerBtn) {
115            triggerBtn.disabled = true;
116            triggerBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Redirecting...';
117        }
118
119        fetch('/api/marketplace/checkout', {
120            method: 'POST',
121            headers: { 'Content-Type': 'application/json' },
122            body: JSON.stringify({ listing_id: listingId, channel: channel || 'card' })
123        }).then(function(r) {
124            return r.json().then(function(data) { return { ok: r.ok, data: data }; });
125        }).then(function(res) {
126            if (res.ok && res.data.success && res.data.checkout_url) {
127                window.location = res.data.checkout_url;
128                return;
129            }
130            alert(res.data && res.data.error ? res.data.error : 'Could not start checkout. Please try again.');
131            if (triggerBtn) {
132                triggerBtn.disabled = false;
133                triggerBtn.innerHTML = origLabel;
134            }
135        }).catch(function() {
136            alert('Network error. Please try again.');
137            if (triggerBtn) {
138                triggerBtn.disabled = false;
139                triggerBtn.innerHTML = origLabel;
140            }
141        });
142    }
143
144    var buyBtn = document.getElementById('buyNowBtn');
145    if (buyBtn && !buyBtn.disabled) {
146        buyBtn.addEventListener('click', function() { startBuyNowCheckout(buyBtn, 'card'); });
147    }
148
149    // Alipay (zh pages): familiar rail for Chinese buyers
150    var alipayBtn = document.getElementById('payAlipayBtn');
151    if (alipayBtn && !alipayBtn.disabled) {
152        alipayBtn.addEventListener('click', function() { startBuyNowCheckout(alipayBtn, 'alipay'); });
153    }
154
155    // Crypto/stablecoins: instant, no surcharge
156    var cryptoBtn = document.getElementById('payCryptoBtn');
157    if (cryptoBtn && !cryptoBtn.disabled) {
158        cryptoBtn.addEventListener('click', function() { startBuyNowCheckout(cryptoBtn, 'crypto'); });
159    }
160
161    // US bank debit (ACH): no card surcharge; settles in a few business days
162    var bankBtn = document.getElementById('payBankBtn');
163    if (bankBtn && !bankBtn.disabled) {
164        bankBtn.addEventListener('click', function() { startBuyNowCheckout(bankBtn, 'bank'); });
165    }
166
167    // ---- Buy via Escrow.com ----
168    // Delegates to the shared modal in /js/marketplace-escrow-modal.js, which
169    // collects the buyer's Escrow.com email (with pre-fill / localStorage),
170    // then POSTs to escrow-checkout.php and redirects to the agreement URL.
171    function startEscrowCheckout(triggerBtn) {
172        if (!priceCard) return;
173
174        // Non-canonical host redirect — same pattern as Stripe Buy Now.
175        var canonicalUrl = priceCard.dataset.canonicalUrl;
176        try {
177            if (canonicalUrl && new URL(canonicalUrl).host !== window.location.host) {
178                window.location = canonicalUrl + (canonicalUrl.indexOf('?') === -1 ? '?' : '&') + 'autoescrow=1';
179                return;
180            }
181        } catch (e) { /* ignore */ }
182
183        var listingId = parseInt(priceCard.dataset.listingId, 10);
184        if (!listingId) return;
185
186        if (typeof window.openEscrowModal === 'function') {
187            window.openEscrowModal({
188                mode: 'listing',
189                listingId: listingId,
190                triggerBtn: triggerBtn
191            });
192        } else {
193            // Defensive fallback if the modal module wasn't loaded for some
194            // reason — should never hit in production.
195            alert('Escrow.com checkout is temporarily unavailable. Please try again or use card payment.');
196        }
197    }
198
199    var escrowBtn = document.getElementById('buyEscrowBtn');
200    if (escrowBtn && !escrowBtn.disabled) {
201        escrowBtn.addEventListener('click', function() { startEscrowCheckout(escrowBtn); });
202    }
203
204    // ---- LTO Start Lease (mirrors Buy Now but hits lto-checkout.php) ----
205    function startLtoCheckout(triggerBtn) {
206        if (!priceCard) return;
207
208        var canonicalUrl = priceCard.dataset.canonicalUrl;
209        try {
210            if (canonicalUrl && new URL(canonicalUrl).host !== window.location.host) {
211                window.location = canonicalUrl + (canonicalUrl.indexOf('?') === -1 ? '?' : '&') + 'autolto=1';
212                return;
213            }
214        } catch (e) { /* ignore */ }
215
216        var listingId = parseInt(priceCard.dataset.listingId, 10);
217        if (!listingId) return;
218
219        var origLabel = triggerBtn ? triggerBtn.innerHTML : '';
220        if (triggerBtn) {
221            triggerBtn.disabled = true;
222            triggerBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Redirecting...';
223        }
224
225        fetch('/api/marketplace/lto-checkout', {
226            method: 'POST',
227            headers: { 'Content-Type': 'application/json' },
228            body: JSON.stringify({ listing_id: listingId })
229        }).then(function(r) {
230            return r.json().then(function(data) { return { ok: r.ok, data: data }; });
231        }).then(function(res) {
232            if (res.ok && res.data.success && res.data.checkout_url) {
233                window.location = res.data.checkout_url;
234                return;
235            }
236            alert(res.data && res.data.error ? res.data.error : 'Could not start lease. Please try again.');
237            if (triggerBtn) { triggerBtn.disabled = false; triggerBtn.innerHTML = origLabel; }
238        }).catch(function() {
239            alert('Network error. Please try again.');
240            if (triggerBtn) { triggerBtn.disabled = false; triggerBtn.innerHTML = origLabel; }
241        });
242    }
243
244    var ltoBtn = document.getElementById('ltoStartBtn');
245    if (ltoBtn && !ltoBtn.disabled) {
246        ltoBtn.addEventListener('click', function() { startLtoCheckout(ltoBtn); });
247    }
248
249    // Auto-fire flows when landed with ?autobuy=1 / ?autolto=1 (from a parked-domain handoff).
250    function stripParam(name) {
251        try {
252            var pattern = new RegExp('([?&])' + name + '=1&?');
253            var cleanUrl = window.location.pathname + window.location.search.replace(pattern, '$1').replace(/[?&]$/, '') + window.location.hash;
254            window.history.replaceState({}, '', cleanUrl);
255        } catch (e) { /* ignore */ }
256    }
257    if (buyBtn && !buyBtn.disabled && /[?&]autobuy=1\b/.test(window.location.search)) {
258        stripParam('autobuy');
259        startBuyNowCheckout(buyBtn);
260    }
261    if (ltoBtn && !ltoBtn.disabled && /[?&]autolto=1\b/.test(window.location.search)) {
262        stripParam('autolto');
263        startLtoCheckout(ltoBtn);
264    }
265
266    // ---- Make Offer / Inquiry modal ----
267    var priceCard = document.querySelector('[data-card="price"]');
268    var offerModal = document.getElementById('offerModal');
269
270    function openOfferModal(mode) {
271        if (!priceCard || !offerModal) return;
272
273        // If served on a non-canonical host (seller's parked domain via catchall),
274        // show a handoff interstitial before bouncing to appraise.net so the
275        // origin change isn't a surprise to the buyer.
276        var canonicalUrl = priceCard.dataset.canonicalUrl;
277        if (canonicalUrl) {
278            try {
279                var canonicalHost = new URL(canonicalUrl).host;
280                if (canonicalHost && canonicalHost !== window.location.host) {
281                    openHandoffModal(canonicalUrl, mode);
282                    return;
283                }
284            } catch (e) { /* ignore parse errors */ }
285        }
286
287        if (priceCard.dataset.isLoggedIn !== '1') {
288            var returnTo = (canonicalUrl || window.location.href);
289            window.location = 'https://appraise.net/login?redirect=' + encodeURIComponent(returnTo);
290            return;
291        }
292
293        var isInquiry = mode === 'inquiry';
294        var domain = priceCard.dataset.domain || 'this domain';
295        var amountField = document.getElementById('offerAmountField');
296        var msgInput = document.getElementById('offerMessageInput');
297        var amountInput = document.getElementById('offerAmountInput');
298
299        document.getElementById('offerModalTitle').textContent = isInquiry ? 'Inquire About Price' : 'Make an Offer';
300        document.getElementById('offerModalSub').textContent = isInquiry
301            ? 'Send a question to the seller of ' + domain + '.'
302            : 'Submit an offer for ' + domain + '. You choose how long it stays open.';
303        document.getElementById('offerSubmitLabel').textContent = isInquiry ? 'Send Inquiry' : 'Send Offer';
304
305        amountField.hidden = isInquiry;
306        var expiryField = document.getElementById('offerExpiryField');
307        if (expiryField) expiryField.hidden = isInquiry;
308        amountInput.value = '';
309        msgInput.value = '';
310        if (isInquiry) {
311            msgInput.placeholder = 'What would you like to ask the seller?';
312        } else {
313            msgInput.placeholder = 'Add a note for the seller...';
314        }
315
316        var err = document.getElementById('offerModalError');
317        err.hidden = true; err.textContent = '';
318
319        offerModal.hidden = false;
320        offerModal.dataset.mode = mode;
321        setTimeout(function() {
322            (isInquiry ? msgInput : amountInput).focus();
323        }, 50);
324    }
325
326    function closeOfferModal() {
327        if (offerModal) offerModal.hidden = true;
328    }
329
330    function submitOffer() {
331        if (!offerModal || !priceCard) return;
332        var mode = offerModal.dataset.mode;
333        var isInquiry = mode === 'inquiry';
334        var listingId = parseInt(priceCard.dataset.listingId, 10);
335        var amountInput = document.getElementById('offerAmountInput');
336        var msgInput = document.getElementById('offerMessageInput');
337        var submitBtn = document.getElementById('offerSubmitBtn');
338        var err = document.getElementById('offerModalError');
339        err.hidden = true; err.textContent = '';
340
341        var amount = isInquiry ? 0 : parseFloat(amountInput.value);
342        var message = msgInput.value.trim();
343
344        if (!isInquiry && (!amount || amount <= 0)) {
345            err.textContent = 'Enter a valid offer amount.';
346            err.hidden = false;
347            return;
348        }
349        var minOffer = parseFloat(priceCard.dataset.minOfferPrice) || 0;
350        if (!isInquiry && minOffer > 0 && amount < minOffer) {
351            err.textContent = 'Minimum offer is $' + minOffer.toLocaleString();
352            err.hidden = false;
353            return;
354        }
355        if (isInquiry && !message) {
356            err.textContent = 'Please type your question for the seller.';
357            err.hidden = false;
358            return;
359        }
360
361        submitBtn.disabled = true;
362        var origLabel = document.getElementById('offerSubmitLabel').textContent;
363        document.getElementById('offerSubmitLabel').textContent = 'Sending...';
364
365        fetch('/api/marketplace/deal', {
366            method: 'POST',
367            headers: { 'Content-Type': 'application/json' },
368            body: JSON.stringify({
369                action: 'create_offer',
370                listing_id: listingId,
371                amount: amount,
372                message: message,
373                inquiry: isInquiry,
374                expiry_days: parseInt((document.getElementById('offerExpiryInput') || {}).value, 10) || 14
375            })
376        }).then(function(r) { return r.json(); }).then(function(data) {
377            if (data.success && data.thread_token) {
378                window.location = '/marketplace/deal/' + data.thread_token;
379            } else {
380                err.textContent = data.error || 'Could not submit. Please try again.';
381                err.hidden = false;
382                submitBtn.disabled = false;
383                document.getElementById('offerSubmitLabel').textContent = origLabel;
384            }
385        }).catch(function() {
386            err.textContent = 'Network error. Please try again.';
387            err.hidden = false;
388            submitBtn.disabled = false;
389            document.getElementById('offerSubmitLabel').textContent = origLabel;
390        });
391    }
392
393    // Delegated handlers — survive artist-tool rebuilds of priceCardContent
394    document.addEventListener('click', function(e) {
395        if (!e.target.closest) return;
396        if (e.target.closest('#makeOfferBtn')) { openOfferModal('offer'); return; }
397        if (e.target.closest('#inquireBtn')) { openOfferModal('inquiry'); return; }
398        if (e.target.closest('.mobile-cta .btn-cta') && priceCard) {
399            var lt = priceCard.dataset.listingType;
400            // buy_now_or_offer's mobile CTA shows "Buy Now" as the primary action,
401            // so route it through the BIN flow just like a buy_now listing.
402            if (lt === 'buy_now' || lt === 'buy_now_or_offer') {
403                var bn = document.getElementById('buyNowBtn');
404                if (bn && !bn.disabled) { startBuyNowCheckout(bn); }
405                else if (lt === 'buy_now_or_offer') { openOfferModal('offer'); }
406                return;
407            }
408            if (lt === 'make_offer') { openOfferModal('offer'); return; }
409            if (lt === 'inquiry') { openOfferModal('inquiry'); return; }
410            if (lt === 'lease_to_own') { var lto = document.getElementById('ltoStartBtn'); if (lto) lto.click(); }
411        }
412    });
413
414    if (offerModal) {
415        offerModal.querySelectorAll('[data-offer-close]').forEach(function(el) {
416            el.addEventListener('click', closeOfferModal);
417        });
418        document.getElementById('offerSubmitBtn').addEventListener('click', submitOffer);
419        document.addEventListener('keydown', function(e) {
420            if (e.key === 'Escape' && !offerModal.hidden) closeOfferModal();
421        });
422    }
423
424    // Handoff modal (non-canonical hosts)
425    var handoffModal = document.getElementById('handoffModal');
426
427    function openHandoffModal(canonicalUrl, mode) {
428        if (!handoffModal) { window.location = canonicalUrl; return; }
429        var isInquiry = mode === 'inquiry';
430        document.getElementById('handoffTitle').textContent = isInquiry ? 'Continue on Appraise.net' : 'Continue on Appraise.net';
431        document.getElementById('handoffSub').innerHTML = isInquiry
432            ? "Inquiries are handled on <strong>Appraise.net Marketplace</strong>, where you can message the seller securely. You'll be taken there now and can return any time."
433            : "Offers are negotiated and paid securely on <strong>Appraise.net Marketplace</strong>. You'll be taken there to complete your offer — and you can return here any time.";
434        document.getElementById('handoffContinueBtn').setAttribute('href', canonicalUrl);
435        handoffModal.hidden = false;
436    }
437
438    function closeHandoffModal() {
439        if (handoffModal) handoffModal.hidden = true;
440    }
441
442    if (handoffModal) {
443        handoffModal.querySelectorAll('[data-handoff-close]').forEach(function(el) {
444            el.addEventListener('click', closeHandoffModal);
445        });
446        document.addEventListener('keydown', function(e) {
447            if (e.key === 'Escape' && !handoffModal.hidden) closeHandoffModal();
448        });
449    }
450
451    // Seller: Toggle publish
452    var publishBtn = document.getElementById('togglePublishBtn');
453    if (publishBtn) {
454        publishBtn.addEventListener('click', function() {
455            // TODO: POST to toggle status
456            alert('Publish toggle coming soon.');
457        });
458    }
459
460    // Seller: Delete
461    var deleteBtn = document.getElementById('deleteListingBtn');
462    if (deleteBtn) {
463        deleteBtn.addEventListener('click', function() {
464            if (confirm('Are you sure you want to delete this listing? This cannot be undone.')) {
465                // TODO: POST to delete
466                alert('Delete coming soon.');
467            }
468        });
469    }
470
471    // Left sidebar visibility on 1280px+
472    var leftSidebar = document.querySelector('.sticky-sidebar');
473    if (leftSidebar && window.innerWidth >= 1280) {
474        leftSidebar.style.display = 'block';
475    }
476    window.addEventListener('resize', function() {
477        if (leftSidebar && window.innerWidth >= 1280) {
478            leftSidebar.style.display = 'block';
479        }
480    });
481    // Certificate modal
482    var certModal = document.getElementById('certModal');
483    if (certModal) {
484        var certIframe = document.getElementById('certIframe');
485        var certBackdrop = certModal.querySelector('.cert-modal-backdrop');
486        var certClose = certModal.querySelector('.cert-modal-close');
487
488        var certLoading = certModal.querySelector('.cert-modal-loading');
489
490        document.querySelectorAll('.cert-link').forEach(function(link) {
491            link.addEventListener('click', function(e) {
492                e.preventDefault();
493                var certId = this.dataset.certId;
494                if (certId) {
495                    if (certLoading) certLoading.style.display = 'flex';
496                    certIframe.style.opacity = '0';
497                    certIframe.src = 'https://appraise.net/certificate?id=' + encodeURIComponent(certId);
498                    certModal.classList.add('open');
499                }
500            });
501        });
502
503        certIframe.addEventListener('load', function() {
504            if (certLoading) certLoading.style.display = 'none';
505            certIframe.style.opacity = '1';
506        });
507
508        function closeCertModal() {
509            certModal.classList.remove('open');
510            certIframe.src = '';
511            certIframe.style.opacity = '0';
512        }
513
514        certBackdrop.addEventListener('click', closeCertModal);
515        certClose.addEventListener('click', closeCertModal);
516        document.addEventListener('keydown', function(e) {
517            if (e.key === 'Escape' && certModal.classList.contains('open')) closeCertModal();
518        });
519    }
520})();
521
522/* ---- Nav brand-logo lightbox: the badge shows a cropped strip of the logo; click it to
523        see the full logo in an overlay (X, backdrop, the logo itself, or Esc all close). ---- */
524(function() {
525    var badge = document.querySelector('.nav-brand-logo.logo-zoomable');
526    var box = document.getElementById('logo-lightbox');
527    if (!badge || !box) return;
528    var img = box.querySelector('.logo-lightbox-img');
529
530    function openLogo() {
531        img.src = badge.getAttribute('data-logo-src') || '';
532        img.alt = badge.getAttribute('data-logo-alt') || '';
533        box.hidden = false;
534        document.addEventListener('keydown', onKey);
535    }
536    function closeLogo() {
537        box.hidden = true;
538        img.removeAttribute('src');
539        document.removeEventListener('keydown', onKey);
540    }
541    function onKey(e) { if (e.key === 'Escape') closeLogo(); }
542
543    badge.addEventListener('click', function(e) { e.preventDefault(); openLogo(); });
544    badge.addEventListener('keydown', function(e) {
545        if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openLogo(); }
546    });
547    // Any click on the overlay (backdrop, close button, or the enlarged logo) dismisses it.
548    box.addEventListener('click', closeLogo);
549})();

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.