Spaces:
Running
Running
| // Review System Component | |
| class ReviewSystem { | |
| static init() { | |
| console.log('ReviewSystem component initialized'); | |
| this.setupReviewSubmission(); | |
| this.setupPeerAgreement(); | |
| this.loadSampleReviews(); | |
| } | |
| static setupReviewSubmission() { | |
| const reviewButtons = document.querySelectorAll('button'); | |
| reviewButtons.forEach(btn => { | |
| if (btn.textContent.includes('Write Review')) { | |
| btn.addEventListener('click', () => this.showReviewModal()); | |
| } | |
| }); | |
| } | |
| static showReviewModal() { | |
| // Create review submission modal | |
| const modal = this.createReviewModal(); | |
| document.body.appendChild(modal); | |
| setTimeout(() => { | |
| modal.classList.remove('opacity-0'); | |
| modal.classList.add('opacity-100'); | |
| }, 10); | |
| } | |
| static createReviewModal() { | |
| const modal = document.createElement('div'); | |
| modal.className = 'fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center z-50 opacity-0 transition-opacity duration-300'; | |
| modal.innerHTML = ` | |
| <div class="bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-96 overflow-y-auto"> | |
| <div class="px-6 py-4 border-b"> | |
| <div class="flex justify-between items-center"> | |
| <h3 class="text-lg font-semibold">Write a Review</h3> | |
| <button class="text-gray-400 hover:text-gray-600" onclick="this.closest('.fixed').remove()"> | |
| <i data-feather="x" class="h-6 w-6"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="p-6"> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Product Rating</label> | |
| <div class="flex items-center space-x-2"> | |
| ${[1, 2, 3, 4, 5].map(star => ` | |
| <button class="star-rating text-gray-300 hover:text-yellow-400" data-rating="${star}"> | |
| <i data-feather="star" class="h-6 w-6"></i> | |
| </button> | |
| `).join('')} | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| ${this.getReviewDimensions().map(dim => ` | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">${dim.label}</label> | |
| <div class="flex items-center space-x-2"> | |
| <button class="dimension-negative text-red-400 hover:text-red-600" data-dimension="${dim.key}" data-value="-1"> | |
| <i data-feather="thumbs-down" class="h-4 w-4"></i> | |
| </button> | |
| <span class="text-sm text-gray-600 dimension-value" data-dimension="${dim.key}">0</span> | |
| <button class="dimension-positive text-green-400 hover:text-green-600" data-dimension="${dim.key}" data-value="1"> | |
| <i data-feather="thumbs-up" class="h-4 w-4"></i> | |
| </button> | |
| </div> | |
| </div> | |
| `).join('')} | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Review Title</label> | |
| <input type="text" class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm" placeholder="Summarize your experience"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Detailed Review</label> | |
| <textarea class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm" rows="4" placeholder="Share your experience with this product..."></textarea> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Perceived Value ($)</label> | |
| <input type="number" class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm" placeholder="How much do you think this product is worth?"> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="px-6 py-4 border-t bg-gray-50"> | |
| <div class="flex justify-between items-center"> | |
| <div class="text-sm text-gray-600"> | |
| <span class="flex items-center gap-1"> | |
| <i data-feather="clock" class="h-4 w-4"></i> | |
| Review will be eligible after 14 days | |
| </span> | |
| </div> | |
| <div class="space-x-3"> | |
| <button class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"> | |
| Cancel | |
| </button> | |
| <button class="px-4 py-2 text-sm font-medium text-white bg-blue-500 border border-transparent rounded-md hover:bg-blue-600"> | |
| Submit Review | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| this.setupModalInteractions(modal); | |
| return modal; | |
| } | |
| static getReviewDimensions() { | |
| return [ | |
| { key: 'quality', label: 'Quality' }, | |
| { key: 'packaging', label: 'Packaging' }, | |
| { key: 'support', label: 'Customer Support' }, | |
| { key: 'value', label: 'Value for Money' }, | |
| { key: 'durability', label: 'Durability' } | |
| ]; | |
| } | |
| static setupModalInteractions(modal) { | |
| // Star rating interactions | |
| const stars = modal.querySelectorAll('.star-rating'); | |
| let currentRating = 0; | |
| stars.forEach((star, index) => { | |
| star.addEventListener('click', () => { | |
| currentRating = index + 1; | |
| this.updateStarDisplay(stars, currentRating); | |
| }); | |
| star.addEventListener('mouseenter', () => { | |
| this.updateStarDisplay(stars, index + 1); | |
| }); | |
| }); | |
| modal.addEventListener('mouseleave', () => { | |
| this.updateStarDisplay(stars, currentRating); | |
| }); | |
| // Dimension rating interactions | |
| const dimensionButtons = modal.querySelectorAll('.dimension-positive, .dimension-negative'); | |
| const dimensionValues = {}; | |
| dimensionButtons.forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| const dimension = btn.dataset.dimension; | |
| const value = parseInt(btn.dataset.value); | |
| dimensionValues[dimension] = value; | |
| this.updateDimensionDisplay(modal, dimensionValues); | |
| }); | |
| }); | |
| // Replace icons | |
| setTimeout(() => { | |
| if (window.feather) { | |
| window.feather.replace(); | |
| } | |
| }, 0); | |
| } | |
| static updateStarDisplay(stars, rating) { | |
| stars.forEach((star, index) => { | |
| const icon = star.querySelector('i'); | |
| if (index < rating) { | |
| icon.className = 'h-6 w-6 text-yellow-400 fill-current'; | |
| } else { | |
| icon.className = 'h-6 w-6 text-gray-300'; | |
| } | |
| }); | |
| } | |
| static updateDimensionDisplay(modal, values) { | |
| Object.entries(values).forEach(([dimension, value]) => { | |
| const valueElement = modal.querySelector(`.dimension-value[data-dimension="${dimension}"]`); | |
| if (valueElement) { | |
| valueElement.textContent = value > 0 ? `+${value}` : value; | |
| valueElement.className = `text-sm dimension-value ${value > 0 ? 'text-green-600' : 'text-red-600'}`; | |
| } | |
| }); | |
| } | |
| static submitReview(reviewData) { | |
| // Validate review | |
| if (!this.validateReview(reviewData)) { | |
| return { success: false, error: 'Review validation failed' }; | |
| } | |
| // Calculate review maturity timestamp | |
| const maturityDays = this.getMaturityDays(reviewData.category || 'general'); | |
| const maturityTimestamp = new Date(Date.now() + maturityDays * 24 * 60 * 60 * 1000).toISOString(); | |
| const review = { | |
| id: this.generateReviewId(), | |
| userId: window.OneSearchApp?.user?.id || 'anonymous', | |
| productId: reviewData.productId, | |
| rating: reviewData.rating, | |
| title: reviewData.title, | |
| content: reviewData.content, | |
| dimensions: reviewData.dimensions, | |
| perceivedValue: reviewData.perceivedValue, | |
| salePrice: reviewData.salePrice, | |
| verifiedPurchase: true, | |
| maturityTimestamp, | |
| createdAt: new Date().toISOString(), | |
| peerAgreements: [] | |
| }; | |
| // Store review (in real implementation, this would go to the database) | |
| this.storeReview(review); | |
| return { success: true, review }; | |
| } | |
| static validateReview(reviewData) { | |
| // Check minimum requirements for review validation | |
| if (!reviewData.rating || reviewData.rating < 1) return false; | |
| if (!reviewData.content || reviewData.content.length < 50) return false; | |
| // Check if at least 3 dimensions are rated | |
| const ratedDimensions = Object.values(reviewData.dimensions || {}).filter(v => v !== 0); | |
| if (ratedDimensions.length < 3) return false; | |
| // For negative reviews, require more detail | |
| const averageDimension = Object.values(reviewData.dimensions || {}).reduce((a, b) => a + b, 0) / Object.values(reviewData.dimensions || {}).length; | |
| if (averageDimension < 0 && reviewData.content.length < 100) return false; | |
| return true; | |
| } | |
| static getMaturityDays(category) { | |
| const maturityPeriods = { | |
| electronics: 30, | |
| consumables: 14, | |
| services: 7, | |
| furniture: 21, | |
| general: 14 | |
| }; | |
| return maturityPeriods[category] || 14; | |
| } | |
| static setupPeerAgreement() { | |
| // Setup peer agreement/disagreement handlers | |
| document.addEventListener('click', (e) => { | |
| if (e.target.closest('.agreement-btn')) { | |
| const button = e.target.closest('.agreement-btn'); | |
| const reviewId = button.closest('.review-card')?.dataset.reviewId; | |
| const action = button.dataset.action; | |
| if (reviewId && action) { | |
| this.handlePeerAgreement(reviewId, action, button); | |
| } | |
| } | |
| }); | |
| } | |
| static handlePeerAgreement(reviewId, action, buttonElement) { | |
| const userId = window.OneSearchApp?.user?.id; | |
| if (!userId) { | |
| if (window.OneSearchApp) { | |
| window.OneSearchApp.showNotification('Please sign in to participate in peer review', 'warning'); | |
| } | |
| return; | |
| } | |
| // Check if user has already voted on this review | |
| const existingVote = this.getExistingVote(reviewId, userId); | |
| if (existingVote && existingVote.action === action) { | |
| // Remove vote if clicking the same action | |
| this.removeVote(reviewId, userId); | |
| this.updateVoteDisplay(buttonElement, action, -1); | |
| } else if (existingVote) { | |
| // Change vote | |
| this.updateVote(reviewId, userId, action); | |
| this.updateVoteDisplay(buttonElement, action, 1); | |
| this.updateOppositeVote(buttonElement, action, -1); | |
| } else { | |
| // New vote | |
| this.addVote(reviewId, userId, action); | |
| this.updateVoteDisplay(buttonElement, action, 1); | |
| } | |
| // Trigger review weight recalculation if significant change | |
| this.checkForReviewFlag(reviewId); | |
| } | |
| static getExistingVote(reviewId, userId) { | |
| // Get user's existing vote on this review | |
| const votes = JSON.parse(localStorage.getItem(`reviewVotes_${reviewId}`) || '[]'); | |
| return votes.find(vote => vote.userId === userId) || null; | |
| } | |
| static addVote(reviewId, userId, action) { | |
| const votes = JSON.parse(localStorage.getItem(`reviewVotes_${reviewId}`) || '[]'); | |
| votes.push({ | |
| userId, | |
| action, | |
| timestamp: new Date().toISOString(), | |
| userTrust: window.OneSearchApp?.user?.profileTrust || 0.5 | |
| }); | |
| localStorage.setItem(`reviewVotes_${reviewId}`, JSON.stringify(votes)); | |
| } | |
| static updateVote(reviewId, userId, newAction) { | |
| const votes = JSON.parse(localStorage.getItem(`reviewVotes_${reviewId}`) || '[]'); | |
| const voteIndex = votes.findIndex(vote => vote.userId === userId); | |
| if (voteIndex !== -1) { | |
| votes[voteIndex].action = newAction; | |
| localStorage.setItem(`reviewVotes_${reviewId}`, JSON.stringify(votes)); | |
| } | |
| } | |
| static removeVote(reviewId, userId) { | |
| const votes = JSON.parse(localStorage.getItem(`reviewVotes_${reviewId}`) || '[]'); | |
| const filteredVotes = votes.filter(vote => vote.userId !== userId); | |
| localStorage.setItem(`reviewVotes_${reviewId}`, JSON.stringify(filteredVotes)); | |
| } | |
| static updateVoteDisplay(buttonElement, action, delta) { | |
| const countSpan = buttonElement.querySelector('span'); | |
| if (countSpan) { | |
| const currentCount = parseInt(countSpan.textContent.match(/\d+/)[0]) || 0; | |
| const newCount = Math.max(0, currentCount + delta); | |
| countSpan.textContent = countSpan.textContent.replace(/\d+/, newCount); | |
| } | |
| } | |
| static updateOppositeVote(buttonElement, action, delta) { | |
| const isAgree = action === 'agree'; | |
| const oppositeButton = buttonElement.closest('.peer-agreements').querySelector( | |
| isAgree ? '.agreement-disagree' : '.agreement-agree' | |
| ); | |
| if (oppositeButton) { | |
| this.updateVoteDisplay(oppositeButton, action, delta); | |
| } | |
| } | |
| static checkForReviewFlag(reviewId) { | |
| const votes = JSON.parse(localStorage.getItem(`reviewVotes_${reviewId}`) || '[]'); | |
| const disagreeVotes = votes.filter(vote => vote.action === 'disagree'); | |
| const highTrustDisagrees = disagreeVotes.filter(vote => vote.userTrust > 0.8); | |
| if (highTrustDisagrees.length >= 3) { | |
| this.flagReviewForAudit(reviewId, 'Multiple high-trust users disagree'); | |
| } | |
| } | |
| static flagReviewForAudit(reviewId, reason) { | |
| // Mark review for audit | |
| const flaggedReviews = JSON.parse(localStorage.getItem('flaggedReviews') || '[]'); | |
| flaggedReviews.push({ | |
| reviewId, | |
| reason, | |
| flaggedAt: new Date().toISOString(), | |
| status: 'pending_review' | |
| }); | |
| localStorage.setItem('flaggedReviews', JSON.stringify(flaggedReviews)); | |
| if (window.OneSearchApp) { | |
| window.OneSearchApp.showNotification('Review flagged for audit', 'info'); | |
| } | |
| } | |
| static storeReview(review) { | |
| // Store review in local storage for demo purposes | |
| const reviews = JSON.parse(localStorage.getItem('reviews') || '[]'); | |
| reviews.push(review); | |
| localStorage.setItem('reviews', JSON.stringify(reviews)); | |
| } | |
| static loadSampleReviews() { | |
| // Load and display sample reviews (already in HTML) | |
| // This method can be extended to load dynamic review data | |
| } | |
| static generateReviewId() { | |
| return 'review_' + Date.now().toString(36) + Math.random().toString(36).substring(2, 8); | |
| } | |
| static getReviewWeight(reviewId) { | |
| // Calculate review weight based on peer agreements | |
| const votes = JSON.parse(localStorage.getItem(`reviewVotes_${reviewId}`) || '[]'); | |
| let weight = 1.0; // Base weight | |
| votes.forEach(vote => { | |
| if (vote.action === 'agree') { | |
| weight += vote.userTrust * 0.1; | |
| } else if (vote.action === 'disagree') { | |
| weight -= vote.userTrust * 0.15; | |
| } | |
| }); | |
| return Math.max(0.1, Math.min(2.0, weight)); // Clamp between 0.1 and 2.0 | |
| } | |
| static exportReviewData(reviewId) { | |
| const review = this.getReviewById(reviewId); | |
| const votes = JSON.parse(localStorage.getItem(`reviewVotes_${reviewId}`) || '[]'); | |
| return { | |
| review, | |
| votes, | |
| weight: this.getReviewWeight(reviewId), | |
| maturityTimestamp: review.maturityTimestamp, | |
| flags: this.getReviewFlags(reviewId) | |
| }; | |
| } | |
| static getReviewById(reviewId) { | |
| const reviews = JSON.parse(localStorage.getItem('reviews') || '[]'); | |
| return reviews.find(review => review.id === reviewId); | |
| } | |
| static getReviewFlags(reviewId) { | |
| const flaggedReviews = JSON.parse(localStorage.getItem('flaggedReviews') || '[]'); | |
| return flaggedReviews.filter(flag => flag.reviewId === reviewId); | |
| } | |
| } | |
| // Export for use in main app | |
| if (typeof window !== 'undefined') { | |
| window.ReviewSystem = ReviewSystem; | |
| } | |
| if (typeof module !== 'undefined' && module.exports) { | |
| module.exports = ReviewSystem; | |
| } |