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 | |
| // ================================================================== |