Trivia Tournament Planner

Trivia Tournament Planner

Core Event Details

Live Tournament Summary

Total Rounds

0

Total Questions

0

Total Points

0

Teams Registered

0

Fees Collected

$0

Net (Fees - Prize)

$0

Tournament Structure (Rounds)

Team Registration

Manage Round Categories

Add or remove categories for the "Round Category" dropdown on the dashboard.

'; } state.teams.forEach(team => { const card = document.createElement('div'); card.className = 'ttp-team-card'; card.innerHTML = ` `; teamListContainer.appendChild(card); }); } /** * Renders the list in the Data Configuration tab. */ function renderCategoryConfig() { if (!categoryConfigList) return; categoryConfigList.innerHTML = ''; state.categories.forEach(cat => { categoryConfigList.innerHTML += `
${cat}
`; }); } /** * Calculates and renders all summary statistics. */ function renderSummary() { if (!statRounds || !statQuestions || !statPoints || !statTeams || !statFees || !statNet) return; const numRounds = state.rounds.length; const numTeams = state.teams.length; const numQuestions = state.rounds.reduce((sum, r) => sum + (parseInt(r.questions) || 0), 0); const numPoints = state.rounds.reduce((sum, r) => sum + (parseInt(r.questions) || 0) * (parseInt(r.points) || 0), 0); const fee = parseFloat(state.eventDetails.fee) || 0; const prize = parseFloat(state.eventDetails.prize) || 0; const totalFees = numTeams * fee; const net = totalFees - prize; statRounds.textContent = numRounds; statQuestions.textContent = numQuestions; statPoints.textContent = numPoints; statTeams.textContent = numTeams; statFees.textContent = formatCurrency(totalFees); statNet.textContent = formatCurrency(net); statNet.style.color = net < 0 ? '#dc3545' : '#28a745'; } /** * Central "update" function. Call this after any state change. */ function updateAll() { // Save state to localStorage localStorage.setItem('ttpState', JSON.stringify(state)); // Re-render all dynamic components renderRoundList(); renderTeamList(); renderCategoryConfig(); renderSummary(); } /** * Full re-render. Call when state *structure* changes (e.g., config). */ function fullReRender() { renderEventDetails(); updateAll(); } // --- EVENT LISTENERS --- // /** * Attaches listeners for tab navigation. */ function attachTabListeners() { if (!tabLinks || !nextTabBtn || !prevTabBtn) return; const tabIds = Array.from(tabLinks).map(link => link.dataset.tab); function showTab(tabId) { state.activeTab = tabId; tabContents.forEach(content => { content.classList.remove('ttp-active'); }); container.querySelector(`#${tabId}`).classList.add('ttp-active'); tabLinks.forEach(link => { link.classList.toggle('ttp-active', link.dataset.tab === tabId); }); // Update nav buttons const currentIndex = tabIds.indexOf(tabId); prevTabBtn.style.visibility = (currentIndex === 0) ? 'hidden' : 'visible'; nextTabBtn.style.visibility = (currentIndex === tabIds.length - 1) ? 'hidden' : 'visible'; } tabLinks.forEach(link => { link.addEventListener('click', () => showTab(link.dataset.tab)); }); nextTabBtn.addEventListener('click', () => { const currentIndex = tabIds.indexOf(state.activeTab); if (currentIndex < tabIds.length - 1) { showTab(tabIds[currentIndex + 1]); } }); prevTabBtn.addEventListener('click', () => { const currentIndex = tabIds.indexOf(state.activeTab); if (currentIndex > 0) { showTab(tabIds[currentIndex - 1]); } }); } /** * Attaches listeners for the Core Event Details inputs. */ function attachDetailListeners() { const inputs = [ { el: eventNameInput, key: 'name' }, { el: eventDateInput, key: 'date' }, { el: eventLocationInput, key: 'location' }, { el: eventFeeInput, key: 'fee' }, { el: eventPrizeInput, key: 'prize' } ]; inputs.forEach(item => { if (item.el) { item.el.addEventListener('input', (e) => { const value = (item.el.type === 'number') ? parseFloat(e.target.value) || 0 : e.target.value; state.eventDetails[item.key] = value; renderSummary(); // Only summary needs update localStorage.setItem('ttpState', JSON.stringify(state)); }); } }); } /** * Attaches listeners for adding/removing/editing rounds and teams. */ function attachListListeners() { // Add Round if (addRoundBtn) { addRoundBtn.addEventListener('click', () => { state.rounds.push({ id: `r${Date.now()}`, name: "New Round", category: state.categories[0] || "General Knowledge", questions: 10, points: 1 }); updateAll(); }); } // Add Team if (addTeamBtn) { addTeamBtn.addEventListener('click', () => { state.teams.push({ id: `t${Date.now()}`, name: "New Team" }); updateAll(); }); } // Event delegation for dynamic lists container.addEventListener('click', (e) => { // Remove Round if (e.target.classList.contains('ttp-remove-round-btn')) { const roundId = e.target.dataset.id; state.rounds = state.rounds.filter(r => r.id !== roundId); updateAll(); } // Remove Team if (e.target.classList.contains('ttp-remove-team-btn')) { const teamId = e.target.dataset.id; state.teams = state.teams.filter(t => t.id !== teamId); updateAll(); } }); // Event delegation for inputs within lists container.addEventListener('input', (e) => { // Edit Round Card if (e.target.classList.contains('ttp-round-input')) { const roundId = e.target.dataset.id; const field = e.target.dataset.field; const round = state.rounds.find(r => r.id === roundId); if (round) { const value = (e.target.type === 'number') ? parseInt(e.target.value) || 0 : e.target.value; round[field] = value; // Need to partially re-render *just* this card's footer and summary const card = e.target.closest('.ttp-round-card'); if (card) { const footer = card.querySelector('.ttp-round-card-footer'); if (footer) { footer.textContent = `Total Points: ${round.questions * round.points}`; } } renderSummary(); localStorage.setItem('ttpState', JSON.stringify(state)); } } // Edit Team Name if (e.target.classList.contains('ttp-team-name-input')) { const teamId = e.target.dataset.id; const team = state.teams.find(t => t.id === teamId); if (team) { team.name = e.target.value; localStorage.setItem('ttpState', JSON.stringify(state)); } } }); } /** * Attaches listeners for the Data Configuration tab. */ function attachConfigListeners() { // Add Category if (addCategoryBtn) { addCategoryBtn.addEventListener('click', () => { const newName = newCategoryNameInput.value.trim(); if (newName && !state.categories.includes(newName)) { state.categories.push(newName); newCategoryNameInput.value = ''; updateAll(); // Update config list and round dropdowns } }); } // Remove Category (Event Delegation) if (categoryConfigList) { categoryConfigList.addEventListener('click', (e) => { if (e.target.classList.contains('ttp-remove-category-btn')) { const catName = e.target.dataset.name; state.categories = state.categories.filter(c => c !== catName); // Set any rounds using this category to the first available const fallback = state.categories[0] || ""; state.rounds.forEach(r => { if (r.category === catName) r.category = fallback; }); updateAll(); } }); } } /** * Attaches listener for the PDF Download button. */ function attachPdfListener() { if (!downloadPdfBtn) return; downloadPdfBtn.addEventListener('click', async () => { const { jsPDF } = window.jspdf; const canvas = window.html2canvas; if (!jsPDF || !canvas) { alert('Error: PDF generation libraries not loaded.'); return; } downloadPdfBtn.textContent = 'Generating...'; downloadPdfBtn.disabled = true; try { // 1. Build clean HTML for PDF // This adheres to Spec #9 (no edit buttons, etc.) // --- Event Details & Summary --- const details = state.eventDetails; let summaryHTML = `

