Add visibility toggle for picks features and rename Mes choix to Mes picks
- Renamed 'Mes choix' to 'Mes picks' across all pages and JavaScript files - Added visibility toggle in control panel (Réglages > Visibilité) to control: - Display of 'Mes picks' button in navigation - Display of 'Bon picks' column in user statistics - Combined both visibility options into single toggle for simplicity - Added backend API endpoints for visibility configuration - Implemented localStorage caching for instant visibility updates - Fixed flash of content issues when navigating between pages - Updated stats endpoint to calculate correct picks on-the-fly from current winners - Renamed 'Bonnes réponses' to 'Bon picks' in user statistics table
This commit is contained in:
@@ -72,7 +72,7 @@
|
|||||||
<a href="movies.html" class="nav-link btn btn-outline-light me-2">Films</a>
|
<a href="movies.html" class="nav-link btn btn-outline-light me-2">Films</a>
|
||||||
<a href="checklist.html" class="nav-link btn btn-outline-light me-2">Checklist</a>
|
<a href="checklist.html" class="nav-link btn btn-outline-light me-2">Checklist</a>
|
||||||
<a href="user-stat.html" class="nav-link btn btn-outline-light me-2">Statistiques</a>
|
<a href="user-stat.html" class="nav-link btn btn-outline-light me-2">Statistiques</a>
|
||||||
<a href="picks.html" class="nav-link btn btn-outline-light me-2">Mes choix</a>
|
<a href="picks.html" class="nav-link btn btn-outline-light me-2" id="nav-link-picks" style="display: none;">Mes picks</a>
|
||||||
<a id="admin-control-link" href="control.html" class="nav-link btn btn-warning me-2 d-none">Admin</a>
|
<a id="admin-control-link" href="control.html" class="nav-link btn btn-warning me-2 d-none">Admin</a>
|
||||||
</div>
|
</div>
|
||||||
<!-- Move user info inside collapsible navbar and align it properly -->
|
<!-- Move user info inside collapsible navbar and align it properly -->
|
||||||
|
|||||||
+32
-1
@@ -43,7 +43,7 @@
|
|||||||
<a href="movies.html" class="nav-link btn btn-outline-light me-2">Films</a>
|
<a href="movies.html" class="nav-link btn btn-outline-light me-2">Films</a>
|
||||||
<a href="checklist.html" class="nav-link btn btn-outline-light me-2">Checklist</a>
|
<a href="checklist.html" class="nav-link btn btn-outline-light me-2">Checklist</a>
|
||||||
<a href="user-stat.html" class="nav-link btn btn-outline-light me-2">Statistiques</a>
|
<a href="user-stat.html" class="nav-link btn btn-outline-light me-2">Statistiques</a>
|
||||||
<a href="picks.html" class="nav-link btn btn-outline-light me-2">Mes choix</a>
|
<a href="picks.html" class="nav-link btn btn-outline-light me-2" id="nav-link-picks" style="display: none;">Mes picks</a>
|
||||||
<a href="control.html" class="nav-link btn btn-warning me-2">Admin</a>
|
<a href="control.html" class="nav-link btn btn-warning me-2">Admin</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex ms-auto mt-2 mt-lg-0 align-items-center">
|
<div class="d-flex ms-auto mt-2 mt-lg-0 align-items-center">
|
||||||
@@ -136,6 +136,11 @@
|
|||||||
Points
|
Points
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="admin-settings-subtab-visibility" data-bs-toggle="tab" data-bs-target="#admin-settings-pane-visibility" type="button" role="tab" aria-controls="admin-settings-pane-visibility" aria-selected="false">
|
||||||
|
Visibilité
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="tab-content pt-3">
|
<div class="tab-content pt-3">
|
||||||
@@ -245,6 +250,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Visibility settings -->
|
||||||
|
<div class="tab-pane fade" id="admin-settings-pane-visibility" role="tabpanel" aria-labelledby="admin-settings-subtab-visibility" tabindex="0">
|
||||||
|
<div class="border rounded p-3 bg-light">
|
||||||
|
<div class="text-muted small mb-3">
|
||||||
|
Contrôle la visibilité de certains éléments dans l'interface.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="visibility-config-alert" class="alert d-none" role="alert"></div>
|
||||||
|
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="visibility-show-picks-features">
|
||||||
|
<label class="form-check-label" for="visibility-show-picks-features">
|
||||||
|
Afficher les fonctionnalités "Picks"
|
||||||
|
</label>
|
||||||
|
<div class="form-text">Contrôle l'affichage du bouton "Mes picks" dans la navigation et de la colonne "Bon picks" dans les statistiques.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="button" id="visibility-config-reload" class="btn btn-outline-secondary">Recharger</button>
|
||||||
|
<button type="button" id="visibility-config-save" class="btn btn-warning">Enregistrer</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2676,6 +2676,90 @@ window.onload = async function () {
|
|||||||
|
|
||||||
await loadPointsConfig();
|
await loadPointsConfig();
|
||||||
|
|
||||||
|
// Visibility config
|
||||||
|
const visibilityShowPicksFeaturesEl = document.getElementById('visibility-show-picks-features');
|
||||||
|
const visibilityConfigReloadBtn = document.getElementById('visibility-config-reload');
|
||||||
|
const visibilityConfigSaveBtn = document.getElementById('visibility-config-save');
|
||||||
|
const visibilityConfigAlert = document.getElementById('visibility-config-alert');
|
||||||
|
|
||||||
|
async function loadVisibilityConfig() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/settings/visibility-config', { cache: 'no-store' });
|
||||||
|
if (!res.ok) throw new Error('Failed to load visibility config');
|
||||||
|
const config = await res.json();
|
||||||
|
|
||||||
|
// Both settings should be the same - use showPicksButton as the master
|
||||||
|
const isEnabled = config.showPicksButton !== false && config.showBonPicksColumn !== false;
|
||||||
|
if (visibilityShowPicksFeaturesEl) visibilityShowPicksFeaturesEl.checked = isEnabled;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading visibility config:', err);
|
||||||
|
// Default to showing both
|
||||||
|
if (visibilityShowPicksFeaturesEl) visibilityShowPicksFeaturesEl.checked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visibilityConfigReloadBtn) {
|
||||||
|
visibilityConfigReloadBtn.addEventListener('click', async () => {
|
||||||
|
await loadVisibilityConfig();
|
||||||
|
showResponse('success', 'Configuration rechargée');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visibilityConfigSaveBtn) {
|
||||||
|
visibilityConfigSaveBtn.addEventListener('click', async () => {
|
||||||
|
const isEnabled = visibilityShowPicksFeaturesEl ? visibilityShowPicksFeaturesEl.checked : true;
|
||||||
|
const showPicksButton = isEnabled;
|
||||||
|
const showBonPicksColumn = isEnabled;
|
||||||
|
|
||||||
|
try {
|
||||||
|
visibilityConfigSaveBtn.disabled = true;
|
||||||
|
const oldText = visibilityConfigSaveBtn.textContent;
|
||||||
|
visibilityConfigSaveBtn.textContent = 'Enregistrement...';
|
||||||
|
|
||||||
|
const res = await fetch('/api/settings/visibility-config', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
showPicksButton,
|
||||||
|
showBonPicksColumn
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.json();
|
||||||
|
throw new Error(error.message || 'Failed to save');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
// Clear cache so all pages get the new setting immediately
|
||||||
|
localStorage.removeItem('visibility_config_cache');
|
||||||
|
|
||||||
|
showResponse('success', 'Configuration de visibilité enregistrée');
|
||||||
|
} catch (err) {
|
||||||
|
if (visibilityConfigAlert) {
|
||||||
|
visibilityConfigAlert.className = 'alert alert-danger';
|
||||||
|
visibilityConfigAlert.textContent = 'Erreur: ' + err.message;
|
||||||
|
visibilityConfigAlert.classList.remove('d-none');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
visibilityConfigSaveBtn.disabled = false;
|
||||||
|
visibilityConfigSaveBtn.textContent = oldText;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load visibility config when visibility tab is shown
|
||||||
|
const visibilitySubTab = document.getElementById('admin-settings-subtab-visibility');
|
||||||
|
if (visibilitySubTab) {
|
||||||
|
visibilitySubTab.addEventListener('shown.bs.tab', async () => {
|
||||||
|
await loadVisibilityConfig();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Categories management
|
// Categories management
|
||||||
const importPdfForm = document.getElementById('import-pdf-form');
|
const importPdfForm = document.getElementById('import-pdf-form');
|
||||||
const createCategoryForm = document.getElementById('create-category-form');
|
const createCategoryForm = document.getElementById('create-category-form');
|
||||||
|
|||||||
@@ -50,5 +50,59 @@
|
|||||||
if (adminLink && decoded.admin) {
|
if (adminLink && decoded.admin) {
|
||||||
adminLink.classList.remove('d-none');
|
adminLink.classList.remove('d-none');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check visibility config for picks button
|
||||||
|
const picksButton = document.getElementById('nav-link-picks');
|
||||||
|
if (picksButton) {
|
||||||
|
// Check cached value first for immediate display
|
||||||
|
const cachedConfig = localStorage.getItem('visibility_config_cache');
|
||||||
|
let cachedValue = null;
|
||||||
|
let cachedTimestamp = null;
|
||||||
|
|
||||||
|
if (cachedConfig) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(cachedConfig);
|
||||||
|
cachedValue = parsed.value;
|
||||||
|
cachedTimestamp = parsed.timestamp;
|
||||||
|
} catch (e) {
|
||||||
|
// Invalid cache, ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use cached value if it's less than 5 minutes old
|
||||||
|
const cacheAge = cachedTimestamp ? Date.now() - cachedTimestamp : Infinity;
|
||||||
|
const useCache = cachedValue !== null && cacheAge < 5 * 60 * 1000;
|
||||||
|
|
||||||
|
if (useCache) {
|
||||||
|
// Show button immediately based on cached value
|
||||||
|
if (cachedValue.showPicksButton !== false) {
|
||||||
|
picksButton.style.display = '';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Default to showing while we fetch
|
||||||
|
picksButton.style.display = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch fresh value from server
|
||||||
|
fetch('/api/settings/visibility-config', { cache: 'no-store' })
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(config => {
|
||||||
|
// Cache the result
|
||||||
|
localStorage.setItem('visibility_config_cache', JSON.stringify({
|
||||||
|
value: config,
|
||||||
|
timestamp: Date.now()
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Update display based on fresh value
|
||||||
|
if (config.showPicksButton !== false) {
|
||||||
|
picksButton.style.display = '';
|
||||||
|
} else {
|
||||||
|
picksButton.style.display = 'none';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// On error, keep current state (already set from cache or default)
|
||||||
|
});
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
+168
-212
@@ -24,8 +24,6 @@
|
|||||||
|
|
||||||
// Check if admin and show admin tabs
|
// Check if admin and show admin tabs
|
||||||
if (decoded.admin) {
|
if (decoded.admin) {
|
||||||
const adminTabAllPicks = document.getElementById('tab-all-picks');
|
|
||||||
if (adminTabAllPicks) adminTabAllPicks.classList.remove('d-none');
|
|
||||||
const adminTabWinners = document.getElementById('tab-winners');
|
const adminTabWinners = document.getElementById('tab-winners');
|
||||||
if (adminTabWinners) adminTabWinners.classList.remove('d-none');
|
if (adminTabWinners) adminTabWinners.classList.remove('d-none');
|
||||||
}
|
}
|
||||||
@@ -80,7 +78,7 @@
|
|||||||
// Use active year if not set
|
// Use active year if not set
|
||||||
if (!currentYear) {
|
if (!currentYear) {
|
||||||
currentYear = await fetchActiveYear();
|
currentYear = await fetchActiveYear();
|
||||||
document.title = `Pool Oscars (${currentYear}) - Mes choix`;
|
document.title = `Pool Oscars (${currentYear}) - Mes picks`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(`/api/picks/categories?year=${currentYear}`, {
|
const res = await fetch(`/api/picks/categories?year=${currentYear}`, {
|
||||||
@@ -101,9 +99,6 @@
|
|||||||
currentYear = data.year || currentYear;
|
currentYear = data.year || currentYear;
|
||||||
|
|
||||||
document.getElementById('current-year').textContent = currentYear;
|
document.getElementById('current-year').textContent = currentYear;
|
||||||
if (document.getElementById('all-picks-year')) {
|
|
||||||
document.getElementById('all-picks-year').textContent = currentYear;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (categories.length === 0) {
|
if (categories.length === 0) {
|
||||||
document.getElementById('categories-container').innerHTML = `
|
document.getElementById('categories-container').innerHTML = `
|
||||||
@@ -156,29 +151,49 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = categories.map(cat => `
|
container.innerHTML = `
|
||||||
<div class="card category-card shadow-sm">
|
<div class="table-responsive">
|
||||||
<div class="card-header">
|
<table class="table table-sm table-hover align-middle mb-0">
|
||||||
<h5 class="mb-0">${cat.categoryNumber}. ${cat.categoryName}</h5>
|
<thead class="table-light">
|
||||||
</div>
|
<tr>
|
||||||
<div class="card-body">
|
<th style="width: 50px;">#</th>
|
||||||
<div class="row g-2">
|
<th>Catégorie</th>
|
||||||
${cat.nominees.map(nominee => {
|
<th>Choix</th>
|
||||||
const isSelected = myPicks[cat.categoryNumber] === nominee.name;
|
<th>Nommés</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
${categories.map(cat => {
|
||||||
|
const selectedNominee = myPicks[cat.categoryNumber];
|
||||||
return `
|
return `
|
||||||
<div class="col-12 col-md-6 col-lg-4">
|
<tr class="${selectedNominee ? 'table-primary' : ''}">
|
||||||
<div class="nominee-option p-3 border rounded ${isSelected ? 'selected' : ''}"
|
<td class="fw-semibold">${cat.categoryNumber}</td>
|
||||||
data-category="${cat.categoryNumber}"
|
<td>${cat.categoryName}</td>
|
||||||
data-nominee="${nominee.name}">
|
<td>
|
||||||
${nominee.name}
|
${selectedNominee ? `<span class="badge bg-primary">${selectedNominee}</span>` : '<span class="text-muted">—</span>'}
|
||||||
</div>
|
</td>
|
||||||
</div>
|
<td>
|
||||||
|
<div class="d-flex flex-wrap gap-1">
|
||||||
|
${cat.nominees.map(nominee => {
|
||||||
|
const isSelected = selectedNominee === nominee.name;
|
||||||
|
return `
|
||||||
|
<span class="nominee-option badge ${isSelected ? 'bg-primary' : 'bg-secondary'} ${isSelected ? '' : 'bg-opacity-50'}"
|
||||||
|
style="cursor: pointer; font-weight: normal;"
|
||||||
|
data-category="${cat.categoryNumber}"
|
||||||
|
data-nominee="${nominee.name}">
|
||||||
|
${nominee.name}
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
}).join('')}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
`;
|
`;
|
||||||
}).join('')}
|
}).join('')}
|
||||||
</div>
|
</tbody>
|
||||||
</div>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
`).join('');
|
`;
|
||||||
|
|
||||||
// Add click handlers
|
// Add click handlers
|
||||||
container.querySelectorAll('.nominee-option').forEach(el => {
|
container.querySelectorAll('.nominee-option').forEach(el => {
|
||||||
@@ -186,15 +201,17 @@
|
|||||||
const category = parseInt(this.dataset.category);
|
const category = parseInt(this.dataset.category);
|
||||||
const nominee = this.dataset.nominee;
|
const nominee = this.dataset.nominee;
|
||||||
|
|
||||||
// Remove selection from other options in same category
|
// Toggle selection
|
||||||
container.querySelectorAll(`[data-category="${category}"]`).forEach(opt => {
|
if (myPicks[category] === nominee) {
|
||||||
opt.classList.remove('selected');
|
// Deselect if clicking the same nominee
|
||||||
});
|
delete myPicks[category];
|
||||||
|
} else {
|
||||||
|
// Select this nominee
|
||||||
|
myPicks[category] = nominee;
|
||||||
|
}
|
||||||
|
|
||||||
// Select this option
|
// Re-render to update the display
|
||||||
this.classList.add('selected');
|
renderCategories();
|
||||||
myPicks[category] = nominee;
|
|
||||||
updateProgress();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -250,144 +267,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// All picks functionality (admin only)
|
|
||||||
let allPicks = [];
|
|
||||||
let allPicksCategories = [];
|
|
||||||
|
|
||||||
async function fetchAllPicks() {
|
|
||||||
try {
|
|
||||||
// Use active year if not set
|
|
||||||
if (!currentYear) {
|
|
||||||
currentYear = await fetchActiveYear();
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await fetch(`/api/picks/all?year=${currentYear}`, {
|
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
if (res.status === 401) {
|
|
||||||
localStorage.removeItem('auth_token');
|
|
||||||
window.location.href = '/';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (res.status === 403) {
|
|
||||||
showAlert('Vous n\'avez pas les privilèges administrateur', 'warning');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new Error('Failed to fetch picks');
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await res.json();
|
|
||||||
allPicks = data.picks || [];
|
|
||||||
allPicksCategories = data.categories || [];
|
|
||||||
currentYear = data.year || currentYear;
|
|
||||||
|
|
||||||
const yearEl = document.getElementById('all-picks-year');
|
|
||||||
if (yearEl) yearEl.textContent = currentYear;
|
|
||||||
|
|
||||||
const countEl = document.getElementById('users-count');
|
|
||||||
if (countEl) countEl.textContent = allPicks.length;
|
|
||||||
|
|
||||||
renderAllPicks();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error fetching all picks:', err);
|
|
||||||
showAlert('Erreur lors du chargement des choix: ' + err.message, 'danger');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderAllPicks() {
|
|
||||||
const container = document.getElementById('all-picks-container');
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
if (allPicks.length === 0) {
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="alert alert-info">
|
|
||||||
Aucun utilisateur n'a encore soumis ses choix pour l'année ${currentYear}.
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a map of category info
|
|
||||||
const categoryMap = new Map();
|
|
||||||
allPicksCategories.forEach(cat => {
|
|
||||||
categoryMap.set(cat.categoryNumber, cat);
|
|
||||||
});
|
|
||||||
|
|
||||||
container.innerHTML = allPicks.map(pick => {
|
|
||||||
const userName = pick.userId?.name || 'Utilisateur inconnu';
|
|
||||||
const userEmail = pick.userId?.email || '';
|
|
||||||
const submittedDate = pick.submittedAt ? new Date(pick.submittedAt).toLocaleString('fr-FR') : '—';
|
|
||||||
|
|
||||||
// Create a map of picks by category
|
|
||||||
const picksMap = new Map();
|
|
||||||
pick.picks.forEach(p => {
|
|
||||||
picksMap.set(p.categoryNumber, p.selectedNominee);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Build picks list
|
|
||||||
const picksList = allPicksCategories.map(cat => {
|
|
||||||
const selected = picksMap.get(cat.categoryNumber);
|
|
||||||
if (!selected) return null;
|
|
||||||
|
|
||||||
// Check if it's a winner (if winners are set)
|
|
||||||
const nominee = cat.nominees.find(n => n.name === selected);
|
|
||||||
const isWinner = nominee?.isWinner || false;
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="pick-item ${isWinner ? 'correct-pick' : ''}">
|
|
||||||
<div class="d-flex justify-content-between align-items-start">
|
|
||||||
<div>
|
|
||||||
<strong>${cat.categoryNumber}. ${cat.categoryName}</strong><br>
|
|
||||||
<span class="text-muted">${selected}</span>
|
|
||||||
</div>
|
|
||||||
${isWinner ? '<span class="badge bg-success">Gagnant</span>' : ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}).filter(Boolean).join('');
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="card user-picks-card shadow-sm mb-3">
|
|
||||||
<div class="card-header">
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
<div>
|
|
||||||
<h5 class="mb-0">${userName}</h5>
|
|
||||||
<small class="text-muted">${userEmail}</small>
|
|
||||||
</div>
|
|
||||||
<div class="text-end">
|
|
||||||
<small class="text-muted">Soumis le: ${submittedDate}</small><br>
|
|
||||||
<span class="badge bg-primary">${pick.picks.length} / ${allPicksCategories.length} catégories</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
${picksList || '<div class="text-muted">Aucun choix enregistré</div>'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Event listeners
|
// Event listeners
|
||||||
document.getElementById('submit-picks')?.addEventListener('click', submitPicks);
|
document.getElementById('submit-picks')?.addEventListener('click', submitPicks);
|
||||||
document.getElementById('load-my-picks')?.addEventListener('click', () => {
|
document.getElementById('load-my-picks')?.addEventListener('click', () => {
|
||||||
loadMyPicks();
|
loadMyPicks();
|
||||||
showAlert('Choix rechargés', 'info');
|
showAlert('Choix rechargés', 'info');
|
||||||
});
|
});
|
||||||
document.getElementById('refresh-all-picks')?.addEventListener('click', () => {
|
|
||||||
fetchAllPicks();
|
|
||||||
showAlert('Choix rafraîchis', 'info');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load all picks when admin tab is shown
|
|
||||||
const allPicksTab = document.getElementById('tab-all-picks');
|
|
||||||
if (allPicksTab) {
|
|
||||||
allPicksTab.addEventListener('shown.bs.tab', () => {
|
|
||||||
fetchAllPicks();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Winners & Scores functionality (admin only)
|
// Winners & Scores functionality (admin only)
|
||||||
let winnersCategories = [];
|
let winnersCategories = [];
|
||||||
@@ -437,28 +322,42 @@
|
|||||||
const progressEl = document.getElementById('winners-progress');
|
const progressEl = document.getElementById('winners-progress');
|
||||||
if (progressEl) progressEl.textContent = `${winnersCount} / ${winnersCategories.length}`;
|
if (progressEl) progressEl.textContent = `${winnersCount} / ${winnersCategories.length}`;
|
||||||
|
|
||||||
container.innerHTML = winnersCategories.map(cat => {
|
container.innerHTML = `
|
||||||
const winner = cat.nominees.find(n => n.isWinner);
|
<div class="table-responsive">
|
||||||
return `
|
<table class="table table-sm table-hover align-middle mb-0">
|
||||||
<div class="card category-winner-card shadow-sm mb-3">
|
<thead class="table-light">
|
||||||
<div class="card-header">
|
<tr>
|
||||||
<h5 class="mb-0">${cat.categoryNumber}. ${cat.categoryName}</h5>
|
<th style="width: 50px;">#</th>
|
||||||
</div>
|
<th>Catégorie</th>
|
||||||
<div class="card-body">
|
<th style="width: 200px;">Sélection</th>
|
||||||
<div class="mb-2">
|
<th>Gagnant</th>
|
||||||
<label class="form-label fw-semibold">Gagnant:</label>
|
</tr>
|
||||||
<select class="form-select winner-select" data-category-id="${cat._id}" data-category-number="${cat.categoryNumber}">
|
</thead>
|
||||||
<option value="">-- Sélectionner un gagnant --</option>
|
<tbody>
|
||||||
${cat.nominees.map(nominee => `
|
${winnersCategories.map(cat => {
|
||||||
<option value="${nominee.name}" ${nominee.isWinner ? 'selected' : ''}>${nominee.name}</option>
|
const winner = cat.nominees.find(n => n.isWinner);
|
||||||
`).join('')}
|
return `
|
||||||
</select>
|
<tr class="${winner ? 'table-success' : ''}">
|
||||||
</div>
|
<td class="fw-semibold">${cat.categoryNumber}</td>
|
||||||
${winner ? `<div class="alert alert-success mb-0 mt-2"><strong>Gagnant actuel:</strong> ${winner.name}</div>` : ''}
|
<td>${cat.categoryName}</td>
|
||||||
</div>
|
<td>
|
||||||
</div>
|
<select class="form-select form-select-sm winner-select" data-category-id="${cat._id}" data-category-number="${cat.categoryNumber}">
|
||||||
`;
|
<option value="">—</option>
|
||||||
}).join('');
|
${cat.nominees.map(nominee => `
|
||||||
|
<option value="${nominee.name}" ${nominee.isWinner ? 'selected' : ''}>${nominee.name}</option>
|
||||||
|
`).join('')}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
${winner ? `<span class="badge bg-success">${winner.name}</span>` : '<span class="text-muted">—</span>'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join('')}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
// Add event listeners for winner selection
|
// Add event listeners for winner selection
|
||||||
container.querySelectorAll('.winner-select').forEach(select => {
|
container.querySelectorAll('.winner-select').forEach(select => {
|
||||||
@@ -466,34 +365,35 @@
|
|||||||
const categoryId = this.dataset.categoryId;
|
const categoryId = this.dataset.categoryId;
|
||||||
const winnerName = this.value;
|
const winnerName = this.value;
|
||||||
|
|
||||||
if (!winnerName) return;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// If empty value, clear the winner (set to null/empty)
|
||||||
const res = await fetch(`/api/categories/${categoryId}/winner`, {
|
const res = await fetch(`/api/categories/${categoryId}/winner`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${token}`
|
'Authorization': `Bearer ${token}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ winnerName })
|
body: JSON.stringify({ winnerName: winnerName || null })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const error = await res.json();
|
const error = await res.json();
|
||||||
throw new Error(error.message || 'Failed to mark winner');
|
throw new Error(error.message || 'Failed to update winner');
|
||||||
}
|
}
|
||||||
|
|
||||||
showAlert('Gagnant marqué avec succès', 'success');
|
showAlert(winnerName ? 'Gagnant marqué avec succès' : 'Gagnant supprimé', 'success');
|
||||||
await fetchWinnersCategories();
|
await fetchWinnersCategories();
|
||||||
|
// Auto-refresh scores after marking/clearing a winner (silent mode)
|
||||||
|
await calculateScores(true);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error marking winner:', err);
|
console.error('Error updating winner:', err);
|
||||||
showAlert('Erreur: ' + err.message, 'danger');
|
showAlert('Erreur: ' + err.message, 'danger');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function calculateScores() {
|
async function calculateScores(silent = false) {
|
||||||
try {
|
try {
|
||||||
if (!currentYear) {
|
if (!currentYear) {
|
||||||
currentYear = await fetchActiveYear();
|
currentYear = await fetchActiveYear();
|
||||||
@@ -516,15 +416,29 @@
|
|||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const error = await res.json();
|
const error = await res.json();
|
||||||
|
// If no winners marked, handle gracefully (especially for auto-refresh)
|
||||||
|
if (error.message && error.message.includes('No winners marked')) {
|
||||||
|
if (!silent) {
|
||||||
|
// Only show as info/warning, not error
|
||||||
|
showAlert('Aucun gagnant marqué. Les scores seront calculés une fois les gagnants définis.', 'info');
|
||||||
|
}
|
||||||
|
// Still try to fetch existing scores
|
||||||
|
await fetchScores();
|
||||||
|
return;
|
||||||
|
}
|
||||||
throw new Error(error.message || 'Failed to calculate scores');
|
throw new Error(error.message || 'Failed to calculate scores');
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
showAlert(`Scores calculés avec succès! ${data.scores.length} utilisateur(s) évalué(s).`, 'success');
|
if (!silent) {
|
||||||
|
showAlert(`Scores calculés avec succès! ${data.scores.length} utilisateur(s) évalué(s).`, 'success');
|
||||||
|
}
|
||||||
await fetchScores();
|
await fetchScores();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error calculating scores:', err);
|
console.error('Error calculating scores:', err);
|
||||||
showAlert('Erreur: ' + err.message, 'danger');
|
if (!silent) {
|
||||||
|
showAlert('Erreur: ' + err.message, 'danger');
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
const btn = document.getElementById('calculate-scores');
|
const btn = document.getElementById('calculate-scores');
|
||||||
if (btn) {
|
if (btn) {
|
||||||
@@ -540,21 +454,29 @@
|
|||||||
currentYear = await fetchActiveYear();
|
currentYear = await fetchActiveYear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update scores year display
|
||||||
|
const scoresYearEl = document.getElementById('scores-year');
|
||||||
|
if (scoresYearEl) {
|
||||||
|
scoresYearEl.textContent = currentYear;
|
||||||
|
}
|
||||||
|
|
||||||
const res = await fetch(`/api/picks/scores?year=${currentYear}`, {
|
const res = await fetch(`/api/picks/scores?year=${currentYear}`, {
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
if (res.status === 403) {
|
const errorData = await res.json().catch(() => ({ message: 'Failed to fetch scores' }));
|
||||||
return; // Not admin, skip
|
throw new Error(errorData.message || 'Failed to fetch scores');
|
||||||
}
|
|
||||||
throw new Error('Failed to fetch scores');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
renderScores(data.scores || [], data.totalCategories || 0);
|
renderScores(data.scores || [], data.totalCategories || 0);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching scores:', err);
|
console.error('Error fetching scores:', err);
|
||||||
|
const container = document.getElementById('scores-container');
|
||||||
|
if (container) {
|
||||||
|
container.innerHTML = '<div class="text-danger">Erreur lors du chargement des scores.</div>';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -569,15 +491,15 @@
|
|||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped table-hover">
|
<table class="table table-sm table-striped table-hover mb-0">
|
||||||
<thead>
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 50px;">#</th>
|
<th style="width: 40px;">#</th>
|
||||||
<th>Utilisateur</th>
|
<th>Utilisateur</th>
|
||||||
<th class="text-center">Score</th>
|
<th class="text-center" style="width: 80px;">Score</th>
|
||||||
<th class="text-center">Total</th>
|
<th class="text-center" style="width: 70px;">Total</th>
|
||||||
<th class="text-center">%</th>
|
<th class="text-center" style="width: 70px;">%</th>
|
||||||
<th class="text-center" style="width: 120px;">Actions</th>
|
<th class="text-center" style="width: 100px;">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -588,13 +510,13 @@
|
|||||||
return `
|
return `
|
||||||
<tr>
|
<tr>
|
||||||
<td><strong>${rank}${medal ? ' ' + medal : ''}</strong></td>
|
<td><strong>${rank}${medal ? ' ' + medal : ''}</strong></td>
|
||||||
<td>${score.userName}${score.userEmail ? ` <small class="text-muted">(${score.userEmail})</small>` : ''}</td>
|
<td>${score.userName}</td>
|
||||||
<td class="text-center"><strong class="text-success">${score.score}</strong></td>
|
<td class="text-center"><strong class="text-success">${score.score}</strong></td>
|
||||||
<td class="text-center text-muted">${totalCategories}</td>
|
<td class="text-center text-muted small">${totalCategories}</td>
|
||||||
<td class="text-center"><strong>${percentage}%</strong></td>
|
<td class="text-center"><strong>${percentage}%</strong></td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
<button class="btn btn-sm btn-outline-primary view-picks-btn" data-user-id="${score.userId}" data-user-name="${score.userName}" data-score-index="${index}">
|
<button class="btn btn-sm btn-outline-primary view-picks-btn" data-user-id="${score.userId}" data-user-name="${score.userName}" data-score-index="${index}">
|
||||||
Voir détails
|
Détails
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -723,19 +645,53 @@
|
|||||||
showAlert('Données rafraîchies', 'info');
|
showAlert('Données rafraîchies', 'info');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Load scores when scores tab is shown
|
||||||
|
const scoresTab = document.getElementById('tab-scores');
|
||||||
|
if (scoresTab) {
|
||||||
|
scoresTab.addEventListener('shown.bs.tab', async () => {
|
||||||
|
await fetchScores();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Load winners when admin tab is shown
|
// Load winners when admin tab is shown
|
||||||
const winnersTab = document.getElementById('tab-winners');
|
const winnersTab = document.getElementById('tab-winners');
|
||||||
if (winnersTab) {
|
if (winnersTab) {
|
||||||
winnersTab.addEventListener('shown.bs.tab', () => {
|
winnersTab.addEventListener('shown.bs.tab', async () => {
|
||||||
fetchWinnersCategories();
|
await fetchWinnersCategories();
|
||||||
|
// Auto-refresh scores when entering the winners tab (silent mode)
|
||||||
|
await calculateScores(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Refresh scores button
|
||||||
|
document.getElementById('refresh-scores')?.addEventListener('click', async () => {
|
||||||
|
await fetchScores();
|
||||||
|
showAlert('Scores rafraîchis', 'info');
|
||||||
|
});
|
||||||
|
|
||||||
// Initial load
|
// Initial load
|
||||||
(async () => {
|
(async () => {
|
||||||
currentYear = await fetchActiveYear();
|
currentYear = await fetchActiveYear();
|
||||||
document.title = `Pool Oscars (${currentYear}) - Mes choix`;
|
document.title = `Pool Oscars (${currentYear}) - Mes choix`;
|
||||||
await fetchCategories();
|
await fetchCategories();
|
||||||
|
|
||||||
|
// Auto-refresh scores on page load if scores tab is active
|
||||||
|
const scoresTab = document.getElementById('tab-scores');
|
||||||
|
const scoresPane = document.getElementById('pane-scores');
|
||||||
|
if (scoresTab && scoresPane && (scoresTab.classList.contains('active') || scoresPane.classList.contains('active'))) {
|
||||||
|
await fetchScores();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-refresh winners on page load if admin and winners tab is active
|
||||||
|
if (decoded.admin) {
|
||||||
|
const winnersTab = document.getElementById('tab-winners');
|
||||||
|
const winnersPane = document.getElementById('pane-winners');
|
||||||
|
// Check if winners tab is active (either by default or if it becomes active)
|
||||||
|
if (winnersTab && winnersPane && (winnersTab.classList.contains('active') || winnersPane.classList.contains('active'))) {
|
||||||
|
await fetchWinnersCategories();
|
||||||
|
await calculateScores(true); // Silent mode for auto-refresh
|
||||||
|
}
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
@@ -649,6 +649,24 @@ window.addEventListener('DOMContentLoaded', async function () {
|
|||||||
pageLoader.setProgress(70);
|
pageLoader.setProgress(70);
|
||||||
const stats = await statsRes.json();
|
const stats = await statsRes.json();
|
||||||
const userTableBody = document.getElementById('user-table-body');
|
const userTableBody = document.getElementById('user-table-body');
|
||||||
|
|
||||||
|
// Check visibility config for Bon picks column
|
||||||
|
let showBonPicksColumn = true;
|
||||||
|
try {
|
||||||
|
const visibilityRes = await fetch('/api/settings/visibility-config', { cache: 'no-store' });
|
||||||
|
if (visibilityRes.ok) {
|
||||||
|
const visibilityConfig = await visibilityRes.json();
|
||||||
|
showBonPicksColumn = visibilityConfig.showBonPicksColumn !== false;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading visibility config:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide/show Bon picks header
|
||||||
|
const bonPicksHeader = document.getElementById('bon-picks-header');
|
||||||
|
if (bonPicksHeader) {
|
||||||
|
bonPicksHeader.style.display = showBonPicksColumn ? '' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
// Sort by total points (descending), then by watched ratio
|
// Sort by total points (descending), then by watched ratio
|
||||||
stats.sort((a, b) => {
|
stats.sort((a, b) => {
|
||||||
@@ -686,6 +704,9 @@ window.addEventListener('DOMContentLoaded', async function () {
|
|||||||
const picksTd = document.createElement('td');
|
const picksTd = document.createElement('td');
|
||||||
const pickPointsConfig = userStat.pointsConfig || { pointsPerCorrectPick: 1 };
|
const pickPointsConfig = userStat.pointsConfig || { pointsPerCorrectPick: 1 };
|
||||||
picksTd.innerHTML = `${userStat.correctPicks || 0} <small class="text-muted">(${userStat.pickPoints || 0} pts)</small>`;
|
picksTd.innerHTML = `${userStat.correctPicks || 0} <small class="text-muted">(${userStat.pickPoints || 0} pts)</small>`;
|
||||||
|
if (!showBonPicksColumn) {
|
||||||
|
picksTd.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
const pointsTd = document.createElement('td');
|
const pointsTd = document.createElement('td');
|
||||||
pointsTd.className = 'text-center';
|
pointsTd.className = 'text-center';
|
||||||
|
|||||||
+1
-1
@@ -59,7 +59,7 @@
|
|||||||
<a href="movies.html" class="nav-link btn btn-outline-light me-2">Films</a>
|
<a href="movies.html" class="nav-link btn btn-outline-light me-2">Films</a>
|
||||||
<a href="checklist.html" class="nav-link btn btn-outline-light me-2">Checklist</a>
|
<a href="checklist.html" class="nav-link btn btn-outline-light me-2">Checklist</a>
|
||||||
<a href="user-stat.html" class="nav-link btn btn-outline-light me-2">Statistiques</a>
|
<a href="user-stat.html" class="nav-link btn btn-outline-light me-2">Statistiques</a>
|
||||||
<a href="picks.html" class="nav-link btn btn-outline-light me-2">Mes choix</a>
|
<a href="picks.html" class="nav-link btn btn-outline-light me-2" id="nav-link-picks" style="display: none;">Mes picks</a>
|
||||||
<a id="admin-control-link" href="control.html" class="nav-link btn btn-warning me-2 d-none">Admin</a>
|
<a id="admin-control-link" href="control.html" class="nav-link btn btn-warning me-2 d-none">Admin</a>
|
||||||
</div>
|
</div>
|
||||||
<!-- Move user info inside collapsible navbar and align it properly -->
|
<!-- Move user info inside collapsible navbar and align it properly -->
|
||||||
|
|||||||
+45
-87
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Pool Oscars - Mes choix</title>
|
<title>Pool Oscars - Mes picks</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link href="./css/app.css" rel="stylesheet">
|
<link href="./css/app.css" rel="stylesheet">
|
||||||
<link rel="preload" as="image" href="img/logo_w.png">
|
<link rel="preload" as="image" href="img/logo_w.png">
|
||||||
@@ -26,22 +26,21 @@
|
|||||||
.navbar-toggler-icon {
|
.navbar-toggler-icon {
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3E%3Cpath stroke='white' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E");
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3E%3Cpath stroke='white' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 7h22M4 15h22M4 23h22'/%3E%3C/svg%3E");
|
||||||
}
|
}
|
||||||
.category-card {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
.nominee-option {
|
.nominee-option {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
.nominee-option:hover {
|
.nominee-option:hover {
|
||||||
background-color: #f8f9fa;
|
opacity: 0.8;
|
||||||
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
.nominee-option.selected {
|
.table-sm td {
|
||||||
background-color: #0d6efd;
|
padding: 0.5rem;
|
||||||
color: white;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
.nominee-option.selected:hover {
|
.table-sm th {
|
||||||
background-color: #0b5ed7;
|
padding: 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
.user-picks-card {
|
.user-picks-card {
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
@@ -76,7 +75,7 @@
|
|||||||
<a href="movies.html" class="nav-link btn btn-outline-light me-2">Films</a>
|
<a href="movies.html" class="nav-link btn btn-outline-light me-2">Films</a>
|
||||||
<a href="checklist.html" class="nav-link btn btn-outline-light me-2">Checklist</a>
|
<a href="checklist.html" class="nav-link btn btn-outline-light me-2">Checklist</a>
|
||||||
<a href="user-stat.html" class="nav-link btn btn-outline-light me-2">Statistiques</a>
|
<a href="user-stat.html" class="nav-link btn btn-outline-light me-2">Statistiques</a>
|
||||||
<a href="picks.html" class="nav-link btn btn-outline-light me-2">Mes choix</a>
|
<a href="picks.html" class="nav-link btn btn-outline-light me-2" id="nav-link-picks" style="display: none;">Mes picks</a>
|
||||||
<a id="admin-control-link" href="control.html" class="nav-link btn btn-warning me-2 d-none">Admin</a>
|
<a id="admin-control-link" href="control.html" class="nav-link btn btn-warning me-2 d-none">Admin</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex ms-auto mt-2 mt-lg-0 align-items-center">
|
<div class="d-flex ms-auto mt-2 mt-lg-0 align-items-center">
|
||||||
@@ -88,7 +87,7 @@
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="container mt-5 pb-5">
|
<div class="container mt-5 pb-5">
|
||||||
<h2>Mes choix Oscars</h2>
|
<h2>Mes picks Oscars</h2>
|
||||||
|
|
||||||
<div id="alert-container"></div>
|
<div id="alert-container"></div>
|
||||||
|
|
||||||
@@ -96,18 +95,17 @@
|
|||||||
<ul class="nav nav-tabs mt-3" id="picksTabs" role="tablist">
|
<ul class="nav nav-tabs mt-3" id="picksTabs" role="tablist">
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link active" id="tab-my-picks" data-bs-toggle="tab" data-bs-target="#pane-my-picks" type="button" role="tab" aria-controls="pane-my-picks" aria-selected="true">
|
<button class="nav-link active" id="tab-my-picks" data-bs-toggle="tab" data-bs-target="#pane-my-picks" type="button" role="tab" aria-controls="pane-my-picks" aria-selected="true">
|
||||||
Mes choix
|
Mes picks
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link d-none" id="tab-all-picks" data-bs-toggle="tab" data-bs-target="#pane-all-picks" type="button" role="tab" aria-controls="pane-all-picks" aria-selected="false">
|
<button class="nav-link" id="tab-scores" data-bs-toggle="tab" data-bs-target="#pane-scores" type="button" role="tab" aria-controls="pane-scores" aria-selected="false">
|
||||||
Voir tous les choix
|
Scores
|
||||||
<span class="badge bg-warning text-dark ms-1">Admin</span>
|
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link d-none" id="tab-winners" data-bs-toggle="tab" data-bs-target="#pane-winners" type="button" role="tab" aria-controls="pane-winners" aria-selected="false">
|
<button class="nav-link d-none" id="tab-winners" data-bs-toggle="tab" data-bs-target="#pane-winners" type="button" role="tab" aria-controls="pane-winners" aria-selected="false">
|
||||||
Gagnants & Scores
|
Gagnants
|
||||||
<span class="badge bg-warning text-dark ms-1">Admin</span>
|
<span class="badge bg-warning text-dark ms-1">Admin</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
@@ -116,93 +114,53 @@
|
|||||||
<div class="tab-content pt-3" id="picksTabsContent">
|
<div class="tab-content pt-3" id="picksTabsContent">
|
||||||
<!-- My Picks Tab -->
|
<!-- My Picks Tab -->
|
||||||
<div class="tab-pane fade show active" id="pane-my-picks" role="tabpanel" aria-labelledby="tab-my-picks" tabindex="0">
|
<div class="tab-pane fade show active" id="pane-my-picks" role="tabpanel" aria-labelledby="tab-my-picks" tabindex="0">
|
||||||
<div class="card shadow-sm mb-4">
|
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
|
||||||
<div class="card-body">
|
<div>
|
||||||
<div class="d-flex flex-column flex-md-row align-items-start align-items-md-center justify-content-between gap-2">
|
<span class="fw-semibold">Année: <span id="current-year">—</span></span>
|
||||||
<div>
|
<span class="text-muted small ms-2">Progression: <span id="picks-progress">0 / 0</span></span>
|
||||||
<div class="fw-semibold">Année: <span id="current-year">—</span></div>
|
</div>
|
||||||
<div class="text-muted small">Sélectionne tes choix pour chaque catégorie</div>
|
<div class="d-flex gap-2">
|
||||||
</div>
|
<button type="button" id="load-my-picks" class="btn btn-sm btn-outline-secondary">Recharger</button>
|
||||||
<div class="d-flex gap-2">
|
<button type="button" id="submit-picks" class="btn btn-sm btn-primary">Enregistrer</button>
|
||||||
<button type="button" id="load-my-picks" class="btn btn-outline-secondary">Recharger mes choix</button>
|
|
||||||
<button type="button" id="submit-picks" class="btn btn-primary">Enregistrer mes choix</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-3">
|
|
||||||
<div class="text-muted small">
|
|
||||||
Progression: <span id="picks-progress">0 / 0</span> catégories complétées
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="categories-container">
|
<div id="categories-container">
|
||||||
<div class="text-center py-4 text-muted">Chargement des catégories…</div>
|
<div class="text-center py-3 text-muted small">Chargement des catégories…</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- All Picks Tab (Admin only) -->
|
<!-- Scores Tab -->
|
||||||
<div class="tab-pane fade" id="pane-all-picks" role="tabpanel" aria-labelledby="tab-all-picks" tabindex="0">
|
<div class="tab-pane fade" id="pane-scores" role="tabpanel" aria-labelledby="tab-scores" tabindex="0">
|
||||||
<div class="card shadow-sm mb-4">
|
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
|
||||||
<div class="card-body">
|
<div>
|
||||||
<div class="d-flex flex-column flex-md-row align-items-start align-items-md-center justify-content-between gap-2">
|
<span class="fw-semibold">Année: <span id="scores-year">—</span></span>
|
||||||
<div>
|
</div>
|
||||||
<div class="fw-semibold">Année: <span id="all-picks-year">—</span></div>
|
<div class="d-flex gap-2">
|
||||||
<div class="text-muted small">Vue d'ensemble de tous les choix des utilisateurs</div>
|
<button type="button" id="refresh-scores" class="btn btn-sm btn-outline-secondary">Rafraîchir</button>
|
||||||
</div>
|
|
||||||
<div class="d-flex gap-2">
|
|
||||||
<button type="button" id="refresh-all-picks" class="btn btn-outline-secondary">Rafraîchir</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-3">
|
|
||||||
<div class="text-muted small">
|
|
||||||
<span id="users-count">0</span> utilisateur(s) ont soumis leurs choix
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="all-picks-container">
|
<div id="scores-container">
|
||||||
<div class="text-center py-4 text-muted">Chargement des choix…</div>
|
<div class="text-center py-3 text-muted small">Les scores seront calculés après avoir marqué les gagnants.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Winners & Scores Tab (Admin only) -->
|
<!-- Gagnants Tab (Admin only) -->
|
||||||
<div class="tab-pane fade" id="pane-winners" role="tabpanel" aria-labelledby="tab-winners" tabindex="0">
|
<div class="tab-pane fade" id="pane-winners" role="tabpanel" aria-labelledby="tab-winners" tabindex="0">
|
||||||
<div class="card shadow-sm mb-4">
|
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
|
||||||
<div class="card-body">
|
<div>
|
||||||
<div class="d-flex flex-column flex-md-row align-items-start align-items-md-center justify-content-between gap-2">
|
<span class="fw-semibold">Année: <span id="winners-year">—</span></span>
|
||||||
<div>
|
<span class="text-muted small ms-2"><span id="winners-progress">0 / 0</span> gagnants marqués</span>
|
||||||
<div class="fw-semibold">Année: <span id="winners-year">—</span></div>
|
</div>
|
||||||
<div class="text-muted small">Marquez les gagnants et calculez les scores</div>
|
<div class="d-flex gap-2">
|
||||||
</div>
|
<button type="button" id="calculate-scores" class="btn btn-sm btn-success">Calculer</button>
|
||||||
<div class="d-flex gap-2">
|
<button type="button" id="refresh-winners" class="btn btn-sm btn-outline-secondary">Rafraîchir</button>
|
||||||
<button type="button" id="calculate-scores" class="btn btn-success">Calculer les scores</button>
|
|
||||||
<button type="button" id="refresh-winners" class="btn btn-outline-secondary">Rafraîchir</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mt-3">
|
|
||||||
<div class="text-muted small">
|
|
||||||
<span id="winners-progress">0 / 0</span> catégories avec gagnants marqués
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="winners-container">
|
<div id="winners-container">
|
||||||
<div class="text-center py-4 text-muted">Chargement des catégories…</div>
|
<div class="text-center py-3 text-muted small">Chargement des catégories…</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Scores Table -->
|
|
||||||
<div class="card shadow-sm mt-4">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="mb-0">Scores des utilisateurs</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div id="scores-container">
|
|
||||||
<div class="text-center py-4 text-muted">Les scores seront calculés après avoir marqué les gagnants.</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-1
@@ -127,7 +127,7 @@
|
|||||||
<a href="movies.html" class="nav-link btn btn-outline-light me-2">Films</a>
|
<a href="movies.html" class="nav-link btn btn-outline-light me-2">Films</a>
|
||||||
<a href="checklist.html" class="nav-link btn btn-outline-light me-2">Checklist</a>
|
<a href="checklist.html" class="nav-link btn btn-outline-light me-2">Checklist</a>
|
||||||
<a href="user-stat.html" class="nav-link btn btn-outline-light me-2">Statistiques</a>
|
<a href="user-stat.html" class="nav-link btn btn-outline-light me-2">Statistiques</a>
|
||||||
<a href="picks.html" class="nav-link btn btn-outline-light me-2">Mes choix</a>
|
<a href="picks.html" class="nav-link btn btn-outline-light me-2" id="nav-link-picks" style="display: none;">Mes picks</a>
|
||||||
<a id="admin-control-link" href="control.html" class="nav-link btn btn-warning me-2 d-none">Admin</a>
|
<a id="admin-control-link" href="control.html" class="nav-link btn btn-warning me-2 d-none">Admin</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex ms-auto mt-2 mt-lg-0 align-items-center gap-2">
|
<div class="d-flex ms-auto mt-2 mt-lg-0 align-items-center gap-2">
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
<a href="movies.html" class="nav-link btn btn-outline-light me-2">Films</a>
|
<a href="movies.html" class="nav-link btn btn-outline-light me-2">Films</a>
|
||||||
<a href="checklist.html" class="nav-link btn btn-outline-light me-2">Checklist</a>
|
<a href="checklist.html" class="nav-link btn btn-outline-light me-2">Checklist</a>
|
||||||
<a href="user-stat.html" class="nav-link btn btn-outline-light me-2">Statistiques</a>
|
<a href="user-stat.html" class="nav-link btn btn-outline-light me-2">Statistiques</a>
|
||||||
<a href="picks.html" class="nav-link btn btn-outline-light me-2">Mes choix</a>
|
<a href="picks.html" class="nav-link btn btn-outline-light me-2" id="nav-link-picks" style="display: none;">Mes picks</a>
|
||||||
<a id="admin-control-link" href="control.html" class="nav-link btn btn-warning me-2 d-none">Admin</a>
|
<a id="admin-control-link" href="control.html" class="nav-link btn btn-warning me-2 d-none">Admin</a>
|
||||||
</div>
|
</div>
|
||||||
<!-- Move user info inside collapsible navbar and align it properly -->
|
<!-- Move user info inside collapsible navbar and align it properly -->
|
||||||
@@ -112,7 +112,7 @@
|
|||||||
<th scope="col">#</th>
|
<th scope="col">#</th>
|
||||||
<th scope="col">Nom</th>
|
<th scope="col">Nom</th>
|
||||||
<th scope="col">Films</th>
|
<th scope="col">Films</th>
|
||||||
<th scope="col">Bonnes réponses</th>
|
<th scope="col" id="bon-picks-header">Bon picks</th>
|
||||||
<th scope="col">Points</th>
|
<th scope="col">Points</th>
|
||||||
<th scope="col">Ratio</th>
|
<th scope="col">Ratio</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
<a href="movies.html" class="nav-link btn btn-outline-light me-2">Films</a>
|
<a href="movies.html" class="nav-link btn btn-outline-light me-2">Films</a>
|
||||||
<a href="checklist.html" class="nav-link btn btn-outline-light me-2">Checklist</a>
|
<a href="checklist.html" class="nav-link btn btn-outline-light me-2">Checklist</a>
|
||||||
<a href="user-stat.html" class="nav-link btn btn-outline-light me-2">Statistiques</a>
|
<a href="user-stat.html" class="nav-link btn btn-outline-light me-2">Statistiques</a>
|
||||||
<a href="picks.html" class="nav-link btn btn-outline-light me-2">Mes choix</a>
|
<a href="picks.html" class="nav-link btn btn-outline-light me-2" id="nav-link-picks" style="display: none;">Mes picks</a>
|
||||||
<a href="control.html" class="nav-link btn btn-warning me-2">Admin</a>
|
<a href="control.html" class="nav-link btn btn-warning me-2">Admin</a>
|
||||||
<a href="view-picks.html" class="nav-link btn btn-info me-2 active">Voir Tous les Choix</a>
|
<a href="view-picks.html" class="nav-link btn btn-info me-2 active">Voir Tous les Choix</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-5
@@ -248,13 +248,9 @@ router.post("/calculate-scores", verifyToken, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get scores for all users (admin only)
|
// Get scores for all users (available to all authenticated users)
|
||||||
router.get("/scores", verifyToken, async (req, res) => {
|
router.get("/scores", verifyToken, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
if (!isAdmin(req.user)) {
|
|
||||||
return res.status(403).json({ message: "You do not have admin privileges" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const year = parseOscarYear(req.query.year) || await getOrInitActiveYear();
|
const year = parseOscarYear(req.query.year) || await getOrInitActiveYear();
|
||||||
|
|
||||||
const picks = await OscarPick.find({ year })
|
const picks = await OscarPick.find({ year })
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const OSCAR_DATES_BY_YEAR_KEY = "oscar_date_by_year";
|
|||||||
const APP_VERSION_KEY = "app_version_control";
|
const APP_VERSION_KEY = "app_version_control";
|
||||||
const PLAYER_ADMIN_STATUS_UI_KEY = "player_admin_status_ui";
|
const PLAYER_ADMIN_STATUS_UI_KEY = "player_admin_status_ui";
|
||||||
const POINTS_CONFIG_KEY = "points_config";
|
const POINTS_CONFIG_KEY = "points_config";
|
||||||
|
const VISIBILITY_CONFIG_KEY = "visibility_config";
|
||||||
|
|
||||||
const crypto = require("crypto");
|
const crypto = require("crypto");
|
||||||
|
|
||||||
@@ -36,6 +37,11 @@ const DEFAULT_POINTS_CONFIG = Object.freeze({
|
|||||||
pointsPerCorrectPick: 1,
|
pointsPerCorrectPick: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const DEFAULT_VISIBILITY_CONFIG = Object.freeze({
|
||||||
|
showPicksButton: true,
|
||||||
|
showBonPicksColumn: true,
|
||||||
|
});
|
||||||
|
|
||||||
function parseOscarYear(raw) {
|
function parseOscarYear(raw) {
|
||||||
if (raw === undefined || raw === null || raw === "") return null;
|
if (raw === undefined || raw === null || raw === "") return null;
|
||||||
const n = Number(raw);
|
const n = Number(raw);
|
||||||
@@ -762,6 +768,13 @@ function normalizePointsConfig(rawValue) {
|
|||||||
return { pointsPerMovie, pointsPerCorrectPick };
|
return { pointsPerMovie, pointsPerCorrectPick };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeVisibilityConfig(rawValue) {
|
||||||
|
const v = rawValue && typeof rawValue === "object" && !Array.isArray(rawValue) ? rawValue : {};
|
||||||
|
const showPicksButton = typeof v.showPicksButton === "boolean" ? v.showPicksButton : DEFAULT_VISIBILITY_CONFIG.showPicksButton;
|
||||||
|
const showBonPicksColumn = typeof v.showBonPicksColumn === "boolean" ? v.showBonPicksColumn : DEFAULT_VISIBILITY_CONFIG.showBonPicksColumn;
|
||||||
|
return { showPicksButton, showBonPicksColumn };
|
||||||
|
}
|
||||||
|
|
||||||
// Public: get points configuration
|
// Public: get points configuration
|
||||||
router.get("/points-config", async (req, res) => {
|
router.get("/points-config", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -800,5 +813,43 @@ router.put("/points-config", async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Public: get visibility configuration
|
||||||
|
router.get("/visibility-config", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const existing = await Setting.findOne({ key: VISIBILITY_CONFIG_KEY }).select("value");
|
||||||
|
const normalized = normalizeVisibilityConfig(existing?.value);
|
||||||
|
return res.status(200).json(normalized);
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(200).json(DEFAULT_VISIBILITY_CONFIG);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Admin-only: set visibility configuration
|
||||||
|
router.put("/visibility-config", async (req, res) => {
|
||||||
|
const token = req.headers.authorization?.split(" ")[1];
|
||||||
|
if (!token) return res.status(401).json({ message: "Authentication token is required" });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||||
|
if (!decoded.admin) {
|
||||||
|
return res.status(403).json({ message: "You do not have admin privileges" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = normalizeVisibilityConfig(req.body);
|
||||||
|
const updated = await Setting.findOneAndUpdate(
|
||||||
|
{ key: VISIBILITY_CONFIG_KEY },
|
||||||
|
{ $set: { value: normalized } },
|
||||||
|
{ upsert: true, new: true }
|
||||||
|
).select("value");
|
||||||
|
|
||||||
|
return res.status(200).json(normalizeVisibilityConfig(updated?.value));
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof jwt.JsonWebTokenError) {
|
||||||
|
return res.status(401).json({ message: "Invalid or expired token" });
|
||||||
|
}
|
||||||
|
return res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
||||||
|
|||||||
+26
-3
@@ -7,6 +7,7 @@ const Movie = require("../models/Movie");
|
|||||||
const Setting = require("../models/Setting");
|
const Setting = require("../models/Setting");
|
||||||
const PlaybackProgress = require("../models/PlaybackProgress");
|
const PlaybackProgress = require("../models/PlaybackProgress");
|
||||||
const OscarPick = require("../models/OscarPick");
|
const OscarPick = require("../models/OscarPick");
|
||||||
|
const OscarCategory = require("../models/OscarCategory");
|
||||||
const axios = require("axios");
|
const axios = require("axios");
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@@ -153,16 +154,38 @@ router.get("/stats", verifyToken, async (req, res) => {
|
|||||||
}
|
}
|
||||||
: { pointsPerMovie: 1, pointsPerCorrectPick: 1 };
|
: { pointsPerMovie: 1, pointsPerCorrectPick: 1 };
|
||||||
|
|
||||||
// Get all picks for this year with scores
|
// Get all categories with winners marked for this year
|
||||||
|
const categories = await OscarCategory.find({ year }).lean();
|
||||||
|
const categoryWinners = new Map();
|
||||||
|
categories.forEach(cat => {
|
||||||
|
const winner = cat.nominees.find(n => n.isWinner);
|
||||||
|
if (winner) {
|
||||||
|
categoryWinners.set(cat.categoryNumber, winner.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get all picks for this year and calculate correct picks on-the-fly
|
||||||
let picksByUserId = new Map();
|
let picksByUserId = new Map();
|
||||||
try {
|
try {
|
||||||
const allPicks = await OscarPick.find({ year })
|
const allPicks = await OscarPick.find({ year })
|
||||||
.select("userId score")
|
.select("userId picks")
|
||||||
.lean();
|
.lean();
|
||||||
|
|
||||||
allPicks.forEach(pick => {
|
allPicks.forEach(pick => {
|
||||||
const userId = String(pick.userId);
|
const userId = String(pick.userId);
|
||||||
picksByUserId.set(userId, pick.score !== null && pick.score !== undefined ? pick.score : 0);
|
let correctCount = 0;
|
||||||
|
|
||||||
|
// Calculate correct picks based on current winners
|
||||||
|
if (Array.isArray(pick.picks)) {
|
||||||
|
pick.picks.forEach(p => {
|
||||||
|
const correctWinner = categoryWinners.get(p.categoryNumber);
|
||||||
|
if (correctWinner && p.selectedNominee === correctWinner) {
|
||||||
|
correctCount++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
picksByUserId.set(userId, correctCount);
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error fetching picks for stats:", err);
|
console.error("Error fetching picks for stats:", err);
|
||||||
|
|||||||
Reference in New Issue
Block a user