Spaces:
Sleeping
Sleeping
// ================================================================== | |
// Main application JavaScript for the frontend | |
// Handles theme switching, sidebar navigation, and content display. | |
// ================================================================== | |
// Wait for the DOM to be fully loaded before executing scripts | |
document.addEventListener('DOMContentLoaded', function() { | |
// 1. Initialize Theme (Dark/Light Mode) | |
initTheme(); | |
setupThemeToggle(); | |
// 2. Setup Sidebar Navigation System | |
setupSidebarNavigation(); | |
// 3. Setup Feedback Form Validation (Basic) | |
setupFeedbackForm(); | |
// NOTE: Old setup functions for direct page selection are removed/commented out | |
// setupSubjectSelection(); // Replaced by sidebar logic | |
// setupCategorySelection(); // Replaced by sidebar logic | |
// setupTextSelection(); // Replaced by sidebar logic | |
}); | |
// ================================================================== | |
// THEME SWITCHING FUNCTIONS | |
// ================================================================== | |
/** | |
* Initializes the theme (dark/light) based on localStorage preference or system default. | |
*/ | |
function initTheme() { | |
// Default to 'light' if no preference is found | |
const userPreference = localStorage.getItem('theme') || 'light'; | |
document.documentElement.setAttribute('data-theme', userPreference); | |
updateThemeIcon(userPreference); // Set the correct icon on load | |
} | |
/** | |
* Sets up the event listener for the theme toggle button. | |
*/ | |
function setupThemeToggle() { | |
const themeToggle = document.getElementById('theme-toggle'); | |
if (!themeToggle) { | |
console.warn("Theme toggle button (#theme-toggle) not found."); | |
return; | |
} | |
themeToggle.addEventListener('click', function() { | |
const currentTheme = document.documentElement.getAttribute('data-theme'); | |
const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; | |
// Apply the new theme | |
document.documentElement.setAttribute('data-theme', newTheme); | |
// Save preference to localStorage | |
localStorage.setItem('theme', newTheme); | |
// Update the button icon | |
updateThemeIcon(newTheme); | |
// Optional: Send theme preference to the server (if needed) | |
// saveThemePreference(newTheme); | |
}); | |
} | |
/** | |
* Updates the icon (sun/moon) inside the theme toggle button. | |
* @param {string} theme - The current theme ('light' or 'dark'). | |
*/ | |
function updateThemeIcon(theme) { | |
const themeToggle = document.getElementById('theme-toggle'); | |
if (!themeToggle) return; // Exit if button not found | |
if (theme === 'dark') { | |
themeToggle.innerHTML = '<i class="fas fa-sun"></i>'; // Show sun icon | |
themeToggle.setAttribute('title', 'Activer le mode clair'); | |
} else { | |
themeToggle.innerHTML = '<i class="fas fa-moon"></i>'; // Show moon icon | |
themeToggle.setAttribute('title', 'Activer le mode sombre'); | |
} | |
} | |
/** | |
* Optional: Sends the chosen theme preference to the server. | |
* Uncomment the call in setupThemeToggle if needed. | |
* @param {string} theme - The theme to save ('light' or 'dark'). | |
*/ | |
function saveThemePreference(theme) { | |
const formData = new FormData(); | |
formData.append('theme', theme); | |
fetch('/set_theme', { // Ensure this endpoint exists in your Flask app | |
method: 'POST', | |
body: formData | |
}) | |
.then(response => { | |
if (!response.ok) { | |
console.error(`Error saving theme: ${response.statusText}`); | |
return response.json().catch(() => ({})); // Try to parse error body | |
} | |
return response.json(); | |
}) | |
.then(data => { | |
console.log('Theme preference saved on server:', data); | |
}) | |
.catch(error => { | |
console.error('Error sending theme preference to server:', error); | |
}); | |
} | |
// ================================================================== | |
// SIDEBAR NAVIGATION FUNCTIONS | |
// ================================================================== | |
/** | |
* Sets up all event listeners and logic for the sidebar navigation. | |
*/ | |
function setupSidebarNavigation() { | |
// Get references to all necessary DOM elements | |
const burgerMenu = document.getElementById('burger-menu'); | |
const sidebarMatieres = document.getElementById('sidebar-matieres'); | |
const sidebarSousCategories = document.getElementById('sidebar-sous-categories'); | |
const sidebarOverlay = document.getElementById('sidebar-overlay'); | |
const matieresList = document.getElementById('matieres-list-sidebar'); | |
const sousCategoriesList = document.getElementById('sous-categories-list-sidebar'); | |
const backButton = document.getElementById('sidebar-back-button'); | |
const closeButtons = document.querySelectorAll('.close-sidebar-btn'); | |
const initialInstructions = document.getElementById('initial-instructions'); | |
const contentSection = document.getElementById('content-section'); | |
// --- Helper Function to Close All Sidebars --- | |
const closeAllSidebars = () => { | |
if (sidebarMatieres) sidebarMatieres.classList.remove('active'); | |
if (sidebarSousCategories) sidebarSousCategories.classList.remove('active'); | |
if (sidebarOverlay) sidebarOverlay.classList.remove('active'); | |
// Optional: Reset scroll position of lists when closing | |
// if (matieresList) matieresList.scrollTop = 0; | |
// if (sousCategoriesList) sousCategoriesList.scrollTop = 0; | |
}; | |
// --- Event Listeners --- | |
// 1. Open Sidebar 1 (Matières) with Burger Menu | |
if (burgerMenu && sidebarMatieres && sidebarOverlay) { | |
burgerMenu.addEventListener('click', (e) => { | |
e.stopPropagation(); // Prevent immediate closing if overlay listener fires | |
closeAllSidebars(); // Ensure only one sidebar is open initially | |
sidebarMatieres.classList.add('active'); | |
sidebarOverlay.classList.add('active'); | |
}); | |
} else { | |
console.warn("Burger menu, matières sidebar, or overlay not found."); | |
} | |
// 2. Close sidebars via Overlay click | |
if (sidebarOverlay) { | |
sidebarOverlay.addEventListener('click', closeAllSidebars); | |
} | |
// 3. Close sidebars via dedicated Close buttons (X) | |
closeButtons.forEach(button => { | |
button.addEventListener('click', closeAllSidebars); | |
}); | |
// 4. Handle click on a Matière in Sidebar 1 | |
if (matieresList && sidebarSousCategories && sidebarMatieres) { | |
matieresList.addEventListener('click', (e) => { | |
// Use closest to handle clicks even if icon is clicked | |
const listItem = e.target.closest('li'); | |
if (listItem) { | |
const matiereId = listItem.getAttribute('data-matiere-id'); | |
const matiereNom = listItem.getAttribute('data-matiere-nom') || 'Inconnu'; // Fallback name | |
if (matiereId) { | |
// Update Sidebar 2 title | |
const titleElement = document.getElementById('sidebar-sous-categories-title'); | |
if (titleElement) { | |
titleElement.textContent = `Sous-catégories (${matiereNom})`; | |
} | |
// Load sous-categories into Sidebar 2's list | |
loadSubCategoriesForSidebar(matiereId, sousCategoriesList); | |
// Switch Sidebars: Hide 1, Show 2 | |
sidebarMatieres.classList.remove('active'); // Slide out sidebar 1 | |
sidebarSousCategories.classList.add('active'); // Slide in sidebar 2 | |
// Keep overlay active | |
if (sidebarOverlay) sidebarOverlay.classList.add('active'); | |
} | |
} | |
}); | |
} else { | |
console.warn("Matières list, sous-catégories sidebar, or matières sidebar not found."); | |
} | |
// 5. Handle click on Back Button in Sidebar 2 | |
if (backButton && sidebarMatieres && sidebarSousCategories) { | |
backButton.addEventListener('click', () => { | |
sidebarSousCategories.classList.remove('active'); // Slide out sidebar 2 | |
sidebarMatieres.classList.add('active'); // Slide in sidebar 1 | |
// Keep overlay active | |
if (sidebarOverlay) sidebarOverlay.classList.add('active'); | |
}); | |
} else { | |
console.warn("Sidebar back button, matières sidebar, or sous-catégories sidebar not found."); | |
} | |
// 6. Handle click on a Sous-catégorie in Sidebar 2 | |
if (sousCategoriesList && initialInstructions && contentSection) { | |
sousCategoriesList.addEventListener('click', (e) => { | |
const listItem = e.target.closest('li'); | |
if (listItem && listItem.getAttribute('data-category-id')) { // Ensure it's a valid category item | |
const categoryId = listItem.getAttribute('data-category-id'); | |
// Load and display the content for the first text in this category | |
loadAndDisplayFirstTexte(categoryId); | |
// Hide initial instructions, show content section | |
if (initialInstructions) initialInstructions.classList.add('d-none'); | |
if (contentSection) contentSection.classList.remove('d-none'); | |
// Close both sidebars and overlay after selection | |
closeAllSidebars(); | |
} | |
}); | |
} else { | |
console.warn("Sous-catégories list, initial instructions, or content section not found."); | |
} | |
} | |
/** | |
* Fetches and loads subcategories for a given matiereId into the specified list element. | |
* @param {string} matiereId - The ID of the selected matière. | |
* @param {HTMLElement} listElement - The UL element to populate. | |
*/ | |
function loadSubCategoriesForSidebar(matiereId, listElement) { | |
if (!listElement) { | |
console.error("Target list element for subcategories is missing."); | |
return; | |
} | |
listElement.innerHTML = '<li>Chargement...</li>'; // Show loading state | |
fetch(`/get_sous_categories/${matiereId}`) // Ensure this endpoint exists in Flask | |
.then(response => { | |
if (!response.ok) { | |
throw new Error(`Erreur HTTP: ${response.status} ${response.statusText}`); | |
} | |
return response.json(); | |
}) | |
.then(data => { | |
listElement.innerHTML = ''; // Clear loading/previous items | |
if (data && data.length > 0) { | |
data.forEach(category => { | |
const item = document.createElement('li'); | |
item.setAttribute('data-category-id', category.id); | |
item.textContent = category.nom; // Use category name | |
// Add chevron icon for visual cue | |
const icon = document.createElement('i'); | |
icon.className = 'fas fa-chevron-right float-end'; | |
item.appendChild(icon); | |
listElement.appendChild(item); | |
}); | |
} else { | |
listElement.innerHTML = '<li>Aucune sous-catégorie trouvée.</li>'; | |
} | |
}) | |
.catch(error => { | |
console.error('Erreur lors du chargement des sous-catégories:', error); | |
listElement.innerHTML = `<li>Erreur: ${error.message}</li>`; | |
}); | |
} | |
/** | |
* Fetches the list of texts for a category and displays the first one found. | |
* @param {string} categoryId - The ID of the selected sous-catégorie. | |
*/ | |
function loadAndDisplayFirstTexte(categoryId) { | |
fetch(`/get_textes/${categoryId}`) // Ensure this endpoint exists and returns a list of texts [{id, titre}, ...] | |
.then(response => { | |
if (!response.ok) { | |
throw new Error(`Erreur HTTP: ${response.status} ${response.statusText}`); | |
} | |
return response.json(); | |
}) | |
.then(data => { | |
if (data && data.length > 0) { | |
const firstTexteId = data[0].id; // Get the ID of the very first text | |
if (firstTexteId) { | |
displayTexte(firstTexteId); // Call displayTexte with this ID | |
} else { | |
throw new Error("Le premier texte reçu n'a pas d'ID."); | |
} | |
} else { | |
// Handle case where a category has no texts associated with it | |
const contentTitle = document.getElementById('content-title'); | |
const contentBlocks = document.getElementById('content-blocks'); | |
const contentSection = document.getElementById('content-section'); | |
if (contentTitle) contentTitle.textContent = "Contenu non disponible"; | |
if (contentBlocks) contentBlocks.innerHTML = '<div class="alert alert-info">Aucun texte trouvé pour cette sous-catégorie.</div>'; | |
if (contentSection) contentSection.classList.remove('d-none'); // Ensure section is visible | |
} | |
}) | |
.catch(error => { | |
console.error('Erreur lors du chargement des textes pour la catégorie:', error); | |
// Display error message in the content area | |
const contentTitle = document.getElementById('content-title'); | |
const contentBlocks = document.getElementById('content-blocks'); | |
const contentSection = document.getElementById('content-section'); | |
if (contentTitle) contentTitle.textContent = "Erreur"; | |
if (contentBlocks) contentBlocks.innerHTML = `<div class="alert alert-danger">Impossible de charger le contenu. ${error.message}</div>`; | |
if (contentSection) contentSection.classList.remove('d-none'); // Ensure section is visible | |
}); | |
} | |
// ================================================================== | |
// CONTENT DISPLAY FUNCTION | |
// ================================================================== | |
/** | |
* Fetches and displays the content (title and blocks) for a specific texteId. | |
* @param {string} texteId - The ID of the text to display. | |
*/ | |
function displayTexte(texteId) { | |
const contentSection = document.getElementById('content-section'); | |
const contentTitle = document.getElementById('content-title'); | |
const contentBlocks = document.getElementById('content-blocks'); | |
// Check if essential elements exist | |
if (!contentSection || !contentTitle || !contentBlocks) { | |
console.error("Éléments d'affichage du contenu (#content-section, #content-title, #content-blocks) introuvables."); | |
alert("Erreur interne: Impossible d'afficher le contenu."); | |
return; | |
} | |
// Indicate loading state visually | |
contentTitle.textContent = "Chargement du contenu..."; | |
contentBlocks.innerHTML = '<div class="text-center p-3"><i class="fas fa-spinner fa-spin fa-2x"></i></div>'; // Simple spinner | |
fetch(`/get_texte/${texteId}`) // Ensure this endpoint exists and returns detailed text object {titre, contenu, blocks, color_code, ...} | |
.then(response => { | |
if (!response.ok) { | |
throw new Error(`Erreur HTTP: ${response.status} ${response.statusText}`); | |
} | |
return response.json(); | |
}) | |
.then(data => { | |
// --- Update Content Title --- | |
contentTitle.textContent = data.titre || "Titre non disponible"; | |
// --- Update Theme/Color Styling --- | |
const dynamicStyleId = 'dynamic-block-styles'; | |
let style = document.getElementById(dynamicStyleId); | |
if (!style) { // Create style tag if it doesn't exist | |
style = document.createElement('style'); | |
style.id = dynamicStyleId; | |
document.head.appendChild(style); | |
} | |
if (data.color_code) { | |
// Apply color to title underline and block border/title | |
contentTitle.style.borderBottomColor = data.color_code; | |
style.textContent = ` | |
#content-section .content-block-title { border-bottom-color: ${data.color_code} !important; } | |
#content-section .content-block { border-left: 4px solid ${data.color_code} !important; } | |
`; | |
} else { | |
// Reset styles if no color code is provided (use CSS defaults) | |
contentTitle.style.borderBottomColor = ''; // Reset specific style | |
style.textContent = ''; // Clear dynamic rules | |
} | |
// --- Render Content Blocks --- | |
contentBlocks.innerHTML = ''; // Clear loading indicator/previous content | |
if (data.blocks && Array.isArray(data.blocks) && data.blocks.length > 0) { | |
// Sort blocks by 'order' property if it exists, otherwise render as received | |
const sortedBlocks = data.blocks.sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); | |
sortedBlocks.forEach(block => { | |
const blockDiv = document.createElement('div'); | |
blockDiv.className = 'content-block fade-in'; // Add animation class | |
// Block with Image | |
if (block.image && block.image.src) { | |
blockDiv.classList.add('block-with-image', `image-${block.image_position || 'left'}`); | |
// Image Container | |
const imageDiv = document.createElement('div'); | |
imageDiv.className = 'block-image-container'; | |
const imageEl = document.createElement('img'); | |
imageEl.className = 'block-image'; | |
imageEl.src = block.image.src; // Ensure backend provides full URL if needed | |
imageEl.alt = block.image.alt || 'Illustration'; | |
imageEl.loading = 'lazy'; // Add lazy loading for images | |
imageDiv.appendChild(imageEl); | |
blockDiv.appendChild(imageDiv); | |
// Content Container (Text part) | |
const contentDiv = document.createElement('div'); | |
contentDiv.className = 'block-content-container'; | |
if (block.title) { | |
const titleEl = document.createElement('h3'); | |
titleEl.className = 'content-block-title'; | |
titleEl.textContent = block.title; | |
contentDiv.appendChild(titleEl); | |
} | |
const contentEl = document.createElement('div'); | |
contentEl.className = 'content-block-content'; | |
// IMPORTANT: Sanitize HTML content if it comes from user input or untrusted sources | |
// For now, assuming safe HTML from backend: | |
contentEl.innerHTML = block.content ? block.content.replace(/\n/g, '<br>') : ''; | |
contentDiv.appendChild(contentEl); | |
blockDiv.appendChild(contentDiv); | |
} else { // Block without Image | |
if (block.title) { | |
const titleEl = document.createElement('h3'); | |
titleEl.className = 'content-block-title'; | |
titleEl.textContent = block.title; | |
blockDiv.appendChild(titleEl); | |
} | |
const contentEl = document.createElement('div'); | |
contentEl.className = 'content-block-content'; | |
// IMPORTANT: Sanitize HTML content | |
contentEl.innerHTML = block.content ? block.content.replace(/\n/g, '<br>') : ''; | |
blockDiv.appendChild(contentEl); | |
} | |
contentBlocks.appendChild(blockDiv); | |
}); | |
} else if (data.contenu) { // Fallback: Use simple 'contenu' field if no blocks | |
const blockDiv = document.createElement('div'); | |
blockDiv.className = 'content-block'; | |
// IMPORTANT: Sanitize HTML content | |
blockDiv.innerHTML = data.contenu.replace(/\n/g, '<br>'); | |
contentBlocks.appendChild(blockDiv); | |
} else { | |
// No blocks and no simple content | |
contentBlocks.innerHTML = '<div class="alert alert-warning">Le contenu de ce texte est vide ou non structuré.</div>'; | |
} | |
// --- Final Steps --- | |
// Ensure the content section is visible | |
contentSection.classList.remove('d-none'); | |
// Scroll to the top of the content title for better UX | |
contentTitle.scrollIntoView({ behavior: 'smooth', block: 'start' }); | |
}) | |
.catch(error => { | |
console.error(`Erreur lors du chargement du texte ID ${texteId}:`, error); | |
// Display error message in the content area | |
contentTitle.textContent = "Erreur de chargement"; | |
contentBlocks.innerHTML = `<div class="alert alert-danger">Impossible de charger le contenu demandé. ${error.message}</div>`; | |
// Ensure section is visible even on error | |
contentSection.classList.remove('d-none'); | |
}); | |
} | |
// ================================================================== | |
// FEEDBACK FORM SETUP | |
// ================================================================== | |
/** | |
* Sets up basic validation for the feedback form. | |
*/ | |
function setupFeedbackForm() { | |
const feedbackForm = document.getElementById('feedback-form'); | |
if (feedbackForm) { | |
feedbackForm.addEventListener('submit', function(e) { | |
const feedbackMessage = document.getElementById('feedback-message'); | |
// Simple check if message textarea is empty or only whitespace | |
if (!feedbackMessage || !feedbackMessage.value.trim()) { | |
e.preventDefault(); // Stop form submission | |
alert('Veuillez entrer un message avant d\'envoyer votre avis.'); | |
if (feedbackMessage) feedbackMessage.focus(); // Focus the textarea | |
} | |
// Add more complex validation here if needed | |
}); | |
} | |
} | |
// ================================================================== | |
// OLD FUNCTIONS (Removed/Commented Out) - Kept for reference only | |
// ================================================================== | |
/* | |
function setupSubjectSelection() { // No longer used directly on page | |
// ... old logic targeting .subject-card elements on the main page ... | |
} | |
function loadSubCategories(matiereId) { // Replaced by loadSubCategoriesForSidebar | |
// ... old logic targeting #sous-categories-list on the main page ... | |
} | |
function setupCategorySelection() { // No longer used directly on page | |
// ... old logic targeting #sous-categorie-select or similar ... | |
} | |
function loadTextes(categoryId) { // Logic integrated into loadAndDisplayFirstTexte | |
// ... old logic targeting #textes-list on the main page ... | |
} | |
function setupTextSelection() { // No longer used directly on page | |
// ... old logic targeting #texte-select or similar ... | |
} | |
*/ | |
// ================================================================== | |
// END OF FILE | |
// ================================================================== |