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.