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.