/** * Review Submission Modal & Handler * Handles review form display, validation, and submission */ (function() { 'use strict'; let currentProductId = null; let currentOrderId = null; let selectedRating = 0; let uploadedImages = []; // Get CSRF token from meta tag function getCsrfToken() { const metaTag = document.querySelector('meta[name="csrf-token"]'); return metaTag ? metaTag.getAttribute('content') : ''; } // Initialize function init() { createReviewModal(); attachEventListeners(); loadReviewableOrders(); } // Create review modal HTML function createReviewModal() { const modalHTML = ` `; document.body.insertAdjacentHTML('beforeend', modalHTML); } // Attach event listeners function attachEventListeners() { // Modal triggers - event delegation document.addEventListener('click', function(e) { const btn = e.target.closest('.btn-write-review'); if (btn) { e.preventDefault(); openReviewModal( btn.dataset.productId, btn.dataset.orderId, btn.dataset.productName, btn.dataset.productImage, btn.dataset.productSku ); } }); // Close modal setTimeout(() => { const closeBtn = document.getElementById('review-modal-close'); const cancelBtn = document.getElementById('review-cancel'); const overlay = document.getElementById('review-modal-overlay'); if (closeBtn) { closeBtn.addEventListener('click', closeReviewModal); } if (cancelBtn) { cancelBtn.addEventListener('click', closeReviewModal); } if (overlay) { overlay.addEventListener('click', function(e) { if (e.target === overlay) closeReviewModal(); }); } }, 100); // Star rating setTimeout(() => { const starButtons = document.querySelectorAll('.star-btn'); starButtons.forEach(btn => { btn.addEventListener('click', function(e) { e.preventDefault(); const rating = parseInt(this.dataset.rating); setRating(rating); }); }); }, 100); // Character count setTimeout(() => { const textarea = document.getElementById('review-text'); if (textarea) { textarea.addEventListener('input', function() { updateCharCount(); calculatePotentialPoints(); }); } }, 100); // Image upload setTimeout(() => { const uploadTrigger = document.getElementById('upload-trigger'); const fileInput = document.getElementById('review-images'); if (uploadTrigger && fileInput) { uploadTrigger.addEventListener('click', () => fileInput.click()); fileInput.addEventListener('change', handleImageSelection); } }, 100); // Form submit setTimeout(() => { const form = document.getElementById('review-form'); if (form) { form.addEventListener('submit', handleSubmit); } }, 100); } // Open review modal function openReviewModal(productId, orderId, productName, productImage, productSku) { const overlay = document.getElementById('review-modal-overlay'); if (!overlay) { console.error('Modal overlay not found!'); return; } currentProductId = productId; currentOrderId = orderId; selectedRating = 0; uploadedImages = []; // Set product info const nameEl = document.getElementById('review-product-name'); const imageEl = document.getElementById('review-product-image'); const skuEl = document.getElementById('review-product-sku'); if (nameEl) nameEl.textContent = productName; if (imageEl) imageEl.src = productImage || '/assets/img/no-image.png'; if (skuEl) skuEl.textContent = 'SKU: ' + productSku; // Update privacy preview with actual user name updatePrivacyPreview(); // Reset form const form = document.getElementById('review-form'); const previewGrid = document.getElementById('image-preview-grid'); const errorEl = document.getElementById('review-form-error'); if (form) form.reset(); if (previewGrid) previewGrid.innerHTML = ''; if (errorEl) errorEl.style.display = 'none'; setRating(0); updateCharCount(); calculatePotentialPoints(); // Show modal overlay.style.display = 'flex'; overlay.classList.add('show'); document.body.style.overflow = 'hidden'; } // Close review modal function closeReviewModal() { const overlay = document.getElementById('review-modal-overlay'); if (overlay) { overlay.classList.remove('show'); setTimeout(() => { overlay.style.display = 'none'; }, 200); // Wait for fade animation } document.body.style.overflow = ''; } // Set star rating function setRating(rating) { selectedRating = rating; const stars = document.querySelectorAll('.star-btn'); const labels = ['', 'Sangat Buruk', 'Buruk', 'Cukup', 'Bagus', 'Sangat Bagus']; stars.forEach((star, index) => { if (index < rating) { star.classList.add('active'); } else { star.classList.remove('active'); } }); document.getElementById('rating-label').textContent = labels[rating] || 'Pilih rating Anda'; } // Update character count function updateCharCount() { const textarea = document.getElementById('review-text'); const count = textarea.value.length; document.getElementById('review-char-count').textContent = count; } // Calculate potential points function calculatePotentialPoints() { const textarea = document.getElementById('review-text'); if (!textarea) return; const charCount = textarea.value.trim().length; const hasImages = uploadedImages.length > 0; let totalPoints = 5000; let longTextActive = charCount >= 100; let imagesActive = hasImages; if (longTextActive) totalPoints += 1000; if (imagesActive) totalPoints += 2000; // Update UI const longTextEl = document.getElementById('pri-long-text'); const imagesEl = document.getElementById('pri-images'); const totalEl = document.getElementById('pri-total-points'); if (longTextEl) longTextEl.style.opacity = longTextActive ? '1' : '0.5'; if (imagesEl) imagesEl.style.opacity = imagesActive ? '1' : '0.5'; if (totalEl) totalEl.textContent = totalPoints.toLocaleString('id-ID') + ' point'; } // Handle image selection function handleImageSelection(e) { const files = Array.from(e.target.files); if (files.length > 5) { showError('Maksimal 5 foto yang dapat diupload'); e.target.value = ''; // Reset input return; } uploadedImages = []; const previewGrid = document.getElementById('image-preview-grid'); previewGrid.innerHTML = ''; files.forEach((file, index) => { // Validate file type if (!file.type.match(/image\/(jpeg|jpg|png|webp)/)) { showError('Format file harus JPG, PNG, atau WebP'); return; } // Validate file size (2MB) if (file.size > 2 * 1024 * 1024) { showError('Ukuran file maksimal 2MB'); return; } uploadedImages.push(file); // Create preview const reader = new FileReader(); reader.onload = function(e) { const previewHTML = `
Preview ${index + 1}
`; previewGrid.insertAdjacentHTML('beforeend', previewHTML); }; reader.readAsDataURL(file); }); calculatePotentialPoints(); // Attach remove handlers setTimeout(() => { document.querySelectorAll('.remove-image').forEach(btn => { btn.addEventListener('click', function() { const index = parseInt(this.dataset.index); removeImage(index); }); }); }, 100); } // Remove image function removeImage(index) { uploadedImages.splice(index, 1); const previewGrid = document.getElementById('image-preview-grid'); previewGrid.innerHTML = ''; uploadedImages.forEach((file, idx) => { const reader = new FileReader(); reader.onload = function(e) { const previewHTML = `
Preview ${idx + 1}
`; previewGrid.insertAdjacentHTML('beforeend', previewHTML); }; reader.readAsDataURL(file); }); calculatePotentialPoints(); setTimeout(() => { document.querySelectorAll('.remove-image').forEach(btn => { btn.addEventListener('click', function() { const idx = parseInt(this.dataset.index); removeImage(idx); }); }); }, 100); } // Handle form submission async function handleSubmit(e) { e.preventDefault(); // Validate if (selectedRating === 0) { showError('Silakan pilih rating terlebih dahulu'); return; } const reviewText = document.getElementById('review-text').value.trim(); if (reviewText.length < 20) { showError('Ulasan minimal 20 karakter'); return; } const submitBtn = document.getElementById('review-submit'); submitBtn.disabled = true; try { // Step 1: Upload images if any let imageUrls = []; if (uploadedImages.length > 0) { submitBtn.textContent = 'Mengupload foto...'; imageUrls = await uploadImages(); if (!imageUrls) { // Upload failed submitBtn.disabled = false; submitBtn.textContent = 'Kirim Ulasan'; return; } } // Step 2: Submit review with image URLs submitBtn.textContent = 'Mengirim ulasan...'; const payload = { csrf_token: getCsrfToken(), product_id: currentProductId, order_id: currentOrderId, rating: selectedRating, review_text: reviewText, is_anonymous: document.getElementById('review-anonymous').checked, images: imageUrls }; const response = await fetch('/api/reviews/submit.php', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrfToken() }, body: JSON.stringify(payload) }); const result = await response.json(); if (result.success) { showSuccess(result.message || 'Ulasan berhasil dikirim!'); closeReviewModal(); // Refresh reviewable orders if on account page if (typeof loadReviewableOrders === 'function') { setTimeout(loadReviewableOrders, 500); } } else { showError(result.message || 'Gagal mengirim ulasan'); } } catch (error) { console.error('Submit error:', error); showError('Terjadi kesalahan saat mengirim ulasan'); } finally { submitBtn.disabled = false; submitBtn.textContent = 'Kirim Ulasan'; } } /** * Upload images to server * @returns {Promise} Array of image URLs or null if failed */ async function uploadImages() { if (uploadedImages.length === 0) { return []; } const formData = new FormData(); uploadedImages.forEach(file => { formData.append('images[]', file); }); try { const response = await fetch('/api/reviews/upload_images.php', { method: 'POST', headers: { 'X-CSRF-Token': getCsrfToken() }, body: formData }); // Check if response is JSON const contentType = response.headers.get('content-type'); if (!contentType || !contentType.includes('application/json')) { const text = await response.text(); console.error('Non-JSON response:', text); showError('Server error: Response bukan JSON. Cek browser console untuk detail.'); return null; } const result = await response.json(); if (result.success && result.images) { // Extract URLs from uploaded images return result.images.map(img => img.url); } else { showError(result.message || 'Gagal mengupload foto'); return null; } } catch (error) { console.error('Upload images error:', error); showError('Terjadi kesalahan saat mengupload foto. Cek console untuk detail.'); return null; } } // Load reviewable orders (for account page) async function loadReviewableOrders() { // Only run if on account/profile page const container = document.getElementById('reviewable-orders-list'); const loading = document.getElementById('reviewable-orders-loading'); const empty = document.getElementById('reviewable-orders-empty'); if (!container) { return; } // Show loading if (loading) loading.style.display = 'block'; if (empty) empty.style.display = 'none'; container.innerHTML = ''; try { const response = await fetch('/api/reviews/reviewable.php'); const result = await response.json(); if (loading) loading.style.display = 'none'; if (result.success && result.data.length > 0) { renderReviewableOrders(result.data); if (empty) empty.style.display = 'none'; } else { if (empty) empty.style.display = 'block'; } } catch (error) { console.error('Load reviewable orders error:', error); if (loading) loading.style.display = 'none'; if (empty) { empty.innerHTML = '

Gagal memuat data. Silakan refresh halaman.

'; empty.style.display = 'block'; } } } // Render reviewable orders function renderReviewableOrders(orders) { const container = document.getElementById('reviewable-orders-list'); if (!container) return; let html = ''; orders.forEach(order => { html += `
${escapeHtml(order.product_name)}

${escapeHtml(order.product_name)}

Order: ${escapeHtml(order.order_number)}

Dibeli: ${escapeHtml(order.order_date)}

`; }); container.innerHTML = html; } // Escape HTML for XSS protection function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Show error function showError(message) { const errorEl = document.getElementById('review-form-error'); errorEl.textContent = message; errorEl.style.display = 'block'; setTimeout(() => { errorEl.style.display = 'none'; }, 5000); } // Show success function showSuccess(message) { // Create toast notification const toast = document.createElement('div'); toast.className = 'toast-notification success'; toast.textContent = message; toast.style.cssText = 'position:fixed;top:20px;right:20px;background:#059669;color:white;padding:16px 24px;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.15);z-index:10000;font-size:14px;font-weight:500;'; document.body.appendChild(toast); setTimeout(() => { toast.style.opacity = '0'; toast.style.transition = 'opacity 0.3s'; setTimeout(() => toast.remove(), 300); }, 3000); } // Mask user name for privacy preview (like PHP maskReviewerName) function maskUserName(fullName) { if (!fullName || fullName.trim() === '') { return 'Anonymous'; } const words = fullName.trim().split(/\s+/); const maskedWords = words.map(word => { const len = word.length; if (len <= 1) return word; return word.charAt(0) + '*'.repeat(len - 1); }); return maskedWords.join(' '); } // Update privacy preview text with user's actual masked name function updatePrivacyPreview() { const previewEl = document.getElementById('review-anonymous-preview'); if (!previewEl) return; const userName = window.userFullName || ''; if (userName) { const maskedName = maskUserName(userName); previewEl.textContent = `Nama Anda akan ditampilkan sebagai "${maskedName}" untuk menjaga privasi`; } else { previewEl.textContent = 'Nama Anda akan ditampilkan dengan disamarkan untuk menjaga privasi'; } } // Initialize when DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } // Export openReviewModal to global scope for use in other scripts window.openReviewModal = openReviewModal; })();