Event Details

Event Date ${details.date || 'N/A'}
Location ${details.location || 'N/A'}
Team Entry Fee ${formatCurrency(details.fee)}
Total Prize Pool ${formatCurrency(details.prize)}

Tournament Summary

Total Rounds${statRounds.textContent}
Total Questions${statQuestions.textContent}
Total Points${statPoints.textContent}
Teams Registered${statTeams.textContent}
Total Fees${statFees.textContent}
Net (Fees-Prize)${statNet.textContent}
`; // --- Rounds Table --- let roundsTableHTML = `

Rounds Breakdown

`; state.rounds.forEach((round, index) => { const total = (round.questions || 0) * (round.points || 0); roundsTableHTML += ` `; }); roundsTableHTML += `
Round # Round Name Category Questions Points/Q Total Points
${index + 1} ${round.name} ${round.category} ${round.questions} ${round.points} ${total}
Total: ${statQuestions.textContent} ${statPoints.textContent}
`; // --- Teams List --- let teamsListHTML = `

Registered Teams (${state.teams.length})

    `; state.teams.forEach(team => { teamsListHTML += `
  1. ${team.name}
  2. `; }); teamsListHTML += '
'; // --- Combine and Render --- pdfOutputContainer.innerHTML = `

${details.name || 'Trivia Tournament Plan'}

${summaryHTML} ${roundsTableHTML} ${teamsListHTML} `; // 2. Use html2canvas to capture the *hidden* div const pdfCanvas = await canvas(pdfOutputContainer, { scale: 2 }); const imgData = pdfCanvas.toDataURL('image/png'); // 3. Create PDF with jspdf const pdf = new jsPDF({ orientation: 'portrait', unit: 'px', format: [pdfCanvas.width, pdfCanvas.height] }); pdf.addImage(imgData, 'PNG', 0, 0, pdfCanvas.width, pdfCanvas.height); // 4. Download const safeFileName = (details.name || 'trivia-plan').replace(/[^a-z0-9]/gi, '_').toLowerCase(); pdf.save(`${safeFileName}.pdf`); } catch (error) { console.error('PDF Generation Error:', error); alert('An error occurred while generating the PDF.'); } finally { // 5. Cleanup pdfOutputContainer.innerHTML = ''; // Clear hidden div downloadPdfBtn.textContent = 'Download Tournament Plan'; downloadPdfBtn.disabled = false; } }); } /** * Loads state from localStorage, if it exists. */ function loadState() { const savedState = localStorage.getItem('ttpState'); if (savedState) { const parsedState = JSON.parse(savedState); // Merge saved state with default state to ensure all keys exist state = { ...state, ...parsedState }; } } // --- INITIALIZATION --- // /** * Initializes the entire application. */ function init() { loadState(); // Load saved data attachTabListeners(); attachDetailListeners(); attachListListeners(); attachConfigListeners(); attachPdfListener(); fullReRender(); // Initial render of all components } init(); });