// About tab — Steam-style comment wall + global click counter (() => { const FIREBASE_CONFIG = { apiKey: "AIzaSyDuSjEGEKx5FnWYnQq8f_owbpYRBRrl5x0", authDomain: "esef-514bf.firebaseapp.com", projectId: "esef-514bf", storageBucket: "esef-514bf.firebasestorage.app", messagingSenderId: "353969651290", appId: "1:353969651290:web:41e3aaf8e54c743702a263" }; const MAX_CHARS = 300; const MAX_NAME = 32; const COMMENTS_PER_PAGE = 5; const COLLECTION = "comments"; const DEFAULT_COLOR = "#5b6fde"; const COUNT_COLOR = "#364858"; const COUNT_LOW_COLOR = "#e05555"; document.addEventListener('DOMContentLoaded', () => { const form = document.getElementById('comment-form'); const nameInput = document.getElementById('comment-name'); const msgInput = document.getElementById('comment-message'); const submitBtn = document.getElementById('comment-submit'); const charCountEl = document.getElementById('char-count'); const listEl = document.getElementById('comments-list'); const titleEl = document.getElementById('comments-title'); const paginationEl = document.getElementById('comments-pagination'); const colorInput = document.getElementById('comment-avatar-color'); const b64Input = document.getElementById('comment-avatar-b64'); const pfpUpload = document.getElementById('pfp-upload'); const pfpPreview = document.getElementById('pfp-preview'); const pfpClear = document.getElementById('pfp-clear'); if (!form || !listEl) return; if (!firebase.apps.length) firebase.initializeApp(FIREBASE_CONFIG); const db = firebase.firestore(); // ── Avatar colour picker ───────────────────────────────────── const colorOpts = document.querySelectorAll('.av-opt'); colorOpts.forEach(btn => { btn.addEventListener('click', () => { colorOpts.forEach(b => b.classList.remove('selected')); btn.classList.add('selected'); colorInput.value = btn.dataset.color; }); }); // ── Custom PFP upload (center-cropped + shrunk to 64×64 JPEG) ─ pfpUpload.addEventListener('change', () => { const file = pfpUpload.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => { const img = new Image(); img.onload = () => { const canvas = document.createElement('canvas'); canvas.width = 64; canvas.height = 64; const size = Math.min(img.width, img.height); const sx = (img.width - size) / 2; const sy = (img.height - size) / 2; canvas.getContext('2d').drawImage(img, sx, sy, size, size, 0, 0, 64, 64); const dataUrl = canvas.toDataURL('image/jpeg', 0.8); b64Input.value = dataUrl; pfpPreview.style.backgroundImage = `url(${dataUrl})`; pfpPreview.style.display = 'block'; pfpClear.style.display = 'inline-flex'; }; img.src = reader.result; }; reader.readAsDataURL(file); }); pfpClear.addEventListener('click', () => { b64Input.value = ''; pfpUpload.value = ''; pfpPreview.style.display = 'none'; pfpPreview.style.backgroundImage = 'none'; pfpClear.style.display = 'none'; }); // ── Char counter ───────────────────────────────────────────── function updateCharCount() { const left = MAX_CHARS - msgInput.value.length; charCountEl.textContent = left; charCountEl.style.color = left < 40 ? COUNT_LOW_COLOR : COUNT_COLOR; } msgInput.addEventListener('input', updateCharCount); updateCharCount(); // ── Real-time feed + pagination ────────────────────────────── let allComments = []; let currentPage = 1; function renderPage() { listEl.innerHTML = ''; paginationEl.innerHTML = ''; if (allComments.length === 0) { listEl.innerHTML = '

No messages yet — be the first!

