PageSourceSearch

https://abjamboreebuilder.netlify.app/script.js

js abjamboreebuilder.netlify.app collected 2026-10-03 10:24:13 UTC 47,190 bytes, 1,423 lines download raw bytes

1// VERSION 2.0 - COMPLETE FIXED SCRIPT WITH SELECTIVE CAROUSEL SHUFFLE
2// Only allow your builder to work on your domain and Netlify
3// Simple bot detection - won't block your real site
4(function() {
5  const botPatterns = /bot|crawler|spider|scraper|wget|curl|python|java(?!script)/i;
6  const isBot = botPatterns.test(navigator.userAgent);
7  
8  if (isBot) {
9    console.log('Bot detected:', navigator.userAgent);
10    document.body.style.display = 'none';
11    return;
12  }
13  
14  // Check for headless Chrome (often used by scrapers)
15  if (navigator.webdriver || !navigator.languages || navigator.languages.length === 0) {
16    console.log('Automated browser detected');
17    document.body.style.display = 'none';
18  }
19})();
20// At the very top of your script.js, after the bot detection:
21
22// Simple bot detection (you just added this)
23(function() {
24  const botPatterns = /bot|crawler|spider|scraper|wget|curl|python|java(?!script)/i;
25  // ... bot detection code ...
26})();
27
28// ADD RATE LIMITER RIGHT HERE - before DOMContentLoaded
29const rateLimiter = {
30  actions: [],
31  
32  check: function() {
33    const now = Date.now();
34    // Remove actions older than 1 minute
35    this.actions = this.actions.filter(time => now - time < 60000);
36    
37    // If more than 50 actions per minute, likely a bot
38    if (this.actions.length > 50) {
39      console.log('Rate limit exceeded');
40      document.body.innerHTML = '<h1>Too many requests</h1>';
41      return false;
42    }
43    
44    this.actions.push(now);
45    return true;
46  }
47};
48
49
50console.log('SCRIPT LOADED - VERSION 2.0 - WITH SELECTIVE SHUFFLE');
51
52document.addEventListener('DOMContentLoaded', async () => {
53  const basePath = './';
54
55  // ——— SHUFFLE FUNCTION ———
56  function shuffleArray(array) {
57    const shuffled = [...array];
58    for (let i = shuffled.length - 1; i > 0; i--) {
59      const j = Math.floor(Math.random() * (i + 1));
60      [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
61    }
62    return shuffled;
63  }
64
65  // ——— LOAD DATA FROM CSV ———
66  const resp = await fetch(`${basePath}data/products.csv`);
67  if (!resp.ok) {
68    console.error('Failed to load CSV:', resp.statusText);
69    return;
70  }
71  const csvText = await resp.text();
72  // Parse CSV
73  const [headerLine, ...rows] = csvText.trim().split(/\r?\n/);
74  const headers = headerLine.split(',').map(h => h.trim());
75  const records = rows.map(line => {
76    const cols = line.split(',').map(c => c.trim());
77    return headers.reduce((obj, key, i) => (obj[key] = cols[i] || '', obj), {});
78  });
79  // ——— BUILD DATA ———
80  const chains = records
81    .filter(r => r.type === 'chain')
82    .map(r => ({
83      id: r.id,
84      name: r.name,
85      img: `${basePath}${r.img}`,
86      lengths: {
87        12: +r.length12 || 0,
88        14: +r.length14 || 0,
89        16: +r.length16 || 0,
90        18: +r.length18 || 0,
91        20: +r.length20 || 0,
92        22: +r.length22 || 0,
93        24: +r.length24 || 0
94      }
95    }));
96  const beads = {};
97  records
98    .filter(r => r.type === 'bead')
99    .forEach(r => {
100      beads[r.category] = beads[r.category] || [];
101      beads[r.category].push({
102        id: r.id,
103        name: r.name,
104        line1: r.line1 || r.name,
105        line2: r.line2 || '',
106        img: `${basePath}${r.img}`,
107        price: +r.price || 0,
108        stoneColor: r.stone_color ? r.stone_color.split('|').map(c => c.trim()).filter(c => c) : []
109      });
110    });
111
112  // ——— SHUFFLE BEADS IN EACH CATEGORY (EXCEPT STONE) ———
113  Object.keys(beads).forEach(category => {
114    if (category !== 'stone') {
115      beads[category] = shuffleArray(beads[category]);
116      console.log(`Shuffled ${beads[category].length} items in ${category} category`);
117    } else {
118      console.log(`Kept original order for ${beads[category].length} items in ${category} category`);
119    }
120  });
121
122  // ——— STATE VARIABLES ———
123  let selectedChain = null;
124  let selectedLength = 0;
125  const placedBeads = [];
126  const chainButtons = document.getElementById('chain-buttons');
127  const lengthButtons = document.getElementById('length-buttons');
128  const chainImg = document.getElementById('chain-img');
129  const totalPriceEl = document.getElementById('total-price');
130  const beadSections = document.getElementById('bead-sections');
131  const builder = document.getElementById('builder');
132  // ——— DRAG VARIABLES ———
133  let dragEl = null;
134  let offsetX = 0;
135  let offsetY = 0;
136  // ——— CHAIN SIZING FUNCTION ———
137  function applyChainSizing() {
138    if (chainImg && chainImg.classList.contains('visible')) {
139      if (window.innerWidth <= 768) {
140        const isMobile = window.innerWidth <= 480;
141        
142        chainImg.style.setProperty('position', 'absolute', 'important');
143        chainImg.style.setProperty('top', isMobile ? '20%' : '25%', 'important');
144        chainImg.style.setProperty('left', '50%', 'important');
145        chainImg.style.setProperty('transform', 'translate(-50%, -50%)', 'important');
146        chainImg.style.setProperty('height', isMobile ? '120vh' : '130vh', 'important');
147        chainImg.style.setProperty('width', 'auto', 'important');
148        chainImg.style.setProperty('max-width', isMobile ? '160%' : '150%', 'important');
149        chainImg.style.setProperty('max-height', 'none', 'important');
150        chainImg.style.setProperty('object-fit', 'contain', 'important');
151        chainImg.style.setProperty('pointer-events', 'none', 'important');
152        chainImg.style.setProperty('z-index', '0', 'important');
153        chainImg.style.setProperty('display', 'block', 'important');
154        chainImg.style.setProperty('filter', 'drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1)) drop-shadow(0 4px 8px rgba(0, 0, 0, 0.15)) drop-shadow(0 8px 16px rgba(0, 0, 0, 0.1))', 'important');
155      } else {
156        chainImg.style.setProperty('position', 'absolute');
157        chainImg.style.setProperty('top', '40%');
158        chainImg.style.setProperty('left', '50%');
159        chainImg.style.setProperty('transform', 'translate(-50%, -50%)');
160        chainImg.style.setProperty('height', '60vh');
161        chainImg.style.setProperty('width', 'auto');
162        chainImg.style.setProperty('max-width', '100%');
163        chainImg.style.setProperty('max-height', '100%');
164        chainImg.style.setProperty('object-fit', 'contain');
165        chainImg.style.setProperty('pointer-events', 'none');
166        chainImg.style.setProperty('z-index', '0');
167        chainImg.style.setProperty('display', 'block');
168        chainImg.style.setProperty('filter', 'drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1)) drop-shadow(0 4px 8px rgba(0, 0, 0, 0.15)) drop-shadow(0 8px 16px rgba(0, 0, 0, 0.1))');
169      }
170    }
171  }
172  // ——— PRICE CALCULATION ———
173  function updatePrice() {
174    let total = 0;
175    if (selectedChain && selectedLength) {
176      total += selectedChain.lengths[selectedLength] || 0;
177    }
178    placedBeads.forEach(x => total += x.price);
179    totalPriceEl.textContent = `$${total}`;
180  }
181  // ——— RENDER CHAIN BUTTONS ———
182  chains.forEach(chain => {
183    const btn = document.createElement('button');
184    btn.textContent = chain.name;
185    btn.className = 'chain-btn'; // Add class for CSS targeting
186    btn.style.whiteSpace = 'nowrap';
187    btn.style.minWidth = 'auto';
188    btn.style.fontSize = '15px';
189    btn.style.padding = '0.3rem 0.6rem';
190    
191    btn.addEventListener('click', () => {
192      const placeholder = document.getElementById('placeholder-img');
193      if (placeholder) placeholder.classList.add('hidden');
194      
195      // Reset all chain buttons and add highlighting to selected one
196      chainButtons.querySelectorAll('button').forEach(b => {
197        b.classList.remove('active');
198        b.style.background = 'transparent';
199        b.style.color = '#3B2A1A';
200      });
201      btn.classList.add('active');
202      btn.style.background = '#fdd9b5'; // Orange cream color
203      btn.style.color = '#3B2A1A';
204      
205      selectedChain = chain;
206      chainImg.src = chain.img;
207      chainImg.classList.add('visible');
208      
209      setTimeout(applyChainSizing, 50);
210      setTimeout(applyChainSizing, 200);
211      setTimeout(applyChainSizing, 500);
212      setTimeout(applyChainSizing, 1000);
213      setTimeout(applyChainSizing, 2000);
214      
215      lengthButtons.innerHTML = '';
216      
217      let smallestButton = null;
218      Object.entries(chain.lengths).forEach(([len, price]) => {
219        if (price && price > 0) {
220          const lb = document.createElement('button');
221          lb.textContent = `${len}"`;
222          lb.className = 'length-btn'; // Add class for CSS targeting
223          lb.style.background = 'transparent';
224          lb.style.border = '1px solid #3B2A1A';
225          lb.style.borderRadius = '4px';
226          lb.style.padding = '0.3rem 0.6rem';
227          lb.style.fontSize = '0.9rem';
228          lb.style.cursor = 'pointer';
229          lb.style.transition = 'background 0.2s, color 0.2s';
230          lb.style.marginRight = '0.5rem';
231          
232          lb.addEventListener('click', () => {
233            lengthButtons.querySelectorAll('button').forEach(x => {
234              x.classList.remove('active');
235              x.style.background = 'transparent';
236              x.style.color = '#3B2A1A';
237            });
238            lb.classList.add('active');
239            lb.style.background = '#fdd9b5'; // Orange cream color
240            lb.style.color = '#3B2A1A';
241            selectedLength = +len;
242            updatePrice();
243          });
244          
245          lengthButtons.appendChild(lb);
246          
247          if (!smallestButton) {
248            smallestButton = lb;
249          }
250        }
251      });
252      
253      if (smallestButton) {
254        smallestButton.click();
255      }
256      
257      updatePrice();
258    });
259    chainButtons.append(btn);
260
261  });
262// Prevent right-click on the entire page
263document.addEventListener('contextmenu', (e) => {
264  e.preventDefault();
265  return false;
266});
267
268// Prevent text selection
269document.addEventListener('selectstart', (e) => {
270  e.preventDefault();
271  return false;
272});
273
274// Prevent drag on images
275document.querySelectorAll('img').forEach(img => {
276  img.draggable = false;
277  img.addEventListener('dragstart', (e) => {
278    e.preventDefault();
279    return false;
280  });
281});
282
283// Prevent keyboard shortcuts
284document.addEventListener('keydown', (e) => {
285  // Prevent Ctrl+S (save)
286  if ((e.ctrlKey || e.metaKey) && e.key === 's') {
287    e.preventDefault();
288    return false;
289  }
290  // Prevent Ctrl+P (print)
291  if ((e.ctrlKey || e.metaKey) && e.key === 'p') {
292    e.preventDefault();
293    alert('Please use the Email Your Design button instead of printing.');
294    return false;
295  }
296  // Prevent Ctrl+A (select all)
297  if ((e.ctrlKey || e.metaKey) && e.key === 'a') {
298    e.preventDefault();
299    return false;
300  }
301});
302   // ——— ADD MOBILE-SPECIFIC CSS STYLES ———
303  const mobileStyles = document.createElement('style');
304  mobileStyles.textContent = `
305    @media (max-width: 768px) {
306      .chain-btn {
307        font-size: 10px !important;
308        padding: 0.2rem 0.4rem !important;
309      }
310      .length-btn {
311        font-size: 0.7rem !important;
312        padding: 0.15rem 0.3rem !important;
313      }
314      .site-title {
315        width: 90% !important;
316        height: auto !important;
317      }
318	.pendant-carousel-img {
319        width: 140px !important;
320        height: 140px !important;
321      }
322    }
323    
324    @media (min-width: 769px) {
325      #builder {
326        margin-top: -20px !important;
327	margin-bottom: -20px !important;
328      }
329    }
330 /* Reduce pendant carousel card padding on all devices */
331    .pendant-card-content {
332      padding: .5px !important;
333    }
334 /* Fix placeholder image stretching */
335    #placeholder-img {
336      object-fit: contain !important;
337      width: auto !important;
338      height: auto !important;
339      max-width: 100% !important;
340      max-height: 100% !important;
341    }
342  `;
343  document.head.appendChild(mobileStyles);
344  // ——— CHAIN BUTTONS STYLING ———
345  chainButtons.style.display = 'flex';
346  chainButtons.style.flexWrap = window.innerWidth >= 768 ? 'nowrap' : 'wrap';
347  chainButtons.style.gap = '8px';
348  chainButtons.style.overflowX = window.innerWidth >= 768 ? 'auto' : 'visible';
349
350// ——— BUILD BEAD CAROUSELS WITH SWIPER.JS ———
351const categoryNames = {
352  'gold': 'Beads',
353  'stone': 'Carved Stone',
354  'pendant': 'Bubbles & Pendants'
355};
356
357// Add Swiper-specific styles to match original design
358const swiperStyles = document.createElement('style');
359swiperStyles.textContent = `
360  /* Container for swiper and buttons */
361  .swiper-container {
362    position: relative;
363    width: 100%;
364    margin: 0 auto;
365  }
366  
367  /* Main swiper styling */
368  .swiper {
369    width: calc(100% - 80px);
370    margin: 0 40px;
371    overflow: hidden;
372  }
373  
374  .swiper-wrapper {
375    display: flex;
376    transition-property: transform;
377  }
378  
379  .swiper-slide {
380    flex-shrink: 0;
381    height: auto;
382  }
383  
384  /* Arrow buttons positioned outside swiper */
385  .swiper-button-prev,
386  .swiper-button-next {
387    position: absolute !important;
388    width: 32px !important;
389    height: 20px !important;
390    background: transparent !important;
391    border: none !important;
392    border-radius: 4px !important;
393    top: 50% !important;
394    margin-top: -10px !important;
395    cursor: pointer;
396    display: flex !important;
397    align-items: center !important;
398    justify-content: center !important;
399    z-index: 10;
400  }
401  
402  .swiper-button-prev {
403    left: 0 !important;
404  }
405  
406  .swiper-button-next {
407    right: 0 !important;
408  }
409  
410  .swiper-button-prev::after,
411  .swiper-button-next::after {
412    font-size: 12px !important;
413    color: #3B2A1A !important;
414  }
415  
416  .swiper-button-prev::after {
417    content: '<' !important;
418  }
419  
420  .swiper-button-next::after {
421    content: '>' !important;
422  }
423  
424  .swiper-button-disabled {
425    opacity: 0.35 !important;
426  }
427  
428  /* Mobile adjustments */
429  @media (max-width: 768px) {
430    .swiper {
431      width: calc(100% - 60px);
432      margin: 0 30px;
433    }
434    .swiper-button-prev,
435    .swiper-button-next {
436      width: 28px !important;
437      height: 18px !important;
438    }
439    .swiper-button-prev::after,
440    .swiper-button-next::after {
441      font-size: 10px !important;
442    }
443  }
444`;
445document.head.appendChild(swiperStyles);
446
447Object.entries(beads).forEach(([category, items]) => {
448  const section = document.createElement('div');
449  section.className = 'section';
450  const categoryName = categoryNames[category] || category;
451  
452  // Color filtering setup
453  const allColors = [...new Set(
454    items.flatMap(item => Array.isArray(item.stoneColor) ? item.stoneColor : [item.stoneColor])
455  )].filter(color => color);
456  
457  const colorOrder = ['blue', 'yellow', 'pink', 'green', 'brown', 'purple', 'orange', 'white', 'multicolor', 'red', 'black', 'white gold'];
458  
459  const stoneColors = allColors.sort((a, b) => {
460    const aIndex = colorOrder.indexOf(a.toLowerCase());
461    const bIndex = colorOrder.indexOf(b.toLowerCase());
462    
463    if (aIndex !== -1 && bIndex !== -1) {
464      return aIndex - bIndex;
465    }
466    if (aIndex !== -1) return -1;
467    if (bIndex !== -1) return 1;
468    return a.localeCompare(b);
469  });
470  
471  let filterButtons = `<div class="filter-buttons" style="display: flex; gap: 0.5rem; margin-bottom: 0.5rem; justify-content: center; flex-wrap: wrap;">`;
472  
473  stoneColors.forEach(color => {
474    const colorPng = `colors/${category}/${color}.png`;
475    filterButtons += `
476      <button class="filter-btn color-filter" data-filter="${color}" style="
477        padding: 0; 
478        width: 32px; 
479        height: 32px; 
480        border: none; 
481        background: transparent; 
482        border-radius: 4px; 
483        cursor: pointer;
484        overflow: hidden;
485        display: flex;
486        align-items: center;
487        justify-content: center;
488      ">
489        <img src="${colorPng}" alt="${color}" style="
490          width: 100%; 
491          height: 100%; 
492          object-fit: cover; 
493          border-radius: 4px;
494        ">
495      </button>`;
496  });
497  
498  filterButtons += `</div>`;
499  
500  // Create unique ID for this swiper instance
501  const swiperId = `swiper-${category}`;
502  
503  section.innerHTML = `
504    <h2>${categoryName.toUpperCase()}</h2>
505    ${filterButtons}
506    <div class="swiper-container" style="position: relative;">
507      <div class="swiper" id="${swiperId}">
508        <div class="swiper-wrapper"></div>
509      </div>
510      <div class="swiper-button-prev"></div>
511      <div class="swiper-button-next"></div>
512    </div>`;
513  
514  beadSections.append(section);
515  
516  const swiperWrapper = section.querySelector('.swiper-wrapper');
517  const prevBtn = section.querySelector('.swiper-button-prev');
518  const nextBtn = section.querySelector('.swiper-button-next');
519  const allSlides = [];
520  const allItems = []; // Store item data for filtering
521  
522  // Create slides
523  items.forEach((item, index) => {
524    const slide = document.createElement('div');
525    slide.className = 'swiper-slide';
526    slide.style.cursor = 'pointer';
527    slide.dataset.itemIndex = index; // Track original item index
528    slide.dataset.stoneColor = Array.isArray(item.stoneColor) ? item.stoneColor.join('|') : (item.stoneColor || 'none');
529    
530    const cardContent = document.createElement('div');
531    cardContent.className = 'card-content';
532    if (category === 'pendant') {
533      cardContent.classList.add('pendant-card-content');
534    }
535    cardContent.style.display = 'flex';
536    cardContent.style.flexDirection = 'column';
537    cardContent.style.alignItems = 'center';
538    cardContent.style.textAlign = 'center';
539    cardContent.style.height = '100%';
540    cardContent.style.justifyContent = 'space-between';
541    cardContent.style.padding = '8px';
542    
543    const imgEl = document.createElement('img');
544    imgEl.src = item.img;
545    imgEl.alt = item.name;
546    imgEl.className = 'bead-img';
547    imgEl.draggable = false;
548    imgEl.style.width = '90px';
549    imgEl.style.height = '90px';
550    imgEl.style.objectFit = 'contain';
551    imgEl.style.backgroundColor = 'transparent';
552    imgEl.style.marginBottom = '8px';
553    imgEl.style.filter = 'drop-shadow(0 4px 8px rgba(0, 0, 0, 0.2))';
554    
555    if (category === 'pendant') {
556      imgEl.style.width = '155px';
557      imgEl.style.height = '155px';
558      imgEl.style.marginBottom = '0px';
559      imgEl.classList.add('pendant-carousel-img');
560    }
561    
562    const lbl = document.createElement('div');
563    lbl.className = 'bead-info';
564    lbl.style.fontSize = '12px';
565    lbl.style.lineHeight = '1.2';
566    lbl.style.display = 'flex';
567    lbl.style.flexDirection = 'column';
568    lbl.style.alignItems = 'center';
569    lbl.style.gap = '2px';
570    
571    const line1El = document.createElement('div');
572    line1El.textContent = item.line1;
573    line1El.style.fontWeight = 'normal';
574    line1El.style.fontSize = '11px';
575    line1El.style.color = '#3B2A1A';
576    
577    const line2El = document.createElement('div');
578    line2El.textContent = item.line2;
579    line2El.style.fontWeight = 'normal';
580    line2El.style.fontSize = '10px';
581    line2El.style.color = '#666';
582    
583    const line3El = document.createElement('div');
584    line3El.textContent = `$${item.price}`;
585    line3El.style.fontWeight = 'bold';
586    line3El.style.fontSize = '12px';
587    line3El.style.color = '#3B2A1A';
588    
589    lbl.appendChild(line1El);
590    lbl.appendChild(line2El);
591    lbl.appendChild(line3El);
592    
593    cardContent.appendChild(imgEl);
594    cardContent.appendChild(lbl);
595    slide.appendChild(cardContent);
596    
597    // Add click handler
598    slide.addEventListener('click', () => addBead({...item, category: category}));
599    
600    swiperWrapper.appendChild(slide);
601    allSlides.push(slide);
602    allItems.push(item);
603  });
604  
605  // Initialize Swiper with settings matching original Glider
606  const swiper = new Swiper(`#${swiperId}`, {
607    slidesPerView: window.innerWidth < 768 ? 3 : 6,
608    spaceBetween: 10, // Adjust spacing for better visual
609    loop: items.length > 6, // Enable loop only if we have enough items
610    
611    // Navigation arrows
612    navigation: {
613      nextEl: section.querySelector('.swiper-button-next'),
614      prevEl: section.querySelector('.swiper-button-prev'),
615    },
616    
617    // Responsive breakpoints matching original
618    breakpoints: {
619      320: {
620        slidesPerView: 3,  // Mobile shows 3 like original
621        spaceBetween: 8
622      },
623      768: {
624        slidesPerView: 4,
625        spaceBetween: 10
626      },
627      1024: {
628        slidesPerView: 6,  // Desktop shows 6 like original
629        spaceBetween: 10
630      }
631    },
632    
633    // Settings to match original behavior
634    speed: 500,
635    grabCursor: true,
636    watchOverflow: true,  // Disable navigation when not needed
637    
638    // Prevent clicks from being blocked
639    preventClicks: false,
640    preventClicksPropagation: false,
641    
642    // Better touch handling for mobile
643    touchRatio: 1,
644    touchReleaseOnEdges: true,
645
646 	mousewheel: {
647    forceToAxis: true,
648    sensitivity: 1,
649  },
650  freeMode: {
651    enabled: true,
652    sticky: true,
653  },
654  touchEventsTarget: 'wrapper',
655  simulateTouch: true,
656  allowTouchMove: true,
657  touchRatio: 1.5,
658  touchAngle: 45,
659  longSwipesRatio: 0.5,
660  });
661  
662  // Filter functionality with proper slide management
663  let activeFilter = null;
664  const filterBtns = section.querySelectorAll('.filter-btn');
665  
666  filterBtns.forEach(btn => {
667    btn.addEventListener('click', () => {
668      const filter = btn.dataset.filter;
669      
670      if (activeFilter === filter) {
671        // Clear filter - restore all slides
672        activeFilter = null;
673        
674        filterBtns.forEach(b => {
675          b.style.opacity = '1';
676          b.style.transform = 'scale(1)';
677        });
678        
679        // Remove all slides first
680        swiper.removeAllSlides();
681        
682        // Add all original slides back
683        allSlides.forEach((slide, index) => {
684          const newSlide = slide.cloneNode(true);
685          newSlide.addEventListener('click', () => addBead({...allItems[index], category: category}));
686          swiper.appendSlide(newSlide);
687        });
688        
689        // Update loop setting based on total items
690        swiper.params.loop = items.length > 6;
691        if (swiper.params.loop) {
692          swiper.loopDestroy();
693          swiper.loopCreate();
694        }
695        swiper.update();
696        
697      } else {
698        // Apply filter
699        activeFilter = filter;
700        
701        // Update button styles
702        filterBtns.forEach(b => {
703          if (b.dataset.filter === filter) {
704            b.style.opacity = '1';
705            b.style.transform = 'scale(1.1)';
706          } else {
707            b.style.opacity = '0.5';
708            b.style.transform = 'scale(1)';
709          }
710        });
711        
712        // Filter and rebuild slides
713        const filteredIndexes = [];
714        allSlides.forEach((slide, index) => {
715          const beadColors = slide.dataset.stoneColor.split('|').map(c => c.trim()).filter(c => c);
716          if (beadColors.includes(filter)) {
717            filteredIndexes.push(index);
718          }
719        });
720        
721        // Remove all slides
722        swiper.removeAllSlides();
723        
724        // Add only filtered slides
725        filteredIndexes.forEach(index => {
726          const newSlide = allSlides[index].cloneNode(true);
727          newSlide.addEventListener('click', () => addBead({...allItems[index], category: category}));
728          swiper.appendSlide(newSlide);
729        });
730        
731        // Disable loop if filtered items are too few
732        swiper.params.loop = filteredIndexes.length > 6;
733        if (swiper.params.loop) {
734          swiper.loopDestroy();
735          swiper.loopCreate();
736        }
737        swiper.update();
738      }
739      
740      // Go to first slide after filter change
741      swiper.slideTo(0, 0);
742    });
743  });
744});
745  // ——— ADD BEAD FUNCTION ———
746  function addBead(item) {
747    if (!rateLimiter.check()) return;
748
749    const isPendant = (item.category === 'pendant');
750    const isMobile = window.innerWidth <= 768;
751	let beadSize;
752	if (isMobile) {
753  		beadSize = isPendant ? 110 : 90; // Half size on mobile
754	} else {
755  		beadSize = isPendant ? 220 : 120; // Full size on desktop
756	}
757    
758    const container = document.createElement('div');
759    container.className = 'generator-bead';
760    if (isPendant) {
761      container.classList.add('pendant');
762    }
763    
764    const initialX = (builder.clientWidth / 2) - (beadSize / 2);
765    const initialY = 100;
766    
767    container.style.cssText = `
768      position: absolute !important;
769      cursor: grab !important;
770      z-index: 1 !important;
771      width: ${beadSize}px !important;
772      height: ${beadSize}px !important;
773      left: ${initialX}px !important;
774      top: ${initialY}px !important;
775      overflow: hidden !important;
776      pointer-events: auto !important;
777    `;
778    const imgEl = document.createElement('img');
779    imgEl.src = item.img;
780    imgEl.alt = item.name;
781    imgEl.className = 'generator-bead-img';
782    imgEl.dataset.price = item.price;
783    imgEl.dataset.id = item.id;
784    imgEl.dataset.name = item.name;
785    imgEl.dataset.category = item.category;
786    imgEl.draggable = false;
787    imgEl.style.width = '100%';
788    imgEl.style.height = '100%';
789    imgEl.style.objectFit = 'contain';
790    imgEl.style.pointerEvents = 'none';
791    imgEl.style.filter = 'drop-shadow(0 6px 12px rgba(0, 0, 0, 0.25))';
792    container.appendChild(imgEl);
793    builder.appendChild(container);
794    placedBeads.push({ 
795      node: container, 
796      price: item.price, 
797      name: item.name,
798      category: item.category,
799      size: beadSize
800    });
801    
802    updatePrice();
803    container.addEventListener('mousedown', startDrag);
804    container.addEventListener('touchstart', startDrag, { passive: false });
805    
806    container.addEventListener('mouseenter', (e) => {
807      if (!dragEl) {
808        container.style.setProperty('z-index', '10', 'important');
809        container.style.setProperty('transform', 'scale(1.05)', 'important');
810        container.style.setProperty('transition', 'transform 0.2s ease', 'important');
811      }
812    });
813    
814    container.addEventListener('mouseleave', (e) => {
815      if (!dragEl) {
816        container.style.setProperty('z-index', '2', 'important');
817        container.style.setProperty('transform', 'scale(1)', 'important');
818      }
819    });
820    
821    container.addEventListener('click', (e) => {
822      if (!dragEl) {
823        container.style.setProperty('transform', 'scale(1.1)', 'important');
824        setTimeout(() => {
825          container.style.setProperty('transform', 'scale(1)', 'important');
826        }, 150);
827      }
828    });
829  }
830  // ——— DRAG FUNCTIONS ———
831  function startDrag(e) {
832    e.preventDefault();
833    e.stopPropagation();
834    dragEl = e.currentTarget;
835    dragEl.style.zIndex = 1000;
836    
837    placedBeads.forEach(bead => {
838      if (bead.node !== dragEl) {
839        bead.node.style.zIndex = 1;
840      }
841    });
842    const clientX = e.touches ? e.touches[0].clientX : e.clientX;
843    const clientY = e.touches ? e.touches[0].clientY : e.clientY;
844    const rect = dragEl.getBoundingClientRect();
845    
846    offsetX = clientX - rect.left;
847    offsetY = clientY - rect.top;
848    dragEl.style.cursor = 'grabbing';
849    dragEl.style.pointerEvents = 'none';
850    document.addEventListener('mousemove', onDrag, { passive: false });
851    document.addEventListener('mouseup', endDrag);
852    document.addEventListener('touchmove', onDrag, { passive: false });
853    document.addEventListener('touchend', endDrag);
854  }
855  function onDrag(e) {
856    e.preventDefault();
857    if (!dragEl) return;
858    const clientX = e.touches ? e.touches[0].clientX : e.clientX;
859    const clientY = e.touches ? e.touches[0].clientY : e.clientY;
860    const builderRect = builder.getBoundingClientRect();
861    
862    const newX = clientX - builderRect.left - offsetX;
863    const newY = clientY - builderRect.top - offsetY;
864    dragEl.style.left = `${newX}px`;
865    dragEl.style.top = `${newY}px`;
866  }
867  function endDrag(e) {
868    if (!dragEl) return;
869    
870    dragEl.style.zIndex = '2';
871    dragEl.style.cursor = 'grab';
872    dragEl.style.pointerEvents = '';
873    document.removeEventListener('mousemove', onDrag);
874    document.removeEventListener('mouseup', endDrag);
875    document.removeEventListener('touchmove', onDrag);
876    document.removeEventListener('touchend', endDrag);
877    const builderRect = builder.getBoundingClientRect();
878    const beadRect = dragEl.getBoundingClientRect();
879// Add some buffer space for mobile to prevent accidental removal
880    const isMobile = window.innerWidth <= 768;
881    const buffer = isMobile ? 10 : 10; // More buffer on mobile was 20 70 
882     // Check if bead is outside the builder area (with buffer)
883    if (
884      beadRect.right < (builderRect.left - buffer) || 
885      beadRect.left > (builderRect.right + buffer) ||
886      beadRect.bottom < (builderRect.top - buffer) ||
887      beadRect.top > (builderRect.bottom + buffer)
888    ) {
889      console.log('Bead removed - outside builder area');
890      dragEl.remove();
891      const idx = placedBeads.findIndex(x => x.node === dragEl);
892      if (idx > -1) placedBeads.splice(idx, 1);
893      updatePrice();
894    }
895    dragEl = null;
896  }
897// ——— IMPROVED AUTO ALIGN ———
898const autoAlignBtn = document.getElementById('auto-align');
899if (autoAlignBtn) {
900  autoAlignBtn.addEventListener('click', () => {
901    if (placedBeads.length === 0) {
902      alert('Please add some beads first!');
903      return;
904    }
905    const builderRect = builder.getBoundingClientRect();
906    const w = builderRect.width;
907    const h = builderRect.height;
908    const n = placedBeads.length;
909    
910    if (n === 1) {
911      const bead = placedBeads[0];
912      const beadSize = bead.size || 120;
913      const centerX = w / 2 - beadSize / 2;
914      const centerY = h * 0.4;
915      bead.node.style.left = `${centerX}px`;
916      bead.node.style.top = `${centerY}px`;
917      bead.node.style.transform = 'none';
918      updatePrice();
919      return;
920    }
921    
922    const isMobile = window.innerWidth <= 768;
923    let P0, P1, P2;
924    
925    if (isMobile) {
926      // Mobile: gentle downward curve
927      const margin = 90;
928      P0 = { x: margin, y: h * 0.3 };
929      P1 = { x: w / 2, y: h * 0.5 };
930      P2 = { x: w - margin, y: h * 0.3 };
931    } else {
932      // Desktop: DEEPER curve that follows necklace shape better
933      // Moderate margins to keep beads within chain bounds
934      const margin = 110; // Balanced margin to prevent overflow
935      
936      // Deeper V-shaped curve for realistic necklace drape
937	P0 = { x: margin, y: h * 0.2 }; // Lower start
938	P1 = { x: w / 2, y: h * 0.95 }; // Lower center
939	P2 = { x: w - margin, y: h * 0.2 }; // Lower end
940    }
941    
942    console.log('Auto-align debug:', {
943      windowWidth: window.innerWidth,
944      isMobile: isMobile,
945      curveType: isMobile ? 'gentle curve' : 'necklace curve',
946      builderWidth: w,
947      builderHeight: h,
948      P0: P0,
949      P1: P1, 
950      P2: P2
951    });
952    
953    const samples = 1000;
954    const curvePoints = [];
955    let totalLen = 0;
956    
957    for (let i = 0; i <= samples; i++) {
958      const t = i / samples;
959      const x = (1 - t) * (1 - t) * P0.x + 2 * (1 - t) * t * P1.x + t * t * P2.x;
960      const y = (1 - t) * (1 - t) * P0.y + 2 * (1 - t) * t * P1.y + t * t * P2.y;
961      curvePoints.push({ x, y, t, length: totalLen });
962      
963      if (i > 0) {
964        const prev = curvePoints[i - 1];
965        totalLen += Math.hypot(x - prev.x, y - prev.y);
966        curvePoints[i].length = totalLen;
967      }
968    }
969    
970    // Adjusted to keep beads well-distributed on the deeper curve
971    const startLength = totalLen * 0.12; // Start a bit later to avoid edges
972    const endLength = totalLen * 0.88; // End a bit earlier to avoid edges
973    const usableLength = endLength - startLength;
974    
975    placedBeads.forEach((bead, i) => {
976      const beadSize = bead.size || 120;
977      
978      const ratio = i / (n - 1);
979      const targetLength = startLength + (ratio * usableLength);
980      
981      let bestPoint = curvePoints[0];
982      let minDiff = Math.abs(curvePoints[0].length - targetLength);
983      
984      for (let j = 1; j < curvePoints.length; j++) {
985        const diff = Math.abs(curvePoints[j].length - targetLength);
986        if (diff < minDiff) {
987          minDiff = diff;
988          bestPoint = curvePoints[j];
989        }
990      }
991      
992      const finalX = bestPoint.x - beadSize / 2;
993      const finalY = bestPoint.y - beadSize / 2;
994      
995      bead.node.style.left = `${finalX}px`;
996      bead.node.style.top = `${finalY}px`;
997      bead.node.style.transform = 'none';
998    });
999    
1000  updatePrice();
1001  });
1002}
1003    
1004  // ——— RESET ———
1005  const resetBtn = document.getElementById('reset');
1006  if (resetBtn) {
1007    resetBtn.addEventListener('click', () => {
1008      document.querySelectorAll('.generator-bead').forEach(bc => bc.remove());
1009      placedBeads.length = 0;
1010      updatePrice();
1011    });
1012  }
1013  // ——— STRIPE ADD TO CART - V2.0 ———
1014  const addToCartBtn = document.getElementById('add-to-cart');
1015  console.log('🔍 V2.0 - Checking for add-to-cart button:', addToCartBtn);
1016  
1017  if (addToCartBtn) {
1018    console.log('✅ V2.0 - Button found! Attaching event listener');
1019    
1020    addToCartBtn.addEventListener('click', async () => {
1021      console.log('🛒 V2.0 - ADD TO CART CLICKED!');
1022      
1023      if (!selectedChain || !selectedLength || placedBeads.length === 0) {
1024        alert('Please select a chain, length, and add some beads before adding to cart!');
1025        return;
1026      }
1027      
1028      addToCartBtn.textContent = 'Processing...';
1029      addToCartBtn.disabled = true;
1030      
1031      try {
1032        const timestamp = Date.now();
1033        const chainSku = `${selectedChain.id}-${selectedLength}`;
1034        const beadSkus = placedBeads
1035          .map(bead => bead.node.querySelector('img').dataset.id)
1036          .sort()
1037          .join('-');
1038        const uniqueSku = `NB-${chainSku}-${beadSkus}-${timestamp}`;
1039        
1040        let totalPrice = 0;
1041        if (selectedChain && selectedLength) {
1042          totalPrice += selectedChain.lengths[selectedLength] || 0;
1043        }
1044        placedBeads.forEach(bead => totalPrice += bead.price);
1045        
1046        const beadsData = placedBeads.map(bead => {
1047          const beadImg = bead.node.querySelector('img');
1048          return {
1049            name: beadImg.dataset.name || bead.name || 'Custom Bead',
1050            price: bead.price || 0,
1051            category: bead.category || 'unknown',
1052            id: beadImg.dataset.id || 'unknown',
1053            size: bead.size || 120
1054          };
1055        });
1056        
1057        const stripeData = {
1058          sku: uniqueSku,
1059          total_price: totalPrice,
1060          chain: {
1061            name: selectedChain.name,
1062            length: selectedLength,
1063            price: selectedChain.lengths[selectedLength] || 0
1064          },
1065          beads: beadsData
1066        };
1067        
1068        console.log('🚀 V2.0 - SENDING TO STRIPE:');
1069        console.log(JSON.stringify(stripeData, null, 2));
1070        
1071        const response = await fetch('/.netlify/functions/create-checkout-session', {
1072          method: 'POST',
1073          headers: {
1074            'Content-Type': 'application/json',
1075          },
1076          body: JSON.stringify(stripeData),
1077        });
1078        
1079        console.log('📡 Response status:', response.status);
1080        
1081        const responseText = await response.text();
1082        console.log('📡 Response text:', responseText);
1083        
1084        if (!response.ok) {
1085          throw new Error(`HTTP ${response.status}: ${responseText}`);
1086        }
1087        
1088        const session = JSON.parse(responseText);
1089        
1090        if (session.url) {
1091          console.log('✅ SUCCESS! Redirecting to Stripe:', session.url);
1092          window.top.location.href = session.url;
1093        } else {
1094          throw new Error('No checkout URL returned from Stripe');
1095        }
1096        
1097      } catch (error) {
1098        console.error('❌ Checkout error:', error);
1099        alert(`Error creating checkout session: ${error.message}`);
1100        
1101        addToCartBtn.textContent = 'Add to Cart';
1102        addToCartBtn.disabled = false;
1103      }
1104    });
1105  } else {
1106    console.error('❌ V2.0 - Add to cart button NOT FOUND!');
1107  }
1108  // ——— EMAIL DESIGN FUNCTIONALITY ———
1109  emailjs.init({ publicKey: "9kLzRbVqEJgUaD855" });
1110  const emailDesignBtn = document.getElementById('email-your-design');
1111  if (emailDesignBtn) {
1112    emailDesignBtn.addEventListener('click', () => {
1113      const userEmail = document.getElementById('user-email').value;
1114      if (!userEmail) {
1115        alert('Please enter an email address!');
1116        return;
1117      }
1118      const beadsList = placedBeads.map(b => b.name).join(', ') || 'No Beads Selected';
1119      const designDetails = {
1120        chain: selectedChain ? selectedChain.name : 'No Chain',
1121        length: selectedLength ? `${selectedLength}"` : 'No Length',
1122        beads: beadsList,
1123        totalPrice: totalPriceEl.textContent,
1124        user_email: userEmail,
1125      };
1126      const canvas = document.createElement("canvas");
1127      const ctx = canvas.getContext("2d");
1128      
1129      const builderRect = builder.getBoundingClientRect();
1130      const isMobile = window.innerWidth <= 768;
1131      
1132      const scaleFactor = 3;
1133      canvas.width = builderRect.width * scaleFactor;
1134      canvas.height = builderRect.height * scaleFactor;
1135      ctx.scale(scaleFactor, scaleFactor);
1136      const chainImage = new Image();
1137      chainImage.src = chainImg.src;
1138      chainImage.onload = () => {
1139        if (isMobile) {
1140          const canvasWidth = builderRect.width;
1141          const canvasHeight = builderRect.height;
1142          
1143          const targetChainHeight = canvasHeight * 1.2;
1144          const chainAspectRatio = chainImage.naturalWidth / chainImage.naturalHeight;
1145          const targetChainWidth = targetChainHeight * chainAspectRatio;
1146          
1147          const chainX = (canvasWidth - targetChainWidth) / 2;
1148          const chainY = (canvasHeight - targetChainHeight) / 2;
1149          
1150          ctx.shadowColor = 'rgba(0, 0, 0, 0.2)';
1151          ctx.shadowBlur = 8;
1152          ctx.shadowOffsetX = 0;
1153          ctx.shadowOffsetY = 4;
1154          
1155          ctx.drawImage(chainImage, chainX, chainY, targetChainWidth, targetChainHeight);
1156          
1157          ctx.shadowColor = 'transparent';
1158          ctx.shadowBlur = 0;
1159          ctx.shadowOffsetX = 0;
1160          ctx.shadowOffsetY = 0;
1161        } else {
1162          const chainImgRect = chainImg.getBoundingClientRect();
1163          const builderOffsetX = chainImgRect.left - builderRect.left;
1164          const builderOffsetY = chainImgRect.top - builderRect.top;
1165          
1166          ctx.shadowColor = 'rgba(0, 0, 0, 0.2)';
1167          ctx.shadowBlur = 8;
1168          ctx.shadowOffsetX = 0;
1169          ctx.shadowOffsetY = 4;
1170          
1171          ctx.drawImage(
1172            chainImage, 
1173            builderOffsetX, 
1174            builderOffsetY, 
1175            chainImgRect.width, 
1176            chainImgRect.height
1177          );
1178          
1179          ctx.shadowColor = 'transparent';
1180          ctx.shadowBlur = 0;
1181          ctx.shadowOffsetX = 0;
1182          ctx.shadowOffsetY = 0;
1183        }
1184        
1185        const beadPromises = placedBeads.map(bead => {
1186          return new Promise((resolve) => { 
1187            const img = new Image();
1188            img.src = bead.node.querySelector('img').src;
1189            img.onload = () => {
1190              const beadRect = bead.node.getBoundingClientRect();
1191              let beadX = beadRect.left - builderRect.left;
1192              let beadY = beadRect.top - builderRect.top;
1193              
1194              if (isMobile) {
1195                beadY += 80;
1196              }
1197              
1198              const aspectRatio = img.naturalWidth / img.naturalHeight;
1199              
1200              const isPendant = bead.category === 'pendant';
1201              const beadSize = bead.size || (isPendant ? 180 : 120);
1202              
1203              let drawWidth = beadSize;
1204              let drawHeight = beadSize;
1205              
1206              if (aspectRatio > 1) {
1207                drawHeight = beadSize / aspectRatio;
1208              } else if (aspectRatio < 1) {
1209                drawWidth = beadSize * aspectRatio;
1210              }
1211              
1212              const offsetX = (beadSize - drawWidth) / 2;
1213              const offsetY = (beadSize - drawHeight) / 2;
1214              
1215              ctx.shadowColor = 'rgba(0, 0, 0, 0.25)';
1216              ctx.shadowBlur = 12;
1217              ctx.shadowOffsetX = 0;
1218              ctx.shadowOffsetY = 6;
1219              
1220              ctx.drawImage(
1221                img, 
1222                beadX + offsetX, 
1223                beadY + offsetY, 
1224                drawWidth, 
1225                drawHeight
1226              );
1227              
1228              ctx.shadowColor = 'transparent';
1229              ctx.shadowBlur = 0;
1230              ctx.shadowOffsetX = 0;
1231              ctx.shadowOffsetY = 0;
1232              
1233              resolve();
1234            };
1235          });
1236        });
1237        Promise.all(beadPromises).then(() => {
1238          canvas.toBlob(function (blob) {
1239            const screenshotBase64 = canvas.toDataURL('image/png');
1240            designDetails.attachment = screenshotBase64;
1241            
1242            emailjs.send('service_y9a2429', 'template_wpzu4qm', designDetails)
1243              .then(response => {
1244                console.log('Design sent successfully!', response);
1245                alert('Your design has been sent to your email!');
1246              })
1247              .catch(error => {
1248                console.error('Error during screenshot or email send', error);
1249                alert('Oops! Something went wrong, please try again later.');
1250              });
1251          }, 'image/png');
1252        });
1253      };
1254    });
1255  }
1256// ——— SEE IT ON FUNCTIONALITY ———
1257// Add this code to your script.js, near the end (after the email design functionality)
1258
1259const seeItOnBtn = document.getElementById('see-it-on');
1260if (seeItOnBtn) {
1261  seeItOnBtn.addEventListener('click', (e) => {
1262    e.preventDefault();
1263    
1264    // Check if there's a design to show
1265    if (!selectedChain) {
1266      alert('Please select a chain first!');
1267      return;
1268    }
1269
1270    // Open window immediately (must be in direct response to click)
1271    const newWindow = window.open('see-it-on.html', '_blank');
1272
1273    // Capture the design image using canvas
1274    const canvas = document.createElement("canvas");
1275    const ctx = canvas.getContext("2d");
1276    
1277    const builderRect = builder.getBoundingClientRect();
1278    const isMobile = window.innerWidth <= 768;
1279    
1280    const scaleFactor = 3;
1281    canvas.width = builderRect.width * scaleFactor;
1282    canvas.height = builderRect.height * scaleFactor;
1283    ctx.scale(scaleFactor, scaleFactor);
1284
1285    const chainImage = new Image();
1286    chainImage.crossOrigin = "anonymous";
1287    chainImage.src = chainImg.src;
1288    
1289    chainImage.onload = () => {
1290      // Draw transparent background
1291      ctx.clearRect(0, 0, canvas.width, canvas.height);
1292      
1293      if (isMobile) {
1294        const canvasWidth = builderRect.width;
1295        const canvasHeight = builderRect.height;
1296        
1297        const targetChainHeight = canvasHeight * 1.2;
1298        const chainAspectRatio = chainImage.naturalWidth / chainImage.naturalHeight;
1299        const targetChainWidth = targetChainHeight * chainAspectRatio;
1300        
1301        const chainX = (canvasWidth - targetChainWidth) / 2;
1302        const chainY = (canvasHeight - targetChainHeight) / 2;
1303        
1304        ctx.shadowColor = 'rgba(0, 0, 0, 0.2)';
1305        ctx.shadowBlur = 8;
1306        ctx.drawImage(chainImage, chainX, chainY, targetChainWidth, targetChainHeight);
1307        ctx.shadowColor = 'transparent';
1308        ctx.shadowBlur = 0;
1309      } else {
1310        const chainImgRect = chainImg.getBoundingClientRect();
1311        const builderOffsetX = chainImgRect.left - builderRect.left;
1312        const builderOffsetY = chainImgRect.top - builderRect.top;
1313        
1314        ctx.shadowColor = 'rgba(0, 0, 0, 0.2)';
1315        ctx.shadowBlur = 8;
1316        ctx.drawImage(chainImage, builderOffsetX, builderOffsetY, chainImgRect.width, chainImgRect.height);
1317        ctx.shadowColor = 'transparent';
1318        ctx.shadowBlur = 0;
1319      }
1320      
1321      // Draw beads
1322      let beadsDrawn = 0;
1323      const totalBeads = placedBeads.length;
1324      
1325      if (totalBeads === 0) {
1326        // No beads, save and navigate
1327        finishAndNavigate();
1328        return;
1329      }
1330      
1331      placedBeads.forEach((bead, index) => {
1332        const beadImgEl = bead.node ? bead.node.querySelector('img') : null;
1333        if (!beadImgEl || !beadImgEl.src) {
1334          beadsDrawn++;
1335          if (beadsDrawn === totalBeads) finishAndNavigate();
1336          return;
1337        }
1338        
1339        const img = new Image();
1340        img.crossOrigin = "anonymous";
1341        
1342        img.onload = () => {
1343          const beadRect = bead.node.getBoundingClientRect();
1344          let beadX = beadRect.left - builderRect.left;
1345          let beadY = beadRect.top - builderRect.top;
1346          
1347          if (isMobile) {
1348            beadY += 80;
1349          }
1350          
1351          const aspectRatio = img.naturalWidth / img.naturalHeight || 1;
1352          const isPendant = bead.category === 'pendant';
1353          const beadSize = bead.size || (isPendant ? 180 : 120);
1354          
1355          let drawWidth = beadSize;
1356          let drawHeight = beadSize;
1357          
1358          if (aspectRatio > 1) {
1359            drawHeight = beadSize / aspectRatio;
1360          } else if (aspectRatio < 1) {
1361            drawWidth = beadSize * aspectRatio;
1362          }
1363          
1364          const offsetX = (beadSize - drawWidth) / 2;
1365          const offsetY = (beadSize - drawHeight) / 2;
1366          
1367          ctx.shadowColor = 'rgba(0, 0, 0, 0.25)';
1368          ctx.shadowBlur = 12;
1369          ctx.drawImage(img, beadX + offsetX, beadY + offsetY, drawWidth, drawHeight);
1370          ctx.shadowColor = 'transparent';
1371          ctx.shadowBlur = 0;
1372          
1373          beadsDrawn++;
1374          if (beadsDrawn === totalBeads) finishAndNavigate();
1375        };
1376        
1377        img.onerror = () => {
1378          beadsDrawn++;
1379          if (beadsDrawn === totalBeads) finishAndNavigate();
1380        };
1381        
1382        img.src = beadImgEl.src;
1383      });
1384      
1385      function finishAndNavigate() {
1386        // Convert canvas to base64 and save
1387        const designImageData = canvas.toDataURL('image/png');
1388        localStorage.setItem('jamboree_design', designImageData);
1389        
1390        // Send data to the new window via postMessage
1391        if (newWindow && !newWindow.closed) {
1392          // Keep trying to send until the window is ready
1393          let attempts = 0;
1394          const sendData = () => {
1395            try {
1396              newWindow.postMessage({ type: 'designData', data: designImageData }, '*');
1397              attempts++;
1398              if (attempts < 20) {
1399                setTimeout(sendData, 100);
1400              }
1401            } catch(e) {
1402              console.log('postMessage error:', e);
1403            }
1404          };
1405          sendData();
1406        }
1407      }
1408    };
1409
1410    chainImage.onerror = () => {
1411      alert('Error loading design. Please try again.');
1412    };
1413  });
1414}
1415  // Apply mobile sizing on window resize
1416  window.addEventListener('resize', applyChainSizing);
1417  // Add orientation change listener for mobile devices
1418  window.addEventListener('orientationchange', () => {
1419    setTimeout(applyChainSizing, 200);
1420    setTimeout(applyChainSizing, 800);
1421    setTimeout(applyChainSizing, 1500);
1422  });
1423});

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.