PageSourceSearch

https://nebogiftideas.com/js/scripts.js

js nebogiftideas.com collected 2026-10-06 13:37:23 UTC 60,969 bytes, 1,971 lines download raw bytes

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.