// ================================================================== // 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 = ''; // Show sun icon themeToggle.setAttribute('title', 'Activer le mode clair'); } else { themeToggle.innerHTML = ''; // 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 = '
  • Chargement...
  • '; // 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 = '
  • Aucune sous-catégorie trouvée.
  • '; } }) .catch(error => { console.error('Erreur lors du chargement des sous-catégories:', error); listElement.innerHTML = `
  • Erreur: ${error.message}
  • `; }); } /** * 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 = '
    Aucun texte trouvé pour cette sous-catégorie.
    '; 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 = `
    Impossible de charger le contenu. ${error.message}
    `; 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 = '
    '; // 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, '
    ') : ''; 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, '
    ') : ''; 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, '
    '); contentBlocks.appendChild(blockDiv); } else { // No blocks and no simple content contentBlocks.innerHTML = '
    Le contenu de ce texte est vide ou non structuré.
    '; } // --- 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 = `
    Impossible de charger le contenu demandé. ${error.message}
    `; // 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 // ==================================================================