Team Discussion Forum

Team Discussion Forum

A collaborative space for your team's conversations.

All Topics

Loading topics...

Topic not found.

'; } }); // Subscribe to the replies subcollection const repliesCollectionRef = collection(db, `artifacts/${appId}/public/data/forum_topics/${topicId}/replies`); const q = query(repliesCollectionRef, orderBy("createdAt", "asc")); unsubscribeReplies = onSnapshot(q, (snapshot) => { const replies = []; snapshot.forEach(doc => { replies.push(doc.data()); }); // Re-render with updated replies const topicDocRef = doc(db, `artifacts/${appId}/public/data/forum_topics`, topicId); getDoc(topicDocRef).then(docSnap => { if (docSnap.exists()) { renderTopicAndReplies(docSnap.data(), replies); } }); }); }; /** * Renders the main topic content and its replies. * @param {object} topic - The topic data object. * @param {Array} [replies=[]] - An array of reply data objects. */ function renderTopicAndReplies(topic, replies = []) { const repliesHtml = replies.map(reply => `

${escapeHTML(reply.content)}

Replied by: ${escapeHTML(reply.author)} on ${reply.createdAt?.toDate().toLocaleString() || 'a while ago'}

`).join(''); singleTopicContent.innerHTML = `

${escapeHTML(topic.title)}

Posted by: ${escapeHTML(topic.author)} on ${topic.createdAt?.toDate().toLocaleString() || 'a while ago'}

${escapeHTML(topic.content)}

${replies.length} Replies

