onesearch / components /ReviewSystem.js
jessikat29's picture
OneSearch Technical Specification
11a16a2 verified
Raw
History Blame Contribute Delete
18.1 kB
// 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;
}