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);
});