1(function() { 2 'use strict'; 3 4 // State management 5 let compareList = []; 6 let isAnalyzing = false; 7 let currentAnalysis = null; 8 9 // DOM elements 10 const compareBtn = document.getElementById('compareBtn'); 11 const compareSidebar = document.getElementById('compareSidebar'); 12 const compareOverlay = document.getElementById('compareOverlay'); 13 const compareClose = document.getElementById('compareClose'); 14 const compareModal = document.getElementById('aiCompareModal'); 15 const compareModalClose = document.getElementById('aiCompareModalClose'); 16 17 // Initialize 18 document.addEventListener('DOMContentLoaded', async function() { 19 await initializeCompare(); 20 setupEventListeners(); 21 }); 22 23 /** 24 * Initialize compare functionality 25 */ 26 async function initializeCompare() { 27 // Load compare list first before setting up buttons 28 await loadCompareList(); 29 30 // Setup product detail compare button if exists 31 const productCompareBtn = document.getElementById('pd-compare-btn'); 32 33 if (productCompareBtn) { 34 setupProductCompareButton(productCompareBtn); 35 } 36 37 // Check for existing compare list and update UI 38 if (compareList.length > 0) { 39 // Only update badge and buttons, no sticky bar 40 } 41 } 42 43 /** 44 * Setup event listeners 45 */ 46 function setupEventListeners() { 47 // Navigation compare button 48 if (compareBtn) { 49 compareBtn.addEventListener('click', toggleCompareSidebar); 50 } 51 52 // Sidebar controls 53 if (compareClose) { 54 compareClose.addEventListener('click', closeCompareSidebar); 55 } 56 57 if (compareOverlay) { 58 compareOverlay.addEventListener('click', closeCompareSidebar); 59 } 60 61 // Sidebar actions 62 const clearAllBtn = document.getElementById('compareClearAll'); 63 if (clearAllBtn) { 64 clearAllBtn.addEventListener('click', clearAllProducts); 65 } 66 67 const analyzeBtn = document.getElementById('compareAnalyze'); 68 if (analyzeBtn) { 69 analyzeBtn.addEventListener('click', startAIAnalysis); 70 } 71 72 // Modal controls 73 if (compareModalClose) { 74 compareModalClose.addEventListener('click', closeModal); 75 } 76 77 const modalBackdrop = compareModal?.querySelector('.modal-backdrop'); 78 if (modalBackdrop) { 79 modalBackdrop.addEventListener('click', closeModal); 80 } 81 82 // Modal actions 83 const retryBtn = document.getElementById('aiRetry'); 84 if (retryBtn) { 85 retryBtn.addEventListener('click', startAIAnalysis); 86 } 87 88 // Removed share and add to cart buttons - replaced with AI disclaimer 89 } 90 91 /** 92 * Setup product detail compare button 93 */ 94 function setupProductCompareButton(button) { 95 const productId = parseInt(button.dataset.productId); 96 if (!productId) return; 97 98 // Check if product is already in compare list 99 updateProductCompareButtonState(button, productId); 100 101 button.addEventListener('click', async function() { 102 // Get current variant ID (if any) 103 const variantIdElement = document.getElementById('pd-variant-id'); 104 const variantId = variantIdElement ? parseInt(variantIdElement.value) || 0 : 0; 105 106 // Update button's variant data for current selection 107 button.dataset.variantId = variantId.toString(); 108 109 await toggleProductInCompare(productId, button, variantId); 110 }); 111 } 112 113 /** 114 * Toggle product in compare list 115 */ 116 async function toggleProductInCompare(productId, button, variantId = 0) { 117 // Check by product ID (variants of same product treated as same item) 118 const isInCompare = compareList.some(p => parseInt(p.id) === parseInt(productId)); 119 120 try { 121 if (isInCompare) { 122 await removeFromCompare(productId); 123 showToast('Produk dihapus dari perbandingan', 'success'); 124 } else { 125 await addToCompare(productId, variantId); 126 showToast('Produk ditambahkan ke perbandingan', 'success'); 127 128 // Trigger fly-to-compare animation 129 triggerFlyToCompareAnimation(button); 130 } 131 132 updateProductCompareButtonState(button, productId); 133 updateCompareBadge(); 134 updateAllCompareButtons(); 135 136 } catch (error) { 137 showToast(error.message || 'Terjadi kesalahan', 'error'); 138 } 139 } 140 141 /** 142 * Update product compare button state 143 */ 144 function updateProductCompareButtonState(button, productId) { 145 // Ensure both IDs are compared as integers 146 const isInCompare = compareList.some(p => parseInt(p.id) === parseInt(productId)); 147 button.classList.toggle('added', isInCompare); 148 149 // Handle visibility of text elements 150 const compareText = button.querySelector('.compare-text'); 151 const compareRemove = button.querySelector('.compare-remove'); 152 const compareNote = document.getElementById('compare-note'); 153 154 if (compareText) { 155 compareText.style.display = isInCompare ? 'none' : 'inline'; 156 } 157 158 if (compareRemove) { 159 if (isInCompare) { 160 compareRemove.removeAttribute('hidden'); 161 compareRemove.style.display = 'flex'; 162 } else { 163 compareRemove.setAttribute('hidden', ''); 164 compareRemove.style.display = 'none'; 165 } 166 } 167 168 // Update info note 169 if (compareNote) { 170 if (isInCompare) { 171 const currentCount = compareList.length; 172 compareNote.textContent = `Produk sudah ada di daftar perbandingan (${currentCount}/5). Klik tombol untuk menghapus.`; 173 } else { 174 compareNote.textContent = 'Bandingkan hingga 5 produk dengan bantuan AI'; 175 } 176 } 177 178 // Inline compare preview removed - keeping it simple 179 } 180 181 /** 182 * Add product to compare list 183 */ 184 async function addToCompare(productId, variantId = 0) { 185 const requestData = { product_id: productId }; 186 if (variantId > 0) { 187 requestData.variant_id = variantId; 188 } 189 190 const response = await fetch('/api/compare/add.php', { 191 method: 'POST', 192 headers: { 193 'Content-Type': 'application/json', 194 }, 195 body: JSON.stringify(requestData), 196 credentials: 'same-origin' 197 }); 198 199 const result = await response.json(); 200 201 if (!result.success) { 202 throw new Error(result.error || 'Gagal menambahkan produk'); 203 } 204 205 // Send Google Analytics event for add to compare 206 if (typeof gtag === 'function') { 207 gtag('event', 'add_to_compare', { 208 event_category: 'Product Compare', 209 event_label: 'Product Added to Compare', 210 product_id: productId, 211 variant_id: variantId || 0, 212 custom_map: { 213 custom_parameter_1: 'compare_add_product' 214 } 215 }); 216 } 217
218 await loadCompareList(); 219 return result; 220 } 221 222 /** 223 * Remove product from compare list 224 */ 225 async function removeFromCompare(productId) { 226 const response = await fetch('/api/compare/remove.php', { 227 method: 'POST', 228 headers: { 229 'Content-Type': 'application/json', 230 }, 231 body: JSON.stringify({ product_id: productId }), 232 credentials: 'same-origin' 233 }); 234 235 const result = await response.json(); 236 237 if (!result.success) { 238 throw new Error(result.error || 'Gagal menghapus produk'); 239 } 240 241 // Send Google Analytics event for remove from compare 242 if (typeof gtag === 'function') { 243 gtag('event', 'remove_from_compare', { 244 event_category: 'Product Compare', 245 event_label: 'Product Removed from Compare', 246 product_id: productId, 247 custom_map: { 248 custom_parameter_1: 'compare_remove_product' 249 } 250 }); 251 } 252 253 await loadCompareList(); 254 return result; 255 } 256 257 /** 258 * Clear all products from compare list 259 */ 260 async function clearAllProducts() { 261 if (compareList.length === 0) return; 262 263 // Show confirmation modal 264 const confirmed = await showConfirmModal({ 265 title: 'Hapus Semua Produk', 266 message: 'Apakah Anda yakin ingin menghapus semua produk dari daftar perbandingan?', 267 confirmText: 'Hapus Semua', 268 cancelText: 'Batal', 269 type: 'danger' 270 }); 271 272 if (!confirmed) return; 273 274 try { 275 const response = await fetch('/api/compare/clear.php', { 276 method: 'POST', 277 credentials: 'same-origin' 278 }); 279 280 const result = await response.json(); 281 282 if (!result.success) { 283 throw new Error(result.error || 'Gagal menghapus produk'); 284 } 285 286 // Send Google Analytics event for clear all products 287 if (typeof gtag === 'function') { 288 gtag('event', 'compare_clear_all', { 289 event_category: 'Product Compare', 290 event_label: 'Clear All Products', 291 products_count: compareList.length, 292 custom_map: { 293 custom_parameter_1: 'compare_clear_action' 294 } 295 }); 296 } 297 298 await loadCompareList(); 299 showToast('Semua produk dihapus dari perbandingan', 'success'); 300 301 } catch (error) { 302 showToast(error.message || 'Terjadi kesalahan', 'error'); 303 } 304 } 305 306 /** 307 * Load compare list from server 308 */ 309 async function loadCompareList() { 310 try { 311 const response = await fetch('/api/compare/get.php', { 312 credentials: 'same-origin' 313 }); 314 315 const result = await response.json(); 316 317 if (result.success) { 318 compareList = result.products || []; 319 updateCompareSidebar(); 320 updateCompareBadge(); 321 updateAllCompareButtons(); 322 } 323 324 } catch (error) { 325 // Silently handle error - compare functionality will work with empty list 326 } 327 } 328 329 /** 330 * Update compare sidebar content 331 */ 332 function updateCompareSidebar() { 333 const emptyState = document.getElementById('compareEmpty'); 334 const listContainer = document.getElementById('compareList'); 335 const actionsContainer = document.getElementById('compareActions'); 336 const countElement = document.getElementById('compareCountSidebar'); 337 338 if (!emptyState || !listContainer || !actionsContainer || !countElement) return; 339 340 // Update count 341 countElement.textContent = `(${compareList.length})`; 342 343 if (compareList.length === 0) { 344 emptyState.style.display = 'block'; 345 listContainer.style.display = 'none'; 346 actionsContainer.style.display = 'none'; 347 } else { 348 emptyState.style.display = 'none'; 349 listContainer.style.display = 'block'; 350 actionsContainer.style.display = 'flex'; 351 352 // Render product list 353 listContainer.innerHTML = compareList.map(product => { 354 // Display variant name if available 355 let displayName = product.name; 356 if (product.combination_name) { 357 displayName += ` (${product.combination_name})`; 358 } 359 360 // Use variant price if available 361 let displayPrice = product.price; 362 if (product.variant_price !== undefined && product.variant_price !== null) { 363 displayPrice = product.variant_price; 364 } 365 366 return ` 367 <div class="compare-item" data-product-id="${product.id}"> 368 <div class="compare-item-image"> 369 <img src="${product.image_url}" alt="${escapeHtml(displayName)}" loading="lazy"> 370 </div> 371 <div class="compare-item-info"> 372 <div class="compare-item-name">${escapeHtml(displayName)}</div> 373 <div class="compare-item-price">${formatPrice(displayPrice)}</div> 374 </div> 375 <button class="compare-item-remove" data-product-id="${product.id}" aria-label="Hapus">
376 <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"> 377 <path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z"/> 378 </svg> 379 </button> 380 </div> 381 `; 382 }).join(''); 383 384 // Setup event listeners for remove buttons 385 setupRemoveButtons(); 386 387 // Update analyze button state 388 const analyzeBtn = document.getElementById('compareAnalyze'); 389 if (analyzeBtn) { 390 analyzeBtn.disabled = compareList.length < 2; 391 } 392 } 393 } 394 395 /** 396 * Setup remove buttons event listeners 397 */ 398 function setupRemoveButtons() { 399 const removeButtons = document.querySelectorAll('.compare-item-remove'); 400 removeButtons.forEach(button => { 401 button.addEventListener('click', async (e) => { 402 e.preventDefault(); 403 const productId = parseInt(button.dataset.productId); 404 if (productId) { 405 await removeProduct(productId); 406 } 407 }); 408 }); 409 } 410 411 /** 412 * Update all compare buttons on the page 413 */ 414 function updateAllCompareButtons() { 415 // Update product detail compare button 416 const productBtn = document.getElementById('pd-compare-btn'); 417 if (productBtn) { 418 const productId = parseInt(productBtn.dataset.productId); 419 if (productId) { 420 updateProductCompareButtonState(productBtn, productId); 421 } 422 } 423 424 // Update any other compare buttons on the page 425 const allCompareButtons = document.querySelectorAll('[data-product-id][class*="compare"]'); 426 allCompareButtons.forEach(button => { 427 const productId = parseInt(button.dataset.productId); 428 if (productId && button.id !== 'pd-compare-btn') { 429 updateProductCompareButtonState(button, productId); 430 } 431 }); 432 } 433 434 /** 435 * Update compare badge 436 */ 437 function updateCompareBadge() { 438 const badge = document.getElementById('compareCount'); 439 if (!badge) return; 440 441 const oldCount = parseInt(badge.textContent) || 0; 442 const newCount = compareList.length; 443 444 badge.textContent = newCount; 445 badge.classList.toggle('active', newCount > 0); 446 447 // Animate badge if count increased 448 if (newCount > oldCount) { 449 animateCompareBadge(); 450 highlightCompareButton(); 451 } 452 453 // Sticky bar removed - keeping only fly-to-navbar animation 454 } 455 456 /** 457 * Animate compare badge when new product added 458 */ 459 function animateCompareBadge() { 460 const badge = document.getElementById('compareCount'); 461 if (!badge) return; 462 463 badge.style.transform = 'scale(1.3)'; 464 badge.style.transition = 'transform 0.3s ease'; 465 466 setTimeout(() => { 467 badge.style.transform = 'scale(1)'; 468 }, 300); 469 } 470 471 /** 472 * Highlight compare button temporarily 473 */ 474 function highlightCompareButton() { 475 const compareBtn = document.getElementById('compareBtn'); 476 if (!compareBtn) return; 477 478 compareBtn.classList.add('pulse-highlight'); 479 480 setTimeout(() => { 481 compareBtn.classList.remove('pulse-highlight'); 482 }, 3000); 483 } 484 485 /** 486 * Trigger fly-to-compare animation 487 */ 488 function triggerFlyToCompareAnimation(sourceButton) { 489 if (!sourceButton) { 490 return; 491 } 492 493 // Check if mobile view (bottom nav) or desktop (top nav) 494 const isMobile = window.innerWidth <= 768; 495 496 // Get source position 497 const sourceRect = sourceButton.getBoundingClientRect(); 498 499 // Calculate target position 500 let targetTop, targetLeft; 501 502 if (isMobile) { 503 // Mobile: fly to fixed bottom position (bottom nav area) 504 // Use fixed coordinates regardless of bottom nav visibility 505 targetTop = window.innerHeight - 80; // Just above bottom nav area 506 targetLeft = window.innerWidth / 2 - 30; // Center horizontally 507 } else { 508 // Desktop: fly to top navbar compare button 509 const targetElement = document.getElementById('compareBtn'); 510 if (!targetElement) { 511 return; 512 } 513 const targetRect = targetElement.getBoundingClientRect(); 514 targetTop = targetRect.top + 10; 515 targetLeft = targetRect.left + 10; 516 } 517 518 // Get product image for animation - try multiple selectors 519 let productImage = document.querySelector('#pd-main-img, .product-gallery img, .pd-image img, .product-image img, .product-main-image img, img[alt*="produk"], img[src*="product"]'); 520 521 // Create animated element 522 const flyingElement = document.createElement('div'); 523 flyingElement.className = 'flying-product'; 524 525 if (productImage) { 526 flyingElement.innerHTML = `<img src="${productImage.src}" alt="Flying product">`; 527 flyingElement.querySelector('img').style.cssText = `
528 width: 100%; 529 height: 100%; 530 object-fit: cover; 531 border-radius: 8px; 532 box-shadow: 0 4px 12px rgba(0,0,0,0.3); 533 `; 534 } else { 535 // Fallback: colored div with icon 536 flyingElement.innerHTML = `<div class="flying-fallback">ð¦</div>`; 537 flyingElement.querySelector('.flying-fallback').style.cssText = ` 538 width: 100%; 539 height: 100%; 540 background: #3b82f6; 541 border-radius: 8px; 542 display: flex; 543 align-items: center; 544 justify-content: center; 545 font-size: 24px; 546 box-shadow: 0 4px 12px rgba(0,0,0,0.3); 547 `; 548 } 549 550 flyingElement.style.cssText = ` 551 position: fixed; 552 top: ${sourceRect.top}px; 553 left: ${sourceRect.left}px; 554 width: 60px; 555 height: 60px; 556 z-index: 9999; 557 pointer-events: none; 558 opacity: 1; 559 transform: scale(1); 560 `; 561 562 document.body.appendChild(flyingElement); 563 564 // Use Web Animations API for more reliable animation 565 flyingElement.animate([ 566 // Keyframe 1: Initial state 567 { 568 top: `${sourceRect.top}px`, 569 left: `${sourceRect.left}px`, 570 transform: 'scale(1)', 571 opacity: 1 572 }, 573 // Keyframe 2: Final state 574 { 575 top: `${targetTop}px`, 576 left: `${targetLeft}px`, 577 transform: 'scale(0.3)', 578 opacity: 0 579 } 580 ], { 581 duration: 800, 582 easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', 583 fill: 'forwards' 584 }); 585 586 // Clean up after animation completes 587 setTimeout(() => { 588 if (flyingElement.parentNode) { 589 flyingElement.parentNode.removeChild(flyingElement); 590 } 591 }, 850); 592 } 593 594 // Sticky compare bar functions removed - using only fly-to-navbar animation 595 596 /** 597 * Toggle compare sidebar 598 */ 599 function toggleCompareSidebar() { 600 if (!compareSidebar) return; 601 602 const isOpen = compareSidebar.classList.contains('open'); 603 604 if (isOpen) { 605 closeCompareSidebar(); 606 } else { 607 openCompareSidebar(); 608 } 609 } 610 611 /** 612 * Open compare sidebar 613 */ 614 function openCompareSidebar() { 615 if (!compareSidebar || !compareOverlay) return; 616 617 compareSidebar.classList.add('open'); 618 compareOverlay.classList.add('active'); 619 document.body.style.overflow = 'hidden'; 620 621 // Send Google Analytics event for compare sidebar open 622 if (typeof gtag === 'function') { 623 gtag('event', 'compare_sidebar_open', { 624 event_category: 'Product Compare', 625 event_label: 'Compare Sidebar Opened', 626 products_count: compareList.length, 627 custom_map: { 628 custom_parameter_1: 'compare_sidebar_view' 629 } 630 }); 631 } 632 } 633 634 /** 635 * Close compare sidebar 636 */ 637 function closeCompareSidebar() { 638 if (!compareSidebar || !compareOverlay) return; 639 640 compareSidebar.classList.remove('open'); 641 compareOverlay.classList.remove('active'); 642 document.body.style.overflow = ''; 643 } 644 645 /** 646 * Start AI analysis 647 */ 648 async function startAIAnalysis() { 649 if (compareList.length < 2) { 650 showToast('Minimal 2 produk diperlukan untuk analisis AI', 'warning'); 651 return; 652 } 653 654 if (isAnalyzing) return; 655 656 // Send Google Analytics event for AI Analysis start 657 if (typeof gtag === 'function') { 658 gtag('event', 'ai_analysis_start', { 659 event_category: 'Product Compare', 660 event_label: 'AI Analysis Button Click', 661 products_count: compareList.length, 662 product_ids: compareList.map(p => p.id).join(','), 663 custom_map: { 664 custom_parameter_1: 'compare_ai_analysis' 665 } 666 }); 667 } 668 669 // Check if products are in similar categories 670 const mismatchInfo = await checkProductCategoryMismatch(); 671 if (mismatchInfo.shouldWarn) { 672 const categoryList = Array.from(mismatchInfo.categories).join(', '); 673 const confirmed = await showConfirmModal({ 674 title: 'Produk Berbeda Kategori', 675 message: `Produk yang Anda pilih berasal dari kategori yang berbeda: ${categoryList}. AI tetap bisa menganalisis, tapi perbandingan mungkin kurang relevan. Lanjutkan analisis?`, 676 confirmText: 'Ya, Analisis Tetap', 677 cancelText: 'Batal', 678 type: 'warning' 679 }); 680 681 if (!confirmed) return; 682 } 683 684 openModal(); 685 showLoadingState(); 686 isAnalyzing = true; 687 688 try { 689 const response = await fetch('/api/compare/ai-analysis.php', { 690 method: 'POST', 691 credentials: 'same-origin' 692 }); 693 694 const result = await response.json(); 695 696 if (!result.success) { 697 throw new Error(result.error || 'Analisis AI gagal'); 698 } 699 700 currentAnalysis = result; 701 showAnalysisResults(result); 702 703 // Send Google Analytics event for successful AI Analysis 704 if (typeof gtag === 'function') { 705 gtag('event', 'ai_analysis_success', { 706 event_category: 'Product Compare', 707 event_label: 'AI Analysis Completed', 708 products_count: compareList.length, 709 analysis_type: 'product_comparison', 710 custom_map: { 711 custom_parameter_1: 'compare_ai_success' 712 } 713 }); 714 } 715 716 } catch (error) { 717 showErrorState(error.message); 718 719 // Send Google Analytics event for failed AI Analysis 720 if (typeof gtag === 'function') { 721 gtag('event', 'ai_analysis_error', { 722 event_category: 'Product Compare', 723 event_label: 'AI Analysis Failed', 724 products_count: compareList.length, 725 error_message: error.message, 726 custom_map: { 727 custom_parameter_1: 'compare_ai_error' 728 } 729 }); 730 } 731 } finally { 732 isAnalyzing = false;
733 } 734 } 735 736 /** 737 * Show modal 738 */ 739 function openModal() { 740 if (!compareModal) return; 741 compareModal.classList.add('open'); 742 document.body.style.overflow = 'hidden'; 743 } 744 745 /** 746 * Close modal 747 */ 748 function closeModal() { 749 if (!compareModal) return; 750 compareModal.classList.remove('open'); 751 document.body.style.overflow = ''; 752 } 753 754 /** 755 * Show loading state 756 */ 757 function showLoadingState() { 758 const loading = document.getElementById('aiLoading'); 759 const error = document.getElementById('aiError'); 760 const results = document.getElementById('aiResults'); 761 762 if (loading) loading.style.display = 'block'; 763 if (error) error.hidden = true; 764 if (results) results.hidden = true; 765 } 766 767 /** 768 * Show error state 769 */ 770 function showErrorState(message) { 771 const loading = document.getElementById('aiLoading'); 772 const error = document.getElementById('aiError'); 773 const results = document.getElementById('aiResults'); 774 const errorMessage = document.getElementById('aiErrorMessage'); 775 776 if (loading) loading.style.display = 'none'; 777 if (error) error.hidden = false; 778 if (results) results.hidden = true; 779 if (errorMessage) errorMessage.textContent = message; 780 } 781 782 /** 783 * Show analysis results 784 */ 785 function showAnalysisResults(analysis) { 786 const loading = document.getElementById('aiLoading'); 787 const error = document.getElementById('aiError'); 788 const results = document.getElementById('aiResults'); 789 790 if (loading) loading.style.display = 'none'; 791 if (error) error.hidden = true; 792 if (results) results.hidden = false; 793 794 const data = analysis.data; 795 796 // Update summary 797 const summaryElement = document.getElementById('aiSummary'); 798 if (summaryElement) summaryElement.textContent = data.summary || ''; 799 800 const winnerProduct = document.getElementById('winnerProduct'); 801 const winnerReason = document.getElementById('winnerReason'); 802 if (winnerProduct && winnerReason && data.winner_overall) { 803 const winner = compareList.find(p => parseInt(p.id) === parseInt(data.winner_overall)); 804 winnerProduct.textContent = winner ? winner.name : 'Tidak diketahui'; 805 winnerReason.textContent = data.winner_reason || ''; 806 } 807 808 // Update comparison table 809 updateComparisonTable(data.comparison_table); 810 811 // Update detailed analysis 812 updateDetailedAnalysis(data.detailed_analysis); 813 814 // Update recommendations 815 updateRecommendations(data.recommendations); 816 817 // Update buying tips 818 updateBuyingTips(data.buying_tips); 819 } 820 821 /** 822 * Update comparison table 823 */ 824 function updateComparisonTable(comparisonData) { 825 const container = document.getElementById('comparisonTable'); 826 if (!container || !comparisonData?.categories) return; 827 828 // First, create product header row with names 829 const productHeaderHtml = ` 830 <div class="comparison-category"> 831 <div class="category-header">Produk</div> 832 <div class="category-products"> 833 ${compareList.map(product => ` 834 <div class="product-score"> 835 <div class="product-name-header"> 836 <strong>${escapeHtml(product.name)}</strong> 837 </div> 838 <div class="product-price"> 839 ${formatPrice(product.price)} 840 </div> 841 </div> 842 `).join('')} 843 </div> 844 </div> 845 `; 846 847 // Then create comparison categories 848 const categoriesHtml = comparisonData.categories.map(category => ` 849 <div class="comparison-category"> 850 <div class="category-header">${escapeHtml(category.name)}</div> 851 <div class="category-products"> 852 ${compareList.map(product => { 853 const productData = category.products?.[product.id]; 854 return ` 855 <div class="product-score"> 856 <div class="score-value"> 857 <span class="score-stars">${'â '.repeat(productData?.score || 0)}${'â'.repeat(5 - (productData?.score || 0))}</span> 858 <span>${productData?.score || 0}/5</span> 859 </div> 860 <div class="score-note">${escapeHtml(productData?.note || '')}</div> 861 </div> 862 `; 863 }).join('')} 864 </div> 865 </div> 866 `).join(''); 867 868 container.innerHTML = productHeaderHtml + categoriesHtml; 869 } 870 871 /** 872 * Update detailed analysis 873 */ 874 function updateDetailedAnalysis(analysisData) { 875 const container = document.getElementById('detailedAnalysis'); 876 if (!container || !analysisData) return; 877 878 container.innerHTML = compareList.map(product => { 879 const productAnalysis = analysisData[product.id]; 880 if (!productAnalysis) return ''; 881 882 // Use product name from API response if available, fallback to local name 883 const productName = productAnalysis.product_name || product.name; 884 885 return ` 886 <div class="product-analysis"> 887 <h5>${escapeHtml(productName)}</h5> 888 889 ${productAnalysis.strengths ? ` 890 <div class="analysis-section"> 891 <h6 class="strengths">â Kelebihan</h6> 892 <ul> 893 ${productAnalysis.strengths.map(strength => 894 `<li>${escapeHtml(strength)}</li>` 895 ).join('')} 896 </ul> 897 </div> 898 ` : ''} 899 900 ${productAnalysis.weaknesses ? ` 901 <div class="analysis-section"> 902 <h6 class="weaknesses">â Kekurangan</h6> 903 <ul> 904 ${productAnalysis.weaknesses.map(weakness => 905 `<li>${escapeHtml(weakness)}</li>` 906 ).join('')} 907 </ul> 908 </div> 909 ` : ''} 910 911 ${productAnalysis.best_for ? ` 912 <div class="analysis-section"> 913 <h6 class="best-for">ð¯ Cocok Untuk</h6> 914 <p class="best-for-text">${escapeHtml(productAnalysis.best_for)}</p> 915 </div> 916 ` : ''} 917 </div> 918 `; 919 }
919).filter(Boolean).join(''); 920 } 921 922 /** 923 * Update recommendations 924 */ 925 function updateRecommendations(recommendations) { 926 if (!recommendations) return; 927 928 const budgetEl = document.getElementById('budgetRecommendation'); 929 const performanceEl = document.getElementById('performanceRecommendation'); 930 const balancedEl = document.getElementById('balancedRecommendation'); 931 932 if (budgetEl && recommendations.budget_conscious) { 933 const product = compareList.find(p => parseInt(p.id) === parseInt(recommendations.budget_conscious)); 934 budgetEl.textContent = product ? product.name : 'Tidak tersedia'; 935 } 936 937 if (performanceEl && recommendations.performance_focused) { 938 const product = compareList.find(p => parseInt(p.id) === parseInt(recommendations.performance_focused)); 939 performanceEl.textContent = product ? product.name : 'Tidak tersedia'; 940 } 941 942 if (balancedEl && recommendations.balanced) { 943 const product = compareList.find(p => parseInt(p.id) === parseInt(recommendations.balanced)); 944 balancedEl.textContent = product ? product.name : 'Tidak tersedia'; 945 } 946 } 947 948 /** 949 * Check if products are from different categories 950 */ 951 async function checkProductCategoryMismatch() { 952 if (compareList.length < 2) { 953 return { shouldWarn: false, categories: new Set() }; 954 } 955 956 // Get the root categories from each product 957 const rootCategories = new Set(); 958 const productCategories = []; 959 960 for (const product of compareList) { 961 // Extract root category from the product's category_path if available 962 const rootCategory = await getProductRootCategory(product); 963 if (rootCategory) { 964 rootCategories.add(rootCategory); 965 productCategories.push({ 966 product: product.name, 967 category: rootCategory 968 }); 969 } 970 } 971 972 // If we have more than 1 different root categories, show warning 973 const shouldWarn = rootCategories.size > 1 && compareList.length >= 2; 974 975 return { 976 shouldWarn: shouldWarn, 977 categories: rootCategories, 978 productCategories: productCategories 979 }; 980 } 981 982 /** 983 * Get product root category from category_path or fallback to name detection 984 */ 985 async function getProductRootCategory(product) { 986 // First try to get from category_path if available 987 if (product.category_path) { 988 const rootCategory = product.category_path.split('/')[0]; 989 return rootCategory; 990 } 991 992 // If no category_path, try to fetch from product detail or use name detection 993 const detectedCategory = detectProductCategory(product.name); 994 return detectedCategory; 995 } 996 997 /** 998 * Detect product category from name (fallback method) 999 */ 1000 function detectProductCategory(productName) { 1001 const name = productName.toLowerCase(); 1002 1003 // Map keywords to actual root categories from the API structure 1004 const categoryMappings = { 1005 // Komputer & Laptop 1006 'Komputer & Laptop': ['laptop', 'notebook', 'macbook', 'thinkpad', 'gaming laptop', 'desktop', 'pc', 'mini pc', 'all-in-one', 'server'], 1007 1008 // Printer & Scanner 1009 'Printer & Scanner': ['printer', 'epson printer', 'canon printer', 'hp printer', 'brother printer', 'scanner', 'laser printer', 'inkjet', 'dot matrix'], 1010 1011 // Gadget 1012 'Gadget': ['smartphone', 'iphone', 'samsung phone', 'xiaomi', 'oppo', 'vivo', 'realme', 'tablet', 'ipad', 'android tablet'], 1013 1014 // Periferal 1015 'Periferal': ['monitor', 'lcd monitor', 'led monitor', 'gaming monitor', 'keyboard', 'mouse', 'gaming keyboard', 'mechanical keyboard', 'webcam'], 1016 1017 // Audio 1018 'Audio': ['headset', 'headphone', 'earphone', 'gaming headset', 'speaker', 'mikrofon', 'sound card'], 1019 1020 // Jaringan 1021 'Jaringan': ['router', 'wifi', 'access point', 'switch', 'modem', 'network'], 1022 1023 // Penyimpanan 1024 'Penyimpanan': ['ssd', 'hdd', 'nvme', 'solid state', 'hard disk', 'flash drive', 'usb drive', 'memory card'], 1025 1026 // Komponen PC 1027 'Komponen PC': ['ram', 'memory', 'ddr4', 'ddr5', 'processor', 'cpu', 'intel', 'amd', 'motherboard', 'graphics card', 'gpu', 'psu', 'power supply', 'casing'], 1028 1029 // UPS & Power 1030 'UPS & Power': ['ups', 'uninterruptible', 'stabilizer', 'power supply'], 1031 1032 // Proyektor 1033 'Proyektor': ['proyektor', 'projector', 'layar proyektor'], 1034 1035 // Security System 1036 'Security System': ['cctv', 'security camera', 'surveillance'], 1037 1038 // Aksesoris 1039 'Aksesoris': ['charger laptop', 'adaptor', 'converter', 'dock', 'hub', 'kabel', 'cable', 'power bank', 'tas laptop'], 1040 1041 // Software 1042 'Software': ['software', 'lisensi', 'license', 'antivirus'], 1043
1044 // Perlengkapan Kantor 1045 'Perlengkapan Kantor': ['fingerprint', 'finger print', 'atk', 'alat tulis'] 1046 }; 1047 1048 // Check which root category matches 1049 for (const [rootCategory, keywords] of Object.entries(categoryMappings)) { 1050 for (const keyword of keywords) { 1051 if (name.includes(keyword)) { 1052 return rootCategory; 1053 } 1054 } 1055 } 1056 1057 return null; 1058 } 1059 1060 /** 1061 * Update buying tips 1062 */ 1063 function updateBuyingTips(tips) { 1064 const container = document.getElementById('buyingTips'); 1065 if (!container || !Array.isArray(tips)) return; 1066 1067 container.innerHTML = tips.map(tip => 1068 `<li>${escapeHtml(tip)}</li>` 1069 ).join(''); 1070 } 1071 1072 // Removed share and cart functions - replaced with AI disclaimer in UI 1073 1074 /** 1075 * Remove product from compare (public method) 1076 */ 1077 async function removeProduct(productId) { 1078 try { 1079 await removeFromCompare(productId); 1080 showToast('Produk dihapus dari perbandingan', 'success'); 1081 1082 // Update all compare buttons 1083 updateAllCompareButtons(); 1084 1085 } catch (error) { 1086 showToast(error.message || 'Terjadi kesalahan', 'error'); 1087 } 1088 } 1089 1090 /** 1091 * Utility functions 1092 */ 1093 function escapeHtml(text) { 1094 const div = document.createElement('div'); 1095 div.textContent = text; 1096 return div.innerHTML; 1097 } 1098 1099 function formatPrice(price) { 1100 return 'Rp ' + parseInt(price).toLocaleString('id-ID'); 1101 } 1102 1103 /** 1104 * Show confirmation modal using the existing modal.css design 1105 */ 1106 function showConfirmModal(options = {}) { 1107 return new Promise((resolve) => { 1108 const { 1109 title = 'Konfirmasi', 1110 message = 'Apakah Anda yakin?', 1111 confirmText = 'Ya', 1112 cancelText = 'Tidak', 1113 type = 'warning' 1114 } = options; 1115 1116 // Create modal HTML 1117 const modalHTML = ` 1118 <div class="modal-overlay" id="confirmModal"> 1119 <div class="modal-dialog"> 1120 <div class="modal-header"> 1121 <h3 class="modal-title"> 1122 <div class="modal-icon ${type}"> 1123 ${getModalIcon(type)} 1124 </div> 1125 ${escapeHtml(title)} 1126 </h3> 1127 </div> 1128 <div class="modal-body"> 1129 <p class="modal-message">${escapeHtml(message)}</p> 1130 <div class="modal-actions"> 1131 <button type="button" class="modal-btn modal-btn-secondary" id="confirmCancel"> 1132 ${escapeHtml(cancelText)} 1133 </button> 1134 <button type="button" class="modal-btn modal-btn-${type === 'danger' ? 'danger' : 'primary'}" id="confirmOk"> 1135 ${escapeHtml(confirmText)} 1136 </button> 1137 </div> 1138 </div> 1139 </div> 1140 </div> 1141 `; 1142 1143 // Add to DOM 1144 document.body.insertAdjacentHTML('beforeend', modalHTML); 1145 const modal = document.getElementById('confirmModal'); 1146 const cancelBtn = document.getElementById('confirmCancel'); 1147 const okBtn = document.getElementById('confirmOk'); 1148 1149 // Show modal 1150 requestAnimationFrame(() => { 1151 modal.classList.add('show'); 1152 }); 1153 1154 // Handle events 1155 function cleanup() { 1156 modal.remove(); 1157 } 1158 1159 function handleCancel() { 1160 modal.classList.remove('show'); 1161 setTimeout(() => { 1162 cleanup(); 1163 resolve(false); 1164 }, 200); 1165 } 1166 1167 function handleConfirm() { 1168 modal.classList.remove('show'); 1169 setTimeout(() => { 1170 cleanup(); 1171 resolve(true); 1172 }, 200); 1173 } 1174 1175 cancelBtn.addEventListener('click', handleCancel); 1176 okBtn.addEventListener('click', handleConfirm); 1177 1178 // Close on overlay click 1179 modal.addEventListener('click', (e) => { 1180 if (e.target === modal) { 1181 handleCancel(); 1182 } 1183 }); 1184 1185 // Close on escape 1186 document.addEventListener('keydown', function escapeHandler(e) { 1187 if (e.key === 'Escape') { 1188 document.removeEventListener('keydown', escapeHandler); 1189 handleCancel(); 1190 } 1191 }); 1192 1193 // Focus on confirm button 1194 setTimeout(() => okBtn.focus(), 100); 1195 }); 1196 } 1197 1198 /** 1199 * Get icon for modal type 1200 */ 1201 function getModalIcon(type) { 1202 const icons = { 1203 danger: 'ð¨ ', 1204 warning: 'â ï¸', 1205 success: 'â ', 1206 info: 'â¹ï¸' 1207 }; 1208 return icons[type] || icons.info; 1209 } 1210 1211 1212 function showToast(message, type = 'info') { 1213 // Integration with existing toast system or create simple one 1214 1215 // Simple alert fallback - replace with proper toast system 1216 if (type === 'error') { 1217 alert('Error: ' + message); 1218 } 1219 } 1220 1221 // Inline compare preview functions removed - keeping it simple and clean 1222 1223 /** 1224 * Get current product ID from page 1225 */ 1226 function getCurrentProductId() { 1227 const button = document.getElementById('pd-compare-btn'); 1228 return button ? parseInt(button.dataset.productId) : null; 1229 } 1230 1231 // hideStickyCompareBar function removed - no longer using sticky bar 1232 1233 // Public API
1234 window.productCompare = { 1235 removeProduct: removeProduct, 1236 addToCompare: addToCompare, 1237 loadCompareList: loadCompareList, 1238 getCompareList: function() { return compareList; }, 1239 openSidebar: openCompareSidebar, 1240 startAnalysis: startAIAnalysis, 1241 // Helper function to add from external sources with variant support 1242 addProductWithVariant: async function(productId, variantId = 0) { 1243 return await addToCompare(productId, variantId); 1244 } 1245 }; 1246 1247})(); 1248 1249
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.