Docfile commited on
Commit
da893ba
·
verified ·
1 Parent(s): 611fd85

Update api/static/js/main.js

Browse files
Files changed (1) hide show
  1. 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
- // Initialize theme
 
6
  initTheme();
7
-
8
- // Setup interactive elements
9
- setupSubjectSelection();
10
- setupCategorySelection();
11
- setupTextSelection();
12
  setupThemeToggle();
13
-
14
- // Setup feedback form submission
15
- const feedbackForm = document.getElementById('feedback-form');
16
- if (feedbackForm) {
17
- feedbackForm.addEventListener('submit', function(e) {
18
- const feedbackMessage = document.getElementById('feedback-message');
19
- if (!feedbackMessage.value.trim()) {
20
- e.preventDefault();
21
- alert('Veuillez entrer un message avant d\'envoyer votre feedback.');
22
- }
23
- });
24
- }
25
  });
26
 
27
- // Initialize theme based on user preference
 
 
 
 
 
 
 
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
- // Setup theme toggle functionality
 
 
37
  function setupThemeToggle() {
38
  const themeToggle = document.getElementById('theme-toggle');
39
- if (!themeToggle) return;
40
-
 
 
 
41
  themeToggle.addEventListener('click', function() {
42
  const currentTheme = document.documentElement.getAttribute('data-theme');
43
  const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
44
-
45
- // Update theme attribute
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
- // Update the theme toggle icon based on current theme
 
 
 
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
- // Save theme preference to server
 
 
 
 
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 => response.json())
 
 
 
 
 
 
84
  .then(data => {
85
- console.log('Theme preference saved:', data);
86
  })
87
  .catch(error => {
88
- console.error('Error saving theme preference:', error);
89
  });
90
  }
91
 
92
- // Setup subject selection functionality
93
- function setupSubjectSelection() {
94
- const subjectCards = document.querySelectorAll('.subject-card');
95
- const subjectSelect = document.getElementById('matiere-select');
96
-
97
- // Handle subject card clicks
98
- subjectCards.forEach(card => {
99
- card.addEventListener('click', function() {
100
- const matiereId = this.getAttribute('data-matiere-id');
101
-
102
- // Update select element if it exists
103
- if (subjectSelect) {
104
- subjectSelect.value = matiereId;
105
- // Trigger change event to load subcategories
106
- const event = new Event('change');
107
- subjectSelect.dispatchEvent(event);
108
- } else {
109
- loadSubCategories(matiereId);
110
- }
111
-
112
- // Highlight the selected card
113
- subjectCards.forEach(c => c.classList.remove('active'));
114
- this.classList.add('active');
115
-
116
- // Show the categories section
117
- const categoriesSection = document.getElementById('sous-categories-section');
118
- if (categoriesSection) {
119
- categoriesSection.classList.remove('d-none');
120
- categoriesSection.scrollIntoView({ behavior: 'smooth' });
121
- }
 
 
 
 
 
 
 
 
 
 
122
  });
 
 
 
 
 
 
 
 
 
 
 
 
123
  });
124
-
125
- // Handle subject select change
126
- if (subjectSelect) {
127
- subjectSelect.addEventListener('change', function() {
128
- const matiereId = this.value;
129
- if (matiereId) {
130
- loadSubCategories(matiereId);
131
-
132
- // Show the categories section
133
- const categoriesSection = document.getElementById('sous-categories-section');
134
- if (categoriesSection) {
135
- categoriesSection.classList.remove('d-none');
 
 
 
 
 
 
 
 
 
 
 
 
136
  }
137
  }
138
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  }
140
  }
141
 
142
- // Load subcategories for the selected subject
143
- function loadSubCategories(matiereId) {
144
- fetch(`/get_sous_categories/${matiereId}`)
145
- .then(response => response.json())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  .then(data => {
147
- // Update subcategories list
148
- const sousCategoriesList = document.getElementById('sous-categories-list');
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 click event
159
- item.addEventListener('click', function() {
160
- const categoryId = this.getAttribute('data-category-id');
161
- loadTextes(categoryId);
162
-
163
- // Highlight the selected category
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
- // Show the subcategories section if it's hidden
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('Error loading subcategories:', error);
 
187
  });
188
  }
189
 
190
- // Setup category selection functionality
191
- function setupCategorySelection() {
192
- const categorySelect = document.getElementById('sous-categorie-select');
193
-
194
- if (categorySelect) {
195
- categorySelect.addEventListener('change', function() {
196
- const categoryId = this.value;
197
- if (categoryId) {
198
- loadTextes(categoryId);
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
- // Update texts list
216
- const textesList = document.getElementById('textes-list');
217
- if (textesList) {
218
- textesList.innerHTML = '';
219
-
220
- data.forEach(texte => {
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
- // Hide the content section since no text is selected yet
 
 
247
  const contentSection = document.getElementById('content-section');
248
- if (contentSection) {
249
- contentSection.classList.add('d-none');
250
- }
251
  }
252
  })
253
  .catch(error => {
254
- console.error('Error loading texts:', 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
- // Display the selected texte with content blocks
 
 
 
 
 
 
 
273
  function displayTexte(texteId) {
274
- fetch(`/get_texte/${texteId}`)
275
- .then(response => response.json())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
  .then(data => {
277
- const contentSection = document.getElementById('content-section');
278
- const contentTitle = document.getElementById('content-title');
279
- const contentBlocks = document.getElementById('content-blocks');
280
-
281
- if (contentSection && contentTitle && contentBlocks) {
282
- // Update content title
283
- contentTitle.textContent = data.titre;
284
-
285
- // Update content theme color based on matiere color
286
- if (data.color_code) {
287
- // Apply color to title underline
288
- contentTitle.style.borderBottomColor = data.color_code;
289
-
290
- // Apply color to all block titles
291
- const style = document.createElement('style');
292
- style.id = 'dynamic-block-styles';
293
- const existingStyle = document.getElementById('dynamic-block-styles');
294
- if (existingStyle) {
295
- existingStyle.remove();
296
- }
297
-
298
- style.textContent = `
299
- .content-block-title {
300
- border-bottom-color: ${data.color_code} !important;
301
- }
302
- .content-block {
303
- border-left: 4px solid ${data.color_code} !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
  }
305
- `;
306
- document.head.appendChild(style);
307
- }
308
-
309
- // Clear existing content
310
- contentBlocks.innerHTML = '';
311
-
312
- // Create content blocks
313
- if (data.blocks && data.blocks.length > 0) {
314
- data.blocks.forEach(block => {
315
- // Create block container
316
- const blockDiv = document.createElement('div');
317
- blockDiv.className = 'content-block fade-in';
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
- // Add the block to the content area
375
- contentBlocks.appendChild(blockDiv);
376
- });
377
- } else {
378
- // Fallback to regular content if no blocks
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
- // Show the content section
386
- contentSection.classList.remove('d-none');
387
- contentSection.scrollIntoView({ behavior: 'smooth' });
 
 
 
 
 
388
  }
 
 
 
 
 
 
 
 
389
  })
390
  .catch(error => {
391
- console.error('Error loading texte:', 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
+ // ==================================================================