Trivia Tournament Planner
Core Event Details
Live Tournament Summary
Total Rounds
0Total Questions
0Total Points
0Teams Registered
0Fees Collected
$0Net (Fees - Prize)
$0Tournament Structure (Rounds)
Team Registration
Manage Round Categories
Add or remove categories for the "Round Category" dropdown on the dashboard.
${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 Breakdown
| 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} | |||
Registered Teams (${state.teams.length})
-
`;
state.teams.forEach(team => {
teamsListHTML += `
- ${team.name} `; }); teamsListHTML += '
