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