'; return; } const totalPages = Math.ceil(allComments.length / COMMENTS_PER_PAGE); currentPage = Math.min(Math.max(currentPage, 1), totalPages); const start = (currentPage - 1) * COMMENTS_PER_PAGE; allComments.slice(start, start + COMMENTS_PER_PAGE) .forEach(data => listEl.appendChild(buildBubble(data))); if (totalPages > 1) { for (let i = 1; i <= totalPages; i++) { const btn = document.createElement('button'); btn.className = 'steam-page-btn' + (i === currentPage ? ' active' : ''); btn.textContent = i; btn.onclick = () => { currentPage = i; renderPage(); }; paginationEl.appendChild(btn); } } } db.collection(COLLECTION) .orderBy('timestamp', 'desc') .onSnapshot(snap => { titleEl.textContent = snap.size > 0 ? `(${snap.size})` : ''; allComments = snap.docs.map(doc => doc.data()); renderPage(); }, err => { console.error(err); listEl.innerHTML = '

Could not load comments.

'; }); // ── Global click counter ───────────────────────────────────── const clickBtn = document.getElementById('yuuka-click-btn'); const clickDisplay = document.getElementById('click-counter-display'); const clickSound = new Audio('sounds/yuuka_click.mp3'); if (clickBtn && clickDisplay) { const statsRef = db.collection('stats').doc('global_clicks'); // Firestore applies local writes to this listener instantly, // so clicks show up immediately without a manual optimistic update. statsRef.onSnapshot(doc => { const count = doc.exists ? doc.data().count : 0; clickDisplay.textContent = (count || 0).toLocaleString(); }, err => { console.error("Click counter error:", err); clickDisplay.textContent = 'Error'; }); clickBtn.addEventListener('click', () => { clickSound.currentTime = 0; // restart so rapid clicks all play clickSound.play().catch(() => {}); statsRef.set({ count: firebase.firestore.FieldValue.increment(1) }, { merge: true }).catch(err => console.error("Failed to click:", err)); }); } // ── Submit ─────────────────────────────────────────────────── form.addEventListener('submit', async e => { e.preventDefault(); const name = nameInput.value.trim().slice(0, MAX_NAME) || 'Anonymous'; const message = msgInput.value.trim().slice(0, MAX_CHARS); const color = colorInput.value || DEFAULT_COLOR; const avatarUrl = b64Input.value; if (!message) { msgInput.focus(); return; } const label = submitBtn.textContent; submitBtn.disabled = true; submitBtn.textContent = 'Posting...'; try { await db.collection(COLLECTION).add({ name, message, color, avatarUrl, timestamp: firebase.firestore.FieldValue.serverTimestamp() }); nameInput.value = ''; msgInput.value = ''; updateCharCount(); } catch (err) { console.error(err); alert('Failed to post. Try again!'); } finally { submitBtn.disabled = false; submitBtn.textContent = label; } }); }); // ── Build a single comment bubble (Steam layout) ───────────────── function buildBubble(data) { const div = document.createElement('div'); div.className = 'comment-bubble'; // Pending server timestamps are null until the write lands const ts = data.timestamp ? data.timestamp.toDate() : new Date(); const timeStr = ts.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); const name = legacyUnescape(data.name) || 'Anonymous'; const message = legacyUnescape(data.message); let avatarHtml; if (typeof data.avatarUrl === 'string' && data.avatarUrl.startsWith('data:image/')) { avatarHtml = ``; } else { const color = /^#[0-9a-f]{6}$/i.test(data.color) ? data.color : '#64748b'; avatarHtml = `
${esc(name.charAt(0).toUpperCase())}
`; } div.innerHTML = ` ${avatarHtml}
${esc(name)} ${timeStr}

${esc(message)}

`; return div; } // ── Helpers ────────────────────────────────────────────────────── // Older comments were stored pre-escaped (< and > only), which made // them render as literal "<" once escaped again. Undo that here. function legacyUnescape(str) { if (!str) return ''; return String(str).replace(/</g, '<').replace(/>/g, '>'); } function esc(str) { if (!str) return ''; return String(str) .replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); } })();