a17c362c81
Replace unsafe innerHTML template literals with document.createElement() and textContent for all user-conrolled data fields in index.html. Event cards and modal details previously interpolated API responses (event.name, event.title, player.fname, staff.role, etc.) directly into innerHTML without escaping, allowing stored XSS if upstream MyIce data is malicious (CWE-79 / vuln-0002). All dynamic text now flows through textContent or createTextNode(), while static HTML structures (SVG icons, strong labels) remain safe hardcoded innerHTML assignments. Refs vuln-0002
776 lines
40 KiB
HTML
776 lines
40 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>MyIce - Games</title>
|
|
<link rel="stylesheet" href="/style.css">
|
|
<link rel="icon" href="favicon.ico" type="image/x-icon">
|
|
</head>
|
|
|
|
<body class="bg-gradient-to-br from-blue-50 to-indigo-100 min-h-screen">
|
|
<!-- Connected User Panel -->
|
|
<div id="connectedUser" class="fixed top-4 right-4 bg-white bg-opacity-80 backdrop-blur-sm rounded-xl shadow-lg p-4 flex items-center hidden z-50">
|
|
<span class="text-indigo-700 font-medium">Connecté: <span id="userName" class="font-semibold"></span></span>
|
|
<button id="disconnect" class="ml-3 bg-rose-500 hover:bg-rose-600 text-white text-sm px-3 py-1 rounded-lg transition duration-200">Déco</button>
|
|
</div>
|
|
|
|
<!-- Login View -->
|
|
<div class="min-h-screen flex items-center justify-center p-4">
|
|
<div class="text-center">
|
|
<h1 id="loginTitle" class="text-4xl md:text-5xl font-bold text-indigo-800 mb-8">MyIce - Games</h1>
|
|
|
|
<div id="loginContainer" class="mb-6">
|
|
<div id="oidcLoginSection">
|
|
<button id="oidcLoginBtn" class="bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-semibold text-lg px-8 py-4 rounded-2xl shadow-xl transform hover:scale-105 transition-all duration-300">
|
|
Se connecter
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-8 max-w-md mx-auto">
|
|
<p class="text-gray-600 text-sm">
|
|
Connectez-vous pour accéder aux événements et informations de votre équipe
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Content -->
|
|
<div id="mainContent" class="container mx-auto px-4 py-6 hidden">
|
|
<!-- Filters Section -->
|
|
<div id="eventFilters" class="bg-white rounded-2xl shadow-lg p-6 mb-8 hidden">
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<div>
|
|
<label for="account" class="block text-sm font-medium text-gray-700 mb-1">Compte</label>
|
|
<select id="account" class="w-full rounded-lg border border-gray-300 bg-white py-2 px-3 text-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-300">
|
|
<option value="default">Défaut</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="agegroup" class="block text-sm font-medium text-gray-700 mb-1">Âge</label>
|
|
<select id="agegroup" class="w-full rounded-lg border border-gray-300 bg-white py-2 px-3 text-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-300">
|
|
<option value="">Tous</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="subgroup" class="block text-sm font-medium text-gray-700 mb-1">Sous-groupe</label>
|
|
<select id="subgroup" class="w-full rounded-lg border border-gray-300 bg-white py-2 px-3 text-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-300">
|
|
<option value="">Tous</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="flex items-end">
|
|
<button id="fetchEvents" class="w-full bg-indigo-500 hover:bg-indigo-600 text-white font-medium py-2 px-4 rounded-lg transition duration-200">
|
|
Charger
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading Indicator -->
|
|
<div id="loadingIndicator" class="text-center py-12 hidden">
|
|
<div class="inline-block animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-indigo-500 mb-4"></div>
|
|
<p class="text-indigo-700 font-medium">Chargement des événements...</p>
|
|
</div>
|
|
|
|
<!-- Events List -->
|
|
<div id="eventList" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"></div>
|
|
</div>
|
|
|
|
<!-- Event Details Modal -->
|
|
<div id="eventDetailsModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50 hidden">
|
|
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
|
<div class="border-b border-gray-200 p-6 flex justify-between items-center">
|
|
<h5 id="eventDetailsLabel" class="text-2xl font-bold text-gray-800">Détails de l'événement</h5>
|
|
<button id="closeModal" class="text-gray-500 hover:text-gray-700">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div id="eventDetailsContent" class="p-6">
|
|
Chargement...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
const loginContainer = document.getElementById("loginContainer");
|
|
const oidcLoginSection = document.getElementById("oidcLoginSection");
|
|
const connectedUser = document.getElementById("connectedUser");
|
|
const eventFilters = document.getElementById("eventFilters");
|
|
const accountSelect = document.getElementById("account");
|
|
const agegroupSelect = document.getElementById("agegroup");
|
|
const subgroupSelect = document.getElementById("subgroup");
|
|
const eventList = document.getElementById("eventList");
|
|
const fetchButton = document.getElementById("fetchEvents");
|
|
const eventDetailsContent = document.getElementById("eventDetailsContent");
|
|
const eventDetailsModal = document.getElementById("eventDetailsModal");
|
|
const closeModal = document.getElementById("closeModal");
|
|
const apiBaseUrl = window.location.origin;
|
|
|
|
let storedApiKey = localStorage.getItem("apikey");
|
|
let lastFetchedEvents = [];
|
|
let storedAccount = localStorage.getItem("account") || "default";
|
|
let userInfo = JSON.parse(localStorage.getItem("userInfo") || "null");
|
|
|
|
// Close modal when close button is clicked
|
|
closeModal.addEventListener("click", () => {
|
|
eventDetailsModal.classList.add("hidden");
|
|
});
|
|
|
|
// Close modal when clicking outside the content
|
|
eventDetailsModal.addEventListener("click", (e) => {
|
|
if (e.target === eventDetailsModal) {
|
|
eventDetailsModal.classList.add("hidden");
|
|
}
|
|
});
|
|
|
|
// Close modal when pressing ESC key
|
|
document.addEventListener("keydown", (e) => {
|
|
if (e.key === "Escape" && !eventDetailsModal.classList.contains("hidden")) {
|
|
eventDetailsModal.classList.add("hidden");
|
|
}
|
|
});
|
|
|
|
// If we have an API key but no userInfo, fetch it from the server
|
|
if (storedApiKey && !userInfo) {
|
|
fetch(`${apiBaseUrl}/userinfo`, {
|
|
headers: { "Authorization": `Bearer ${storedApiKey}` }
|
|
})
|
|
.then(response => response.json())
|
|
.then(userData => {
|
|
userInfo = { email: userData.email || "Utilisateur connecté" };
|
|
localStorage.setItem("userInfo", JSON.stringify(userInfo));
|
|
renderLoginSection();
|
|
})
|
|
.catch(e => {
|
|
console.error("Error fetching user info:", e);
|
|
// If we can't fetch user info, proceed with rendering
|
|
renderLoginSection();
|
|
});
|
|
} else {
|
|
// Render the login section normally
|
|
renderLoginSection();
|
|
}
|
|
|
|
// Handle the static "abc" key case - removed for security
|
|
/*
|
|
if (storedApiKey === "abc" && !userInfo) {
|
|
userInfo = {email: "utilisateur@example.com"};
|
|
localStorage.setItem("userInfo", JSON.stringify(userInfo));
|
|
}
|
|
*/
|
|
|
|
// Handle OIDC callback
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const error = urlParams.get('error');
|
|
const hashParams = new URLSearchParams(window.location.hash.substring(1));
|
|
const accessToken = hashParams.get('access_token');
|
|
|
|
// Display error message if present
|
|
if (error) {
|
|
alert("Erreur d'authentification: " + decodeURIComponent(error));
|
|
// Remove error from URL
|
|
window.history.replaceState({}, document.title, "/");
|
|
}
|
|
|
|
// Handle access token in URL fragment
|
|
if (accessToken) {
|
|
// Store the access token
|
|
localStorage.setItem("apikey", accessToken);
|
|
storedApiKey = accessToken;
|
|
|
|
// Get user info from the userinfo endpoint
|
|
fetch(`${apiBaseUrl}/userinfo`, {
|
|
headers: { "Authorization": `Bearer ${accessToken}` }
|
|
})
|
|
.then(response => response.json())
|
|
.then(userData => {
|
|
userInfo = { email: userData.email || "Utilisateur connecté" };
|
|
localStorage.setItem("userInfo", JSON.stringify(userInfo));
|
|
|
|
// Update UI
|
|
renderLoginSection();
|
|
})
|
|
.catch(e => {
|
|
console.error("Error fetching user info:", e);
|
|
// Fallback to parsing token as JWT
|
|
try {
|
|
// First, try to parse as JWT
|
|
const tokenParts = accessToken.split('.');
|
|
let userData = {};
|
|
|
|
if (tokenParts.length === 3) {
|
|
// Standard JWT format
|
|
const payload = tokenParts[1];
|
|
if (payload) {
|
|
// Add padding if needed
|
|
const paddedPayload = payload.padEnd(payload.length + (4 - payload.length % 4) % 4, '=');
|
|
const decodedPayload = atob(paddedPayload);
|
|
userData = JSON.parse(decodedPayload);
|
|
}
|
|
} else {
|
|
// Non-JWT token, treat as opaque token
|
|
console.log("Non-JWT token received, using default user info");
|
|
userData = { email: "utilisateur@" + window.location.hostname };
|
|
}
|
|
|
|
userInfo = { email: userData.email || "Utilisateur connecté" };
|
|
localStorage.setItem("userInfo", JSON.stringify(userInfo));
|
|
} catch (parseError) {
|
|
console.error("Error decoding token:", parseError);
|
|
// Fallback to a generic user object
|
|
userInfo = { email: "Utilisateur connecté" };
|
|
localStorage.setItem("userInfo", JSON.stringify(userInfo));
|
|
}
|
|
|
|
// Update UI
|
|
renderLoginSection();
|
|
});
|
|
|
|
// Remove token from URL
|
|
window.history.replaceState({}, document.title, "/");
|
|
}
|
|
|
|
function renderLoginSection() {
|
|
const mainContent = document.getElementById("mainContent");
|
|
const loginView = document.querySelector(".min-h-screen.flex");
|
|
const connectedUser = document.getElementById("connectedUser");
|
|
const loginTitle = document.getElementById("loginTitle");
|
|
|
|
if (storedApiKey || userInfo) {
|
|
// User is logged in
|
|
loginView.classList.add("hidden");
|
|
mainContent.classList.remove("hidden");
|
|
loginTitle.classList.add("hidden");
|
|
|
|
connectedUser.classList.remove("hidden");
|
|
oidcLoginSection.classList.add("hidden");
|
|
|
|
const userNameElement = document.getElementById("userName");
|
|
if (userInfo && userInfo.email) {
|
|
userNameElement.textContent = userInfo.email;
|
|
} else {
|
|
userNameElement.textContent = "Utilisateur";
|
|
}
|
|
|
|
eventFilters.classList.remove("hidden");
|
|
updateAccountOptionsAndLoadEvents();
|
|
} else {
|
|
// User is not logged in
|
|
loginView.classList.remove("hidden");
|
|
mainContent.classList.add("hidden");
|
|
loginTitle.classList.remove("hidden");
|
|
|
|
connectedUser.classList.add("hidden");
|
|
oidcLoginSection.classList.remove("hidden");
|
|
eventFilters.classList.add("hidden");
|
|
|
|
// Remove any existing event listeners to prevent duplicates
|
|
const oidcLoginBtn = document.getElementById("oidcLoginBtn");
|
|
const newOidcLoginBtn = oidcLoginBtn.cloneNode(true);
|
|
oidcLoginBtn.parentNode.replaceChild(newOidcLoginBtn, oidcLoginBtn);
|
|
|
|
newOidcLoginBtn.addEventListener("click", initiateOIDCLogin);
|
|
}
|
|
|
|
// Add disconnect handler
|
|
const disconnectBtn = document.getElementById("disconnect");
|
|
if (disconnectBtn) {
|
|
// Remove any existing event listeners to prevent duplicates
|
|
const newDisconnectBtn = disconnectBtn.cloneNode(true);
|
|
disconnectBtn.parentNode.replaceChild(newDisconnectBtn, disconnectBtn);
|
|
newDisconnectBtn.addEventListener("click", logout);
|
|
}
|
|
}
|
|
|
|
function initiateOIDCLogin() {
|
|
// Redirect to backend login endpoint
|
|
window.location.href = `${apiBaseUrl}/login`;
|
|
}
|
|
|
|
function logout() {
|
|
localStorage.removeItem("apikey");
|
|
localStorage.removeItem("account");
|
|
localStorage.removeItem("userInfo");
|
|
storedApiKey = null;
|
|
userInfo = null;
|
|
location.reload();
|
|
}
|
|
|
|
function updateAccountOptionsAndLoadEvents() {
|
|
// Fetch available accounts from the server
|
|
fetch(`${apiBaseUrl}/accounts`, {
|
|
headers: { "Authorization": `Bearer ${storedApiKey}` }
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
return response.json().then(errorData => {
|
|
console.error("Accounts error response:", errorData);
|
|
throw new Error(`HTTP error! status: ${response.status}, message: ${errorData.detail || 'Unknown error'}`);
|
|
});
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(accounts => {
|
|
// Check if accounts is actually an array
|
|
if (!Array.isArray(accounts)) {
|
|
console.error("Accounts data is not an array:", accounts);
|
|
throw new Error("Invalid accounts data format");
|
|
}
|
|
|
|
accountSelect.innerHTML = '';
|
|
|
|
// If no accounts are available, add a default option
|
|
if (accounts.length === 0) {
|
|
const option = document.createElement("option");
|
|
option.value = "default";
|
|
option.textContent = "Default";
|
|
accountSelect.appendChild(option);
|
|
return;
|
|
}
|
|
|
|
// Add all available accounts
|
|
accounts.forEach(account => {
|
|
const option = document.createElement("option");
|
|
option.value = account.name;
|
|
option.textContent = account.label;
|
|
accountSelect.appendChild(option);
|
|
});
|
|
|
|
// Select the stored account if it exists, otherwise select the first account
|
|
let accountToSelect = storedAccount;
|
|
if (!accounts.some(account => account.name === storedAccount)) {
|
|
accountToSelect = accounts[0].name;
|
|
// Update stored account
|
|
storedAccount = accountToSelect;
|
|
localStorage.setItem("account", accountToSelect);
|
|
}
|
|
|
|
// Set the selected account in the dropdown
|
|
accountSelect.value = accountToSelect;
|
|
|
|
// Automatically fetch events for the selected account
|
|
fetchEvents(storedApiKey, accountToSelect);
|
|
})
|
|
.catch(error => {
|
|
console.error("Erreur lors du chargement des comptes:", error);
|
|
alert("Accès refusé. Vous n'êtes pas autorisé à accéder à cette application. Veuillez contacter l'administrateur.");
|
|
logout();
|
|
});
|
|
}
|
|
|
|
renderLoginSection();
|
|
|
|
accountSelect.addEventListener("change", () => {
|
|
const selectedAccount = accountSelect.value;
|
|
localStorage.setItem("account", selectedAccount);
|
|
if (storedApiKey) {
|
|
fetchEvents(storedApiKey, selectedAccount);
|
|
}
|
|
});
|
|
|
|
fetchButton.addEventListener("click", () => {
|
|
if (!storedApiKey) {
|
|
alert("Veuillez vous connecter");
|
|
return;
|
|
}
|
|
const selectedAccount = accountSelect.value;
|
|
fetchEvents(storedApiKey, selectedAccount);
|
|
});
|
|
|
|
agegroupSelect.addEventListener("change", () => {
|
|
// Update subgroup options based on selected agegroup
|
|
updateSubgroupOptions(agegroupSelect.value, lastFetchedEvents);
|
|
displayEvents(lastFetchedEvents);
|
|
});
|
|
|
|
subgroupSelect.addEventListener("change", () => {
|
|
displayEvents(lastFetchedEvents);
|
|
});
|
|
|
|
function fetchEvents(apiKey, account) {
|
|
// Show loading indicator
|
|
const loadingIndicator = document.getElementById("loadingIndicator");
|
|
const eventList = document.getElementById("eventList");
|
|
loadingIndicator.classList.remove("hidden");
|
|
eventList.innerHTML = ""; // Clear previous events
|
|
|
|
fetch(`${apiBaseUrl}/schedule?account=${account}`, {
|
|
headers: { "Authorization": `Bearer ${apiKey}` }
|
|
})
|
|
.then(response => {
|
|
if (response.status === 401) {
|
|
// Token expired or invalid
|
|
alert("Votre session a expiré. Veuillez vous reconnecter.");
|
|
logout();
|
|
return;
|
|
}
|
|
if (!response.ok) {
|
|
// Try to parse error response as JSON, but handle plain text as well
|
|
return response.text().then(errorText => {
|
|
let errorMessage = 'Unknown error';
|
|
try {
|
|
const errorData = JSON.parse(errorText);
|
|
errorMessage = errorData.detail || errorData.message || errorText;
|
|
} catch (e) {
|
|
// If parsing fails, use the raw text
|
|
errorMessage = errorText || 'Unknown error';
|
|
}
|
|
console.error("Schedule error response:", errorText);
|
|
throw new Error(`HTTP error! status: ${response.status}, message: ${errorMessage}`);
|
|
});
|
|
}
|
|
return response.text().then(text => {
|
|
try {
|
|
return JSON.parse(text);
|
|
} catch (e) {
|
|
console.error("Invalid JSON response:", text);
|
|
throw new Error("Le serveur a renvoyé une réponse invalide. Veuillez réessayer.");
|
|
}
|
|
});
|
|
})
|
|
.then(data => {
|
|
// Hide loading indicator
|
|
loadingIndicator.classList.add("hidden");
|
|
|
|
if (data) {
|
|
// Check if data is an array
|
|
if (!Array.isArray(data)) {
|
|
console.error("Schedule data is not an array:", data);
|
|
throw new Error("Invalid schedule data format");
|
|
}
|
|
|
|
lastFetchedEvents = data.filter(event => event.event === "Jeu");
|
|
updateAgeGroupOptions(lastFetchedEvents);
|
|
displayEvents(lastFetchedEvents);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
// Hide loading indicator on error
|
|
loadingIndicator.classList.add("hidden");
|
|
|
|
console.error("Erreur lors du chargement des événements:", error);
|
|
alert("Erreur lors du chargement des événements: " + error.message);
|
|
});
|
|
}
|
|
|
|
function updateAgeGroupOptions(events) {
|
|
let agegroups = new Set(events.map(event => event.agegroup));
|
|
agegroupSelect.innerHTML = '<option value="">Tous</option>';
|
|
Array.from(agegroups).sort().forEach(group => {
|
|
const option = document.createElement("option");
|
|
option.value = group;
|
|
option.textContent = group;
|
|
agegroupSelect.appendChild(option);
|
|
});
|
|
|
|
// Reset subgroup selector
|
|
subgroupSelect.innerHTML = '<option value="">Tous</option>';
|
|
}
|
|
|
|
function extractSubgroup(name) {
|
|
// Extract subgroup from name by removing opponent part after " - "
|
|
if (!name) return "";
|
|
const parts = name.split(" - ");
|
|
if (parts.length > 1) {
|
|
parts.pop(); // Remove opponent
|
|
return parts.join(" - ");
|
|
}
|
|
return name;
|
|
}
|
|
|
|
function updateSubgroupOptions(selectedAgegroup, events) {
|
|
// Reset subgroup options
|
|
subgroupSelect.innerHTML = '<option value="">Tous</option>';
|
|
|
|
if (selectedAgegroup === "") {
|
|
// If no agegroup is selected, disable subgroup selector
|
|
subgroupSelect.disabled = true;
|
|
return;
|
|
}
|
|
|
|
// Enable subgroup selector
|
|
subgroupSelect.disabled = false;
|
|
|
|
// Extract subgroups from events matching the selected agegroup
|
|
let subgroups = new Set();
|
|
events
|
|
.filter(event => event.agegroup === selectedAgegroup)
|
|
.forEach(event => {
|
|
// Extract subgroup from event.name by removing opponent
|
|
if (event.name && event.name !== selectedAgegroup) {
|
|
subgroups.add(extractSubgroup(event.name));
|
|
}
|
|
});
|
|
|
|
// Add subgroups to the selector
|
|
Array.from(subgroups).sort().forEach(subgroup => {
|
|
const option = document.createElement("option");
|
|
option.value = subgroup;
|
|
option.textContent = subgroup;
|
|
subgroupSelect.appendChild(option);
|
|
});
|
|
}
|
|
|
|
function displayEvents(events) {
|
|
eventList.innerHTML = "";
|
|
let selectedAgegroup = agegroupSelect.value;
|
|
let selectedSubgroup = subgroupSelect.value;
|
|
let filteredEvents = events.filter(event => {
|
|
// Filter by event type
|
|
if (event.event !== "Jeu") return false;
|
|
|
|
// Filter by agegroup
|
|
if (selectedAgegroup !== "" && event.agegroup !== selectedAgegroup) return false;
|
|
|
|
// Filter by subgroup
|
|
let eventSubgroup = extractSubgroup(event.name);
|
|
if (selectedSubgroup !== "" && eventSubgroup !== selectedSubgroup) return false;
|
|
|
|
return true;
|
|
});
|
|
|
|
if (filteredEvents.length === 0) {
|
|
eventList.innerHTML = "<p class='text-gray-500 text-center py-8'>Aucun événement 'Jeu' trouvé.</p>";
|
|
return;
|
|
}
|
|
|
|
filteredEvents.forEach(event => {
|
|
const eventCard = document.createElement("div");
|
|
eventCard.className = "bg-white rounded-2xl shadow-lg overflow-hidden transform hover:scale-105 transition-all duration-300 cursor-pointer";
|
|
|
|
const card = document.createElement("div");
|
|
card.className = "p-6 border-l-4";
|
|
card.style.borderColor = event.color || "#818cf8";
|
|
|
|
const inner = document.createElement("div");
|
|
inner.className = "flex justify-between items-start";
|
|
|
|
const innerDiv = document.createElement("div");
|
|
const h3 = document.createElement("h3");
|
|
h3.className = "text-xl font-bold text-gray-800";
|
|
h3.textContent = event.agegroup + " - " + event.name;
|
|
const subtitle = document.createElement("p");
|
|
subtitle.className = "text-gray-600 mt-1";
|
|
subtitle.textContent = event.title;
|
|
innerDiv.appendChild(h3);
|
|
innerDiv.appendChild(subtitle);
|
|
inner.appendChild(innerDiv);
|
|
card.appendChild(inner);
|
|
|
|
const detailsDiv = document.createElement("div");
|
|
detailsDiv.className = "mt-4 space-y-2";
|
|
|
|
const p1 = document.createElement("p");
|
|
p1.className = "flex items-center text-gray-700";
|
|
p1.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" /></svg><strong>Adversaire:</strong> ';
|
|
p1.appendChild(document.createTextNode(event.opponent));
|
|
detailsDiv.appendChild(p1);
|
|
|
|
const p2 = document.createElement("p");
|
|
p2.className = "flex items-center text-gray-700";
|
|
p2.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" /></svg><strong>Lieu:</strong> ';
|
|
p2.appendChild(document.createTextNode(event.place));
|
|
detailsDiv.appendChild(p2);
|
|
|
|
const p3 = document.createElement("p");
|
|
p3.className = "flex items-center text-gray-700";
|
|
p3.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /></svg><strong>Heure:</strong> ';
|
|
p3.appendChild(document.createTextNode(event.start + " - " + event.end));
|
|
detailsDiv.appendChild(p3);
|
|
|
|
card.appendChild(detailsDiv);
|
|
eventCard.appendChild(card);
|
|
eventCard.addEventListener("click", () => fetchEventDetails(event.id_event));
|
|
eventList.appendChild(eventCard);
|
|
});
|
|
}
|
|
|
|
function fetchEventDetails(eventId) {
|
|
const selectedAccount = accountSelect.value;
|
|
fetch(`${apiBaseUrl}/game/${eventId}?account=${selectedAccount}`, {
|
|
headers: { "Authorization": `Bearer ${storedApiKey}` }
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const availablePlayers = data.convocation.available || [];
|
|
const sortedPlayers = availablePlayers
|
|
.sort((a, b) => (a.number || 0) - (b.number || 0));
|
|
|
|
const totalPlayers = sortedPlayers.length;
|
|
const positionCount = {};
|
|
sortedPlayers.forEach(player => {
|
|
const position = player.position || "N/A";
|
|
positionCount[position] = (positionCount[position] || 0) + 1;
|
|
});
|
|
|
|
const positionBreakdown = Object.entries(positionCount)
|
|
.map(([position, count]) => position + ": " + count)
|
|
.join(", ");
|
|
|
|
const playersByPosition = [...sortedPlayers].sort((a, b) => {
|
|
const positionA = a.position || "ZZZ";
|
|
const positionB = b.position || "ZZZ";
|
|
if (positionA !== positionB) {
|
|
return positionA.localeCompare(positionB);
|
|
}
|
|
const numA = parseInt(a.number) || 0;
|
|
const numB = parseInt(b.number) || 0;
|
|
return numA - numB;
|
|
});
|
|
|
|
const staffList = data.convocation.staff || [];
|
|
const totalStaff = staffList.length;
|
|
|
|
eventDetailsContent.innerHTML = "";
|
|
|
|
if (totalPlayers === 0 && totalStaff === 0) {
|
|
const wrapper = document.createElement("div");
|
|
wrapper.className = "rounded-xl border border-amber-300 bg-amber-50 p-6";
|
|
const centerDiv = document.createElement("div");
|
|
centerDiv.className = "text-center";
|
|
const h5 = document.createElement("h5");
|
|
h5.className = "text-2xl font-bold text-gray-800 mb-4";
|
|
h5.textContent = data.title;
|
|
centerDiv.appendChild(h5);
|
|
|
|
const infoDiv = document.createElement("div");
|
|
infoDiv.className = "space-y-2 text-gray-700 mb-6";
|
|
const pType = document.createElement("p");
|
|
pType.innerHTML = "<strong>Type:</strong> ";
|
|
pType.appendChild(document.createTextNode(data.type));
|
|
infoDiv.appendChild(pType);
|
|
const pLieu = document.createElement("p");
|
|
pLieu.innerHTML = "<strong>Lieu:</strong> ";
|
|
pLieu.appendChild(document.createTextNode(data.place));
|
|
infoDiv.appendChild(pLieu);
|
|
const pHeure = document.createElement("p");
|
|
pHeure.innerHTML = "<strong>Heure:</strong> ";
|
|
pHeure.appendChild(document.createTextNode(data.time_start + " - " + data.time_end));
|
|
infoDiv.appendChild(pHeure);
|
|
centerDiv.appendChild(infoDiv);
|
|
|
|
const msgDiv = document.createElement("div");
|
|
msgDiv.className = "bg-amber-100 border border-amber-300 text-amber-800 px-4 py-3 rounded-lg";
|
|
const msgH6 = document.createElement("h6");
|
|
msgH6.className = "font-bold text-lg mb-1";
|
|
msgH6.textContent = "Aucun joueur ni personnel convoqué";
|
|
msgDiv.appendChild(msgH6);
|
|
const msgP = document.createElement("p");
|
|
msgP.textContent = "Il n'y a actuellement aucun joueur ni personnel convoqué pour ce match.";
|
|
msgDiv.appendChild(msgP);
|
|
centerDiv.appendChild(msgDiv);
|
|
wrapper.appendChild(centerDiv);
|
|
eventDetailsContent.appendChild(wrapper);
|
|
} else {
|
|
const container = document.createElement("div");
|
|
const h5 = document.createElement("h5");
|
|
h5.className = "text-2xl font-bold text-gray-800 mb-4";
|
|
h5.textContent = data.title;
|
|
container.appendChild(h5);
|
|
|
|
const infoDiv = document.createElement("div");
|
|
infoDiv.className = "space-y-2 text-gray-700 mb-6";
|
|
const pType = document.createElement("p");
|
|
pType.innerHTML = "<strong>Type:</strong> ";
|
|
pType.appendChild(document.createTextNode(data.type));
|
|
infoDiv.appendChild(pType);
|
|
const pLieu = document.createElement("p");
|
|
pLieu.innerHTML = "<strong>Lieu:</strong> ";
|
|
pLieu.appendChild(document.createTextNode(data.place));
|
|
infoDiv.appendChild(pLieu);
|
|
const pHeure = document.createElement("p");
|
|
pHeure.innerHTML = "<strong>Heure:</strong> ";
|
|
pHeure.appendChild(document.createTextNode(data.time_start + " - " + data.time_end));
|
|
infoDiv.appendChild(pHeure);
|
|
container.appendChild(infoDiv);
|
|
|
|
if (totalPlayers === 0) {
|
|
const noPlayersDiv = document.createElement("div");
|
|
noPlayersDiv.className = "bg-amber-100 border border-amber-300 text-amber-800 px-4 py-3 rounded-lg";
|
|
const noPlayersH6 = document.createElement("h6");
|
|
noPlayersH6.className = "font-bold mb-1";
|
|
noPlayersH6.textContent = "Aucun joueur convoqué";
|
|
noPlayersDiv.appendChild(noPlayersH6);
|
|
const noPlayersP = document.createElement("p");
|
|
noPlayersP.textContent = "Il n'y a actuellement aucun joueur convoqué pour ce match.";
|
|
noPlayersDiv.appendChild(noPlayersP);
|
|
container.appendChild(noPlayersDiv);
|
|
} else {
|
|
const playersP = document.createElement("p");
|
|
playersP.innerHTML = "<strong>Joueurs convoqués:</strong> ";
|
|
playersP.appendChild(document.createTextNode(totalPlayers + " joueur" + (totalPlayers > 1 ? "s" : "") + " (" + positionBreakdown + ")"));
|
|
infoDiv.appendChild(playersP);
|
|
|
|
const playersDiv = document.createElement("div");
|
|
const playersH6 = document.createElement("h6");
|
|
playersH6.className = "text-xl font-semibold text-gray-800 mb-3";
|
|
playersH6.textContent = "Liste des joueurs:";
|
|
playersDiv.appendChild(playersH6);
|
|
const playersUl = document.createElement("ul");
|
|
playersUl.className = "grid grid-cols-1 md:grid-cols-2 gap-3";
|
|
playersByPosition.forEach(player => {
|
|
const li = document.createElement("li");
|
|
li.className = "bg-indigo-50 rounded-lg p-3";
|
|
const number = player.number ? player.number : "N/A";
|
|
const position = player.position ? player.position : "N/A";
|
|
li.textContent = "[" + position + "] " + number + " - " + player.fname + " " + player.lname + " (" + player.dob + ")";
|
|
playersUl.appendChild(li);
|
|
});
|
|
playersDiv.appendChild(playersUl);
|
|
container.appendChild(playersDiv);
|
|
}
|
|
|
|
if (totalStaff > 0) {
|
|
const staffDiv = document.createElement("div");
|
|
staffDiv.className = "mt-6";
|
|
const staffH6 = document.createElement("h6");
|
|
staffH6.className = "text-xl font-semibold text-gray-800 mb-3";
|
|
staffH6.textContent = "Personnel (" + totalStaff + "):";
|
|
staffDiv.appendChild(staffH6);
|
|
const staffUl = document.createElement("ul");
|
|
staffUl.className = "space-y-2";
|
|
staffList.forEach(staff => {
|
|
const li = document.createElement("li");
|
|
li.className = "flex items-center";
|
|
const roleSpan = document.createElement("span");
|
|
roleSpan.className = "font-medium w-32";
|
|
roleSpan.textContent = staff.role + ":";
|
|
li.appendChild(roleSpan);
|
|
li.appendChild(document.createTextNode(" " + staff.fname + " " + staff.lname));
|
|
staffUl.appendChild(li);
|
|
});
|
|
staffDiv.appendChild(staffUl);
|
|
container.appendChild(staffDiv);
|
|
} else {
|
|
const noStaffDiv = document.createElement("div");
|
|
noStaffDiv.className = "mt-6 bg-blue-50 border border-blue-200 text-blue-800 px-4 py-3 rounded-lg";
|
|
const noStaffH6 = document.createElement("h6");
|
|
noStaffH6.className = "font-bold mb-1";
|
|
noStaffH6.textContent = "Aucun personnel convoqué";
|
|
noStaffDiv.appendChild(noStaffH6);
|
|
const noStaffP = document.createElement("p");
|
|
noStaffP.textContent = "Il n'y a actuellement aucun personnel convoqué pour ce match.";
|
|
noStaffDiv.appendChild(noStaffP);
|
|
container.appendChild(noStaffDiv);
|
|
}
|
|
|
|
eventDetailsContent.appendChild(container);
|
|
}
|
|
eventDetailsModal.classList.remove("hidden");
|
|
})
|
|
.catch(error => console.error("Erreur lors du chargement des détails de l'événement:", error));
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|