/* ============================================
Bambuddy Website - Reviews Module
Supabase REST client + review UI logic
============================================ */
const SUPABASE_CONFIG = {
url: 'https://oyrtdabedqlnkroumtlp.supabase.co',
anonKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im95cnRkYWJlZHFsbmtyb3VtdGxwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzAzODg1MzUsImV4cCI6MjA4NTk2NDUzNX0.muG2cOhAdsnLm13Ugmi9_0ZSRq_Vn0CAQZoFNw6wFcA'
};
/* ---- Supabase REST helpers ---- */
function supabaseRequest(endpoint, options = {}) {
const url = `${SUPABASE_CONFIG.url}/rest/v1/${endpoint}`;
const headers = {
'apikey': SUPABASE_CONFIG.anonKey,
'Authorization': `Bearer ${SUPABASE_CONFIG.anonKey}`,
'Content-Type': 'application/json',
...options.headers
};
return fetch(url, { ...options, headers });
}
async function fetchReviews({ featuredOnly = false, limit = 50 } = {}) {
let endpoint = 'reviews?approved=eq.true&order=created_at.desc';
if (featuredOnly) endpoint += '&featured=eq.true';
if (limit) endpoint += `&limit=${limit}`;
const res = await supabaseRequest(endpoint, {
headers: { 'Accept': 'application/json' }
});
if (!res.ok) throw new Error(`Failed to fetch reviews: ${res.status}`);
return res.json();
}
async function submitReview({ rating, review_text, reviewer_name }) {
const body = {
rating,
review_text,
reviewer_name: reviewer_name || null,
approved: false,
featured: false
};
const res = await supabaseRequest('reviews', {
method: 'POST',
headers: { 'Prefer': 'return=minimal' },
body: JSON.stringify(body)
});
if (!res.ok) {
const text = await res.text();
throw new Error(text || `Submit failed: ${res.status}`);
}
}
/* ---- XSS prevention ---- */
function escapeHTML(str) {
const div = document.createElement('div');
div.appendChild(document.createTextNode(str));
return div.innerHTML;
}
/* ---- Date formatting ---- */
function formatReviewDate(isoDate) {
const d = new Date(isoDate);
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
}
/* ---- Star rendering ---- */
function renderStars(rating) {
let html = '';
for (let i = 1; i <= 5; i++) {
if (i <= rating) {
html += ' ';
} else {
html += ' ';
}
}
return html;
}
/* ---- Card HTML ---- */
function createReviewCardHTML(review, index) {
const name = review.reviewer_name ? escapeHTML(review.reviewer_name) : 'Anonymous';
const initial = name.charAt(0).toUpperCase();
const text = escapeHTML(review.review_text);
const date = formatReviewDate(review.created_at);
const delay = Math.min(index * 0.1, 0.6);
return `
${renderStars(review.rating)}
${text}
${initial}
${name}
${date}
`;
}
/* ---- Render cards into container ---- */
function renderReviewCards(reviews, container) {
if (!reviews || reviews.length === 0) {
container.innerHTML = `
No Reviews Yet
Be the first to share your experience with Bambuddy!
`;
return;
}
container.innerHTML = reviews.map((r, i) => createReviewCardHTML(r, i)).join('');
initScrollRevealForNew(container);
}
/* ---- Review stats ---- */
function renderReviewStats(reviews) {
const container = document.getElementById('review-stats');
if (!container) return;
if (!reviews || reviews.length === 0) {
container.style.display = 'none';
return;
}
const total = reviews.length;
const avg = (reviews.reduce((sum, r) => sum + r.rating, 0) / total).toFixed(1);
container.innerHTML = `
${avg}
${renderStars(Math.round(avg))}
Average Rating
${total}
Total Review${total !== 1 ? 's' : ''}
`;
initScrollRevealForNew(container);
}
/* ---- Skeleton loading placeholders ---- */
function showSkeletons(container, count) {
let html = '';
for (let i = 0; i < count; i++) {
html += `
`;
}
container.innerHTML = html;
}
/* ---- Page loaders ---- */
async function loadFeaturedReviews() {
const grid = document.getElementById('featured-reviews-grid');
if (!grid) return;
const section = grid.closest('.section');
try {
showSkeletons(grid, 3);
const reviews = await fetchReviews({ featuredOnly: true, limit: 6 });
if (!reviews || reviews.length === 0) {
if (section) section.style.display = 'none';
return;
}
renderReviewCards(reviews, grid);
} catch (err) {
console.error('Failed to load featured reviews:', err);
if (section) section.style.display = 'none';
}
}
async function loadAllReviews() {
const grid = document.getElementById('all-reviews-grid');
if (!grid) return;
try {
showSkeletons(grid, 6);
const reviews = await fetchReviews({ limit: 100 });
renderReviewCards(reviews, grid);
renderReviewStats(reviews);
} catch (err) {
console.error('Failed to load reviews:', err);
grid.innerHTML = `
Unable to Load Reviews
Please try again later.
`;
}
}
/* ---- Interactive star selector ---- */
function initStarRatingSelector() {
const container = document.querySelector('.star-selector');
if (!container) return;
const buttons = container.querySelectorAll('.star-select-btn');
const hiddenInput = document.getElementById('review-rating');
let selectedRating = 0;
buttons.forEach(btn => {
const value = parseInt(btn.dataset.value, 10);
btn.addEventListener('mouseenter', () => {
buttons.forEach(b => {
const v = parseInt(b.dataset.value, 10);
b.classList.toggle('hovered', v <= value);
});
});
btn.addEventListener('mouseleave', () => {
buttons.forEach(b => b.classList.remove('hovered'));
});
btn.addEventListener('click', () => {
selectedRating = value;
if (hiddenInput) hiddenInput.value = value;
buttons.forEach(b => {
const v = parseInt(b.dataset.value, 10);
b.classList.toggle('selected', v <= value);
});
});
});
}
/* ---- Form handling ---- */
function initReviewForm() {
const form = document.getElementById('review-form');
if (!form) return;
initStarRatingSelector();
form.addEventListener('submit', async (e) => {
e.preventDefault();
const rating = parseInt(document.getElementById('review-rating').value, 10);
const review_text = document.getElementById('review-text').value.trim();
const reviewer_name = document.getElementById('review-name').value.trim();
const submitBtn = form.querySelector('.review-submit-btn');
// Validation
if (!rating || rating < 1 || rating > 5) {
showReviewToast('Please select a star rating.', 'error');
return;
}
if (review_text.length < 10) {
showReviewToast('Review must be at least 10 characters.', 'error');
return;
}
if (review_text.length > 2000) {
showReviewToast('Review must be under 2000 characters.', 'error');
return;
}
// Loading state
submitBtn.disabled = true;
submitBtn.innerHTML = ' Submitting...';
try {
await submitReview({ rating, review_text, reviewer_name });
showReviewToast('Thank you! Your review has been submitted and is pending approval.', 'success');
form.reset();
// Reset star selector
document.querySelectorAll('.star-select-btn').forEach(b => b.classList.remove('selected'));
document.getElementById('review-rating').value = '';
} catch (err) {
console.error('Submit error:', err);
showReviewToast('Failed to submit review. Please try again.', 'error');
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = 'Submit Review';
}
});
}
/* ---- Toast notifications ---- */
function showReviewToast(message, type) {
let container = document.getElementById('review-toast-container');
if (!container) {
container = document.createElement('div');
container.id = 'review-toast-container';
container.className = 'review-toast-container';
document.body.appendChild(container);
}
const toast = document.createElement('div');
toast.className = `review-toast review-toast-${type} toast`;
toast.textContent = message;
container.appendChild(toast);
setTimeout(() => {
toast.classList.add('hide');
toast.addEventListener('animationend', () => toast.remove());
}, 4000);
}
/* ---- Scroll reveal for dynamic content ---- */
function initScrollRevealForNew(container) {
const elements = container.querySelectorAll('.reveal:not(.visible)');
if (elements.length === 0) return;
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
}
});
}, {
threshold: 0.1,
rootMargin: '0px 0px -50px 0px'
});
elements.forEach(el => observer.observe(el));
}
/* ---- Init on DOMContentLoaded ---- */
document.addEventListener('DOMContentLoaded', () => {
if (document.getElementById('featured-reviews-grid')) loadFeaturedReviews();
if (document.getElementById('all-reviews-grid')) loadAllReviews();
if (document.getElementById('review-form')) initReviewForm();
});