1/*! 2* Start Bootstrap - Landing Page v6.0.6 (https://startbootstrap.com/theme/landing-page) 3* Copyright 2013-2025 Start Bootstrap 4* Licensed under MIT (https://github.com/StartBootstrap/startbootstrap-landing-page/blob/master/LICENSE) 5*/ 6 7 8 9 10// async function uploadFile() { 11// const file = await client.files.create({ 12// file: fs.createReadStream("./products_dataset.json"), 13// purpose: "assistants" 14// }); 15// console.log("Uploaded file:", file.id); 16// } 17 18// uploadFile(); 19 20 21// Capture & persist affiliate parameter (?avad=...) 22(function() { 23 const params = new URLSearchParams(window.location.search); 24 const avad = params.get('avad'); 25 if (avad) { 26 sessionStorage.setItem('avad', avad); 27 } 28})(); 29 30 31 32var nGift = nGift || {}; 33nGift.hasRunOnce = false; 34 35 36 37 38nGift.gurunon =` 39 <div class="no-results text-center py-5"> 40 <h3 class="f-chedros text-primary text-uppercase mb-3">No More Gifts Found</h3> 41 <p class="text-muted mb-4"> 42 Try adjusting your search to discover more ideas! 43 </p> 44 <button class="btn btn-outline-primary text-uppercase" id="resetGuruBtn"> 45 Reset Filters 46 </button> 47 </div> 48 `; 49 50nGift.answers = { 51 price: null, 52 recipient: null, 53 interest: null, 54}; 55 56nGift.shownProducts = []; 57 58 59// Load Products JSON 60nGift.loadProducts = async function () { 61 const response = await fetch('./js/products_dataset.json'); 62 const allProducts = await response.json(); 63 // ð¹ Filter out any with "coming soon" tag (case-insensitive) 64 nGift.products = allProducts.filter(p => 65 !p.tags?.some(tag => tag.toLowerCase().includes("coming soon")) 66 ); 67 68 //nGift.products = await response.json(); 69 console.log("Products loaded:", nGift.products.length); 70 return nGift.products; 71}; 72 73 74 75// Quiz navigation 76// Quiz navigation 77nGift.bindQuiz = function () { 78 const priceEl = document.getElementById("quiz-price"); 79 const recipientEl = document.getElementById("quiz-recipient"); 80 const interestEl = document.getElementById("quiz-interest"); 81 82 // Clear shown products whenever a quiz answer changes 83 [priceEl, recipientEl, interestEl].forEach(select => { 84 select.addEventListener("change", () => { 85 if (nGift.shownProducts.length) { 86 nGift.shownProducts = []; 87 console.log("ð Guru reset: unseen product list cleared due to new selection"); 88 } 89 if (window.fbq) { 90 fbq('trackCustom', 'GuruFilterChanged', { 91 field: select.id.replace('quiz-', ''), // e.g. "price", "recipient" 92 value: select.value 93 }); 94 console.log('ð FB Pixel: GuruFilterChanged â', select.id, select.value); 95 } 96 }); 97 }); 98 99 100 if (!priceEl || !recipientEl || !interestEl) { 101 console.warn("Quiz dropdowns not found in DOM."); 102 return; 103 } 104 const submitBtn = document.getElementById("quiz-submit"); 105 const resetBtn = document.getElementById("quiz-reset"); 106 107 submitBtn.addEventListener("click", async () => { 108 // Collect dropdown values 109 nGift.answers = { 110 price: priceEl.value || null, 111 recipient: recipientEl.value || null, 112 interest: interestEl.value || null, 113 }; 114 115 // Run the query directly (no summary) 116 await nGift.runQuery(); 117 document.getElementById("quiz-submit").src = "./assets/img/reveal-btn-2.webp"; 118 }); 119 120 resetBtn.addEventListener("click", () => { 121 // Reset all dropdowns 122 priceEl.selectedIndex = 0; 123 recipientEl.selectedIndex = 0; 124 interestEl.selectedIndex = 0; 125 126 // Clear stored answers & results 127 nGift.answers = {}; 128 nGift.resetQuiz(); 129 }); 130 131 if (window.fbq) { 132 fbq('trackCustom', 'GuruEngaged', { 133 price: priceEl.value, 134 recipient: recipientEl.value, 135 interest: interestEl.value 136 }); 137 console.log('ð FB Pixel: GuruEngaged fired'); 138 } 139 140 141}; 142 143 144 145 146// Loader Functions 147nGift.showLoader = function (messages) { 148 const container = document.getElementById("results"); 149 container.innerHTML = `<div id="loader" class="loader text-center"></div>`; 150 151 let i = 0; 152 const loader = document.getElementById("loader"); 153 loader.textContent = messages[0]; 154 155 nGift.loaderInterval = setInterval(() => { 156 i = (i + 1) % messages.length; 157 loader.textContent = messages[i]; 158 }, 1500); 159}; 160 161nGift.hideLoader = function () { 162 clearInterval(nGift.loaderInterval); 163 nGift.loaderInterval = null; 164}; 165 166nGift.filterProducts = function () { 167 let filtered = nGift.products; 168 169 // --- Price filtering using ranges --- // 170 if (nGift.answers.price && nGift.answers.price !== "not sure") { 171 filtered = nGift.products.filter(p => { 172 const price = Number(p.price); 173 switch (nGift.answers.price) { 174 case "low": 175 return price <= 35; 176 case "medium": 177 return price > 35 && price <= 65; 178 case "high": 179 return price > 60; // overlaps a bit to ensure coverage 180 default: 181 return true; 182 } 183 }); 184 } 185 186 // Recipient filtering 187// if (nGift.answers.recipient && nGift.answers.recipient !== "other") { 188// filtered = filtered.filter(p => 189// p.tags.some(tag => tag.toLowerCase().includes(nGift.answers.recipient.toLowerCase())) 190// ); 191// } 192 193// // Interest filtering 194// if (nGift.answers.interest && nGift.answers.interest !== "other") { 195// filtered = filtered.filter(p => 196// p.tags.some(tag => tag.toLowerCase().includes(nGift.answers.interest.toLowerCase())) 197// ); 198// } 199 200 return filtered; 201}; 202 203 204 205// Ask AI for gift ideas 206nGift.askAI = async function (query, productsSubset) { 207 if (!nGift.products) { 208 await nGift.loadProducts(); 209 } 210 211 const response = await fetch("https://velvetysoft.com/api/gifts", { 212 method: "POST", 213 headers: { "Content-Type": "application/json" }, 214 body: JSON.stringify({ 215 query: query, 216 products: productsSubset || nGift.products 217 }) 218}); 219 220 const data = await response.json(); 221 return data.results; 222}; 223 224// Show summary + restart 225// nGift.showSummary = function () { 226// const { price, recipient, interest } = nGift.answers; 227// const summaryDiv = document.createElement("div"); 228// summaryDiv.className = "quiz-summary mx-auto"; 229 230// summaryDiv.innerHTML = ` 231// <ul> 232// <li><strong>Budget:</strong> ${price || "Not sure"}</li> 233// <li><strong>Recipient:</strong> ${recipient || "Not sure"}</li> 234// <li><strong>Interest:</strong> ${interest || "Not sure"}</li> 235// </ul> 236// <button id="restartQuiz" class="quiz-restart">Start Over</button> 237// `; 238 239// const container = document.getElementById("summary"); 240// container.innerHTML = ""; // clear loader/results 241// container.appendChild(summaryDiv); 242 243// // Bind restart 244// document.getElementById("restartQuiz").addEventListener("click", function () { 245// nGift.resetQuiz(); 246// }); 247// }; 248 249nGift.cart = { 250 items: [], 251 252 add(product) { 253 const existing = this.items.find(p => p.name === product.name); 254 if (existing) { 255 existing.qty += 1; 256 } else { 257 this.items.push({ ...product, qty: 1 }); 258 } 259 260 // â Update cart 261 this.updateMiniCart(); 262 if (window.fbq) { 263 fbq('track', 'AddToCart', { 264 content_name: product.name, 265 content_ids: [product.id], 266 content_type: 'product', 267 value: product.price, 268 currency: 'USD' 269 }); 270 console.log('ð FB Pixel: AddToCart â', product.name); 271 } 272 273 274 // â Automatically expand mini cart 275 const cart = document.getElementById("mini-cart"); 276 if (cart) { 277 const cartBody = cart.querySelector(".cart-body"); 278 const cartHeader = cart.querySelector(".cart-header"); 279 280 if (cartBody && cartHeader) { 281 cartBody.classList.add("show"); 282 cartHeader.classList.add("active"); 283 } 284 } 285 }, 286 287 288 remove(productName) { 289 this.items = this.items.filter(p => p.name !== productName); 290 this.updateMiniCart(); 291 }, 292 293 clear() { 294 this.items = []; 295 this.updateMiniCart(); 296 }, 297 298 get subtotal() { 299 return this.items.reduce((sum, p) => sum + p.price * p.qty, 0); 300 }, 301 302 get discountTier() { 303 const total = this.subtotal; 304 if (total >= 150) return { rate: 0.2, label: "20% Savings", coupon: "MERRY20", threshold: 150 }; 305 if (total > 0) return { rate: 0.1, label: "10% Savings", coupon: "JOLLY10", threshold: 149 }; 306 return { rate: 0, label: "No discount yet", coupon: null, threshold: 149 }; 307 }, 308 309 310 get checkoutUrl() { 311 // Collect product IDs (you can swap this to `sku` or `id` depending on your dataset) 312 const productIds = this.items.map(p => encodeURIComponent(p.id )).join(",");
313 const qtys = this.items.map(p => p.qty).join(","); 314 const coupon = this.discountTier.coupon ? `&coupon=${this.discountTier.coupon}` : ""; 315 316 // Match Nebo syntax exactly 317 //return `https://nebo.acgbrands.com/en_US/direct/index/add?product=${productIds}&qty=${qtys}${coupon}`; 318 const avad = sessionStorage.getItem('avad'); 319 const avadParam = avad ? `&avad=${encodeURIComponent(avad)}` : ""; 320 return `https://nebo.acgbrands.com/en_US/direct/index/add?product=${productIds}&qty=${qtys}${coupon}${avadParam}`; 321 322}, 323 324 325 326}; 327 328nGift.cart.updateMiniCart = function () { 329 const cartBox = document.getElementById("mini-cart"); 330 const list = cartBox.querySelector(".cart-items"); 331 const discount = this.discountTier; 332 const count = this.items.reduce((t, p) => t + p.qty, 0); 333 334 // update summary 335 const summary = cartBox.querySelector(".cart-summary"); 336 summary.textContent = `Cart (${count}) ⢠${discount.label}`; 337 338 // â total 339 const totalEl = document.getElementById("cart-total"); 340 if (totalEl) totalEl.textContent = `$${this.subtotal.toFixed(2)}`; 341 342 // rebuild item list 343 list.innerHTML = this.items.length 344 ? this.items.map(p => ` 345 <div class="cart-item d-flex justify-content-between align-items-center"> 346 <span>${p.name} (${p.qty})</span> 347 <button class="btn btn-sm btn-outline-light remove-item" data-name="${p.name}">Ã</button> 348 </div>`).join("") 349 : `<p class="text-muted mb-0">Cart is empty</p>`; 350 351 // â progress toward next discount 352 const progressPercent = Math.min((this.subtotal / discount.threshold) * 100, 100); 353 const nextTier = discount.rate < 0.2 ? "Spend $150 for 20% off" : "You hit the max savings!"; 354 355 cartBox.querySelector(".cart-progress").innerHTML = ` 356 <div class="text-white small mb-1"> 357 ${discount.label}${discount.coupon ? ` ⢠Code: ${discount.coupon}` : ""} 358 </div> 359 <div class="progress bg-light" style="height:6px;"> 360 <div class="progress-bar bg-secondary" role="progressbar" style="width:${progressPercent}%"></div> 361 </div> 362 <div class="small text-secondary mt-1">${nextTier}</div> 363 `; 364 365 // â checkout button 366 const checkoutBtn = cartBox.querySelector(".checkout-btn"); 367 checkoutBtn.href = this.checkoutUrl; 368 checkoutBtn.disabled = this.items.length === 0; 369 370 // â rebind remove buttons 371 list.querySelectorAll(".remove-item").forEach(btn => { 372 btn.addEventListener("click", e => this.remove(e.target.dataset.name)); 373 }); 374}; 375 376 377 378 379// Reset quiz state 380nGift.resetQuiz = function () { 381 nGift.answers = {}; 382 383 const quizbtn = document.getElementById("quiz-reset"); 384 quizbtn.style.display = "none"; 385 386 document.querySelectorAll(".quiz-select").forEach(select => (select.selectedIndex = 0)); 387 388 const resultsContainer = document.getElementById("results"); 389 if (resultsContainer) resultsContainer.innerHTML = ""; 390 391 const infoBox = document.getElementById("gift-info"); 392 if (infoBox) { 393 infoBox.classList.remove("fade-in", "fade-out"); 394 infoBox.style.opacity = "0"; 395 396 setTimeout(() => { 397 infoBox.querySelector(".gift-title").textContent = ""; 398 infoBox.querySelector(".gift-price").textContent = ""; 399 infoBox.querySelector(".gift-tags").textContent = ""; 400 401 const viewInfoEl = infoBox.querySelector(".view-info"); 402 const linkEl = infoBox.querySelector(".gift-link"); 403 const addBtn = infoBox.querySelector(".add-to-cart"); 404 if (linkEl) { 405 linkEl.href = "#"; 406 linkEl.style.display = "none"; // â fully hidden 407 } 408 409 if (viewInfoEl) { 410 viewInfoEl.style.display = "none"; // â fully hidden 411 } 412 413 if (addBtn) { 414 addBtn.style.display = "none"; // â fully hidden 415 } 416 417 infoBox.style.opacity = "1"; 418 }, 400); 419 } 420 421 if (nGift.swiper) { 422 nGift.swiper.off("slideChange"); 423 nGift.swiper.destroy(true, true); 424 nGift.swiper = null; 425 } 426 427 nGift.shownProducts = []; 428 nGift.carouselProducts = []; 429 430 console.log("ð Quiz, carousel, and info box reset complete."); 431}; 432 433 434 435 436 437 438 439// Render Products 440nGift.renderProducts = function (products) { 441 const container = document.getElementById("results"); 442 container.innerHTML = ""; 443 444 if (!products.length) { 445 container.innerHTML = "<p>No matching products found.</p>"; 446 return; 447 } 448
449 products.forEach(p => { 450 const card = document.createElement("div"); 451 card.className = "gift-card"; 452 453 // <img src="${p.image || './assets/img/placeholder.png'}" alt="${p.name}"> 454 card.innerHTML = ` 455 <div class="gift-image"> 456 <img src="https://vhb-transfer.s3.us-west-2.amazonaws.com/nebo-holiday-test/assets/img/placeholder.png" alt="${p.name}"> 457 </div> 458 <div class="gift-title">${p.name}</div> 459 <div class="gift-price">$${p.price.toFixed(2)}</div> 460 <div class="gift-tags">${p.tags.join(", ")}</div> 461 <a href="${p.link}" class="gift-link" target="_blank">View Product</a> 462 `; 463 464 container.appendChild(card); 465 }); 466}; 467 468// --- Fancy Snowflake Animation --- // 469nGift.showPoofAnimation = function () { 470 const container = document.getElementById("results"); 471 472 // ð¹ Remove any existing poof overlays first 473 container.querySelectorAll(".poof-wrap").forEach(el => el.remove()); 474 475 const totalFlakes = 6; 476 const flakes = [...Array(totalFlakes)].map((_, i) => { 477 const delay = (Math.random() * 0.5).toFixed(2); 478 const rotation = Math.floor(Math.random() * 360); 479 const scale = (0.6 + Math.random() * 1.4).toFixed(2); 480 const extraClass = i < 6 ? `cloud-${i + 1}` : ""; 481 return `<div class="poof-cloud ${extraClass}" 482 style="animation-delay:${delay}s; 483 transform:rotate(${rotation}deg) scale(${scale});"> 484 </div>`; 485 }).join(""); 486 487 // ð¹ Create overlay *on top* of Swiper 488 const poof = document.createElement("div"); 489 poof.className = "poof-wrap text-center position-absolute top-0 start-0 w-100 h-100"; 490 poof.innerHTML = flakes; 491 container.appendChild(poof); 492 493 const globe = document.getElementById("globe"); 494 if (globe) globe.classList.add("shake"); 495 496 return new Promise(resolve => { 497 setTimeout(() => { 498 if (globe) globe.classList.remove("shake"); 499 poof.classList.add("fade-out"); 500 setTimeout(() => poof.remove(), 500); 501 resolve(); 502 }, 2500); 503 }); 504}; 505 506 507 508 509nGift.hidePoofAnimation = function() { 510 const poofWrap = document.querySelector(".poof-wrap"); 511 if (!poofWrap) return; 512 poofWrap.classList.add("fade-out"); 513 setTimeout(() => poofWrap.remove(), 600); 514}; 515 516 517 518 519nGift.hidePoofAnimation = function() { 520 const flakes = document.querySelectorAll(".poof-cloud"); 521 flakes.forEach(flake => flake.classList.add("fade-out")); 522 setTimeout(() => flakes.forEach(flake => flake.remove()), 800); 523}; 524 525 526 527 528nGift.showProductModal = function (product) { 529 const titleEl = document.getElementById("giftModalTitle"); 530 const descEl = document.getElementById("giftModalDesc"); 531 const featureEl = document.getElementById("giftModalFeatures"); 532 const imgEl = document.getElementById("giftModalImage"); 533 const lifestyleImgEl = document.getElementById("giftModalLifestyleImage"); 534 const addBtn = document.getElementById("giftModalAddToCart"); 535 536 titleEl.textContent = product.name || "Gift Details"; 537 descEl.textContent = product.description || product.ai_summary || "A perfect gift for the season."; 538 imgEl.src = product.image || "./assets/img/placeholder.png"; 539 lifestyleImgEl.src = product.lifestyle_image || "./assets/img/placeholder.png"; 540 541 // Build feature list 542 featureEl.innerHTML = ""; 543 if (product.features && product.features.length) { 544 product.features.forEach(f => { 545 const li = document.createElement("li"); 546 li.innerHTML = `${f}`; 547 featureEl.appendChild(li); 548 }); 549 } 550 551 // Reset and bind Add-to-Cart button 552 // Reset and bind Add-to-Cart button (includes Sold Out logic) 553addBtn.classList.remove("btn-secondary", "btn-primary"); 554addBtn.disabled = false; 555 556if (product.soldout === true) { 557 addBtn.textContent = "Sold Out"; 558 addBtn.classList.add("btn-secondary"); 559 addBtn.disabled = true; 560 addBtn.setAttribute("aria-label", `${product.name} is sold out`); 561} else { 562 addBtn.textContent = "Add to Cart"; 563 addBtn.classList.add("btn-primary"); 564 addBtn.setAttribute("aria-label", `Add to cart ${product.name}`); 565 566 addBtn.onclick = () => { 567 nGift.cart.add(product); 568 addBtn.textContent = "Added!"; 569 addBtn.disabled = true; 570 addBtn.classList.remove("btn-primary"); 571 addBtn.classList.add("btn-secondary"); 572 573 // ð Expand mini-cart after adding 574 const cart = document.getElementById("mini-cart"); 575 const cartBody = cart.querySelector(".cart-body"); 576 const cartHeader = cart.querySelector(".cart-header"); 577 if (cartBody && cartHeader) { 578 cartBody.classList.add("show"); 579 cartHeader.classList.add("active"); 580 } 581 582 nGift.cart.updateMiniCart(); 583 584 // â Optional: auto-close 585 setTimeout(() => { 586 closeModal(); 587 }, 1000); 588 }; 589} 590 591 592 593 594 // Show Bootstrap modal 595 const modal = new bootstrap.Modal(document.getElementById("giftModal")); 596 modal.show(); 597}; 598 599 600// Close modal safely 601function closeModal() { 602 console.log("Closing modal safely..."); 603 const modalEl = document.getElementById("giftModal"); 604 if (!modalEl) return; 605 606 const modal = bootstrap.Modal.getInstance(modalEl) || new bootstrap.Modal(modalEl); 607 modalEl.addEventListener("hidden.bs.modal", () => { 608 document.querySelectorAll(".modal-backdrop").forEach(el => el.remove()); 609 document.body.classList.remove("modal-open"); 610 document.body.style.removeProperty("overflow"); 611 document.body.style.removeProperty("padding-right"); 612 }, { once: true }); 613 614 // â force blur before hide 615 if (document.activeElement) document.activeElement.blur(); 616 617 modal.hide(); 618} 619 620// ð Open Notify Me modal
621function openNotifyModal(productName = "") { 622 const modalEl = document.getElementById("notifyModal"); 623 const modal = new bootstrap.Modal(modalEl); 624 625 modalEl.addEventListener("hidden.bs.modal", () => { 626 document.querySelectorAll(".modal-backdrop").forEach(el => el.remove()); 627 document.body.classList.remove("modal-open"); 628 document.body.style.overflow = ''; 629 document.body.style.paddingRight = ''; 630 }, { once: true }); 631 632 modal.show(); 633 634 if (window.fbq) { 635 fbq('trackCustom', 'NotifyMeOpened', { product: productName }); 636 console.log('ð FB Pixel: NotifyMeOpened â', productName); 637 } 638} 639 640 641 642 643// Prevent focus-related Bootstrap modal glitches 644const modalEl = document.getElementById("giftModal"); 645 646modalEl.addEventListener("hide.bs.modal", (e) => { 647 console.log("Modal is starting to closeâ¦"); 648 if (document.activeElement) document.activeElement.blur(); // avoid aria warning 649}); 650 651modalEl.addEventListener("hidden.bs.modal", (e) => { 652 console.log("Modal fully closed, cleaning up..."); 653 document.querySelectorAll(".modal-backdrop").forEach(el => el.remove()); 654 document.body.classList.remove("modal-open"); 655 document.body.style.removeProperty("overflow"); 656 document.body.style.removeProperty("padding-right"); 657}); 658 659 660 661 662// Initialize Guru Carousel once 663// nGift.initGuruCarousel = function () { 664// const container = document.getElementById("results"); 665// container.innerHTML = ` 666// <div class="swiper guru-swiper"> 667// <div class="swiper-wrapper"></div> 668// <div class="swiper-button-prev guru-prev"></div> 669// <div class="swiper-button-next guru-next"></div> 670// </div> 671// `; 672 673// nGift.carouselProducts = []; 674// nGift.swiper = new Swiper(".guru-swiper", { 675// slidesPerView: 1, 676// spaceBetween: 0, 677// loop: false, 678// grabCursor: true, 679// navigation: { 680// nextEl: ".guru-next", 681// prevEl: ".guru-prev", 682// }, 683// on: { 684// slideChange: function () { 685// const active = nGift.carouselProducts[nGift.swiper.activeIndex]; 686// if (active) nGift.updateGiftInfo(active); 687// } 688// } 689// }); 690// }; 691 692 693nGift.runQuery = async function () { 694 695 const { price, recipient, interest } = nGift.answers; 696 697 if (!nGift.products || !nGift.products.length) { 698 await nGift.loadProducts(); 699 nGift.products.sort((a, b) => { 700 if (a.priority && !b.priority) return -1; 701 if (!a.priority && b.priority) return 1; 702 return b.price - a.price; 703 }); 704 } 705 706 console.log("ð Running local query:", nGift.answers); 707 708 // --- Filter products (exact match first) --- // 709 let filtered = nGift.products.slice(); // start fresh 710 711 // if (price && price !== "not sure") { 712 // filtered = filtered.filter(p => { 713 // const productPrice = Number(p.price); // ensure numeric 714 // switch (price) { 715 // case "low": 716 // return productPrice <= 35; // ð THIS LINE right here 717 // case "medium": 718 // return productPrice > 35 && productPrice <= 65; 719 // case "high": 720 // return productPrice > 60; 721 // default: 722 // return true; 723 // } 724 // }); 725 // } 726 727 // --- Soft price prioritization instead of strict filter --- // 728 if (price && price !== "not sure") { 729 const getTier = (p) => { 730 const val = Number(p.price); 731 if (val <= 35) return "low"; 732 if (val > 35 && val <= 65) return "medium"; 733 return "high"; 734 }; 735 736 // Sort the filtered products so matching tier appears first, then by price descending 737 filtered.sort((a, b) => { 738 const aTier = getTier(a); 739 const bTier = getTier(b); 740 741 // 1ï¸â£ prioritize matching tier first 742 if (aTier === price && bTier !== price) return -1; 743 if (aTier !== price && bTier === price) return 1; 744 745 // 2ï¸â£ within same tier, sort high â low 746 return b.price - a.price; 747 }); 748} 749 750 751 752 753 if (recipient && recipient !== "other") { 754 const audienceTag = `audience-${recipient.toLowerCase().replace(/\s+/g, "-")}`; 755 filtered = filtered.filter(p => 756 p.audience_tags?.some(tag => tag.toLowerCase() === audienceTag) 757 ); 758 } 759 760 if (interest && interest !== "other") { 761 const interestTag = `interest-${interest.toLowerCase().replace(/\s+/g, "-")}`; 762 filtered = filtered.filter(p => 763 p.interest_tags?.some(tag => tag.toLowerCase() === interestTag) 764 ); 765 } 766 767 console.log(`â Exact-match results: ${filtered.length}`); 768 console.table(filtered.map(p => ({ 769 name: p.name, 770 price: p.price, 771 audience: p.audience_tags, 772 interest: p.interest_tags 773 }))); 774 775 // --- Ensure at least one product is returned --- // 776 // --- Stage fallback flow --- // 777let isFallback = false;
778 779 // ð¥ Nothing found after filters â show message instead of random product 780if (!filtered.length) { 781 console.warn("â ï¸ No matches found at all â prompting user to refine filters."); 782 783 const container = document.getElementById("results"); 784 container.innerHTML = nGift.gurunon; 785 786 const resetBtn = document.getElementById("resetGuruBtn"); 787 if (resetBtn) { 788 resetBtn.addEventListener("click", () => { 789 nGift.resetQuiz(); 790 const guruSection = document.getElementById("gift-guru"); 791 if (guruSection) { 792 window.scrollTo({ 793 top: guruSection.offsetTop - 80, 794 behavior: "smooth" 795 }); 796 } 797 }); 798 } 799 800 return; // â stop â don't continue or show random 801} 802 803 804 805// --- Pick one random unseen product, weighted by price tier --- // 806let selected; 807console.log("ð§® Before unseen filter:", filtered.length, "filtered products"); 808const unseen = filtered.filter(p => !nGift.shownProducts.includes(p.name)); 809console.log("ð Unseen pool:", unseen.length, "remaining"); 810 811if (unseen.length) { 812 // --- Split unseen into price tiers --- 813 const getTier = (p) => { 814 const val = Number(p.price); 815 if (val <= 35) return "low"; 816 if (val > 35 && val <= 65) return "medium"; 817 return "high"; 818 }; 819 820 // --- Find only items matching selected price tier --- 821 const tierMatches = nGift.answers.price && nGift.answers.price !== "not sure" 822 ? unseen.filter(p => getTier(p) === nGift.answers.price) 823 : unseen; 824 825 let pickPool; 826 827 if (tierMatches.length) { 828 // ð¯ Random from the tier that matches selection 829 pickPool = tierMatches; 830 console.log(`ð¯ Selecting from ${pickPool.length} items in ${nGift.answers.price} tier`); 831 } else { 832 // fallback: pick from remaining unseen if no tier matches 833 pickPool = unseen; 834 console.log("â ï¸ No exact tier matches left, picking from remaining unseen"); 835 } 836 837 // --- Randomly choose one from that tier (or unseen fallback) --- 838 selected = pickPool[Math.floor(Math.random() * pickPool.length)]; 839} else { 840 console.warn("â ï¸ Unseen exhausted â prompting user to refine filters instead of retrying OR logic."); 841 842 const container = document.getElementById("results"); 843 container.innerHTML = nGift.gurunon ; 844 845 const resetBtn = document.getElementById("resetGuruBtn"); 846 if (resetBtn) { 847 resetBtn.addEventListener("click", () => { 848 nGift.resetQuiz(); 849 const guruSection = document.getElementById("gift-guru"); 850 if (guruSection) { 851 window.scrollTo({ 852 top: guruSection.offsetTop - 80, 853 behavior: "smooth" 854 }); 855 } 856 }); 857 } 858 859 return; // stop before OR fallback or random reset 860} 861 862 863 864 865 // Track seen product 866 nGift.shownProducts.push(selected.name); 867 868 869 // --- Clear and prep the results area (no carousel now) --- // 870// --- Ensure the carousel stage exists --- // 871// --- Ensure Guru carousel stage exists --- // 872let container = document.getElementById("results"); 873 874if (!document.querySelector(".guru-swiper")) { 875 container.innerHTML = ` 876 <div class="gift-container text-center position-relative h-100"> 877 <div id="globe-layer"></div> 878 <div class="swiper guru-swiper h-100"> 879 <div class="swiper-wrapper h-100"></div> 880 <div class="swiper-button-prev guru-prev"></div> 881 <div class="swiper-button-next guru-next"></div> 882 </div> 883 </div> 884 `; 885 // ... initialize Swiper 886 887 888 // â initialize Swiper once 889 nGift.carouselProducts = []; 890 nGift.swiper = new Swiper(".guru-swiper", { 891 slidesPerView: 1, 892 spaceBetween: 0, 893 loop: false, 894 grabCursor: true, 895 navigation: { 896 nextEl: ".guru-next", 897 prevEl: ".guru-prev", 898 }, 899 on: { 900 slideChange: () => { 901 const current = nGift.carouselProducts[nGift.swiper.activeIndex]; 902 if (current) nGift.updateGiftInfo(current); 903 } 904 } 905 }); 906} 907 908 909 910// --- Start Poof Animation + AI call concurrently --- // 911const animationPromise = nGift.showPoofAnimation(); 912 913// ð¹ Place the product image behind the flakes over the globe 914const poofWrap = document.querySelector(".poof-wrap"); 915if (poofWrap) { 916 const productImg = document.createElement("img"); 917 productImg.src = selected.image || "/assets/img/placeholder.png"; 918 productImg.alt = selected.name; 919 productImg.className = "poof-product"; 920 921 // add a 2-second animation delay 922 // productImg.style.animationDelay = "2s"; 923 productImg.style.animationDelay = `${1.5 + Math.random() * 1}s`; 924 925 926 poofWrap.appendChild(productImg); 927} 928 929 930const body = { 931 product: selected, 932 query: "holiday gift recommendation", 933 fallback: isFallback ? "true" : "false", 934}; 935 936if (nGift.answers.recipient) body.audience = nGift.answers.recipient; 937if (nGift.answers.interest) body.interest = nGift.answers.interest; 938if (nGift.answers.price) body.price = nGift.answers.price; 939 940// ð¹ Start the AI request
941const aiPromise = (async () => { 942 try { 943 const response = await fetch("https://velvetysoft.com/api/gifts", { 944 method: "POST", 945 headers: { "Content-Type": "application/json" }, 946 body: JSON.stringify(body) 947 }); 948 949 const data = await response.json(); 950 selected.ai_message = data.results?.[0]?.message || "A thoughtful holiday choice !"; 951 } catch (err) { 952 console.error("AI fetch error:", err); 953 selected.ai_message = "A thoughtful holiday choice!"; 954 } 955})(); 956 957 958const infoBox = document.querySelector('.gift-info'); 959 960// Step 1: Hide info box as the globe shakes 961if (infoBox) { 962 infoBox.classList.remove('show'); 963 infoBox.classList.add('hide'); 964} 965 966// Step 2: Run the rest of your animation + AI 967// 1ï¸â£ Fade out current slide 968const swiperEl = document.querySelector(".guru-swiper"); 969if (swiperEl) swiperEl.classList.add("fade-out"); 970 971// 2ï¸â£ Run poof + AI in parallel 972await Promise.all([nGift.showPoofAnimation(), aiPromise]); 973 974// 3ï¸â£ Add the new slide silently 975if (nGift.swiper) { 976 nGift.carouselProducts.push(selected); 977 const wrapper = document.querySelector(".guru-swiper .swiper-wrapper"); 978 979 const slide = document.createElement("div"); 980 slide.className = "swiper-slide text-center"; 981 slide.innerHTML = ` 982 <img src="${selected.image || '/assets/img/placeholder.png'}" 983 alt="${selected.name}" 984 class="img-fluid poof-product opacity-0 transition-opacity duration-500" /> 985 `; 986 wrapper.appendChild(slide); 987 nGift.swiper.update(); 988 nGift.swiper.slideTo(nGift.carouselProducts.length - 1, 0); // no visible motion 989} 990 991// 4ï¸â£ Fade in new slide image just before poof ends 992setTimeout(() => { 993 if (swiperEl) { 994 swiperEl.classList.remove("fade-out"); 995 const activeImg = swiperEl.querySelector(".swiper-slide-active img"); 996 if (activeImg) activeImg.classList.remove("opacity-0"); 997 } 998}, 400); // adjust for timing overlap with poof 999 1000// 5ï¸â£ Update product info 1001nGift.updateGiftInfo(selected); 1002 1003if (window.fbq) { 1004 fbq('trackCustom', 'GuruRecommendation', { 1005 product_name: selected.name, 1006 product_id: selected.id, 1007 price: selected.price, 1008 tags: selected.tags?.join(', ') || null, 1009 }); 1010 console.log('ð FB Pixel: GuruRecommendation â', selected.name); 1011} 1012 1013 1014 1015// Step 4: Reveal new info with a nice pop 1016setTimeout(() => { 1017 if (infoBox) { 1018 infoBox.classList.remove('hide'); 1019 infoBox.classList.add('show'); 1020 } 1021}, 300); // small delay to sync with globe stop 1022 1023 1024 1025}; 1026 1027 1028// nGift.addToCarousel = function (product) { 1029// const container = document.getElementById("results"); 1030 1031// const quizbtn = document.getElementById("quiz-reset"); 1032// // quizbtn.style.display = "inline-block"; 1033 1034// // If container not built yet, create full layout 1035// if (!container.querySelector(".gift-swiper")) { 1036// container.innerHTML = ` 1037// <div class="gift-container d-flex flex-column flex-md-row align-items-start"> 1038// <div class="swiper gift-swiper flex-shrink-0" style="width: 100%; max-width: 500px;"> 1039// <div class="swiper-wrapper"></div> 1040// <div class="swiper-button-prev gift-prev"></div> 1041// <div class="swiper-button-next gift-next"></div> 1042// </div> 1043 1044// </div> 1045// `; 1046 1047// // Initialize Swiper 1048// nGift.swiper = new Swiper(".gift-swiper", { 1049// slidesPerView: 1, 1050// spaceBetween: 0, 1051// grabCursor: true, 1052// loop: false, 1053// navigation: { 1054// nextEl: ".gift-next", 1055// prevEl: ".gift-prev", 1056// }, 1057// on: { 1058// slideChange: function () { 1059// const activeIndex = nGift.swiper.activeIndex; 1060// const currentProduct = nGift.carouselProducts[activeIndex]; 1061// nGift.updateGiftInfo(currentProduct); 1062// }, 1063// }, 1064// }); 1065 1066// // Create product list tracker 1067// nGift.carouselProducts = []; 1068// } 1069 1070// // Add new product to list 1071// nGift.carouselProducts.push(product); 1072 1073// // Add slide 1074// const wrapper = container.querySelector(".swiper-wrapper"); 1075// const slide = document.createElement("div"); 1076// slide.className = "swiper-slide"; 1077// slide.innerHTML = `
1078// <div class="text-center"> 1079// <div class="gift-image"> 1080// <img src="${product.image || '/assets/img/placeholder.png'}" alt="${product.name}" class="img-fluid"> 1081// </div> 1082 1083// </div> 1084// `; 1085// wrapper.appendChild(slide); 1086 1087// // Update Swiper 1088// nGift.swiper.update(); 1089 1090// // Move to last added slide 1091// const lastIndex = nGift.carouselProducts.length - 1; 1092// nGift.swiper.slideTo(lastIndex, 600); // 600ms smooth scroll 1093 1094// // If first product, show info immediately 1095// nGift.updateGiftInfo(product); 1096 1097 1098// console.log(`ð Added to carousel: ${product.name}`); 1099// }; 1100 1101 1102 1103 1104 1105// nGift.runQuery = async function () { 1106// const { price, recipient, interest } = nGift.answers; 1107 1108// if ((recipient === "other" || !recipient) && (interest === "other" || !interest)) { 1109// console.log("Skipping AI, using local filtering only"); 1110// const localResults = nGift.filterProducts(); 1111// nGift.renderProducts(localResults); 1112// return; 1113// } 1114 1115// const query = nGift.buildQuery(); 1116// console.log("Generated query:", query); 1117 1118// const messages = [ 1119// "Searching products...", "Looking for matches...", "Almost found some...", 1120// "Just a moment...", "Finalizing results...", "Here we go..." 1121// ]; 1122// nGift.showLoader(messages); 1123 1124// // â filter locally by price first to reduce payload 1125// const productsSubset = nGift.filterProducts(); 1126// console.log("â Locally filtered products:", productsSubset.length); 1127// const matches = await nGift.askAI(query, productsSubset); 1128 1129// nGift.hideLoader(); 1130// nGift.renderProducts(matches); 1131// }; 1132 1133nGift.updateGiftInfo = function (product) { 1134 const infoBox = document.getElementById("gift-info"); 1135 if (!infoBox) return; 1136 1137 // --- Update text fields --- 1138 const titleEl = infoBox.querySelector(".gift-title"); 1139 const priceEl = infoBox.querySelector(".gift-price"); 1140 const tagsEl = infoBox.querySelector(".gift-tags"); 1141 1142 if (titleEl) titleEl.textContent = product.name; 1143 if (priceEl) priceEl.textContent = `$${product.price.toFixed(2)}`; 1144 if (tagsEl) tagsEl.textContent = product.ai_message || product.tags.join(", "); 1145 1146 // --- View Info button setup --- 1147 const infoBtn = infoBox.querySelector(".view-info"); 1148 if (infoBtn) { 1149 infoBtn.style.display = "inline-block"; 1150 infoBtn.dataset.id = product.id; 1151 infoBtn.onclick = () => nGift.showProductModal(product); 1152 } 1153 1154 // --- Main Add/Notify button --- 1155 // --- Main Add/Notify button --- 1156let addBtn = infoBox.querySelector(".add-to-cart"); 1157if (!addBtn) { 1158 addBtn = document.createElement("button"); 1159 addBtn.className = "btn add-to-cart mt-2 text-uppercase"; 1160 infoBox.appendChild(addBtn); 1161} 1162 1163// â always show button (it was hidden inline in the HTML) 1164addBtn.style.display = "inline-block"; 1165addBtn.className = "btn add-to-cart mt-2 text-uppercase"; 1166addBtn.disabled = false; 1167addBtn.onclick = null; 1168 1169// normalize soldout flag 1170const isSoldOut = product.soldout === true; 1171 1172// remove any stray notify buttons 1173infoBox.querySelectorAll(".notify-btn").forEach(el => el.remove()); 1174 1175if (isSoldOut) { 1176 // ð sold-out state â show notify version 1177 addBtn.textContent = "Notify Me When Back In Stock"; 1178 addBtn.classList.add("btn-outline-primary", "notify-btn"); 1179 addBtn.onclick = () => openNotifyModal(product.name); 1180} else { 1181 // ð normal in-stock state 1182 addBtn.textContent = "Add to Cart"; 1183 addBtn.classList.add("btn-primary"); 1184 addBtn.setAttribute("aria-label", `Add ${product.name} to cart`); 1185 addBtn.onclick = () => { 1186 nGift.cart.add(product); 1187 addBtn.textContent = "Added!"; 1188 addBtn.disabled = true; 1189 addBtn.classList.replace("btn-primary", "btn-secondary"); 1190 setTimeout(() => { 1191 addBtn.textContent = "Add to Cart"; 1192 addBtn.disabled = false; 1193 addBtn.classList.replace("btn-secondary", "btn-primary"); 1194 }, 2000); 1195 }; 1196} 1197 1198 1199 // --- Fade animation --- 1200 infoBox.classList.remove("fade-out"); 1201 void infoBox.offsetWidth; // reflow 1202 infoBox.classList.add("fade-in"); 1203}; 1204 1205 1206 1207 1208 1209 1210 1211 1212nGift.buildQuery = function () { 1213 const { price, recipient, interest } = nGift.answers; 1214 return `Looking for a gift ${ 1215 price && price !== "not sure" ? "under $" + price : "" 1216 } ${ 1217 recipient && recipient !== "other" ? "for " + recipient : "" 1218 } ${ 1219 interest && interest !== "other" ? "who likes " + interest : "" 1220 }`.trim(); 1221}; 1222 1223const categoryDescriptions = { 1224 "Outdoorsy Folk": { 1225 headline: "Outdoorsy Types", 1226 description: 1227 "For the ones whoâd rather be under the stars than under a roof. Find gear that lights trails, powers camps, and keeps adventures rolling." 1228 }, 1229 "DIY Hero": { 1230 headline: "DIY Heroes", 1231 description: 1232 "They fix it, build it, and probably improve it while theyâre at it. Smart, sturdy tools that make every project a little brighter." 1233 }, 1234 "Always Prepped": { 1235 headline: "Practical Preppers", 1236 description: 1237 "They are ready for anything â from breakdowns to power outages. Get them gear that keeps them confident and secure when chaos hits." 1238 }, 1239 "Gadget Guru": { 1240 headline: "Sleek Seekers", 1241 description: 1242 "The more streamlined, compact, and innovative the device, the more obsessed they become. These slick gadgets unfold, transform, and delight with unique details. " 1243 }, 1244 "The Impossibles": { 1245 headline: "Hard-to-Please Peeps", 1246 description: 1247 "They already have everything, so surprise them anyway. Unique finds they didnât know they needed (but totally do)." 1248 }, 1249 "Stocking Stuffers": { 1250 headline: "Stocking Stuffers", 1251 description: 1252 "Small but mightyâproof great things come in tiny packages. Pocket-sized gifts with big wow-factor." 1253 }, 1254 "Lil Explorer": { 1255 headline: "LIâL EXPLORERS", 1256 description: 1257 "Curious hands, big imagination, and endless wonder. Fun, safe gear that sparks discovery and adventure." 1258 }, 1259 "College Kid": { 1260 headline: "College Kids", 1261 description: 1262 "Off on their own, these helpful gifts are little reminders from home. Keep them safe and charged for crossing campus or pulling all-nighters." 1263 }, 1264 "Gearhead": { 1265 headline: "Gearheads", 1266 description: 1267 "Engines, torque, and a little grease never scared them. Auto-ready gadgets built for garages and glory." 1268 }, 1269 "Road Runners": { 1270 headline: "Road Runners", 1271 description: 1272 "They chase sunrises, outrun sunsets, and never wait for daylight. Choose gear that will keep them safe, seen and warm." 1273 }, 1274 "Sideline Superfan": { 1275 headline: "Sideline Superfans", 1276 description: 1277 "Rain or shine, day or night, they never miss a game. Get the game-day gadgets to keep hands and seats warm, phones charged, and bleachers lit â even in OT." 1278 }, 1279 "Hunting Buddy": { 1280 headline: "Hunting Buddies", 1281 description: 1282 "Early mornings, quiet woods, and the thrill of the spot. Durable, dependable tools that never miss the mark." 1283 }, 1284 "Backyard Chef": { 1285 headline: "Backyard Chefs", 1286 description: 1287 "Smoke, sizzle, and bragging rights included. Grill-side gear made for flavor, fun, and a little friendly competition." 1288 }, 1289 "Secret Santa": { 1290 headline: "Secret Santas", 1291 description: 1292 "Create some gift-swap envy with these fun and useful items. This gear is sure to be a hit at your office gift exchange." 1293 }, 1294 "Under $50": { 1295 headline: "Gifts Under 50", 1296 description: 1297 "Big impact, small price. Thoughtful picks that impress without breaking the budget." 1298 }, 1299 "Under $30": { 1300 headline: "Gifts Under 30", 1301 description: 1302 "Smart, affordable, and guaranteed to get a smile. Budget-friendly gems that look way more expensive than they are." 1303 }, 1304 "Tech Nerd": { 1305 headline: "Tech-Setters", 1306 description: 1307 "They always have the latest devices. Help them stay ahead of the curve with cutting-edge gear that powers their passion and turns heads in the process." 1308 }, 1309}; 1310 1311 1312// --------------------------------------------------------------------- 1313// CATEGORY CLICK â FILTER PRODUCTS + SHOW SWIPER CAROUSEL 1314// --------------------------------------------------------------------- 1315nGift.bindCategoryClicks = function () { 1316 const listItems = document.querySelectorAll('.gift-list li'); 1317 const carouselContainer = document.getElementById('category-carousel'); 1318 let currentCategory = null; 1319 let currentSwiper = null; 1320 1321 // --- Helper: render products based on device type --- 1322 function renderCategory(category) { 1323 // â Filter all products matching tag (case-insensitive) 1324 let matches = nGift.products.filter(p => 1325 p.tags?.some(t => t.toLowerCase().includes(category.toLowerCase())) 1326 ); 1327 1328 // â Split into priority/non-priority by this tag 1329 const priority = matches.filter(p => p.priority_by_tag?.[category]); 1330 const normal = matches.filter(p => !p.priority_by_tag?.[category]); 1331 1332 // â Shuffle both groups (FisherâYates)
1333 const shuffle = (arr) => { 1334 const copy = [...arr]; 1335 for (let i = copy.length - 1; i > 0; i--) { 1336 const j = Math.floor(Math.random() * (i + 1)); 1337 [copy[i], copy[j]] = [copy[j], copy[i]]; 1338 } 1339 return copy; 1340 }; 1341 1342 // â Merge: priority first (randomized), then random normal 1343 matches = [...shuffle(priority), ...shuffle(normal)]; 1344 1345 // â Optional fallback: if no matches, show message 1346 carouselContainer.innerHTML = ''; 1347 if (!matches.length) { 1348 carouselContainer.innerHTML = `<p>No products found for "${category}".</p>`; 1349 return; 1350 } 1351 1352 const isMobile = window.innerWidth < 768; 1353 1354 if (isMobile) { 1355 // â Swiper carousel view 1356 const swiperEl = document.createElement('div'); 1357 swiperEl.className = 'swiper category-swiper'; 1358 const wrapper = document.createElement('div'); 1359 wrapper.className = 'swiper-wrapper'; 1360 1361 matches.forEach(p => { 1362 const slide = document.createElement('div'); 1363 slide.className = 'swiper-slide'; 1364 slide.innerHTML = ` 1365 <div class="tag-card text-center"> 1366 <img src="${p.image || '/assets/img/placeholder.png'}" 1367 alt="${p.name}" 1368 class="img-fluid mb-2" 1369 style="cursor:pointer;" 1370 data-id="${p.id}" 1371 data-action="view-info"> 1372 1373 <div class="gift-title mb-1">${p.name}</div> 1374 <div class="gift-price mb-2">$${p.price.toFixed(2)}</div> 1375 1376 <button class="btn btn-white btn-sm text-uppercase view-info mb-2 w-100" data-id="${p.id}"> 1377 View Info 1378 </button> 1379 ${ 1380 p.soldout 1381 ? `<button class="btn btn-outline-primary btn-sm text-uppercase w-100 notify-btn" 1382 data-name="${p.name}" type="button"> 1383 Notify Me When in Stock 1384 </button>` 1385 : `<button class="btn btn-primary btn-sm text-uppercase w-100 add-to-cart" 1386 data-name="${p.name}">Add to Cart</button>` 1387 1388 1389 } 1390 1391 </div> 1392 `; 1393 wrapper.appendChild(slide); 1394 }); 1395 1396 1397 const nextBtn = document.createElement('div'); 1398 nextBtn.className = 'swiper-button-next'; 1399 const prevBtn = document.createElement('div'); 1400 prevBtn.className = 'swiper-button-prev'; 1401 1402 swiperEl.appendChild(wrapper); 1403 swiperEl.appendChild(nextBtn); 1404 swiperEl.appendChild(prevBtn); 1405 1406 const { headline, description } = categoryDescriptions[category] || {}; 1407 if (headline && description) { 1408 carouselContainer.innerHTML = ` 1409 <div class="category-header text-center mb-4"> 1410 <h2 class="f-chedros text-primary text-uppercase">${headline}</h2> 1411 <p class="stage-wrap text-secondary f-chedros fs-4 lh-1" style="max-width: 600px;">${description}</p> 1412 </div> 1413 `; 1414 } 1415 1416 carouselContainer.appendChild(swiperEl); 1417 1418 // Initialize Swiper and store instance 1419 currentSwiper = new Swiper(swiperEl, { 1420 slidesPerView: 2.1, 1421 spaceBetween: 16, 1422 grabCursor: true, 1423 navigation: { nextEl: nextBtn, prevEl: prevBtn }, 1424 breakpoints: { 1425 576: { slidesPerView: 2 }, 1426 768: { slidesPerView: 3 }, 1427 }, 1428 }); 1429 } else { 1430 // â Desktop grid layout 1431 // â Desktop grid layout with "View More" feature 1432 const grid = document.createElement('div'); 1433 grid.className = 'category-grid pb-5'; 1434 1435 // Limit initially visible cards 1436 const visibleCount = 10; // ~2 rows of 4 cards each 1437 matches.forEach((p, idx) => { 1438 const card = document.createElement('div'); 1439 card.className = 'tag-card'; 1440 if (idx >= visibleCount) card.classList.add('hidden-card'); // hidden initially 1441 1442 card.innerHTML = ` 1443 <img class="view-info" data-id="${p.id}" src="${p.image || '/assets/img/placeholder.png'}" alt="${p.name}"> 1444 <div class="gift-title">${p.name}</div> 1445 <div class="gift-price">$${p.price.toFixed(2)}</div> 1446 <button class="btn btn-white view-info btn-sm text-uppercase w-100" data-id="${p.id}">View Info</button> 1447 ${ 1448 p.soldout 1449 ? `<button class="btn btn-outline-primary btn-sm text-uppercase w-100 notify-btn" 1450 data-name="${p.name}" type="button"> 1451 Notify Me When in Stock 1452 </button>` 1453 : `<button class="btn btn-primary btn-sm text-uppercase w-100 add-to-cart" 1454 data-name="${p.name}">Add to Cart</button>` 1455 1456 1457 } 1458 1459 `; 1460 grid.appendChild(card); 1461 }); 1462 1463 const { headline, description } = categoryDescriptions[category] || {}; 1464 if (headline && description) { 1465 carouselContainer.innerHTML = ` 1466 <div class="category-header text-center mb-4"> 1467 <h2 class="f-chedros text-primary text-uppercase">${headline}</h2> 1468 <p class="stage-wrap text-secondary f-chedros fs-4 lh-1" style="max-width: 600px;">${description}</p> 1469 </div> 1470 `; 1471 } 1472 1473 1474 carouselContainer.appendChild(grid); 1475 1476 // Add "View More" button if there are hidden items 1477 if (matches.length > visibleCount) { 1478 const btnWrap = document.createElement('div'); 1479 btnWrap.className = 'text-center mt-3'; 1480 btnWrap.innerHTML = ` 1481 <button class="btn btn-outline-primary btn-view-more text-uppercase">View More</button> 1482 `; 1483 carouselContainer.appendChild(btnWrap); 1484 1485 const viewMoreBtn = btnWrap.querySelector('.btn-view-more'); 1486 viewMoreBtn.addEventListener('click', () => { 1487 grid.querySelectorAll('.hidden-card').forEach(el => { 1488 el.classList.remove('hidden-card'); 1489 el.style.animationDelay = '0s'; 1490 }); 1491 btnWrap.remove();
1491 // remove button after expanding 1492 }); 1493 } 1494 1495 // ð¹ Fade-in animation reset for visible cards 1496 requestAnimationFrame(() => { 1497 const cards = grid.querySelectorAll('.tag-card'); 1498 cards.forEach((card, i) => { 1499 card.style.animationDelay = `${i * 0.05}s`; 1500 card.style.animation = 'fadeInUp 0.6s ease forwards'; 1501 }); 1502 }); 1503 1504 } 1505 } 1506 1507 1508 // --- Category click handler --- 1509 listItems.forEach(li => { 1510 li.addEventListener('click', () => { 1511 const category = li.dataset.tag?.trim() || li.textContent.trim(); 1512 1513 if (window.fbq) { 1514 fbq('trackCustom', 'CategorySelected', { 1515 category_name: category, 1516 source: 'GiftGuru', 1517 }); 1518 console.log('ð FB Pixel: CategorySelected â', category); 1519 } 1520 1521 // highlight active 1522 listItems.forEach(x => x.classList.remove('active')); 1523 li.classList.add('active'); 1524 1525 // destroy existing swiper if any 1526 if (currentSwiper) { 1527 currentSwiper.destroy(true, true); 1528 currentSwiper = null; 1529 } 1530 1531 currentCategory = category; 1532 renderCategory(category); 1533 }); 1534 1535 1536}); 1537 1538 1539 // --- Window resize handler --- 1540 window.addEventListener('resize', () => { 1541 if (!currentCategory) return; // nothing to render yet 1542 // Destroy swiper before re-rendering 1543 if (currentSwiper) { 1544 currentSwiper.destroy(true, true); 1545 currentSwiper = null; 1546 } 1547 renderCategory(currentCategory); 1548 }); 1549}; 1550 1551 1552/* ========================================================== 1553 DEEP LINK CATEGORY LOADER â FULLY GUARANTEED TO WORK 1554 ========================================================== */ 1555nGift.deepLink = function() { 1556 // ---- Safe Trim for IE11 ---- 1557 function trim(str) { 1558 return str.replace(/^\s+|\s+$/g, ""); 1559 } 1560 1561 // ---- Query Param Extraction ---- 1562 function getQueryParam(name) { 1563 var search = window.location.search.substring(1).split("&"); 1564 for (var i = 0; i < search.length; i++) { 1565 var pair = search[i].split("="); 1566 if (pair[0] === name) return decodeURIComponent(pair[1] || ""); 1567 } 1568 return null; 1569 } 1570 1571 // ---- Poll Until Condition True ---- 1572 function waitFor(checkFn, callback, timeout) { 1573 var start = Date.now(); 1574 (function loop() { 1575 if (checkFn()) return callback(); 1576 if (Date.now() - start > timeout) { 1577 console.warn("DeepLink: timed out waiting for DOM elements"); 1578 return; 1579 } 1580 setTimeout(loop, 50); 1581 })(); 1582 } 1583 1584 // ---- RUN Deep Link Logic ---- 1585 function run() { 1586 var category = getQueryParam("category"); 1587 1588 // Hash fallback (#category=something) 1589 if (!category && window.location.hash.indexOf("category=") > -1) { 1590 category = window.location.hash.split("category=")[1]; 1591 } 1592 1593 if (!category) return; 1594 1595 category = trim(category).toLowerCase(); 1596 console.log("DeepLink: category detected â", category); 1597 1598 waitFor( 1599 function () { 1600 var list = document.querySelector(".gift-list"); 1601 return list && list.querySelectorAll("li").length > 0; 1602 }, 1603 function () { 1604 console.log("DeepLink: gift-list ready"); 1605 1606 var list = document.querySelectorAll(".gift-list")[0]; 1607 var items = list.querySelectorAll("li"); 1608 var found = null; 1609 1610 for (var i = 0; i < items.length; i++) { 1611 var tag = (items[i].getAttribute("data-tag") || "").toLowerCase(); 1612 var text = (items[i].textContent || items[i].innerText || "").toLowerCase(); 1613 if (tag === category || text === category) { 1614 found = items[i]; 1615 break; 1616 } 1617 } 1618 1619 if (!found) { 1620 console.warn("DeepLink: category not found:", category); 1621 return; 1622 } 1623 1624 console.log("DeepLink: triggering click on", found); 1625 1626 // IE-safe click 1627 if (typeof found.click === "function") { 1628 found.click(); 1629 } else { 1630 var evt = document.createEvent("MouseEvents"); 1631 evt.initEvent("click", true, true); 1632 found.dispatchEvent(evt); 1633 } 1634 1635 // Scroll to carousel
1636 setTimeout(function () { 1637 var carousel = document.getElementById("category-carousel"); 1638 if (!carousel) return; 1639 1640 var rect = carousel.getBoundingClientRect(); 1641 var y = rect.top + (window.pageYOffset || document.documentElement.scrollTop) - 80; 1642 1643 window.scrollTo(0, y); 1644 }, 350); 1645 1646 }, 1647 8000 // max timeout 1648 ); 1649 } 1650 1651 // ---- ENSURE page is fully loaded before running ---- 1652 waitFor( 1653 function () { 1654 return document.readyState === "complete"; 1655 }, 1656 run, 1657 15000 // safety timeout 1658 ); 1659}; 1660 1661 1662 1663 1664//rows 1665// Render horizontal Netflix-style rows by tag 1666nGift.renderTagRows = function (tags) { 1667 const container = document.getElementById("tag-rows"); 1668 container.innerHTML = ""; // reset 1669 1670 tags.forEach(tag => { 1671 // Create row container 1672 const rowDiv = document.createElement("div"); 1673 rowDiv.className = "tag-row"; 1674 1675 // Title 1676 const title = document.createElement("h3"); 1677 title.textContent = tag; 1678 rowDiv.appendChild(title); 1679 1680 // Create Swiper container 1681 const swiperEl = document.createElement("div"); 1682 swiperEl.className = "swiper tag-swiper"; 1683 1684 // Wrapper (required by Swiper) 1685 const wrapper = document.createElement("div"); 1686 wrapper.className = "swiper-wrapper"; 1687 1688 // Filter products by tag 1689 const products = nGift.products.filter(p => 1690 p.tags.some(t => t.toLowerCase().includes(tag.toLowerCase())) 1691 ); 1692 1693 // Add slides 1694 products.forEach(p => { 1695 const slide = document.createElement("div"); 1696 slide.className = "swiper-slide"; 1697 slide.innerHTML = ` 1698 <div class="tag-card"> 1699 <img src="${p.image || '/assets/img/placeholder.png'}" alt="${p.name}"> 1700 <div class="gift-title">${p.name}</div> 1701 <div class="gift-price">$${p.price.toFixed(2)}</div> 1702 <a href="${p.link}" class="gift-link" target="_blank">View</a> 1703 <button class="btn btn-primary add-to-cart" data-name="${p.name}" aria-label="Add ${p.name} to cart">Add to Cart</button> 1704 </div> 1705 `; 1706 wrapper.appendChild(slide); 1707 }); 1708 1709 // Optional navigation buttons 1710 const nextBtn = document.createElement("div"); 1711 nextBtn.className = "swiper-button-next"; 1712 const prevBtn = document.createElement("div"); 1713 prevBtn.className = "swiper-button-prev"; 1714 1715 // Assemble Swiper structure 1716 swiperEl.appendChild(wrapper); 1717 swiperEl.appendChild(nextBtn); 1718 swiperEl.appendChild(prevBtn); 1719 1720 // Append to the row 1721 rowDiv.appendChild(swiperEl); 1722 container.appendChild(rowDiv); 1723 1724 // â Initialize Swiper instance for this row 1725 new Swiper(swiperEl, { 1726 slidesPerView: 1.3, 1727 spaceBetween: 16, 1728 grabCursor: true, 1729 navigation: { 1730 nextEl: nextBtn, 1731 prevEl: prevBtn, 1732 }, 1733 breakpoints: { 1734 576: { slidesPerView: 2 }, 1735 768: { slidesPerView: 3 }, 1736 992: { slidesPerView: 4 }, 1737 }, 1738 }); 1739 }); 1740}; 1741 1742 1743 1744// Bind UI 1745// nGift.bindUI = function () { 1746// const input = document.getElementById("query"); 1747// const button = document.getElementById("searchBtn"); 1748 1749// button.addEventListener("click", async function () { 1750// const query = input.value.trim(); 1751// if (!query) return; 1752 1753// const messages = ["Searching products...", "Looking for matches...", "Almost found some..."]; 1754// nGift.showLoader(messages); 1755 1756// const matches = await nGift.askAI(query); 1757 1758// nGift.hideLoader(); 1759// nGift.renderProducts(matches); 1760// }); 1761// }; 1762 1763// Document Ready 1764document.addEventListener("DOMContentLoaded", async function () { 1765 1766 1767 const giftListItems = document.querySelectorAll('.gift-list li'); 1768 const categoryCarousel = document.getElementById('category-carousel'); 1769 1770 if (!giftListItems.length || !categoryCarousel) return; 1771 1772 const scrollOffset = 80; // adjust this height to match your fixed cart 1773 1774 giftListItems.forEach(item => { 1775 item.addEventListener('click', () => { 1776 if (window.innerWidth < 768) { 1777 setTimeout(() => { 1778 const elementTop = categoryCarousel.getBoundingClientRect().top + window.scrollY; 1779 const targetPosition = elementTop - scrollOffset; 1780 window.scrollTo({ top: targetPosition, behavior: 'smooth' }); 1781 }, 300); 1782 } 1783 }); 1784 }); 1785 1786 console.log("DOM fully loaded and parsed"); 1787
1788 // --- Load & Bind Quiz --- 1789 await nGift.loadProducts(); 1790 nGift.bindQuiz(); 1791 nGift.bindCategoryClicks(); 1792 1793 1794 1795 1796 // --- Cart Setup --- 1797 const cartEl = document.getElementById("mini-cart"); 1798 const cartBody = cartEl.querySelector(".cart-body"); 1799 const cartHeader = cartEl.querySelector(".cart-header"); 1800 const cartSummary = cartEl.querySelector(".cart-summary"); 1801 const cartTotalEl = cartEl.querySelector("#cart-total"); 1802 const clearCartBtn = document.getElementById("clear-cart"); 1803 1804 // Start collapsed 1805 cartBody.classList.remove("show"); 1806 1807 // Toggle Cart 1808 document.addEventListener("click", (e) => { 1809 const header = e.target.closest(".cart-header"); 1810 const toggleBtn = e.target.closest(".cart-toggle"); 1811 if (header || toggleBtn) { 1812 const isOpen = cartBody.classList.toggle("show"); 1813 cartHeader.classList.toggle("active", isOpen); 1814 } 1815 }); 1816 1817 // Clear Cart 1818 clearCartBtn.addEventListener("click", () => nGift.cart.clear()); 1819 1820 document.body.addEventListener("click", (e) => { 1821 const notifyBtn = e.target.closest(".notify-btn"); 1822 if (notifyBtn) { 1823 const name = notifyBtn.dataset.name || ""; 1824 openNotifyModal(name); 1825 } 1826 }); 1827 1828 // Add to Cart 1829 document.addEventListener("click", (e) => { 1830 if (!e.target.classList.contains("add-to-cart")) return; 1831 const name = e.target.dataset.name; 1832 const product = nGift.products.find((p) => p.name === name); 1833 if (!product) return; 1834 1835 nGift.cart.add(product); 1836 console.log(`ð Added to cart: ${product.name}`); 1837 1838 // Expand Cart + update summary 1839 cartBody.classList.add("show"); 1840 cartHeader.classList.add("active"); 1841 const count = nGift.cart.items?.length || 0; 1842 const total = nGift.cart.subtotal || 0; 1843 cartSummary.textContent = `Cart (${count})`; 1844 cartTotalEl.textContent = `$${total.toFixed(2)}`; 1845 1846 // Feedback 1847 e.target.textContent = "Added!"; 1848 e.target.disabled = true; 1849 e.target.classList.replace("btn-primary", "btn-secondary"); 1850 setTimeout(() => { 1851 e.target.textContent = "Add to Cart"; 1852 e.target.disabled = false; 1853 e.target.classList.replace("btn-secondary", "btn-primary"); 1854 }, 1500); 1855 }); 1856 1857 // View Info 1858 document.body.addEventListener("click", (e) => { 1859 const btn = e.target.closest(".view-info"); 1860 if (!btn) return; 1861 const productId = btn.dataset.id; 1862 const product = nGift.products?.find((p) => String(p.id) === String(productId)); 1863 if (product) nGift.showProductModal(product); 1864 }); 1865 1866 // --- Exit Intent / Gift Guru Modal --- 1867 // --- Exit Intent / Gift Guru Modal --- 1868let exitIntentShown = false; 1869const modalEl = document.getElementById("exit-modal"); 1870const modal = new bootstrap.Modal(modalEl); 1871const showDelay = 10000; // 10s before enabling triggers 1872const idleTimeout = 30000; // 30s idle for mobile 1873let lastScroll = window.scrollY; 1874let idleTimer; 1875 1876const isCartEmpty = () => !nGift.cart?.items || nGift.cart.items.length === 0; 1877 1878function showExitModal() { 1879 if (exitIntentShown || !isCartEmpty() || sessionStorage.getItem("guruExitShown")) return; 1880 exitIntentShown = true; 1881 sessionStorage.setItem("guruExitShown", "true"); 1882 modal.show(); 1883 if (window.fbq) fbq('trackCustom', 'ExitIntentShown'); 1884 console.log("ð Exit modal shown"); 1885} 1886 1887// â Handle âTry the Gift Guruâ click *after* modal opens 1888modalEl.addEventListener("shown.bs.modal", () => { 1889 const btn = modalEl.querySelector("#try-guru-btn"); 1890 if (!btn) { 1891 console.warn("â ï¸ try-guru-btn not found inside modal"); 1892 return; 1893 } 1894 1895 btn.onclick = () => { 1896 const guruSection = document.querySelector("#gift-guru, #guru, .gift-guru"); 1897 if (guruSection) { 1898 modal.hide(); 1899 window.scrollTo({ top: guruSection.offsetTop - 80, behavior: "smooth" }); 1900 if (window.fbq) fbq("trackCustom", "ExitIntentCTA"); 1901 console.log("ð¯ Scrolled to Gift Guru section"); 1902 } else { 1903 console.warn("â ï¸ Gift Guru section not found"); 1904 } 1905 }; 1906}); 1907 1908function handleExitIntent(e) { 1909 if (e.clientY < 40) showExitModal(); 1910} 1911 1912function handleScroll() { 1913 const currentScroll = window.scrollY; 1914 const scrollingUp = currentScroll < lastScroll - 60; 1915 const nearTop = currentScroll < 200; 1916 if (window.innerWidth < 768 && scrollingUp && nearTop && isCartEmpty()) showExitModal(); 1917 lastScroll = currentScroll; 1918 resetIdleTimer(); 1919} 1920 1921 1922 const cart = document.getElementById("mini-cart"); 1923 if (!cart) return; 1924 1925 // Detect its actual starting top (e.g., 100px or 80px) 1926 const initialTop = parseFloat(window.getComputedStyle(cart).top) || 0; 1927 const minTop = 10; // ð clamp limit 1928 1929 function handleMiniCartScroll() { 1930 const scrollY = window.scrollY; 1931 1932 // Clamp scroll amount to the range that moves the cart 1933 const clamped = Math.min(Math.max(scrollY, 0), initialTop - minTop); 1934 1935 // Map scroll progress from initialTop â minTop 1936 const newTop = initialTop - clamped; 1937 cart.style.top = `${newTop}px`; 1938 } 1939 1940 window.addEventListener("scroll", handleMiniCartScroll); 1941 window.addEventListener("resize", handleMiniCartScroll); 1942 handleMiniCartScroll(); 1943 1944 1945 1946function resetIdleTimer() { 1947 clearTimeout(idleTimer); 1948 idleTimer = setTimeout(() => { 1949 if (window.innerWidth < 768 && !exitIntentShown && isCartEmpty()) showExitModal(); 1950 }, idleTimeout); 1951} 1952 1953 // Enable triggers after a short delay 1954 setTimeout(() => { 1955 if (sessionStorage.getItem("guruExitShown")) return; 1956 document.addEventListener("mouseout", handleExitIntent); 1957 document.addEventListener("scroll", handleScroll); 1958 document.addEventListener("mousemove", resetIdleTimer); 1959 document.addEventListener("touchmove", resetIdleTimer); 1960 document.addEventListener("keydown", resetIdleTimer); 1961 resetIdleTimer(); 1962 }, showDelay); 1963 1964 console.log("â Exit intent logic initialized"); 1965 nGift.deepLink(); 1966 1967}); 1968 1969 1970 1971
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.