Spaces:
Running
Running
Update api/static/js/main.js
Browse files- api/static/js/main.js +432 -308
api/static/js/main.js
CHANGED
@@ -1,393 +1,517 @@
|
|
|
|
1 |
// Main application JavaScript for the frontend
|
|
|
|
|
2 |
|
3 |
-
// Wait for the DOM to be loaded before executing
|
4 |
document.addEventListener('DOMContentLoaded', function() {
|
5 |
-
|
|
|
6 |
initTheme();
|
7 |
-
|
8 |
-
// Setup interactive elements
|
9 |
-
setupSubjectSelection();
|
10 |
-
setupCategorySelection();
|
11 |
-
setupTextSelection();
|
12 |
setupThemeToggle();
|
13 |
-
|
14 |
-
// Setup
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
}
|
25 |
});
|
26 |
|
27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
28 |
function initTheme() {
|
|
|
29 |
const userPreference = localStorage.getItem('theme') || 'light';
|
30 |
document.documentElement.setAttribute('data-theme', userPreference);
|
31 |
-
|
32 |
-
// Update theme icon
|
33 |
-
updateThemeIcon(userPreference);
|
34 |
}
|
35 |
|
36 |
-
|
|
|
|
|
37 |
function setupThemeToggle() {
|
38 |
const themeToggle = document.getElementById('theme-toggle');
|
39 |
-
if (!themeToggle)
|
40 |
-
|
|
|
|
|
|
|
41 |
themeToggle.addEventListener('click', function() {
|
42 |
const currentTheme = document.documentElement.getAttribute('data-theme');
|
43 |
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
44 |
-
|
45 |
-
//
|
46 |
document.documentElement.setAttribute('data-theme', newTheme);
|
47 |
-
|
48 |
// Save preference to localStorage
|
49 |
localStorage.setItem('theme', newTheme);
|
50 |
-
|
51 |
-
// Update icon
|
52 |
updateThemeIcon(newTheme);
|
53 |
-
|
54 |
-
// Send theme preference to server
|
55 |
-
saveThemePreference(newTheme);
|
56 |
});
|
57 |
}
|
58 |
|
59 |
-
|
|
|
|
|
|
|
60 |
function updateThemeIcon(theme) {
|
61 |
const themeToggle = document.getElementById('theme-toggle');
|
62 |
-
if (!themeToggle) return;
|
63 |
-
|
64 |
-
// Update icon based on theme
|
65 |
if (theme === 'dark') {
|
66 |
-
themeToggle.innerHTML = '<i class="fas fa-sun"></i>';
|
67 |
themeToggle.setAttribute('title', 'Activer le mode clair');
|
68 |
} else {
|
69 |
-
themeToggle.innerHTML = '<i class="fas fa-moon"></i>';
|
70 |
themeToggle.setAttribute('title', 'Activer le mode sombre');
|
71 |
}
|
72 |
}
|
73 |
|
74 |
-
|
|
|
|
|
|
|
|
|
75 |
function saveThemePreference(theme) {
|
76 |
const formData = new FormData();
|
77 |
formData.append('theme', theme);
|
78 |
-
|
79 |
-
fetch('/set_theme', {
|
80 |
method: 'POST',
|
81 |
body: formData
|
82 |
})
|
83 |
-
.then(response =>
|
|
|
|
|
|
|
|
|
|
|
|
|
84 |
.then(data => {
|
85 |
-
console.log('Theme preference saved:', data);
|
86 |
})
|
87 |
.catch(error => {
|
88 |
-
console.error('Error
|
89 |
});
|
90 |
}
|
91 |
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
122 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
123 |
});
|
124 |
-
|
125 |
-
// Handle
|
126 |
-
if (
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
if (
|
135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
136 |
}
|
137 |
}
|
138 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
139 |
}
|
140 |
}
|
141 |
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
146 |
.then(data => {
|
147 |
-
//
|
148 |
-
|
149 |
-
if (sousCategoriesList) {
|
150 |
-
sousCategoriesList.innerHTML = '';
|
151 |
-
|
152 |
data.forEach(category => {
|
153 |
const item = document.createElement('li');
|
154 |
-
item.className = 'selection-item';
|
155 |
item.setAttribute('data-category-id', category.id);
|
156 |
-
item.textContent = category.nom;
|
157 |
-
|
158 |
-
// Add
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
const items = sousCategoriesList.querySelectorAll('.selection-item');
|
165 |
-
items.forEach(i => i.classList.remove('active'));
|
166 |
-
this.classList.add('active');
|
167 |
-
|
168 |
-
// Show the texts section
|
169 |
-
const textesSection = document.getElementById('textes-section');
|
170 |
-
if (textesSection) {
|
171 |
-
textesSection.classList.remove('d-none');
|
172 |
-
}
|
173 |
-
});
|
174 |
-
|
175 |
-
sousCategoriesList.appendChild(item);
|
176 |
});
|
177 |
-
|
178 |
-
|
179 |
-
const sousCategoriesSection = document.getElementById('sous-categories-section');
|
180 |
-
if (sousCategoriesSection) {
|
181 |
-
sousCategoriesSection.classList.remove('d-none');
|
182 |
-
}
|
183 |
}
|
184 |
})
|
185 |
.catch(error => {
|
186 |
-
console.error('
|
|
|
187 |
});
|
188 |
}
|
189 |
|
190 |
-
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
|
195 |
-
|
196 |
-
|
197 |
-
if (
|
198 |
-
|
199 |
-
|
200 |
-
// Show the texts section
|
201 |
-
const textesSection = document.getElementById('textes-section');
|
202 |
-
if (textesSection) {
|
203 |
-
textesSection.classList.remove('d-none');
|
204 |
-
}
|
205 |
}
|
206 |
-
|
207 |
-
|
208 |
-
}
|
209 |
-
|
210 |
-
// Load texts for the selected category
|
211 |
-
function loadTextes(categoryId) {
|
212 |
-
fetch(`/get_textes/${categoryId}`)
|
213 |
-
.then(response => response.json())
|
214 |
.then(data => {
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
const item = document.createElement('li');
|
222 |
-
item.className = 'selection-item';
|
223 |
-
item.setAttribute('data-texte-id', texte.id);
|
224 |
-
item.textContent = texte.titre;
|
225 |
-
|
226 |
-
// Add click event
|
227 |
-
item.addEventListener('click', function() {
|
228 |
-
const texteId = this.getAttribute('data-texte-id');
|
229 |
-
displayTexte(texteId);
|
230 |
-
|
231 |
-
// Highlight the selected text
|
232 |
-
const items = textesList.querySelectorAll('.selection-item');
|
233 |
-
items.forEach(i => i.classList.remove('active'));
|
234 |
-
this.classList.add('active');
|
235 |
-
});
|
236 |
-
|
237 |
-
textesList.appendChild(item);
|
238 |
-
});
|
239 |
-
|
240 |
-
// Show the texts section
|
241 |
-
const textesSection = document.getElementById('textes-section');
|
242 |
-
if (textesSection) {
|
243 |
-
textesSection.classList.remove('d-none');
|
244 |
}
|
245 |
-
|
246 |
-
//
|
|
|
|
|
247 |
const contentSection = document.getElementById('content-section');
|
248 |
-
if (
|
249 |
-
|
250 |
-
|
251 |
}
|
252 |
})
|
253 |
.catch(error => {
|
254 |
-
console.error('
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
255 |
});
|
256 |
}
|
257 |
|
258 |
-
// Setup text selection functionality
|
259 |
-
function setupTextSelection() {
|
260 |
-
const texteSelect = document.getElementById('texte-select');
|
261 |
-
|
262 |
-
if (texteSelect) {
|
263 |
-
texteSelect.addEventListener('change', function() {
|
264 |
-
const texteId = this.value;
|
265 |
-
if (texteId) {
|
266 |
-
displayTexte(texteId);
|
267 |
-
}
|
268 |
-
});
|
269 |
-
}
|
270 |
-
}
|
271 |
|
272 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
273 |
function displayTexte(texteId) {
|
274 |
-
|
275 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
276 |
.then(data => {
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
289 |
-
|
290 |
-
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
304 |
}
|
305 |
-
|
306 |
-
|
307 |
-
|
308 |
-
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
// Check if block has an image
|
320 |
-
if (block.image) {
|
321 |
-
blockDiv.classList.add('block-with-image');
|
322 |
-
blockDiv.classList.add(`image-${block.image_position || 'left'}`);
|
323 |
-
|
324 |
-
// Create image container
|
325 |
-
const imageDiv = document.createElement('div');
|
326 |
-
imageDiv.className = 'block-image-container';
|
327 |
-
|
328 |
-
// Create image element
|
329 |
-
const imageEl = document.createElement('img');
|
330 |
-
imageEl.className = 'block-image';
|
331 |
-
imageEl.src = block.image.src;
|
332 |
-
imageEl.alt = block.image.alt || 'Illustration';
|
333 |
-
|
334 |
-
imageDiv.appendChild(imageEl);
|
335 |
-
blockDiv.appendChild(imageDiv);
|
336 |
-
|
337 |
-
// Create content container
|
338 |
-
const contentDiv = document.createElement('div');
|
339 |
-
contentDiv.className = 'block-content-container';
|
340 |
-
|
341 |
-
// Add block title if present
|
342 |
-
if (block.title) {
|
343 |
-
const titleEl = document.createElement('h3');
|
344 |
-
titleEl.className = 'content-block-title';
|
345 |
-
titleEl.textContent = block.title;
|
346 |
-
contentDiv.appendChild(titleEl);
|
347 |
-
}
|
348 |
-
|
349 |
-
// Add block content
|
350 |
-
const contentEl = document.createElement('div');
|
351 |
-
contentEl.className = 'content-block-content';
|
352 |
-
contentEl.innerHTML = block.content.replace(/\n/g, '<br>');
|
353 |
-
contentDiv.appendChild(contentEl);
|
354 |
-
|
355 |
-
blockDiv.appendChild(contentDiv);
|
356 |
-
} else {
|
357 |
-
// No image - simple block
|
358 |
-
|
359 |
-
// Add block title if present
|
360 |
-
if (block.title) {
|
361 |
-
const titleEl = document.createElement('h3');
|
362 |
-
titleEl.className = 'content-block-title';
|
363 |
-
titleEl.textContent = block.title;
|
364 |
-
blockDiv.appendChild(titleEl);
|
365 |
-
}
|
366 |
-
|
367 |
-
// Add block content
|
368 |
-
const contentEl = document.createElement('div');
|
369 |
-
contentEl.className = 'content-block-content';
|
370 |
-
contentEl.innerHTML = block.content.replace(/\n/g, '<br>');
|
371 |
-
blockDiv.appendChild(contentEl);
|
372 |
}
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
|
378 |
-
|
379 |
-
const blockDiv = document.createElement('div');
|
380 |
-
blockDiv.className = 'content-block';
|
381 |
-
blockDiv.innerHTML = data.contenu.replace(/\n/g, '<br>');
|
382 |
contentBlocks.appendChild(blockDiv);
|
383 |
-
}
|
384 |
-
|
385 |
-
|
386 |
-
|
387 |
-
|
|
|
|
|
|
|
|
|
|
|
388 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
389 |
})
|
390 |
.catch(error => {
|
391 |
-
console.error(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
392 |
});
|
|
|
393 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// ==================================================================
|
2 |
// Main application JavaScript for the frontend
|
3 |
+
// Handles theme switching, sidebar navigation, and content display.
|
4 |
+
// ==================================================================
|
5 |
|
6 |
+
// Wait for the DOM to be fully loaded before executing scripts
|
7 |
document.addEventListener('DOMContentLoaded', function() {
|
8 |
+
|
9 |
+
// 1. Initialize Theme (Dark/Light Mode)
|
10 |
initTheme();
|
|
|
|
|
|
|
|
|
|
|
11 |
setupThemeToggle();
|
12 |
+
|
13 |
+
// 2. Setup Sidebar Navigation System
|
14 |
+
setupSidebarNavigation();
|
15 |
+
|
16 |
+
// 3. Setup Feedback Form Validation (Basic)
|
17 |
+
setupFeedbackForm();
|
18 |
+
|
19 |
+
// NOTE: Old setup functions for direct page selection are removed/commented out
|
20 |
+
// setupSubjectSelection(); // Replaced by sidebar logic
|
21 |
+
// setupCategorySelection(); // Replaced by sidebar logic
|
22 |
+
// setupTextSelection(); // Replaced by sidebar logic
|
|
|
23 |
});
|
24 |
|
25 |
+
|
26 |
+
// ==================================================================
|
27 |
+
// THEME SWITCHING FUNCTIONS
|
28 |
+
// ==================================================================
|
29 |
+
|
30 |
+
/**
|
31 |
+
* Initializes the theme (dark/light) based on localStorage preference or system default.
|
32 |
+
*/
|
33 |
function initTheme() {
|
34 |
+
// Default to 'light' if no preference is found
|
35 |
const userPreference = localStorage.getItem('theme') || 'light';
|
36 |
document.documentElement.setAttribute('data-theme', userPreference);
|
37 |
+
updateThemeIcon(userPreference); // Set the correct icon on load
|
|
|
|
|
38 |
}
|
39 |
|
40 |
+
/**
|
41 |
+
* Sets up the event listener for the theme toggle button.
|
42 |
+
*/
|
43 |
function setupThemeToggle() {
|
44 |
const themeToggle = document.getElementById('theme-toggle');
|
45 |
+
if (!themeToggle) {
|
46 |
+
console.warn("Theme toggle button (#theme-toggle) not found.");
|
47 |
+
return;
|
48 |
+
}
|
49 |
+
|
50 |
themeToggle.addEventListener('click', function() {
|
51 |
const currentTheme = document.documentElement.getAttribute('data-theme');
|
52 |
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
53 |
+
|
54 |
+
// Apply the new theme
|
55 |
document.documentElement.setAttribute('data-theme', newTheme);
|
56 |
+
|
57 |
// Save preference to localStorage
|
58 |
localStorage.setItem('theme', newTheme);
|
59 |
+
|
60 |
+
// Update the button icon
|
61 |
updateThemeIcon(newTheme);
|
62 |
+
|
63 |
+
// Optional: Send theme preference to the server (if needed)
|
64 |
+
// saveThemePreference(newTheme);
|
65 |
});
|
66 |
}
|
67 |
|
68 |
+
/**
|
69 |
+
* Updates the icon (sun/moon) inside the theme toggle button.
|
70 |
+
* @param {string} theme - The current theme ('light' or 'dark').
|
71 |
+
*/
|
72 |
function updateThemeIcon(theme) {
|
73 |
const themeToggle = document.getElementById('theme-toggle');
|
74 |
+
if (!themeToggle) return; // Exit if button not found
|
75 |
+
|
|
|
76 |
if (theme === 'dark') {
|
77 |
+
themeToggle.innerHTML = '<i class="fas fa-sun"></i>'; // Show sun icon
|
78 |
themeToggle.setAttribute('title', 'Activer le mode clair');
|
79 |
} else {
|
80 |
+
themeToggle.innerHTML = '<i class="fas fa-moon"></i>'; // Show moon icon
|
81 |
themeToggle.setAttribute('title', 'Activer le mode sombre');
|
82 |
}
|
83 |
}
|
84 |
|
85 |
+
/**
|
86 |
+
* Optional: Sends the chosen theme preference to the server.
|
87 |
+
* Uncomment the call in setupThemeToggle if needed.
|
88 |
+
* @param {string} theme - The theme to save ('light' or 'dark').
|
89 |
+
*/
|
90 |
function saveThemePreference(theme) {
|
91 |
const formData = new FormData();
|
92 |
formData.append('theme', theme);
|
93 |
+
|
94 |
+
fetch('/set_theme', { // Ensure this endpoint exists in your Flask app
|
95 |
method: 'POST',
|
96 |
body: formData
|
97 |
})
|
98 |
+
.then(response => {
|
99 |
+
if (!response.ok) {
|
100 |
+
console.error(`Error saving theme: ${response.statusText}`);
|
101 |
+
return response.json().catch(() => ({})); // Try to parse error body
|
102 |
+
}
|
103 |
+
return response.json();
|
104 |
+
})
|
105 |
.then(data => {
|
106 |
+
console.log('Theme preference saved on server:', data);
|
107 |
})
|
108 |
.catch(error => {
|
109 |
+
console.error('Error sending theme preference to server:', error);
|
110 |
});
|
111 |
}
|
112 |
|
113 |
+
|
114 |
+
// ==================================================================
|
115 |
+
// SIDEBAR NAVIGATION FUNCTIONS
|
116 |
+
// ==================================================================
|
117 |
+
|
118 |
+
/**
|
119 |
+
* Sets up all event listeners and logic for the sidebar navigation.
|
120 |
+
*/
|
121 |
+
function setupSidebarNavigation() {
|
122 |
+
// Get references to all necessary DOM elements
|
123 |
+
const burgerMenu = document.getElementById('burger-menu');
|
124 |
+
const sidebarMatieres = document.getElementById('sidebar-matieres');
|
125 |
+
const sidebarSousCategories = document.getElementById('sidebar-sous-categories');
|
126 |
+
const sidebarOverlay = document.getElementById('sidebar-overlay');
|
127 |
+
const matieresList = document.getElementById('matieres-list-sidebar');
|
128 |
+
const sousCategoriesList = document.getElementById('sous-categories-list-sidebar');
|
129 |
+
const backButton = document.getElementById('sidebar-back-button');
|
130 |
+
const closeButtons = document.querySelectorAll('.close-sidebar-btn');
|
131 |
+
const initialInstructions = document.getElementById('initial-instructions');
|
132 |
+
const contentSection = document.getElementById('content-section');
|
133 |
+
|
134 |
+
// --- Helper Function to Close All Sidebars ---
|
135 |
+
const closeAllSidebars = () => {
|
136 |
+
if (sidebarMatieres) sidebarMatieres.classList.remove('active');
|
137 |
+
if (sidebarSousCategories) sidebarSousCategories.classList.remove('active');
|
138 |
+
if (sidebarOverlay) sidebarOverlay.classList.remove('active');
|
139 |
+
// Optional: Reset scroll position of lists when closing
|
140 |
+
// if (matieresList) matieresList.scrollTop = 0;
|
141 |
+
// if (sousCategoriesList) sousCategoriesList.scrollTop = 0;
|
142 |
+
};
|
143 |
+
|
144 |
+
// --- Event Listeners ---
|
145 |
+
|
146 |
+
// 1. Open Sidebar 1 (Matières) with Burger Menu
|
147 |
+
if (burgerMenu && sidebarMatieres && sidebarOverlay) {
|
148 |
+
burgerMenu.addEventListener('click', (e) => {
|
149 |
+
e.stopPropagation(); // Prevent immediate closing if overlay listener fires
|
150 |
+
closeAllSidebars(); // Ensure only one sidebar is open initially
|
151 |
+
sidebarMatieres.classList.add('active');
|
152 |
+
sidebarOverlay.classList.add('active');
|
153 |
});
|
154 |
+
} else {
|
155 |
+
console.warn("Burger menu, matières sidebar, or overlay not found.");
|
156 |
+
}
|
157 |
+
|
158 |
+
// 2. Close sidebars via Overlay click
|
159 |
+
if (sidebarOverlay) {
|
160 |
+
sidebarOverlay.addEventListener('click', closeAllSidebars);
|
161 |
+
}
|
162 |
+
|
163 |
+
// 3. Close sidebars via dedicated Close buttons (X)
|
164 |
+
closeButtons.forEach(button => {
|
165 |
+
button.addEventListener('click', closeAllSidebars);
|
166 |
});
|
167 |
+
|
168 |
+
// 4. Handle click on a Matière in Sidebar 1
|
169 |
+
if (matieresList && sidebarSousCategories && sidebarMatieres) {
|
170 |
+
matieresList.addEventListener('click', (e) => {
|
171 |
+
// Use closest to handle clicks even if icon is clicked
|
172 |
+
const listItem = e.target.closest('li');
|
173 |
+
if (listItem) {
|
174 |
+
const matiereId = listItem.getAttribute('data-matiere-id');
|
175 |
+
const matiereNom = listItem.getAttribute('data-matiere-nom') || 'Inconnu'; // Fallback name
|
176 |
+
|
177 |
+
if (matiereId) {
|
178 |
+
// Update Sidebar 2 title
|
179 |
+
const titleElement = document.getElementById('sidebar-sous-categories-title');
|
180 |
+
if (titleElement) {
|
181 |
+
titleElement.textContent = `Sous-catégories (${matiereNom})`;
|
182 |
+
}
|
183 |
+
// Load sous-categories into Sidebar 2's list
|
184 |
+
loadSubCategoriesForSidebar(matiereId, sousCategoriesList);
|
185 |
+
|
186 |
+
// Switch Sidebars: Hide 1, Show 2
|
187 |
+
sidebarMatieres.classList.remove('active'); // Slide out sidebar 1
|
188 |
+
sidebarSousCategories.classList.add('active'); // Slide in sidebar 2
|
189 |
+
// Keep overlay active
|
190 |
+
if (sidebarOverlay) sidebarOverlay.classList.add('active');
|
191 |
}
|
192 |
}
|
193 |
});
|
194 |
+
} else {
|
195 |
+
console.warn("Matières list, sous-catégories sidebar, or matières sidebar not found.");
|
196 |
+
}
|
197 |
+
|
198 |
+
// 5. Handle click on Back Button in Sidebar 2
|
199 |
+
if (backButton && sidebarMatieres && sidebarSousCategories) {
|
200 |
+
backButton.addEventListener('click', () => {
|
201 |
+
sidebarSousCategories.classList.remove('active'); // Slide out sidebar 2
|
202 |
+
sidebarMatieres.classList.add('active'); // Slide in sidebar 1
|
203 |
+
// Keep overlay active
|
204 |
+
if (sidebarOverlay) sidebarOverlay.classList.add('active');
|
205 |
+
});
|
206 |
+
} else {
|
207 |
+
console.warn("Sidebar back button, matières sidebar, or sous-catégories sidebar not found.");
|
208 |
+
}
|
209 |
+
|
210 |
+
// 6. Handle click on a Sous-catégorie in Sidebar 2
|
211 |
+
if (sousCategoriesList && initialInstructions && contentSection) {
|
212 |
+
sousCategoriesList.addEventListener('click', (e) => {
|
213 |
+
const listItem = e.target.closest('li');
|
214 |
+
if (listItem && listItem.getAttribute('data-category-id')) { // Ensure it's a valid category item
|
215 |
+
const categoryId = listItem.getAttribute('data-category-id');
|
216 |
+
|
217 |
+
// Load and display the content for the first text in this category
|
218 |
+
loadAndDisplayFirstTexte(categoryId);
|
219 |
+
|
220 |
+
// Hide initial instructions, show content section
|
221 |
+
if (initialInstructions) initialInstructions.classList.add('d-none');
|
222 |
+
if (contentSection) contentSection.classList.remove('d-none');
|
223 |
+
|
224 |
+
// Close both sidebars and overlay after selection
|
225 |
+
closeAllSidebars();
|
226 |
+
}
|
227 |
+
});
|
228 |
+
} else {
|
229 |
+
console.warn("Sous-catégories list, initial instructions, or content section not found.");
|
230 |
}
|
231 |
}
|
232 |
|
233 |
+
/**
|
234 |
+
* Fetches and loads subcategories for a given matiereId into the specified list element.
|
235 |
+
* @param {string} matiereId - The ID of the selected matière.
|
236 |
+
* @param {HTMLElement} listElement - The UL element to populate.
|
237 |
+
*/
|
238 |
+
function loadSubCategoriesForSidebar(matiereId, listElement) {
|
239 |
+
if (!listElement) {
|
240 |
+
console.error("Target list element for subcategories is missing.");
|
241 |
+
return;
|
242 |
+
}
|
243 |
+
listElement.innerHTML = '<li>Chargement...</li>'; // Show loading state
|
244 |
+
|
245 |
+
fetch(`/get_sous_categories/${matiereId}`) // Ensure this endpoint exists in Flask
|
246 |
+
.then(response => {
|
247 |
+
if (!response.ok) {
|
248 |
+
throw new Error(`Erreur HTTP: ${response.status} ${response.statusText}`);
|
249 |
+
}
|
250 |
+
return response.json();
|
251 |
+
})
|
252 |
.then(data => {
|
253 |
+
listElement.innerHTML = ''; // Clear loading/previous items
|
254 |
+
if (data && data.length > 0) {
|
|
|
|
|
|
|
255 |
data.forEach(category => {
|
256 |
const item = document.createElement('li');
|
|
|
257 |
item.setAttribute('data-category-id', category.id);
|
258 |
+
item.textContent = category.nom; // Use category name
|
259 |
+
|
260 |
+
// Add chevron icon for visual cue
|
261 |
+
const icon = document.createElement('i');
|
262 |
+
icon.className = 'fas fa-chevron-right float-end';
|
263 |
+
item.appendChild(icon);
|
264 |
+
|
265 |
+
listElement.appendChild(item);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
266 |
});
|
267 |
+
} else {
|
268 |
+
listElement.innerHTML = '<li>Aucune sous-catégorie trouvée.</li>';
|
|
|
|
|
|
|
|
|
269 |
}
|
270 |
})
|
271 |
.catch(error => {
|
272 |
+
console.error('Erreur lors du chargement des sous-catégories:', error);
|
273 |
+
listElement.innerHTML = `<li>Erreur: ${error.message}</li>`;
|
274 |
});
|
275 |
}
|
276 |
|
277 |
+
/**
|
278 |
+
* Fetches the list of texts for a category and displays the first one found.
|
279 |
+
* @param {string} categoryId - The ID of the selected sous-catégorie.
|
280 |
+
*/
|
281 |
+
function loadAndDisplayFirstTexte(categoryId) {
|
282 |
+
fetch(`/get_textes/${categoryId}`) // Ensure this endpoint exists and returns a list of texts [{id, titre}, ...]
|
283 |
+
.then(response => {
|
284 |
+
if (!response.ok) {
|
285 |
+
throw new Error(`Erreur HTTP: ${response.status} ${response.statusText}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
286 |
}
|
287 |
+
return response.json();
|
288 |
+
})
|
|
|
|
|
|
|
|
|
|
|
|
|
289 |
.then(data => {
|
290 |
+
if (data && data.length > 0) {
|
291 |
+
const firstTexteId = data[0].id; // Get the ID of the very first text
|
292 |
+
if (firstTexteId) {
|
293 |
+
displayTexte(firstTexteId); // Call displayTexte with this ID
|
294 |
+
} else {
|
295 |
+
throw new Error("Le premier texte reçu n'a pas d'ID.");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
296 |
}
|
297 |
+
} else {
|
298 |
+
// Handle case where a category has no texts associated with it
|
299 |
+
const contentTitle = document.getElementById('content-title');
|
300 |
+
const contentBlocks = document.getElementById('content-blocks');
|
301 |
const contentSection = document.getElementById('content-section');
|
302 |
+
if (contentTitle) contentTitle.textContent = "Contenu non disponible";
|
303 |
+
if (contentBlocks) contentBlocks.innerHTML = '<div class="alert alert-info">Aucun texte trouvé pour cette sous-catégorie.</div>';
|
304 |
+
if (contentSection) contentSection.classList.remove('d-none'); // Ensure section is visible
|
305 |
}
|
306 |
})
|
307 |
.catch(error => {
|
308 |
+
console.error('Erreur lors du chargement des textes pour la catégorie:', error);
|
309 |
+
// Display error message in the content area
|
310 |
+
const contentTitle = document.getElementById('content-title');
|
311 |
+
const contentBlocks = document.getElementById('content-blocks');
|
312 |
+
const contentSection = document.getElementById('content-section');
|
313 |
+
if (contentTitle) contentTitle.textContent = "Erreur";
|
314 |
+
if (contentBlocks) contentBlocks.innerHTML = `<div class="alert alert-danger">Impossible de charger le contenu. ${error.message}</div>`;
|
315 |
+
if (contentSection) contentSection.classList.remove('d-none'); // Ensure section is visible
|
316 |
});
|
317 |
}
|
318 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
319 |
|
320 |
+
// ==================================================================
|
321 |
+
// CONTENT DISPLAY FUNCTION
|
322 |
+
// ==================================================================
|
323 |
+
|
324 |
+
/**
|
325 |
+
* Fetches and displays the content (title and blocks) for a specific texteId.
|
326 |
+
* @param {string} texteId - The ID of the text to display.
|
327 |
+
*/
|
328 |
function displayTexte(texteId) {
|
329 |
+
const contentSection = document.getElementById('content-section');
|
330 |
+
const contentTitle = document.getElementById('content-title');
|
331 |
+
const contentBlocks = document.getElementById('content-blocks');
|
332 |
+
|
333 |
+
// Check if essential elements exist
|
334 |
+
if (!contentSection || !contentTitle || !contentBlocks) {
|
335 |
+
console.error("Éléments d'affichage du contenu (#content-section, #content-title, #content-blocks) introuvables.");
|
336 |
+
alert("Erreur interne: Impossible d'afficher le contenu.");
|
337 |
+
return;
|
338 |
+
}
|
339 |
+
|
340 |
+
// Indicate loading state visually
|
341 |
+
contentTitle.textContent = "Chargement du contenu...";
|
342 |
+
contentBlocks.innerHTML = '<div class="text-center p-3"><i class="fas fa-spinner fa-spin fa-2x"></i></div>'; // Simple spinner
|
343 |
+
|
344 |
+
fetch(`/get_texte/${texteId}`) // Ensure this endpoint exists and returns detailed text object {titre, contenu, blocks, color_code, ...}
|
345 |
+
.then(response => {
|
346 |
+
if (!response.ok) {
|
347 |
+
throw new Error(`Erreur HTTP: ${response.status} ${response.statusText}`);
|
348 |
+
}
|
349 |
+
return response.json();
|
350 |
+
})
|
351 |
.then(data => {
|
352 |
+
// --- Update Content Title ---
|
353 |
+
contentTitle.textContent = data.titre || "Titre non disponible";
|
354 |
+
|
355 |
+
// --- Update Theme/Color Styling ---
|
356 |
+
const dynamicStyleId = 'dynamic-block-styles';
|
357 |
+
let style = document.getElementById(dynamicStyleId);
|
358 |
+
if (!style) { // Create style tag if it doesn't exist
|
359 |
+
style = document.createElement('style');
|
360 |
+
style.id = dynamicStyleId;
|
361 |
+
document.head.appendChild(style);
|
362 |
+
}
|
363 |
+
|
364 |
+
if (data.color_code) {
|
365 |
+
// Apply color to title underline and block border/title
|
366 |
+
contentTitle.style.borderBottomColor = data.color_code;
|
367 |
+
style.textContent = `
|
368 |
+
#content-section .content-block-title { border-bottom-color: ${data.color_code} !important; }
|
369 |
+
#content-section .content-block { border-left: 4px solid ${data.color_code} !important; }
|
370 |
+
`;
|
371 |
+
} else {
|
372 |
+
// Reset styles if no color code is provided (use CSS defaults)
|
373 |
+
contentTitle.style.borderBottomColor = ''; // Reset specific style
|
374 |
+
style.textContent = ''; // Clear dynamic rules
|
375 |
+
}
|
376 |
+
|
377 |
+
// --- Render Content Blocks ---
|
378 |
+
contentBlocks.innerHTML = ''; // Clear loading indicator/previous content
|
379 |
+
|
380 |
+
if (data.blocks && Array.isArray(data.blocks) && data.blocks.length > 0) {
|
381 |
+
// Sort blocks by 'order' property if it exists, otherwise render as received
|
382 |
+
const sortedBlocks = data.blocks.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
383 |
+
|
384 |
+
sortedBlocks.forEach(block => {
|
385 |
+
const blockDiv = document.createElement('div');
|
386 |
+
blockDiv.className = 'content-block fade-in'; // Add animation class
|
387 |
+
|
388 |
+
// Block with Image
|
389 |
+
if (block.image && block.image.src) {
|
390 |
+
blockDiv.classList.add('block-with-image', `image-${block.image_position || 'left'}`);
|
391 |
+
|
392 |
+
// Image Container
|
393 |
+
const imageDiv = document.createElement('div');
|
394 |
+
imageDiv.className = 'block-image-container';
|
395 |
+
const imageEl = document.createElement('img');
|
396 |
+
imageEl.className = 'block-image';
|
397 |
+
imageEl.src = block.image.src; // Ensure backend provides full URL if needed
|
398 |
+
imageEl.alt = block.image.alt || 'Illustration';
|
399 |
+
imageEl.loading = 'lazy'; // Add lazy loading for images
|
400 |
+
imageDiv.appendChild(imageEl);
|
401 |
+
blockDiv.appendChild(imageDiv);
|
402 |
+
|
403 |
+
// Content Container (Text part)
|
404 |
+
const contentDiv = document.createElement('div');
|
405 |
+
contentDiv.className = 'block-content-container';
|
406 |
+
if (block.title) {
|
407 |
+
const titleEl = document.createElement('h3');
|
408 |
+
titleEl.className = 'content-block-title';
|
409 |
+
titleEl.textContent = block.title;
|
410 |
+
contentDiv.appendChild(titleEl);
|
411 |
}
|
412 |
+
const contentEl = document.createElement('div');
|
413 |
+
contentEl.className = 'content-block-content';
|
414 |
+
// IMPORTANT: Sanitize HTML content if it comes from user input or untrusted sources
|
415 |
+
// For now, assuming safe HTML from backend:
|
416 |
+
contentEl.innerHTML = block.content ? block.content.replace(/\n/g, '<br>') : '';
|
417 |
+
contentDiv.appendChild(contentEl);
|
418 |
+
blockDiv.appendChild(contentDiv);
|
419 |
+
|
420 |
+
} else { // Block without Image
|
421 |
+
if (block.title) {
|
422 |
+
const titleEl = document.createElement('h3');
|
423 |
+
titleEl.className = 'content-block-title';
|
424 |
+
titleEl.textContent = block.title;
|
425 |
+
blockDiv.appendChild(titleEl);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
426 |
}
|
427 |
+
const contentEl = document.createElement('div');
|
428 |
+
contentEl.className = 'content-block-content';
|
429 |
+
// IMPORTANT: Sanitize HTML content
|
430 |
+
contentEl.innerHTML = block.content ? block.content.replace(/\n/g, '<br>') : '';
|
431 |
+
blockDiv.appendChild(contentEl);
|
432 |
+
}
|
|
|
|
|
|
|
433 |
contentBlocks.appendChild(blockDiv);
|
434 |
+
});
|
435 |
+
} else if (data.contenu) { // Fallback: Use simple 'contenu' field if no blocks
|
436 |
+
const blockDiv = document.createElement('div');
|
437 |
+
blockDiv.className = 'content-block';
|
438 |
+
// IMPORTANT: Sanitize HTML content
|
439 |
+
blockDiv.innerHTML = data.contenu.replace(/\n/g, '<br>');
|
440 |
+
contentBlocks.appendChild(blockDiv);
|
441 |
+
} else {
|
442 |
+
// No blocks and no simple content
|
443 |
+
contentBlocks.innerHTML = '<div class="alert alert-warning">Le contenu de ce texte est vide ou non structuré.</div>';
|
444 |
}
|
445 |
+
|
446 |
+
// --- Final Steps ---
|
447 |
+
// Ensure the content section is visible
|
448 |
+
contentSection.classList.remove('d-none');
|
449 |
+
|
450 |
+
// Scroll to the top of the content title for better UX
|
451 |
+
contentTitle.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
452 |
+
|
453 |
})
|
454 |
.catch(error => {
|
455 |
+
console.error(`Erreur lors du chargement du texte ID ${texteId}:`, error);
|
456 |
+
// Display error message in the content area
|
457 |
+
contentTitle.textContent = "Erreur de chargement";
|
458 |
+
contentBlocks.innerHTML = `<div class="alert alert-danger">Impossible de charger le contenu demandé. ${error.message}</div>`;
|
459 |
+
// Ensure section is visible even on error
|
460 |
+
contentSection.classList.remove('d-none');
|
461 |
+
});
|
462 |
+
}
|
463 |
+
|
464 |
+
|
465 |
+
// ==================================================================
|
466 |
+
// FEEDBACK FORM SETUP
|
467 |
+
// ==================================================================
|
468 |
+
|
469 |
+
/**
|
470 |
+
* Sets up basic validation for the feedback form.
|
471 |
+
*/
|
472 |
+
function setupFeedbackForm() {
|
473 |
+
const feedbackForm = document.getElementById('feedback-form');
|
474 |
+
if (feedbackForm) {
|
475 |
+
feedbackForm.addEventListener('submit', function(e) {
|
476 |
+
const feedbackMessage = document.getElementById('feedback-message');
|
477 |
+
// Simple check if message textarea is empty or only whitespace
|
478 |
+
if (!feedbackMessage || !feedbackMessage.value.trim()) {
|
479 |
+
e.preventDefault(); // Stop form submission
|
480 |
+
alert('Veuillez entrer un message avant d\'envoyer votre avis.');
|
481 |
+
if (feedbackMessage) feedbackMessage.focus(); // Focus the textarea
|
482 |
+
}
|
483 |
+
// Add more complex validation here if needed
|
484 |
});
|
485 |
+
}
|
486 |
}
|
487 |
+
|
488 |
+
|
489 |
+
// ==================================================================
|
490 |
+
// OLD FUNCTIONS (Removed/Commented Out) - Kept for reference only
|
491 |
+
// ==================================================================
|
492 |
+
|
493 |
+
/*
|
494 |
+
function setupSubjectSelection() { // No longer used directly on page
|
495 |
+
// ... old logic targeting .subject-card elements on the main page ...
|
496 |
+
}
|
497 |
+
|
498 |
+
function loadSubCategories(matiereId) { // Replaced by loadSubCategoriesForSidebar
|
499 |
+
// ... old logic targeting #sous-categories-list on the main page ...
|
500 |
+
}
|
501 |
+
|
502 |
+
function setupCategorySelection() { // No longer used directly on page
|
503 |
+
// ... old logic targeting #sous-categorie-select or similar ...
|
504 |
+
}
|
505 |
+
|
506 |
+
function loadTextes(categoryId) { // Logic integrated into loadAndDisplayFirstTexte
|
507 |
+
// ... old logic targeting #textes-list on the main page ...
|
508 |
+
}
|
509 |
+
|
510 |
+
function setupTextSelection() { // No longer used directly on page
|
511 |
+
// ... old logic targeting #texte-select or similar ...
|
512 |
+
}
|
513 |
+
*/
|
514 |
+
|
515 |
+
// ==================================================================
|
516 |
+
// END OF FILE
|
517 |
+
// ==================================================================
|