${repliesHtml || '

No replies yet.

'}

Add Your Reply

`; // Re-attach event listener for the new reply form document.getElementById('reply-form').addEventListener('submit', handleReplySubmit); } /** * Handles the submission of a new reply. */ async function handleReplySubmit(e) { e.preventDefault(); if (!currentUser || !currentTopicId) return; const content = document.getElementById('reply-content').value.trim(); if (!content) return; const userDocRef = doc(db, `artifacts/${appId}/public/data/users`, currentUser.uid); try { const userDoc = await getDoc(userDocRef); const authorName = userDoc.exists() && userDoc.data().displayName ? userDoc.data().displayName : 'Anonymous'; const repliesCollectionRef = collection(db, `artifacts/${appId}/public/data/forum_topics/${currentTopicId}/replies`); await addDoc(repliesCollectionRef, { content, author: authorName, userId: currentUser.uid, createdAt: serverTimestamp() }); document.getElementById('reply-form').reset(); } catch (error) { console.error("Error adding reply: ", error); alert("Failed to post reply."); } } /** * Shows the main topic list view and hides the single topic view. */ window.showTopicList = () => { if (unsubscribeReplies) unsubscribeReplies(); currentTopicId = null; singleTopicView.classList.add('hidden'); topicListView.classList.remove('hidden'); }; /** * Generates and downloads a beautifully formatted PDF of the current discussion. */ async function generatePdf() { const { jsPDF } = window.jspdf; const topicTitleEl = document.querySelector('.topic-title'); const topicContentEl = document.querySelector('.topic-content'); const topicMetaEl = document.querySelector('.topic-meta'); const replyItems = Array.from(document.querySelectorAll('.reply-item')); if (!topicTitleEl || !topicContentEl || !topicMetaEl) { alert("Could not find content to generate PDF."); return; } downloadPdfBtn.textContent = 'Generating PDF...'; downloadPdfBtn.disabled = true; try { const pdf = new jsPDF({ orientation: 'p', unit: 'pt', format: 'a4' }); const page_width = pdf.internal.pageSize.getWidth(); const margin = 40; let cursor_y = margin; // --- PDF Header --- pdf.setFontSize(22); pdf.setFont("helvetica", "bold"); pdf.setTextColor(40, 52, 71); // Dark Slate color pdf.text("Team Discussion Report", page_width / 2, cursor_y, { align: 'center' }); cursor_y += 30; pdf.setDrawColor(221, 221, 221); // Light gray line pdf.line(margin, cursor_y, page_width - margin, cursor_y); cursor_y += 25; // --- Topic Title --- pdf.setFontSize(16); pdf.setFont("helvetica", "bold"); pdf.setTextColor(59, 130, 246); // Blue color const splitTitle = pdf.splitTextToSize(topicTitleEl.textContent.trim(), page_width - margin * 2); pdf.text(splitTitle, margin, cursor_y); cursor_y += splitTitle.length * 16 + 10; // --- Topic Meta --- pdf.setFontSize(9); pdf.setFont("helvetica", "normal"); pdf.setTextColor(100, 116, 139); // Slate color pdf.text(topicMetaEl.textContent.trim(), margin, cursor_y); cursor_y += 25; // --- Topic Content --- pdf.setFontSize(11); pdf.setTextColor(51, 65, 85); // Darker Slate color const splitContent = pdf.splitTextToSize(topicContentEl.textContent.trim(), page_width - margin * 2); pdf.text(splitContent, margin, cursor_y); cursor_y += (splitContent.length * 12) + 30; // --- Replies Section --- if (replyItems.length > 0) { pdf.setFontSize(14); pdf.setFont("helvetica", "bold"); pdf.setTextColor(40, 52, 71); pdf.text("Replies", margin, cursor_y); cursor_y += 15; pdf.line(margin, cursor_y, page_width - margin, cursor_y); cursor_y += 20; } // --- Loop through replies --- replyItems.forEach((replyEl) => { const replyContent = replyEl.querySelector('.reply-content').textContent.trim(); const replyMeta = replyEl.querySelector('.reply-meta').textContent.trim(); const replyContentLines = pdf.splitTextToSize(replyContent, page_width - margin * 2 - 20); const requiredHeight = (replyContentLines.length * 12) + 40; // content + meta + padding if (cursor_y + requiredHeight > pdf.internal.pageSize.getHeight() - margin) { pdf.addPage(); cursor_y = margin; } // Reply Box Styling pdf.setFillColor(248, 250, 252); // Very light gray (Slate 50) pdf.roundedRect(margin, cursor_y, page_width - margin * 2, requiredHeight, 5, 5, 'F'); let text_cursor_y = cursor_y + 15; // Reply Meta pdf.setFontSize(9); pdf.setFont("helvetica", "normal"); pdf.setTextColor(100, 116, 139); pdf.text(replyMeta, margin + 10, text_cursor_y); text_cursor_y += 20; // Reply Content pdf.setFontSize(11); pdf.setTextColor(51, 65, 85); pdf.text(replyContentLines, margin + 10, text_cursor_y); cursor_y += requiredHeight + 15; // Add space after each reply }); // --- Footer with Page Numbers --- const pageCount = pdf.internal.getNumberOfPages(); for (let i = 1; i <= pageCount; i++) { pdf.setPage(i); pdf.setFontSize(8); pdf.setTextColor(150); pdf.text(`Page ${i} of ${pageCount}`, page_width / 2, pdf.internal.pageSize.getHeight() - 20, { align: 'center' }); pdf.text(`Generated on: ${new Date().toLocaleString()}`, margin, pdf.internal.pageSize.getHeight() - 20); } const safeFileName = topicTitleEl.textContent.trim().replace(/[^a-z0-9]/gi, '_').toLowerCase(); pdf.save(`Discussion_${safeFileName}.pdf`); } catch (error) { console.error("Error generating PDF:", error); alert("Could not generate PDF. Please try again."); } finally { downloadPdfBtn.textContent = 'Download Discussion as PDF'; downloadPdfBtn.disabled = false; } } /** * Escapes HTML to prevent XSS attacks. * @param {string} str - The string to escape. * @returns {string} The escaped string. */ function escapeHTML(str) { if (typeof str !== 'string') return ''; return str.replace(/[&<>"']/g, function(match) { return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[match]; }); } // --- Event Listeners and Initial Setup --- document.addEventListener('DOMContentLoaded', () => { let authInitialized = false; onAuthStateChanged(auth, (user) => { if (user) { // User is signed in. setupAppForUser(user); } else { // User is signed out or auth state is initializing. if (!authInitialized) { authInitialized = true; // Prevent sign-in from running multiple times (async () => { try { if (initialAuthToken) { await signInWithCustomToken(auth, initialAuthToken); } else { await signInAnonymously(auth); } // onAuthStateChanged will be triggered again with the user object on success. } catch (error) { // This is a genuine authentication failure. console.error("Authentication failed during sign-in attempt:", error); setupAppForUser(null); // Update UI to show failure state } })(); } else { // Auth was already attempted, and user is null. Treat as signed out. setupAppForUser(null); } } }); // Tab navigation changeTab(0); prevBtn.addEventListener('click', () => { if (currentTab > 0) changeTab(currentTab - 1); }); nextBtn.addEventListener('click', () => { if (currentTab < tabs.length - 1) changeTab(currentTab + 1); }); // Form submission newTopicForm.addEventListener('submit', handleNewTopicSubmit); // Settings saveDisplayNameBtn.addEventListener('click', handleSaveDisplayName); // PDF Download downloadPdfBtn.addEventListener('click', generatePdf); });
Scroll to Top