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 // QuietClose: the buyer's "keep this purchase private" box (rendered only where it is offered). 95 // qcSuffix() carries the choice across a parked-domain or login bounce; the page re-checks the box from ?qc=1. 96 function qcWanted() { var c = document.getElementById('qcOptIn'); return !!(c && c.checked && !c.disabled); } 97 function qcSuffix() { return qcWanted() ? '&qc=1' : ''; } 98 99 function startBuyNowCheckout(triggerBtn, channel) { 100 if (!priceCard) return; 101 102 if (priceCard.dataset.buyNowEnabled !== '1') { 103 alert('Buy Now is not available â the seller has not completed payout setup.'); 104 return; 105 } 106 107 var canonicalUrl = priceCard.dataset.canonicalUrl; 108 try { 109 if (canonicalUrl && new URL(canonicalUrl).host !== window.location.host) { 110 window.location = canonicalUrl + (canonicalUrl.indexOf('?') === -1 ? '?' : '&') + 'autobuy=1' + qcSuffix(); 111 return; 112 } 113 } catch (e) { /* ignore parse errors */ } 114 115 var listingId = parseInt(priceCard.dataset.listingId, 10); 116 if (!listingId) return; 117 118 var origLabel = triggerBtn ? triggerBtn.innerHTML : ''; 119 if (triggerBtn) { 120 triggerBtn.disabled = true; 121 triggerBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Redirecting...'; 122 } 123 124 fetch('/api/marketplace/checkout', { 125 method: 'POST', 126 headers: { 'Content-Type': 'application/json' }, 127 body: JSON.stringify({ listing_id: listingId, channel: channel || 'card', quietclose: qcWanted() }) 128 }).then(function(r) { 129 return r.json().then(function(data) { return { ok: r.ok, data: data }; }); 130 }).then(function(res) { 131 if (res.ok && res.data.success && res.data.checkout_url) { 132 window.location = res.data.checkout_url; 133 return; 134 } 135 alert(res.data && res.data.error ? res.data.error : 'Could not start checkout. Please try again.'); 136 if (triggerBtn) { 137 triggerBtn.disabled = false;
138 triggerBtn.innerHTML = origLabel; 139 } 140 }).catch(function() { 141 alert('Network error. Please try again.'); 142 if (triggerBtn) { 143 triggerBtn.disabled = false; 144 triggerBtn.innerHTML = origLabel; 145 } 146 }); 147 } 148 149 var buyBtn = document.getElementById('buyNowBtn'); 150 if (buyBtn && !buyBtn.disabled) { 151 buyBtn.addEventListener('click', function() { startBuyNowCheckout(buyBtn, 'card'); }); 152 } 153 154 // Alipay (zh pages): familiar rail for Chinese buyers 155 var alipayBtn = document.getElementById('payAlipayBtn'); 156 if (alipayBtn && !alipayBtn.disabled) { 157 alipayBtn.addEventListener('click', function() { startBuyNowCheckout(alipayBtn, 'alipay'); }); 158 } 159 160 // Crypto/stablecoins: instant, no surcharge 161 var cryptoBtn = document.getElementById('payCryptoBtn'); 162 if (cryptoBtn && !cryptoBtn.disabled) { 163 cryptoBtn.addEventListener('click', function() { startBuyNowCheckout(cryptoBtn, 'crypto'); }); 164 } 165 166 // Bitcoin (BTCPay on our side): our own invoice page. Login required - a verified email is the only 167 // identity we ask of a Bitcoin buyer - so a signed-out click goes to login and back to this listing. 168 function startBtcCheckout(triggerBtn) { 169 if (!priceCard) return; 170 var canonicalUrl = priceCard.dataset.canonicalUrl; 171 try { 172 if (canonicalUrl && new URL(canonicalUrl).host !== window.location.host) { 173 window.location = canonicalUrl + (canonicalUrl.indexOf('?') === -1 ? '?' : '&') + 'autobtc=1' + qcSuffix(); 174 return; 175 } 176 } catch (e) { /* ignore */ } 177 var listingId = parseInt(priceCard.dataset.listingId, 10); 178 if (!listingId) return; 179 var origLabel = triggerBtn ? triggerBtn.innerHTML : ''; 180 if (triggerBtn) { triggerBtn.disabled = true; triggerBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Creating invoice...'; } 181 fetch('/api/marketplace/btc-checkout', { 182 method: 'POST', 183 headers: { 'Content-Type': 'application/json' }, 184 body: JSON.stringify({ listing_id: listingId, quietclose: qcWanted() }) 185 }).then(function(r) { 186 return r.json().then(function(data) { return { ok: r.ok, data: data }; }); 187 }).then(function(res) { 188 if (res.ok && res.data.success && res.data.pay_url) { window.location = res.data.pay_url; return; } 189 if (res.data && res.data.login_required) { 190 window.location = 'https://appraise.net/login?redirect=' + encodeURIComponent(window.location.pathname + '?autobtc=1' + qcSuffix()); 191 return; 192 } 193 alert(res.data && res.data.error ? res.data.error : 'Could not start Bitcoin checkout. Please try again.'); 194 if (triggerBtn) { triggerBtn.disabled = false; triggerBtn.innerHTML = origLabel; } 195 }).catch(function() { 196 alert('Network error. Please try again.'); 197 if (triggerBtn) { triggerBtn.disabled = false; triggerBtn.innerHTML = origLabel; } 198 }); 199 } 200 // PayPal (Orders v2): our endpoint creates the order and we send the buyer to PayPal's approval page. 201 function startPaypalCheckout(triggerBtn) { 202 if (!priceCard) return; 203 var canonicalUrl = priceCard.dataset.canonicalUrl; 204 try { 205 if (canonicalUrl && new URL(canonicalUrl).host !== window.location.host) { 206 window.location = canonicalUrl + (canonicalUrl.indexOf('?') === -1 ? '?' : '&') + 'autopaypal=1' + qcSuffix(); 207 return; 208 } 209 } catch (e) { /* ignore */ } 210 var listingId = parseInt(priceCard.dataset.listingId, 10); 211 if (!listingId) return; 212 var origLabel = triggerBtn ? triggerBtn.innerHTML : ''; 213 if (triggerBtn) { triggerBtn.disabled = true; triggerBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Redirecting to PayPal...'; } 214 fetch('/api/marketplace/paypal-checkout', { 215 method: 'POST', 216 headers: { 'Content-Type': 'application/json' }, 217 body: JSON.stringify({ listing_id: listingId, quietclose: qcWanted() }) 218 }).then(function(r) { 219 return r.json().then(function(data) { return { ok: r.ok, data: data }; }); 220 }).then(function(res) { 221 if (res.ok && res.data.success && res.data.approve_url) { window.location = res.data.approve_url; return; } 222 if (res.data && res.data.login_required) { 223 window.location = 'https://appraise.net/login?redirect=' + encodeURIComponent(window.location.pathname + '?autopaypal=1' + qcSuffix()); 224 return; 225 } 226 alert(res.data && res.data.error ? res.data.error : 'Could not start PayPal checkout. Please try again.'); 227 if (triggerBtn) { triggerBtn.disabled = false; triggerBtn.innerHTML = origLabel; } 228 }).catch(function() { 229 alert('Network error. Please try again.'); 230 if (triggerBtn) { triggerBtn.disabled = false; triggerBtn.innerHTML = origLabel; } 231 }); 232 } 233 var paypalBtn = document.getElementById('payPaypalBtn'); 234 if (paypalBtn && !paypalBtn.disabled) { 235 paypalBtn.addEventListener('click', function() { startPaypalCheckout(paypalBtn); }); 236 if (/[?&]autopaypal=1/.test(window.location.search)) { startPaypalCheckout(paypalBtn); } 237 } 238 239 var btcBtn = document.getElementById('payBtcBtn'); 240 if (btcBtn && !btcBtn.disabled) { 241 btcBtn.addEventListener('click', function() { startBtcCheckout(btcBtn); }); 242 if (/[?&]autobtc=1/.test(window.location.search)) { startBtcCheckout(btcBtn); } 243 } 244 245 // US bank debit (ACH): no card surcharge; settles in a few business days 246 var bankBtn = document.getElementById('payBankBtn'); 247 if (bankBtn && !bankBtn.disabled) { 248 bankBtn.addEventListener('click', function() { startBuyNowCheckout(bankBtn, 'bank'); }); 249 } 250 251 // ---- Buy via Escrow.com ---- 252 // Delegates to the shared modal in /js/marketplace-escrow-modal.js, which 253 // collects the buyer's Escrow.com email (with pre-fill / localStorage), 254 // then POSTs to escrow-checkout.php and redirects to the agreement URL. 255 function startEscrowCheckout(triggerBtn) { 256 if (!priceCard) return; 257 258 // Non-canonical host redirect â same pattern as Stripe Buy Now. 259 var canonicalUrl = priceCard.dataset.canonicalUrl; 260 try { 261 if (canonicalUrl && new URL(canonicalUrl).host !== window.location.host) { 262 window.location = canonicalUrl + (canonicalUrl.indexOf('?') === -1 ? '?' : '&') + 'autoescrow=1' + qcSuffix(); 263 return; 264 } 265 } catch (e) { /* ignore */ } 266 267 var listingId = parseInt(priceCard.dataset.listingId, 10); 268 if (!listingId) return; 269 270 if (typeof window.openEscrowModal === 'function') { 271 window.openEscrowModal({ 272 mode: 'listing', 273 listingId: listingId, 274 triggerBtn: triggerBtn 275 }); 276 } else { 277 // Defensive fallback if the modal module wasn't loaded for some 278 // reason â should never hit in production. 279 alert('Escrow.com checkout is temporarily unavailable. Please try again or use card payment.'); 280 } 281 } 282 283 var escrowBtn = document.getElementById('buyEscrowBtn'); 284 if (escrowBtn && !escrowBtn.disabled) { 285 escrowBtn.addEventListener('click', function() { startEscrowCheckout(escrowBtn); }); 286 } 287 288 // ---- LTO Start Lease (mirrors Buy Now but hits lto-checkout.php) ---- 289 // The lease is a contract: the buyer confirms they have read the lease-to-own terms in a modal first, and 290 // lto-checkout.php refuses to create the transaction without terms_accepted. 291 var ltoTermsModal = document.getElementById('ltoTermsModal'); 292 var ltoTermsAck = document.getElementById('ltoTermsAck'); 293 var ltoTermsContinue = document.getElementById('ltoTermsContinue'); 294 var ltoPendingBtn = null; 295 function openLtoTerms(triggerBtn) { 296 if (!ltoTermsModal) { startLtoCheckout(triggerBtn, true); return; } 297 ltoPendingBtn = triggerBtn; 298 if (ltoTermsAck) { ltoTermsAck.checked = false; } 299 if (ltoTermsContinue) { ltoTermsContinue.disabled = true; } 300 ltoTermsModal.hidden = false;
301 } 302 if (ltoTermsModal) { 303 ltoTermsModal.addEventListener('click', function(e) { if (e.target.closest('[data-lto-close]')) ltoTermsModal.hidden = true; }); 304 if (ltoTermsAck && ltoTermsContinue) { ltoTermsAck.addEventListener('change', function() { ltoTermsContinue.disabled = !ltoTermsAck.checked; }); } 305 if (ltoTermsContinue) { ltoTermsContinue.addEventListener('click', function() { 306 if (!ltoTermsAck || !ltoTermsAck.checked) return; 307 ltoTermsModal.hidden = true; 308 startLtoCheckout(ltoPendingBtn, true); 309 }); } 310 } 311 function startLtoCheckout(triggerBtn, termsAccepted) { 312 if (!priceCard) return; 313 314 var canonicalUrl = priceCard.dataset.canonicalUrl; 315 try { 316 if (canonicalUrl && new URL(canonicalUrl).host !== window.location.host) { 317 window.location = canonicalUrl + (canonicalUrl.indexOf('?') === -1 ? '?' : '&') + 'autolto=1' + qcSuffix(); 318 return; 319 } 320 } catch (e) { /* ignore */ } 321 322 var listingId = parseInt(priceCard.dataset.listingId, 10); 323 if (!listingId) return; 324 325 var origLabel = triggerBtn ? triggerBtn.innerHTML : ''; 326 if (triggerBtn) { 327 triggerBtn.disabled = true; 328 triggerBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Redirecting...'; 329 } 330 331 fetch('/api/marketplace/lto-checkout', { 332 method: 'POST', 333 headers: { 'Content-Type': 'application/json' }, 334 body: JSON.stringify({ listing_id: listingId, terms_accepted: termsAccepted ? 1 : 0, quietclose: qcWanted() }) 335 }).then(function(r) { 336 return r.json().then(function(data) { return { ok: r.ok, data: data }; }); 337 }).then(function(res) { 338 if (res.ok && res.data.success && res.data.checkout_url) { 339 window.location = res.data.checkout_url; 340 return; 341 } 342 alert(res.data && res.data.error ? res.data.error : 'Could not start lease. Please try again.'); 343 if (triggerBtn) { triggerBtn.disabled = false; triggerBtn.innerHTML = origLabel; } 344 }).catch(function() { 345 alert('Network error. Please try again.'); 346 if (triggerBtn) { triggerBtn.disabled = false; triggerBtn.innerHTML = origLabel; } 347 }); 348 } 349 350 var ltoBtn = document.getElementById('ltoStartBtn'); 351 if (ltoBtn && !ltoBtn.disabled) { 352 ltoBtn.addEventListener('click', function() { openLtoTerms(ltoBtn); }); 353 } 354 355 // Auto-fire flows when landed with ?autobuy=1 / ?autolto=1 (from a parked-domain handoff). 356 function stripParam(name) { 357 try { 358 var pattern = new RegExp('([?&])' + name + '=1&?'); 359 var cleanUrl = window.location.pathname + window.location.search.replace(pattern, '$1').replace(/[?&]$/, '') + window.location.hash; 360 window.history.replaceState({}, '', cleanUrl); 361 } catch (e) { /* ignore */ } 362 } 363 if (buyBtn && !buyBtn.disabled && /[?&]autobuy=1\b/.test(window.location.search)) { 364 stripParam('autobuy'); 365 startBuyNowCheckout(buyBtn); 366 } 367 if (ltoBtn && !ltoBtn.disabled && /[?&]autolto=1\b/.test(window.location.search)) { 368 stripParam('autolto'); 369 startLtoCheckout(ltoBtn); 370 } 371 372 // ---- Make Offer / Inquiry modal ---- 373 var priceCard = document.querySelector('[data-card="price"]'); 374 var offerModal = document.getElementById('offerModal'); 375 376 function openOfferModal(mode) { 377 if (!priceCard || !offerModal) return; 378 379 // If served on a non-canonical host (seller's parked domain via catchall), 380 // show a handoff interstitial before bouncing to appraise.net so the 381 // origin change isn't a surprise to the buyer. 382 var canonicalUrl = priceCard.dataset.canonicalUrl; 383 if (canonicalUrl) { 384 try { 385 var canonicalHost = new URL(canonicalUrl).host; 386 if (canonicalHost && canonicalHost !== window.location.host) { 387 openHandoffModal(canonicalUrl, mode); 388 return; 389 } 390 } catch (e) { /* ignore parse errors */ } 391 } 392 393 if (priceCard.dataset.isLoggedIn !== '1') { 394 var returnTo = (canonicalUrl || window.location.href); 395 window.location = 'https://appraise.net/login?redirect=' + encodeURIComponent(returnTo); 396 return; 397 } 398 399 var isInquiry = mode === 'inquiry'; 400 var domain = priceCard.dataset.domain || 'this domain'; 401 var amountField = document.getElementById('offerAmountField'); 402 var msgInput = document.getElementById('offerMessageInput'); 403 var amountInput = document.getElementById('offerAmountInput'); 404 405 document.getElementById('offerModalTitle').textContent = isInquiry ? 'Inquire About Price' : 'Make an Offer'; 406 document.getElementById('offerModalSub').textContent = isInquiry 407 ? 'Send a question to the seller of ' + domain + '.' 408 : 'Submit an offer for ' + domain + '. You choose how long it stays open.'; 409 document.getElementById('offerSubmitLabel').textContent = isInquiry ? 'Send Inquiry' : 'Send Offer'; 410 411 amountField.hidden = isInquiry; 412 var expiryField = document.getElementById('offerExpiryField'); 413 if (expiryField) expiryField.hidden = isInquiry; 414 amountInput.value = ''; 415 msgInput.value = ''; 416 if (isInquiry) { 417 msgInput.placeholder = 'What would you like to ask the seller?'; 418 } else { 419 msgInput.placeholder = 'Add a note for the seller...'; 420 } 421 422 var err = document.getElementById('offerModalError'); 423 err.hidden = true; err.textContent = ''; 424 425 offerModal.hidden = false;
426 offerModal.dataset.mode = mode; 427 setTimeout(function() { 428 (isInquiry ? msgInput : amountInput).focus(); 429 }, 50); 430 } 431 432 function closeOfferModal() { 433 if (offerModal) offerModal.hidden = true; 434 } 435 436 function submitOffer() { 437 if (!offerModal || !priceCard) return; 438 var mode = offerModal.dataset.mode; 439 var isInquiry = mode === 'inquiry'; 440 var listingId = parseInt(priceCard.dataset.listingId, 10); 441 var amountInput = document.getElementById('offerAmountInput'); 442 var msgInput = document.getElementById('offerMessageInput'); 443 var submitBtn = document.getElementById('offerSubmitBtn'); 444 var err = document.getElementById('offerModalError'); 445 err.hidden = true; err.textContent = ''; 446 447 var amount = isInquiry ? 0 : parseFloat(amountInput.value); 448 var message = msgInput.value.trim(); 449 450 if (!isInquiry && (!amount || amount <= 0)) { 451 err.textContent = 'Enter a valid offer amount.'; 452 err.hidden = false; 453 return; 454 } 455 var minOffer = parseFloat(priceCard.dataset.minOfferPrice) || 0; 456 if (!isInquiry && minOffer > 0 && amount < minOffer) { 457 err.textContent = 'Minimum offer is $' + minOffer.toLocaleString(); 458 err.hidden = false; 459 return; 460 } 461 if (isInquiry && !message) { 462 err.textContent = 'Please type your question for the seller.'; 463 err.hidden = false; 464 return; 465 } 466 467 submitBtn.disabled = true; 468 var origLabel = document.getElementById('offerSubmitLabel').textContent; 469 document.getElementById('offerSubmitLabel').textContent = 'Sending...'; 470 471 fetch('/api/marketplace/deal', { 472 method: 'POST', 473 headers: { 'Content-Type': 'application/json' }, 474 body: JSON.stringify({ 475 action: 'create_offer', 476 listing_id: listingId, 477 amount: amount, 478 message: message, 479 inquiry: isInquiry, 480 expiry_days: parseInt((document.getElementById('offerExpiryInput') || {}).value, 10) || 14 481 }) 482 }).then(function(r) { return r.json(); }).then(function(data) { 483 if (data.success && data.thread_token) { 484 window.location = '/marketplace/deal/' + data.thread_token; 485 } else { 486 err.textContent = data.error || 'Could not submit. Please try again.'; 487 err.hidden = false; 488 submitBtn.disabled = false; 489 document.getElementById('offerSubmitLabel').textContent = origLabel; 490 } 491 }).catch(function() { 492 err.textContent = 'Network error. Please try again.'; 493 err.hidden = false; 494 submitBtn.disabled = false; 495 document.getElementById('offerSubmitLabel').textContent = origLabel; 496 }); 497 } 498 499 // Delegated handlers â survive artist-tool rebuilds of priceCardContent 500 document.addEventListener('click', function(e) { 501 if (!e.target.closest) return; 502 if (e.target.closest('#makeOfferBtn')) { openOfferModal('offer'); return; } 503 if (e.target.closest('#inquireBtn')) { openOfferModal('inquiry'); return; } 504 if (e.target.closest('.mobile-cta .btn-cta') && priceCard) { 505 var lt = priceCard.dataset.listingType; 506 // Escrow.com-only seller: the mobile CTA is the Escrow.com button. 507 if (e.target.closest('.btn-cta').dataset.cta === 'escrow') { var eb = document.getElementById('buyEscrowBtn'); if (eb) eb.click(); return; } 508 // buy_now_or_offer's mobile CTA shows "Buy Now" as the primary action, 509 // so route it through the BIN flow just like a buy_now listing. 510 if (lt === 'buy_now' || lt === 'buy_now_or_offer') { 511 var bn = document.getElementById('buyNowBtn'); 512 if (bn && !bn.disabled) { startBuyNowCheckout(bn); } 513 else if (lt === 'buy_now_or_offer') { openOfferModal('offer'); } 514 return; 515 } 516 if (lt === 'make_offer') { openOfferModal('offer'); return; } 517 if (lt === 'inquiry') { openOfferModal('inquiry'); return; } 518 if (lt === 'lease_to_own') { var lto = document.getElementById('ltoStartBtn');
518 if (lto) lto.click(); } 519 } 520 }); 521 522 if (offerModal) { 523 offerModal.querySelectorAll('[data-offer-close]').forEach(function(el) { 524 el.addEventListener('click', closeOfferModal); 525 }); 526 document.getElementById('offerSubmitBtn').addEventListener('click', submitOffer); 527 document.addEventListener('keydown', function(e) { 528 if (e.key === 'Escape' && !offerModal.hidden) closeOfferModal(); 529 }); 530 } 531 532 // Handoff modal (non-canonical hosts) 533 var handoffModal = document.getElementById('handoffModal'); 534 535 function openHandoffModal(canonicalUrl, mode) { 536 if (!handoffModal) { window.location = canonicalUrl; return; } 537 var isInquiry = mode === 'inquiry'; 538 document.getElementById('handoffTitle').textContent = isInquiry ? 'Continue on Appraise.net' : 'Continue on Appraise.net'; 539 document.getElementById('handoffSub').innerHTML = isInquiry 540 ? "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." 541 : "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."; 542 document.getElementById('handoffContinueBtn').setAttribute('href', canonicalUrl); 543 handoffModal.hidden = false; 544 } 545 546 function closeHandoffModal() { 547 if (handoffModal) handoffModal.hidden = true; 548 } 549 550 if (handoffModal) { 551 handoffModal.querySelectorAll('[data-handoff-close]').forEach(function(el) { 552 el.addEventListener('click', closeHandoffModal); 553 }); 554 document.addEventListener('keydown', function(e) { 555 if (e.key === 'Escape' && !handoffModal.hidden) closeHandoffModal(); 556 }); 557 } 558 559 // Seller: Toggle publish 560 var publishBtn = document.getElementById('togglePublishBtn'); 561 if (publishBtn) { 562 publishBtn.addEventListener('click', function() { 563 // TODO: POST to toggle status 564 alert('Publish toggle coming soon.'); 565 }); 566 } 567 568 // Seller: Delete 569 var deleteBtn = document.getElementById('deleteListingBtn'); 570 if (deleteBtn) { 571 deleteBtn.addEventListener('click', function() { 572 if (confirm('Are you sure you want to delete this listing? This cannot be undone.')) { 573 // TODO: POST to delete 574 alert('Delete coming soon.'); 575 } 576 }); 577 } 578 579 // Left sidebar visibility on 1280px+ 580 var leftSidebar = document.querySelector('.sticky-sidebar'); 581 if (leftSidebar && window.innerWidth >= 1280) { 582 leftSidebar.style.display = 'block'; 583 } 584 window.addEventListener('resize', function() { 585 if (leftSidebar && window.innerWidth >= 1280) { 586 leftSidebar.style.display = 'block'; 587 } 588 }); 589 // Certificate modal 590 var certModal = document.getElementById('certModal'); 591 if (certModal) { 592 var certIframe = document.getElementById('certIframe'); 593 var certBackdrop = certModal.querySelector('.cert-modal-backdrop'); 594 var certClose = certModal.querySelector('.cert-modal-close'); 595
596 var certLoading = certModal.querySelector('.cert-modal-loading'); 597 598 document.querySelectorAll('.cert-link').forEach(function(link) { 599 link.addEventListener('click', function(e) { 600 e.preventDefault(); 601 var certId = this.dataset.certId; 602 if (certId) { 603 if (certLoading) certLoading.style.display = 'flex'; 604 certIframe.style.opacity = '0'; 605 certIframe.src = 'https://appraise.net/certificate?id=' + encodeURIComponent(certId); 606 certModal.classList.add('open'); 607 } 608 }); 609 }); 610 611 certIframe.addEventListener('load', function() { 612 if (certLoading) certLoading.style.display = 'none'; 613 certIframe.style.opacity = '1'; 614 }); 615 616 function closeCertModal() { 617 certModal.classList.remove('open'); 618 certIframe.src = ''; 619 certIframe.style.opacity = '0'; 620 } 621 622 certBackdrop.addEventListener('click', closeCertModal); 623 certClose.addEventListener('click', closeCertModal); 624 document.addEventListener('keydown', function(e) { 625 if (e.key === 'Escape' && certModal.classList.contains('open')) closeCertModal(); 626 }); 627 } 628})(); 629 630/* ---- Nav brand-logo lightbox: the badge shows a cropped strip of the logo; click it to 631 see the full logo in an overlay (X, backdrop, the logo itself, or Esc all close). ---- */ 632(function() { 633 var badge = document.querySelector('.nav-brand-logo.logo-zoomable'); 634 var box = document.getElementById('logo-lightbox'); 635 if (!badge || !box) return; 636 var img = box.querySelector('.logo-lightbox-img'); 637 638 function openLogo() { 639 img.src = badge.getAttribute('data-logo-src') || ''; 640 img.alt = badge.getAttribute('data-logo-alt') || ''; 641 box.hidden = false; 642 document.addEventListener('keydown', onKey); 643 } 644 function closeLogo() { 645 box.hidden = true; 646 img.removeAttribute('src'); 647 document.removeEventListener('keydown', onKey); 648 } 649 function onKey(e) { if (e.key === 'Escape') closeLogo(); } 650 651 badge.addEventListener('click', function(e) { e.preventDefault(); openLogo(); }); 652 badge.addEventListener('keydown', function(e) { 653 if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openLogo(); } 654 }); 655 // Any click on the overlay (backdrop, close button, or the enlarged logo) dismisses it. 656 box.addEventListener('click', closeLogo); 657})();
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.