PageSourceSearch

https://cuan368kara.com/assets/js/product-reviews.js

js cuan368kara.com collected 2026-10-10 01:24:36 UTC 14,256 bytes, 466 lines download raw bytes

1/**
2 * Product Reviews - Frontend JavaScript
3 * Handles review display, submission, and helpful voting
4 */
5
6const ProductReviews = (function() {
7  'use strict';
8
9  let currentPage = 1;
10  let currentSort = 'helpful';
11  let productId = null;
12  let isLoading = false;
13
14  /**
15   * Public API: Initialize reviews for a product
16   */
17  function init(pid) {
18    productId = pid;
19    
20    if (!productId) {
21      console.warn('Product ID not provided to ProductReviews.init()');
22      return;
23    }
24
25    initializeReviewTab();
26    loadReviews();
27  }
28
29  /**
30   * Initialize review tab functionality
31   */
32  function initializeReviewTab() {
33    // Tab switching
34    const tabButtons = document.querySelectorAll('.tab-btn');
35    tabButtons.forEach(btn => {
36      btn.addEventListener('click', function() {
37        const tabName = this.dataset.tab;
38        switchTab(tabName);
39      });
40    });
41
42    // Sort dropdown
43    const sortSelect = document.getElementById('review-sort');
44    if (sortSelect) {
45      sortSelect.addEventListener('change', function() {
46        currentSort = this.value;
47        currentPage = 1;
48        loadReviews();
49      });
50    }
51
52    // Load more button
53    const loadMoreBtn = document.getElementById('load-more-reviews');
54    if (loadMoreBtn) {
55      loadMoreBtn.addEventListener('click', function() {
56        currentPage++;
57        loadReviews(true); // Append mode
58      });
59    }
60
61    // Write review button
62    const writeReviewBtn = document.getElementById('write-review-btn');
63    if (writeReviewBtn) {
64      writeReviewBtn.addEventListener('click', showReviewModal);
65    }
66  }
67
68  /**
69   * Switch between product tabs
70   */
71  function switchTab(tabName) {
72    // Update button states
73    document.querySelectorAll('.tab-btn').forEach(btn => {
74      btn.classList.toggle('active', btn.dataset.tab === tabName);
75    });
76
77    // Update content visibility
78    document.querySelectorAll('.tab-content').forEach(content => {
79      content.style.display = content.id === `${tabName}-tab` ? 'block' : 'none';
80    });
81  }
82
83  /**
84   * Load reviews from API
85   */
86  async function loadReviews(append = false) {
87    if (isLoading) return;
88    isLoading = true;
89
90    const container = document.getElementById('reviews-list');
91    const loadMoreBtn = document.getElementById('load-more-reviews');
92
93    // Check if review section exists (product might not have reviews section)
94    if (!container) {
95      console.log('Review section not found - product may not have reviews yet');
96      isLoading = false;
97      return;
98    }
99
100    try {
101      if (!append) {
102        container.innerHTML = '<div class="review-loading">Memuat ulasan...</div>';
103      } else {
104        loadMoreBtn.disabled = true;
105        loadMoreBtn.textContent = 'Memuat...';
106      }
107
108      const response = await fetch(`/api/reviews/list.php?product_id=${productId}&sort=${currentSort}&page=${currentPage}&limit=10`);
109      const data = await response.json();
110
111      if (!data.success) {
112        throw new Error(data.message || 'Gagal memuat ulasan');
113      }
114
115      // Update statistics (only on initial load)
116      if (!append && data.stats) {
117        updateReviewStats(data.stats);
118      }
119
120      // Render reviews
121      if (!append) {
122        container.innerHTML = '';
123      }
124
125      const reviews = data.reviews || [];
126      
127      if (reviews.length === 0 && !append) {
128        container.innerHTML = '<div class="no-reviews">Belum ada ulasan untuk produk ini. Jadilah yang pertama!</div>';
129      } else {
130        reviews.forEach(review => {
131          container.appendChild(createReviewElement(review));
132        });
133      }
134
135      // Update load more button
136      if (loadMoreBtn) {
137        if (data.pagination.has_more) {
138          loadMoreBtn.style.display = 'block';
139          loadMoreBtn.disabled = false;
140          loadMoreBtn.textContent = 'Muat Lebih Banyak';
141        } else {
142          loadMoreBtn.style.display = 'none';
143        }
144      }
145
146    } catch (error) {
147      console.error('Load reviews error:', error);
148      container.innerHTML = '<div class="review-error">Gagal memuat ulasan. Silakan coba lagi.</div>';
149    } finally {
150      isLoading = false;
151    }
152  }
153
154  /**
155   * Update review statistics display
156   */
157  function updateReviewStats(stats) {
158    const avgRatingEl = document.getElementById('avg-rating');
159    const totalReviewsEl = document.getElementById('total-reviews');
160    const ratingBarsContainer = document.getElementById('rating-bars');
161
162    if (avgRatingEl) {
163      avgRatingEl.textContent = stats.average_rating.toFixed(1);
164    }
165
166    if (totalReviewsEl) {
167      totalReviewsEl.textContent = `${stats.total_reviews} ulasan`;
168    }
169
170    // Update rating breakdown bars
171    if (ratingBarsContainer) {
172      ratingBarsContainer.innerHTML = '';
173      stats.rating_breakdown.forEach(item => {
174        const bar = document.createElement('div');
175        bar.className = 'rating-bar';
176        bar.innerHTML = `
177          <span class="rating-label">${item.rating}★</span>
178          <div class="bar-container">
179            <div class="bar-fill" style="width: ${item.percentage}%"></div>
180          </div>
181          <span class="rating-count">${item.count}</span>
182        `;
183        ratingBarsContainer.appendChild(bar);
184      });
185    }
186
187    // Update tab count
188    const reviewTab = document.querySelector('[data-tab="reviews"]');
189    if (reviewTab && stats.total_reviews > 0) {
190      reviewTab.textContent = `Ulasan (${stats.total_reviews})`;
191    }
192  }
193
194  /**
195   * Create review element
196   */
197  function createReviewElement(review) {
198    const div = document.createElement('div');
199    div.className = 'review-item';
200    div.dataset.reviewId = review.ID;
201
202    // Build stars
203    const stars = '★'.repeat(review.RATING) + '☆'.repeat(5 - review.RATING);
204
205    // Build images if available
206    let imagesHtml = '';
207    if (review.images && review.images.length > 0) {
208      imagesHtml = '<div class="review-images">';
209      review.images.forEach(img => {
210        imagesHtml += `<img src="${escapeHtml(img)}" alt="Review image" class="review-img" loading="lazy">`;
211      });
212      imagesHtml += '</div>';
213    }
214
215    // Verified purchase badge
216    const verifiedBadge = review.IS_VERIFIED_PURCHASE === 'Y' 
217      ? '<span class="verified-badge">✓ Pembelian Terverifikasi</span>' 
218      : '';
219    
220    // Use DISPLAY_NAME if available (for anonymous reviews), otherwise use FULL_NAME
221    const displayName = review.DISPLAY_NAME || review.FULL_NAME || 'Anonymous';
222    const anonymousIndicator = review.IS_ANONYMOUS === 'Y' 
223      ? '<span class="anonymous-badge" title="Nama pengguna disamark
223an untuk privasi">🔒</span>' 
224      : '';
225
226    div.innerHTML = `
227      <div class="review-header">
228        <div class="review-author">
229          <strong>${escapeHtml(displayName)}</strong>
230          ${anonymousIndicator}
231          ${verifiedBadge}
232        </div>
233        <div class="review-rating">
234          <span class="stars">${stars}</span>
235        </div>
236      </div>
237      <div class="review-date">${review.created_ago}</div>
238      <div class="review-text">${escapeHtml(review.REVIEW_TEXT).replace(/\n/g, '<br>')}</div>
239      ${imagesHtml}
240      <div class="review-actions">
241        <button class="btn-helpful" data-review-id="${review.ID}">
242          <span class="helpful-icon">👍</span>
243          <span class="helpful-text">Membantu (${review.HELPFUL_COUNT || 0})</span>
244        </button>
245      </div>
246    `;
247
248    // Add helpful button listener
249    const helpfulBtn = div.querySelector('.btn-helpful');
250    helpfulBtn.addEventListener('click', () => markAsHelpful(review.ID));
251
252    return div;
253  }
254
255  /**
256   * Get CSRF token from meta tag
257   */
258  function getCsrfToken() {
259    const metaTag = document.querySelector('meta[name="csrf-token"]');
260    return metaTag ? metaTag.getAttribute('content') : '';
261  }
262
263  /**
264   * Mark review as helpful
265   */
266  async function markAsHelpful(reviewId) {
267    // Check if logged in
268    const isLoggedIn = document.body.classList.contains('logged-in') || 
269                      document.querySelector('meta[name="user-logged-in"]')?.content === 'true';
270
271    if (!isLoggedIn) {
272      showModal({
273        title: 'Login Diperlukan',
274        message: 'Silakan login terlebih dahulu untuk memberikan vote pada ulasan ini.',
275        icon: 'info',
276        buttons: [
277          { text: 'Batal', className: 'modal-btn-secondary' },
278          { text: 'Login', className: 'modal-btn-primary', callback: () => window.location.href = '/login' }
279        ]
280      });
281      return;
282    }
283
284    // Get current button element
285    const reviewItem = document.querySelector(`[data-review-id="${reviewId}"]`);
286    const helpfulBtn = reviewItem?.querySelector('.btn-helpful');
287    
288    if (!helpfulBtn) return;
289    
290    // Disable button during request
291    helpfulBtn.disabled = true;
292    const originalText = helpfulBtn.querySelector('.helpful-text').textContent;
293
294    try {
295      const response = await fetch('/api/reviews/helpful.php', {
296        method: 'POST',
297        headers: { 
298          'Content-Type': 'application/json',
299          'X-CSRF-Token': getCsrfToken()
300        },
301        credentials: 'same-origin',
302        body: JSON.stringify({ 
303          csrf_token: getCsrfToken(),
304          review_id: reviewId 
305        })
306      });
307
308      const data = await response.json();
309
310      if (data.success) {
311        // Update button count immediately based on action
312        const currentCount = parseInt(originalText.match(/\d+/)?.[0] || 0);
313        const newCount = data.action === 'added' ? currentCount + 1 : currentCount - 1;
314        helpfulBtn.querySelector('.helpful-text').textContent = `Membantu (${newCount})`;
315        
316        // Show success message
317        showToast(data.message, 'success');
318        
319        // Re-enable button
320        helpfulBtn.disabled = false;
321      } else {
322        showToast(data.message, 'error');
323        helpfulBtn.disabled = false;
324      }
325
326    } catch (error) {
327      console.error('Mark helpful error:', error);
328      showToast('Gagal memberikan vote. Silakan coba lagi.', 'error');
329      helpfulBtn.disabled = false;
330    }
331  }
332
333  /**
334   * Show review submission modal (placeholder)
335   */
336  function showReviewModal() {
337    // TODO: Implement review modal
338    // For now, redirect to account page
339    window.location.href = '/account/orders';
340  }
341
342  /**
343   * Show toast notification
344   */
345  function showToast(message, type = 'info') {
346    const toast = document.createElement('div');
347    toast.className = `review-toast toast-${type}`;
348    toast.textContent = message;
349    
350    document.body.appendChild(toast);
351    
352    setTimeout(() => {
353      toast.classList.add('show');
354    }, 100);
355    
356    setTimeout(() => {
357      toast.classList.remove('show');
358      setTimeout(() => toast.remove(), 300);
359    }, 3000);
360  }
361
362  /**
363   * Show modal dialog
364   * @param {Object} options - Modal options
365   * @param {string} options.title - Modal title
366   * @param {string} options.message - Modal message
367   * @param {string} options.icon - Icon type (info, success, warning, danger)
368   * @param {Array} options.buttons - Array of button objects {text, className, callback}
369   */
370  function showModal(options) {
371    const {
372      title = 'Konfirmasi',
373      message = '',
374      icon = 'info',
375      buttons = [{ text: 'OK', className: 'modal-btn-primary' }]
376    } = options;
377
378    // Icon emoji mapping
379    const iconEmojis = {
380      info: 'ℹ️',
381      success: '✓',
382      warning: '⚠️',
383      danger: '✕'
384    };
385
386    // Create modal HTML
387    const modalHTML = `
388      <div class="modal-overlay" id="helpful-vote-modal" style="display: flex !important; align-items: center !important; justify-content: center !important;">
389        <div class="modal-dialog">
390          <div class="modal-header">
391            <h3 class="modal-title">
392              <span class="modal-icon ${icon}">${iconEmojis[icon] || iconEmojis.info}</span>
393              ${escapeHtml(title)}
394            </h3>
395          </div>
396          <div class="modal-body">
397            <p class="modal-message">${escapeHtml(message)}</p>
398            <div class="modal-actions">
399              ${buttons.map((btn, index) => 
400                `<button class="modal-btn ${btn.className || 'modal-btn-secondary'}" data-index="${index}">${escapeHtml(btn.text)}</button>`
401              ).join('')}
402            </div>
403          </div>
404        </div>
405      </div>
406    `;
407
408    // Append to body
409    const tempDiv = document.createElement('div');
410    tempDiv.innerHTML = modalHTML;
411    const modal = tempDiv.firstElementChild;
412    document.body.appendChild(modal);
413
414    // Add event listeners to buttons
415    buttons.forEach((btn, index) => {
416      const btnElement = modal.querySelector(`[data-index="${index}"]`);
417      btnElement.addEventListener('click', () => {
418        closeModal(modal);
419        if (btn.callback && typeof btn.callback === 'function') {
420          btn.callback();
421        }
422      });
423    });
424
425    // Close on overlay click
426    modal.addEventListener('click', (e) => {
427      if (e.target === modal) {
428        closeModal(modal);
429      }
430    });
431
432    // Show modal with animation
433    setTimeout(() => modal.classList.add('show'), 10);
434  }
435
436  /**
437   * Close modal dialog
438   */
439  function closeModal(modal) {
440    modal.classList.remove('show');
441    setTimeout(() => modal.remove(), 200);
442  }
443
444  /**
445   * Escape HTML for XSS protection
446   */
447  function escapeHtml(text) {
448    const div = document.createElement('div');
449    div.textContent = text;
450    return div.innerHTML;
451  }
452
453  /**
454   * Get product ID from URL
455   */
456  function getProductIdFromUrl() {
457    const match = window.location.pathname.match(/\/product\/(\d+)/);
458    return match ? match[1] : null;
459  }
460
461  // Public API
462  return {
463    init: init
464  };
465
466})();

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.