PageSourceSearch

https://www.smallfarm-chiangmai.com/blackgoat/product-compare.js.download

js smallfarm-chiangmai.com collected 2026-10-10 00:32:15 UTC 38,509 bytes, 1,249 lines download raw bytes

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.