PageSourceSearch

https://jmwedding2020.com/assets/js/product-card-actions.js

js jmwedding2020.com collected 2026-10-10 00:28:56 UTC 23,308 bytes, 717 lines download raw bytes

1/**
2 * Product Card Quick Actions
3 * Bottom sheet menu for quick add to cart & compare
4 * Integrates with existing product-compare.js and cart.js
5 */
6
7(function() {
8  'use strict';
9  
10  let activeSheet = null;
11  let activeProductData = null;
12  
13  // DOM elements (will be created dynamically)
14  let overlay = null;
15  let bottomSheet = null;
16  
17  /**
18   * Initialize on DOM ready
19   */
20  document.addEventListener('DOMContentLoaded', function() {
21    createBottomSheet();
22    attachEventListeners();
23  });
24  
25  /**
26   * Create bottom sheet HTML structure
27   */
28  function createBottomSheet() {
29    // Create overlay
30    overlay = document.createElement('div');
31    overlay.className = 'pca-overlay';
32    overlay.addEventListener('click', closeBottomSheet);
33    
34    // Create bottom sheet
35    bottomSheet = document.createElement('div');
36    bottomSheet.className = 'pca-bottom-sheet';
37    bottomSheet.innerHTML = `
38      <div class="pca-sheet-handle"></div>
39      <div class="pca-sheet-product" id="pca-product-preview">
40        <!-- Product info will be inserted here -->
41      </div>
42      <div class="pca-sheet-actions">
43        <button type="button" class="pca-action-item cart" id="pca-add-cart">
44          <svg class="pca-action-icon" viewBox="0 0 24 24" fill="currentColor">
45            <path d="M17,18A2,2 0 0,1 19,20A2,2 0 0,1 17,22C15.89,22 15,21.1 15,20C15,18.89 15.89,18 17,18M1,2H4.27L5.21,4H20A1,1 0 0,1 21,5C21,5.17 20.95,5.34 20.88,5.5L17.3,11.97C16.96,12.58 16.3,13 15.55,13H8.1L7.2,14.63L7.17,14.75A0.25,0.25 0 0,0 7.42,15H19V17H7C5.89,17 5,16.1 5,15C5,14.65 5.09,14.32 5.24,14.04L6.6,11.59L3,4H1V2M7,18A2,2 0 0,1 9,20A2,2 0 0,1 7,22C5.89,22 5,21.1 5,20C5,18.89 5.89,18 7,18M16,11L18.78,6H6.14L8.5,11H16Z"/>
46          </svg>
47          <div class="pca-action-content">
48            <div class="pca-action-label">Tambah ke Keranjang</div>
49            <div class="pca-action-desc">Lanjut belanja produk lain</div>
50          </div>
51        </button>
52        
53        <button type="button" class="pca-action-item compare" id="pca-add-compare">
54          <svg class="pca-action-icon" viewBox="0 0 24 24" fill="currentColor">
55            <path d="M21,9L17,5V8H10V10H17V13M7,11L3,15L7,19V16H14V14H7V11Z"/>
56          </svg>
57          <div class="pca-action-content">
58            <div class="pca-action-label">Bandingkan Produk</div>
59            <div class="pca-action-desc">Analisis dengan AI</div>
60          </div>
61          <span class="pca-action-badge compare" id="pca-compare-count" style="display:none;">0</span>
62        </button>
63      </div>
64    `;
65    
66    // Append to body
67    document.body.appendChild(overlay);
68    document.body.appendChild(bottomSheet);
69    
70    // Setup action buttons
71    document.getElementById('pca-add-cart').addEventListener('click', handleAddToCart);
72    document.getElementById('pca-add-compare').addEventListener('click', handleAddToCompare);
73    
74    // Prevent clicks inside bottom sheet from closing it
75    bottomSheet.addEventListener('click', function(e) {
76      e.stopPropagation();
77    });
78  }
79  
80  /**
81   * Attach event listeners to all 3-dot buttons
82   */
83  function attachEventListeners() {
84    // Use event delegation for dynamically loaded product cards
85    document.addEventListener('click', function(e) {
86      const btn = e.target.closest('.prod-card-actions-btn');
87      if (btn) {
88        e.preventDefault();
89        e.stopPropagation();
90        handleActionsButtonClick(btn);
91      }
92    });
93    
94    // Handle ESC key
95    document.addEventListener('keydown', function(e) {
96      if (e.key === 'Escape' && activeSheet) {
97        closeBottomSheet();
98      }
99    });
100  }
101  
102  /**
103   * Handle 3-dot button click
104   */
105  function handleActionsButtonClick(button) {
106    // Get product ID directly from button
107    const productId = parseInt(button.dataset.productId) || 0;
108    if (!productId) {
109      console.error('Product ID not found on button:', button);
110      showToast('Data produk tidak ditemukan', 'error');
111      return;
112    }
113    
114    // Find parent card for additional data
115    const card = button.closest('.prod-card, .pa-card');
116    if (!card) {
117      console.error('Product card not found');
118      showToast('Data produk tidak ditemukan', 'error');
119      return;
120    }
121    
122    // Extract product data from card
123    const productData = extractProductData(card, productId);
124    if (!productData) {
125      showToast('Data produk tidak ditemukan', 'error');
126      return;
127    }
128    
129    activeProductData = productData;
130    openBottomSheet(productData);
131  }
132  
133  /**
134   * Extract product data from card element
135   */
136  function extractProductData(card, productId) {
137    try {
138      // Product ID already passed from button
139      if (!productId) {
140        console.error('Product ID not provided');
141        return null;
142      }
143      // Product ID already passed from button
144      if (!productId) {
145        console.error('Product ID not provided');
146        return null;
147      }
148      
149      const id = productId;
150      
151      // Get product name
152      const nameEl = card.querySelector('.prod-name, .title');
153      const name = nameEl ? nameEl.textContent.trim() : 'Produk';
154      
155      // Get product image
156      const imgEl = card.querySelector('.thumb img, img');
157      const image = imgEl ? imgEl.src : '/assets/img/placeholder.png';
158      
159      // Get product price and detect variants
160      const priceEl = card.querySelector('.prod-price-final, .final, .prod-price');
161      let price = 0;
162      let hasVariants = false;
163      let priceDisplay = '';
164      
165      if (priceEl) {
166        // Get text content and clean it
167        let priceText = priceEl.textContent.trim();
168        priceDisplay = priceText;
169        
170        // Check if it's a range (indicates product has variants)
171        if (priceText.includes('-')) {
172          hasVariants = true;
173          // Take the first price for calculation
174          priceText = priceText.split('-')[0].trim();
175        }
176        
177        // Remove all non-numeric characters
178        priceText = priceText.replace(/[^0-9]/g, '');
179        price = parseInt(priceText) || 0;
180      }
181      
182      // Get stock status
183      const stockDot = card.querySelector('.stock-dot');
184      let stockStatus = 'unknown';
185      let stockLabel = 'Cek stok';
186      
187      if (stockDot) {
188        if (stockDot.classList.contains('ready')) {
189          stockStatus = 'in';
190          stockLabel = 'Stok tersedia';
191        } else if (stockDot.classList.contains('low')) {
192          stockStatus = 'low';
193          stockLabel = 'Stok terbatas';
194        } else if (stockDot.classList.contains('out')) {
195          stockStatus = 'out';
196          stockLabel = 'Stok habis';
197        }
198      }
199      
200      // Get category path (if available)
201      const categoryPath = card.dataset.category || '';
202      
203      // Get URL
204      let url = '#';
205      if (card.tagName === 'A') {
206        url = card.href;
207      } else {
208        const link = card.querySelector('a[href*="/product/"]');
209        if (link) url = link.href;
210      }
211      
212      return {
213        id: id,
214        name: name,
215        image: image,
216        price: price,
217        priceDisplay: priceDisplay,
218        hasVariants: hasVariants,
219        stockStatus: stockStatus,
220        stockLabel: stockLabel,
221        categoryPath: categoryPath,
222        url: url
223      };
224    } catch (error) {
225      console.error('Error extracting product data:', error);
226      return null;
227    }
228  }
229  
230  /**
231   * Open bottom sheet
232   */
233  function openBottomSheet(productData) {
234    if (!overlay || !bottomSheet) return;
235    
236    // Update product preview
237    updateProductPreview(productData);
238    
239    // Update cart button based on variant status
240    updateCartButtonForVariant(productData);
241    
242    // Update compare button state
243    updateCompareButtonState(productData.id);
244    
245    // Show overlay and sheet
246    overlay.classList.add('active');
247    bottomSheet.classList.add('active');
248    document.body.style.overflow = 'hidden';
249    
250    activeSheet = true;
251    
252    // Google Analytics
253    if (typeof gtag === 'function') {
254      gtag('event', 'quick_actions_open', {
255        event_category: 'Product Card',
256        event_label: 'Quick Actions Menu Opened',
257        product_id: productData.id,
258        product_name: productData.name
259      });
260    }
261  }
262  
263  /**
264   * Close bottom sheet
265   */
266  function closeBottomSheet() {
267    if (!overlay || !bottomSheet) return;
268    
269    overlay.classList.remove('active');
270    bottomSheet.classList.remove('active');
271    document.body.style.overflow = '';
272    
273    activeSheet = false;
274    activeProductData = null;
275  }
276  
277  /**
278   * Update product preview in bottom sheet
279   */
280  function updateProductPreview(productData) {
281    const preview = document.getElementById('pca-product-preview');
282    if (!preview) return;
283    
284    // Use full price display (shows range for variants)
285    const priceDisplay = productData.priceDisplay || formatPrice(productData.price);
286    
287    preview.innerHTML = `
288      <img src="${escapeHtml(productData.image)}" 
289           alt="${escapeHtml(productData.name)}" 
290           class="pca-product-image"
291           loading="lazy">
292      <div class="pca-product-info">
293        <h4 class="pca-product-name">${escapeHtml(productData.name)}</h4>
294        <p class="pca-product-price">${escapeHtml(priceDisplay)}</p>
295        <p class="pca-product-stock ${productData.stockStatus}">${productData.stockLabel}</p>
296      </div>
297    `;
298  }
299  
300  /**
301   * Update cart button for variant products
302   */
303  function updateCartButtonForVariant(productData) {
304    const cartBtn = document.getElementById('pca-add-cart');
305    if (!cartBtn) return;
306    
307    if (productData.hasVariants) {
308      // Product has variants - change to "View Detail"
309      cartBtn.dataset.hasVariants = 'true';
310      cartBtn.dataset.productUrl = productData.url;
311      
312      // Update icon to eye/external
313      const iconSvg = cartBtn.querySelector('svg');
314      if (iconSvg) {
315        iconSvg.innerHTML = '<path d="M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9M12,17A5,5 0 0,1 7,12A5,5 0 0,1 12,7A5,5 0 0,1 17,12A5,5 0 0,1 12,17M12,4.5C7,4.5 2.73,7.61 1,12C2.73,16.39 7,19.5 12,19.5C17,19.5 21.27,16.39 23,12C21.27,7.61 17,4.5 12,4.5Z"/>';
316      }
317      
318      // Update text
319      const label = cartBtn.querySelector('.pca-action-label');
320      const desc = cartBtn.querySelector('.pca-action-desc');
321      if (label) label.textContent = 'Lihat Detail Produk';
322      if (desc) desc.textContent = 'Pilih varian terlebih dahulu';
323      
324    } else {
325      // Single product - normal cart button
326      cartBtn.dataset.hasVariants = 'false';
327      
328      // Restore cart icon
329      const iconSvg = cartBtn.querySelector('svg');
330      if (iconSvg) {
331        iconSvg.innerHTML = '<path d="M17,18A2,2 0 0,1 19,20A2,2 0 0,1 17,22C15.89,22 15,21.1 15,20C15,18.89 15.89,18 17,18M1,2H4.27L5.21,4H20A1,1 0 0,1 21,5C21,5.17 20.95,5.34 20.88,5.5L17.3,11.97C16.96,12.58 16.3,13 15.55,13H8.1L7.2,14.63L7.17,14.75A0.25,0.25 0 0,0 7.42,15H19V17H7C5.89,17 5,16.1 5,15C5,14.65 5.09,14.32 5.24,14.04L6.6,11.59L3,4H1V2M7,18A2,2 0 0,1 9,20A2,2 0 0,1 7,22C5.89,22 5,21.1 5,20C5,18.89 5.89,18 7,18M16,11L18.78,6H6.14L8.5,11H16Z"/>';
332      }
333      
334      // Restore text
335      const label = cartBtn.querySelector('.pca-action-label');
336      const desc = cartBtn.querySelector('.pca-action-desc');
337      if (label) label.textContent = 'Tambah ke Keranjang';
338      if (desc) desc.textContent = 'Lanjut belanja produk lain';
339    }
340  }
341  
342  /**
343   * Update compare button state
344   */
345  function updateCompareButtonState(productId) {
346    const compareBtn = document.getElementById('pca-add-compare');
347    const compareBadge = document.getElementById('pca-compare-count');
348    
349    if (!compareBtn) return;
350    
351    // Check if product is in compare list (using global compareList if available)
352    let isInCompare = false;
353    let compareCount = 0;
354    
355    // Try to get compare list from product-compare.js via window.productCompare
356    if (typeof window.productCompare !== 'undefined' && typeof window.productCompare.getCompareList === 'function') {
357      const compareList = window.productCompare.getCompareList();
358      compareCount = compareList.length;
359      isInCompare = compareList.some(p => parseInt(p.id) === parseInt(productId));
360    } else {
361      // Fallback: check badge count
362      const mainBadge = document.getElementById('compareCount');
363      if (mainBadge) {
364        compareCount = parseInt(mainBadge.textContent) || 0;
365      }
366    }
367    
368    // Update button state
369    if (isInCompare) {
370      compareBtn.classList.add('active');
371      compareBtn.querySelector('.pca-action-label').textContent = 'Hapus dari Perbandingan';
372      compareBtn.querySelector('.pca-action-desc').textContent = 'Sudah ada di daftar';
373    } else {
374      compareBtn.classList.remove('active');
375      compareBtn.querySelector('.pca-action-label').textContent = 'Bandingkan Produk';
376      compareBtn.querySelector('.pca-action-desc').textContent = 'Analisis dengan AI';
377    }
378    
379    // Update badge
380    if (compareBadge && compareCount > 0) {
381      compareBadge.textContent = compareCount;
382      compareBadge.style.display = 'inline-flex';
383    } else if (compareBadge) {
384      compareBadge.style.display = 'none';
385    }
386  }
387  
388  /**
389   * Handle add to cart
390   */
391  async function handleAddToCart() {
392    if (!activeProductData) return;
393    
394    const button = document.getElementById('pca-add-cart');
395    if (!button) return;
396    
397    // If product has variants, redirect to detail page
398    if (activeProductData.hasVariants) {
399      window.location.href = activeProductData.url;
400      return;
401    }
402    
403    // Check stock
404    if (activeProductData.stockStatus === 'out') {
405      showToast('Produk sedang habis', 'warning');
406      return;
407    }
408    
409    // Show loading state
410    button.classList.add('loading');
411    button.disabled = true;
412    
413    // CSRF Token Helper
414    function getCsrfToken() {
415      return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
416    }
417    
418    try {
419      // Call cart API with proper format
420      const response = await fetch('/api/cart/add.php', {
421        method: 'POST',
422        headers: {
423          'Content-Type': 'application/json',
424          'X-CSRF-Token': getCsrfToken()
425        },
426        body: JSON.stringify({
427          csrf_token: getCsrfToken(),
428          product_id: activeProductData.id,
429          qty: 1, // Use 'qty' not 'quantity'
430          variant_id: 0, // Default variant
431          meta: {
432            name: activeProductData.name,
433            image_url: activeProductData.image
434          }
435        }),
436        credentials: 'same-origin'
437      });
438      
439      const result = await response.json();
440      
441      // Check both response status and result.ok
442      if (!response.ok || !result.ok) {
443        throw new Error(result.error || result.message || 'Gagal menambahkan ke keranjang');
444      }
445      
446      // Success animation
447      button.classList.remove('loading');
448      button.classList.add('success');
449      
450      // Update cart badge
451      updateCartBadge();
452      
453      // Show success toast
454      showToast('Produk ditambahkan ke keranjang', 'success');
455      
456      // Google Analytics
457      if (typeof gtag === 'function') {
458        gtag('event', 'add_to_cart', {
459          event_category: 'Ecommerce',
460          event_label: 'Quick Add to Cart',
461          items: [{
462            item_id: activeProductData.id,
463            item_name: activeProductData.name,
464            price: activeProductData.price,
465            quantity: 1
466          }]
467        });
468      }
469      
470      // Close sheet after short delay
471      setTimeout(() => {
472        closeBottomSheet();
473        button.classList.remove('success');
474        button.disabled = false;
475      }, 1000);
476      
477    } catch (error) {
478      console.error('Add to cart error:', error);
479      button.classList.remove('loading');
480      button.disabled = false;
481      showToast(error.message || 'Terjadi kesalahan', 'error');
482    }
483  }
484  
485  /**
486   * Handle add to compare
487   */
488  async function handleAddToCompare() {
489    if (!activeProductData) return;
490    
491    const button = document.getElementById('pca-add-compare');
492    if (!button) return;
493    
494    // Check if already in compare
495    const isActive = button.classList.contains('active');
496    
497    // Check 5-product limit before adding
498    if (!isActive) {
499      const mainBadge = document.getElementById('compareCount');
500      const currentCount = mainBadge ? parseInt(mainBadge.textContent) || 0 : 0;
501      
502      if (currentCount >= 5) {
503        showToast('Maksimal 5 produk untuk perbandingan', 'warning');
504        return;
505      }
506    }
507    
508    // Show loading state
509    button.classList.add('loading');
510    button.disabled = true;
511    
512    try {
513      if (isActive) {
514        // Remove from compare
515        await removeFromCompare(activeProductData.id);
516        showToast('Produk dihapus dari perbandingan', 'success');
517      } else {
518        // Add to compare
519        await addToCompare(activeProductData.id);
520        showToast('Produk ditambahkan ke perbandingan', 'success');
521        
522        // Trigger fly animation
523        triggerFlyToCompareAnimation();
524      }
525      
526      // Success animation
527      button.classList.remove('loading');
528      button.classList.add('success');
529      
530      // Update compare button state
531      updateCompareButtonState(activeProductData.id);
532      
533      // Close sheet after short delay
534      setTimeout(() => {
535        closeBottomSheet();
536        button.classList.remove('success');
537        button.disabled = false;
538      }, 800);
539      
540    } catch (error) {
541      console.error('Compare error:', error);
542      button.classList.remove('loading');
543      button.disabled = false;
544      showToast(error.message || 'Terjadi kesalahan', 'error');
545    }
546  }
547  
548  /**
549   * Add to compare (calls existing API)
550   */
551  async function addToCompare(productId) {
552    const response = await fetch('/api/compare/add.php', {
553      method: 'POST',
554      headers: {
555        'Content-Type': 'application/json',
556      },
557      body: JSON.stringify({
558        product_id: productId,
559        variant_id: 0
560      }),
561      credentials: 'same-origin'
562    });
563    
564    const result = await response.json();
565    
566    if (!result.success) {
567      throw new Error(result.error || 'Gagal menambahkan produk');
568    }
569    
570    // Trigger compare list reload if function exists
571    if (typeof window.productCompare !== 'undefined' && typeof window.productCompare.loadCompareList === 'function') {
572      await window.productCompare.loadCompareList();
573    }
574    
575    return result;
576  }
577  
578  /**
579   * Remove from compare (calls existing API)
580   */
581  async function removeFromCompare(productId) {
582    const response = await fetch('/api/compare/remove.php', {
583      method: 'POST',
584      headers: {
585        'Content-Type': 'application/json',
586      },
587      body: JSON.stringify({
588        product_id: productId
589      }),
590      credentials: 'same-origin'
591    });
592    
593    const result = await response.json();
594    
595    if (!result.success) {
596      throw new Error(result.error || 'Gagal menghapus produk');
597    }
598    
599    // Trigger compare list reload if function exists
600    if (typeof window.productCompare !== 'undefined' && typeof window.productCompare.loadCompareList === 'function') {
601      await window.productCompare.loadCompareList();
602    }
603    
604    return result;
605  }
606  
607  /**
608   * Update cart badge
609   */
610  function updateCartBadge() {
611    // Try to call global cart update function if exists
612    if (typeof window.updateCartBadge === 'function') {
613      window.updateCartBadge();
614    } else {
615      // Fallback: increment badge manually
616      const badge = document.getElementById('cartCount');
617      if (badge) {
618        const currentCount = parseInt(badge.textContent) || 0;
619        badge.textContent = currentCount + 1;
620        badge.classList.add('active');
621      }
622    }
623  }
624  
625  /**
626   * Trigger fly to compare animation
627   */
628  function triggerFlyToCompareAnimation() {
629    const compareBtn = document.getElementById('compareBtn');
630    if (!compareBtn || !activeProductData) return;
631    
632    // Create flying element
633    const flyingElement = document.createElement('div');
634    flyingElement.className = 'flying-product';
635    flyingElement.style.cssText = `
636      position: fixed;
637      width: 60px;
638      height: 60px;
639      z-index: 99999;
640      pointer-events: none;
641    `;
642    
643    flyingElement.innerHTML = `
644      <img src="${activeProductData.image}" 
645           style="width:100%;height:100%;object-fit:cover;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.3);">
646    `;
647    
648    // Position at bottom sheet
649    const sheetRect = bottomSheet.getBoundingClientRect();
650    const targetRect = compareBtn.getBoundingClientRect();
651    
652    flyingElement.style.top = `${sheetRect.top + 20}px`;
653    flyingElement.style.left = `${sheetRect.left + 20}px`;
654    
655    document.body.appendChild(flyingElement);
656    
657    // Animate to target
658    requestAnimationFrame(() => {
659      flyingElement.style.transition = 'all 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94)';
660      flyingElement.style.top = `${targetRect.top + 10}px`;
661      flyingElement.style.left = `${targetRect.left + 10}px`;
662      flyingElement.style.transform = 'scale(0.3)';
663      flyingElement.style.opacity = '0';
664    });
665    
666    // Clean up
667    setTimeout(() => {
668      if (flyingElement.parentNode) {
669        flyingElement.parentNode.removeChild(flyingElement);
670      }
671    }, 800);
672  }
673  
674  /**
675   * Format price as Rupiah
676   */
677  function formatPrice(price) {
678    if (!price || price === 0) return 'Rp 0';
679    return 'Rp ' + parseInt(price).toLocaleString('id-ID');
680  }
681  
682  /**
683   * Escape HTML
684   */
685  function escapeHtml(text) {
686    const div = document.createElement('div');
687    div.textContent = text;
688    return div.innerHTML;
689  }
690  
691  /**
692   * Show toast notification (uses global showToast if available)
693   */
694  function showToast(message, type = 'info') {
695    if (typeof window.showToast === 'function') {
696      window.showToast({
697        title: type === 'success' ? 'Berhasil' : type === 'error' ? 'Gagal' : 'Info',
698        message: message,
699        type: type,
700        duration: 3000
701      });
702    } else {
703      // Fallback: console log
704      console.log(`[${type.toUpperCase()}] ${message}`);
705    }
706  }
707  
708  /**
709   * Expose functions for external access
710   */
711  window.ProductCardActions = {
712    openSheet: openBottomSheet,
713    closeSheet: closeBottomSheet,
714    updateCompareState: updateCompareButtonState
715  };
716  
717})();

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.