Docfile commited on
Commit
26c2541
·
verified ·
1 Parent(s): 212a077

Delete api

Browse files
api/index.py DELETED
@@ -1,948 +0,0 @@
1
- # --- START OF index.py ---
2
-
3
- import os
4
- import logging
5
- import json
6
- from flask import Flask, Blueprint, render_template, request, redirect, url_for, flash, jsonify, session, current_app, send_file, Response
7
- from flask_sqlalchemy import SQLAlchemy
8
- from sqlalchemy.orm import DeclarativeBase
9
- from werkzeug.utils import secure_filename
10
- from werkzeug.security import check_password_hash, generate_password_hash
11
- from datetime import datetime
12
- from functools import wraps
13
- import requests
14
- from io import BytesIO
15
- import base64
16
-
17
- # Configure logging
18
- logging.basicConfig(level=logging.INFO)
19
-
20
- # ---------------------------------------------------------------------------
21
- # Configuration (Hardcoded as requested)
22
- # ---------------------------------------------------------------------------
23
-
24
- # Database configuration
25
- SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
26
-
27
- # Session secret key
28
- SECRET_KEY = os.environ.get('SESSION_SECRET', 'dev_secret_key_change_in_production')
29
- if SECRET_KEY == 'dev_secret_key_change_in_production':
30
- print("WARNING: Using default SECRET_KEY. Set SESSION_SECRET environment variable for production.")
31
-
32
- # Admin login credentials (simple authentication for single admin)
33
- ADMIN_USERNAME = os.environ.get('ADMIN_USERNAME', 'admin')
34
- ADMIN_PASSWORD = os.environ.get('ADMIN_PASSWORD', 'password')
35
-
36
- # Telegram configuration for feedback
37
- TELEGRAM_BOT_TOKEN = "7126991043:AAEzeKswNo6eO7oJA49Hxn_bsbzgzUoJ-6A"
38
- TELEGRAM_CHAT_ID = "-1002081124539"
39
-
40
- # Application host/port/debug
41
- DEBUG = os.environ.get('DEBUG', 'False') == 'True'
42
- HOST = '0.0.0.0'
43
- PORT = int(os.environ.get('PORT', 5000))
44
-
45
- # ---------------------------------------------------------------------------
46
- # Flask App Initialization and SQLAlchemy Setup
47
- # ---------------------------------------------------------------------------
48
-
49
- # Create base class for SQLAlchemy models
50
- class Base(DeclarativeBase):
51
- pass
52
-
53
- # Initialize SQLAlchemy with the Base class
54
- db = SQLAlchemy(model_class=Base)
55
-
56
- # Create Flask application
57
- app = Flask(__name__)
58
-
59
- # Apply configuration
60
- app.config['SQLALCHEMY_DATABASE_URI'] = SQLALCHEMY_DATABASE_URI
61
- app.config['SECRET_KEY'] = SECRET_KEY
62
- app.config['ADMIN_USERNAME'] = ADMIN_USERNAME
63
- app.config['ADMIN_PASSWORD'] = ADMIN_PASSWORD
64
- app.config['TELEGRAM_BOT_TOKEN'] = TELEGRAM_BOT_TOKEN
65
- app.config['TELEGRAM_CHAT_ID'] = TELEGRAM_CHAT_ID
66
- app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
67
- app.config["SQLALCHEMY_ENGINE_OPTIONS"] = {
68
- "pool_recycle": 300,
69
- "pool_pre_ping": True,
70
- }
71
- app.config['HOST'] = HOST
72
- app.config['PORT'] = PORT
73
- app.config['DEBUG'] = DEBUG
74
- app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max upload
75
-
76
- # Initialize the app with SQLAlchemy
77
- db.init_app(app)
78
-
79
- # ---------------------------------------------------------------------------
80
- # Database Models
81
- # ---------------------------------------------------------------------------
82
-
83
- # Matiere (Subject) Model
84
- class Matiere(db.Model):
85
- id = db.Column(db.Integer, primary_key=True)
86
- nom = db.Column(db.String(100), unique=True, nullable=False)
87
- color_code = db.Column(db.String(7), nullable=False, default="#3498db") # Add color code field for subject
88
-
89
- # Relationships
90
- sous_categories = db.relationship('SousCategorie', backref='matiere', lazy=True, cascade="all, delete-orphan")
91
-
92
- def __repr__(self):
93
- return f'<Matiere {self.nom}>'
94
-
95
- # SousCategorie (SubCategory) Model
96
- class SousCategorie(db.Model):
97
- id = db.Column(db.Integer, primary_key=True)
98
- nom = db.Column(db.String(100), nullable=False)
99
- matiere_id = db.Column(db.Integer, db.ForeignKey('matiere.id'), nullable=False)
100
-
101
- # Enforce unique constraint on name within same matiere
102
- __table_args__ = (db.UniqueConstraint('nom', 'matiere_id', name='_nom_matiere_uc'),)
103
-
104
- # Relationships
105
- textes = db.relationship('Texte', backref='sous_categorie', lazy=True, cascade="all, delete-orphan")
106
-
107
- def __repr__(self):
108
- return f'<SousCategorie {self.nom} (Matiere ID: {self.matiere_id})>'
109
-
110
- # ContentBlock model for the new block-based content
111
- class ContentBlock(db.Model):
112
- id = db.Column(db.Integer, primary_key=True)
113
- texte_id = db.Column(db.Integer, db.ForeignKey('texte.id'), nullable=False)
114
- title = db.Column(db.String(200), nullable=True)
115
- content = db.Column(db.Text, nullable=False)
116
- order = db.Column(db.Integer, nullable=False, default=0)
117
- image_id = db.Column(db.Integer, db.ForeignKey('image.id'), nullable=True)
118
- image_position = db.Column(db.String(10), nullable=True, default='left') # 'left', 'right', 'top', 'bottom'
119
-
120
- # Relationship
121
- image = db.relationship('Image', foreign_keys=[image_id])
122
-
123
- def __repr__(self):
124
- return f'<ContentBlock {self.id} (Texte ID: {self.texte_id})>'
125
-
126
- # Texte (Text content) Model
127
- class Texte(db.Model):
128
- id = db.Column(db.Integer, primary_key=True)
129
- titre = db.Column(db.String(200), nullable=False)
130
- contenu = db.Column(db.Text, nullable=False)
131
- sous_categorie_id = db.Column(db.Integer, db.ForeignKey('sous_categorie.id'), nullable=False)
132
- created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
133
- updated_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow)
134
-
135
- # Relationships
136
- historiques = db.relationship('TexteHistorique', backref='texte', lazy=True, cascade="all, delete-orphan")
137
- content_blocks = db.relationship('ContentBlock', backref='texte', lazy=True, cascade="all, delete-orphan",
138
- order_by="ContentBlock.order")
139
-
140
- def __repr__(self):
141
- return f'<Texte {self.titre} (SousCategorie ID: {self.sous_categorie_id})>'
142
-
143
- # Image Model for storing content images
144
- class Image(db.Model):
145
- id = db.Column(db.Integer, primary_key=True)
146
- nom_fichier = db.Column(db.String(255))
147
- mime_type = db.Column(db.String(100), nullable=False)
148
- data = db.Column(db.LargeBinary, nullable=False) # BLOB/BYTEA for image data
149
- uploaded_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
150
-
151
- # Additional fields for image management
152
- description = db.Column(db.String(255), nullable=True)
153
- alt_text = db.Column(db.String(255), nullable=True)
154
-
155
- def __repr__(self):
156
- return f'<Image {self.id} ({self.nom_fichier})>'
157
-
158
- # TexteHistorique (Text History) Model
159
- class TexteHistorique(db.Model):
160
- id = db.Column(db.Integer, primary_key=True)
161
- texte_id = db.Column(db.Integer, db.ForeignKey('texte.id'), nullable=False)
162
- contenu_precedent = db.Column(db.Text, nullable=False)
163
- date_modification = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
164
-
165
- def __repr__(self):
166
- return f'<TexteHistorique {self.id} (Texte ID: {self.texte_id})>'
167
-
168
- # UserPreference Model to store user theme preferences
169
- class UserPreference(db.Model):
170
- id = db.Column(db.Integer, primary_key=True)
171
- user_id = db.Column(db.String(50), unique=True, nullable=False) # Use session ID or similar for anonymous users
172
- theme = db.Column(db.String(10), nullable=False, default='light') # 'light' or 'dark'
173
- created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
174
- updated_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow)
175
-
176
- def __repr__(self):
177
- return f'<UserPreference {self.id} (User ID: {self.user_id})>'
178
-
179
- # ---------------------------------------------------------------------------
180
- # Utility Functions
181
- # ---------------------------------------------------------------------------
182
-
183
- # Admin authentication decorator
184
- def admin_required(f):
185
- @wraps(f)
186
- def decorated_function(*args, **kwargs):
187
- if not session.get('admin_logged_in'):
188
- flash('Veuillez vous connecter pour accéder à cette page.', 'warning')
189
- return redirect(url_for('admin_bp.login'))
190
- return f(*args, **kwargs)
191
- return decorated_function
192
-
193
- # Admin login check
194
- def check_admin_credentials(username, password):
195
- # Access config via current_app proxy
196
- admin_username = current_app.config['ADMIN_USERNAME']
197
- admin_password = current_app.config['ADMIN_PASSWORD']
198
-
199
- return username == admin_username and password == admin_password
200
-
201
- # Send feedback to Telegram
202
- def send_telegram_feedback(message):
203
- token = current_app.config.get('TELEGRAM_BOT_TOKEN')
204
- chat_id = current_app.config.get('TELEGRAM_CHAT_ID')
205
-
206
- if not token or not chat_id:
207
- current_app.logger.error("Telegram bot token or chat ID not configured")
208
- return False
209
-
210
- api_url = f"https://api.telegram.org/bot{token}/sendMessage"
211
- payload = {
212
- "chat_id": chat_id,
213
- "text": f"📝 Nouveau feedback:\n\n{message}",
214
- "parse_mode": "HTML"
215
- }
216
-
217
- try:
218
- response = requests.post(api_url, data=payload, timeout=10)
219
- response.raise_for_status()
220
- current_app.logger.info("Feedback sent to Telegram successfully")
221
- return True
222
- except requests.exceptions.RequestException as e:
223
- current_app.logger.error(f"Error sending feedback to Telegram: {str(e)}")
224
- if hasattr(e, 'response') and e.response is not None:
225
- current_app.logger.error(f"Telegram API Response: {e.response.text}")
226
- return False
227
- except Exception as e:
228
- current_app.logger.error(f"Unexpected exception while sending feedback to Telegram: {str(e)}")
229
- return False
230
-
231
- # Get or create user preferences
232
- def get_user_preferences():
233
- user_id = session.get('user_id')
234
- if not user_id:
235
- # Generate a unique ID for new users
236
- user_id = str(datetime.utcnow().timestamp())
237
- session['user_id'] = user_id
238
-
239
- user_pref = UserPreference.query.filter_by(user_id=user_id).first()
240
- if not user_pref:
241
- user_pref = UserPreference(user_id=user_id)
242
- db.session.add(user_pref)
243
- db.session.commit()
244
-
245
- return user_pref
246
-
247
- # Parse text content into blocks
248
- def parse_content_to_blocks(text_content):
249
- # Simple parser that creates blocks based on paragraphs or headings
250
- blocks = []
251
- current_block = {"title": None, "content": ""}
252
-
253
- for line in text_content.split('\n'):
254
- line = line.strip()
255
- if not line:
256
- # Empty line might indicate a block break
257
- if current_block["content"]:
258
- blocks.append(current_block)
259
- current_block = {"title": None, "content": ""}
260
- continue
261
-
262
- # Check if line might be a heading (simplistic approach)
263
- if len(line) < 100 and not current_block["content"]:
264
- current_block["title"] = line
265
- else:
266
- if current_block["content"]:
267
- current_block["content"] += "\n" + line
268
- else:
269
- current_block["content"] = line
270
-
271
- # Add the last block if not empty
272
- if current_block["content"]:
273
- blocks.append(current_block)
274
-
275
- # If no blocks were created, create one with all content
276
- if not blocks:
277
- blocks.append({"title": None, "content": text_content})
278
-
279
- return blocks
280
-
281
- # ---------------------------------------------------------------------------
282
- # Blueprints Definition
283
- # ---------------------------------------------------------------------------
284
- main_bp = Blueprint('main_bp', __name__)
285
- admin_bp = Blueprint('admin_bp', __name__, url_prefix='/gestion')
286
-
287
-
288
- # ---------------------------------------------------------------------------
289
- # Main Routes
290
- # ---------------------------------------------------------------------------
291
-
292
- @main_bp.route('/')
293
- def index():
294
- # Get user theme preference
295
- user_pref = get_user_preferences()
296
-
297
- # Fetch all subjects (matieres)
298
- matieres = Matiere.query.all()
299
-
300
- return render_template('index.html', matieres=matieres, theme=user_pref.theme)
301
-
302
- @main_bp.route('/get_sous_categories/<int:matiere_id>')
303
- def get_sous_categories(matiere_id):
304
- sous_categories = SousCategorie.query.filter_by(matiere_id=matiere_id).all()
305
- return jsonify([{'id': sc.id, 'nom': sc.nom} for sc in sous_categories])
306
-
307
- @main_bp.route('/get_textes/<int:sous_categorie_id>')
308
- def get_textes(sous_categorie_id):
309
- textes = Texte.query.filter_by(sous_categorie_id=sous_categorie_id).all()
310
- return jsonify([{'id': t.id, 'titre': t.titre} for t in textes])
311
-
312
- @main_bp.route('/get_texte/<int:texte_id>')
313
- def get_texte(texte_id):
314
- texte = Texte.query.get_or_404(texte_id)
315
-
316
- # Get the subject color for theming
317
- matiere = Matiere.query.join(SousCategorie).filter(SousCategorie.id == texte.sous_categorie_id).first()
318
- color_code = matiere.color_code if matiere else "#3498db"
319
-
320
- # Check if the texte has content blocks
321
- if texte.content_blocks:
322
- blocks = []
323
- for block in texte.content_blocks:
324
- block_data = {
325
- 'id': block.id,
326
- 'title': block.title,
327
- 'content': block.content,
328
- 'order': block.order,
329
- 'image_position': block.image_position,
330
- 'image': None
331
- }
332
-
333
- # Add image data if available
334
- if block.image:
335
- image_data = base64.b64encode(block.image.data).decode('utf-8')
336
- block_data['image'] = {
337
- 'id': block.image.id,
338
- 'src': f"data:{block.image.mime_type};base64,{image_data}",
339
- 'alt': block.image.alt_text or block.title or "Image illustration"
340
- }
341
-
342
- blocks.append(block_data)
343
- else:
344
- # If no blocks exist yet, parse the content to create blocks
345
- # This is useful for existing content migration
346
- parsed_blocks = parse_content_to_blocks(texte.contenu)
347
- blocks = []
348
- for i, block_data in enumerate(parsed_blocks):
349
- blocks.append({
350
- 'id': None,
351
- 'title': block_data['title'],
352
- 'content': block_data['content'],
353
- 'order': i,
354
- 'image_position': 'left',
355
- 'image': None
356
- })
357
-
358
- return jsonify({
359
- 'id': texte.id,
360
- 'titre': texte.titre,
361
- 'contenu': texte.contenu,
362
- 'blocks': blocks,
363
- 'color_code': color_code
364
- })
365
-
366
- @main_bp.route('/feedback', methods=['POST'])
367
- def submit_feedback():
368
- message = request.form.get('message', '').strip()
369
- if not message:
370
- flash("Le message ne peut pas être vide.", "error")
371
- return redirect(url_for('main_bp.index'))
372
-
373
- success = send_telegram_feedback(message)
374
- if success:
375
- flash("Merci pour votre feedback!", "success")
376
- else:
377
- flash("Une erreur s'est produite lors de l'envoi de votre feedback. Veuillez réessayer plus tard.", "error")
378
-
379
- return redirect(url_for('main_bp.index'))
380
-
381
- @main_bp.route('/set_theme', methods=['POST'])
382
- def set_theme():
383
- theme = request.form.get('theme', 'light')
384
- if theme not in ['light', 'dark']:
385
- theme = 'light'
386
-
387
- user_pref = get_user_preferences()
388
- user_pref.theme = theme
389
- db.session.commit()
390
-
391
- return jsonify({'success': True, 'theme': theme})
392
-
393
- @main_bp.route('/image/<int:image_id>')
394
- def get_image(image_id):
395
- image = Image.query.get_or_404(image_id)
396
- return Response(image.data, mimetype=image.mime_type)
397
-
398
- # ---------------------------------------------------------------------------
399
- # Admin Routes
400
- # ---------------------------------------------------------------------------
401
-
402
- @admin_bp.route('/login', methods=['GET', 'POST'])
403
- def login():
404
- if request.method == 'POST':
405
- username = request.form.get('username')
406
- password = request.form.get('password')
407
-
408
- if check_admin_credentials(username, password):
409
- session['admin_logged_in'] = True
410
- flash('Connexion réussie !', 'success')
411
- return redirect(url_for('admin_bp.dashboard'))
412
- else:
413
- flash('Nom d\'utilisateur ou mot de passe incorrect.', 'danger')
414
-
415
- return render_template('admin/login.html')
416
-
417
- @admin_bp.route('/logout')
418
- def logout():
419
- session.pop('admin_logged_in', None)
420
- flash('Vous avez été déconnecté.', 'info')
421
- return redirect(url_for('admin_bp.login'))
422
-
423
- @admin_bp.route('/')
424
- @admin_required
425
- def dashboard():
426
- # Count of each entity type for dashboard stats
427
- stats = {
428
- 'matieres': Matiere.query.count(),
429
- 'sous_categories': SousCategorie.query.count(),
430
- 'textes': Texte.query.count(),
431
- 'images': Image.query.count()
432
- }
433
-
434
- # Get recent texts for dashboard
435
- recent_textes = Texte.query.order_by(Texte.updated_at.desc()).limit(5).all()
436
-
437
- return render_template('admin/dashboard.html', stats=stats, recent_textes=recent_textes)
438
-
439
- # Matières (Subjects) Management
440
- @admin_bp.route('/matieres', methods=['GET', 'POST'])
441
- @admin_required
442
- def matieres():
443
- if request.method == 'POST':
444
- action = request.form.get('action')
445
-
446
- if action == 'add':
447
- nom = request.form.get('nom', '').strip()
448
- color_code = request.form.get('color_code', '#3498db')
449
-
450
- if not nom:
451
- flash('Le nom de la matière est requis.', 'danger')
452
- else:
453
- matiere = Matiere.query.filter_by(nom=nom).first()
454
- if matiere:
455
- flash(f'La matière "{nom}" existe déjà.', 'warning')
456
- else:
457
- new_matiere = Matiere(nom=nom, color_code=color_code)
458
- db.session.add(new_matiere)
459
- db.session.commit()
460
- flash(f'Matière "{nom}" ajoutée avec succès !', 'success')
461
-
462
- elif action == 'edit':
463
- matiere_id = request.form.get('matiere_id')
464
- nom = request.form.get('nom', '').strip()
465
- color_code = request.form.get('color_code', '#3498db')
466
-
467
- if not matiere_id or not nom:
468
- flash('Informations incomplètes pour la modification.', 'danger')
469
- else:
470
- matiere = Matiere.query.get(matiere_id)
471
- if not matiere:
472
- flash('Matière non trouvée.', 'danger')
473
- else:
474
- existing = Matiere.query.filter_by(nom=nom).first()
475
- if existing and existing.id != int(matiere_id):
476
- flash(f'Une autre matière avec le nom "{nom}" existe déjà.', 'warning')
477
- else:
478
- matiere.nom = nom
479
- matiere.color_code = color_code
480
- db.session.commit()
481
- flash(f'Matière "{nom}" modifiée avec succès !', 'success')
482
-
483
- elif action == 'delete':
484
- matiere_id = request.form.get('matiere_id')
485
-
486
- if not matiere_id:
487
- flash('ID de matière manquant pour la suppression.', 'danger')
488
- else:
489
- matiere = Matiere.query.get(matiere_id)
490
- if not matiere:
491
- flash('Matière non trouvée.', 'danger')
492
- else:
493
- nom = matiere.nom
494
- db.session.delete(matiere)
495
- db.session.commit()
496
- flash(f'Matière "{nom}" supprimée avec succès !', 'success')
497
-
498
- matieres = Matiere.query.all()
499
- return render_template('admin/matieres.html', matieres=matieres)
500
-
501
- # Sous-Catégories (Subcategories) Management
502
- @admin_bp.route('/sous_categories', methods=['GET', 'POST'])
503
- @admin_required
504
- def sous_categories():
505
- if request.method == 'POST':
506
- action = request.form.get('action')
507
-
508
- if action == 'add':
509
- nom = request.form.get('nom', '').strip()
510
- matiere_id = request.form.get('matiere_id')
511
-
512
- if not nom or not matiere_id:
513
- flash('Le nom et la matière sont requis.', 'danger')
514
- else:
515
- sous_categorie = SousCategorie.query.filter_by(nom=nom, matiere_id=matiere_id).first()
516
- if sous_categorie:
517
- flash(f'La sous-catégorie "{nom}" existe déjà pour cette matière.', 'warning')
518
- else:
519
- new_sous_categorie = SousCategorie(nom=nom, matiere_id=matiere_id)
520
- db.session.add(new_sous_categorie)
521
- db.session.commit()
522
- flash(f'Sous-catégorie "{nom}" ajoutée avec succès !', 'success')
523
-
524
- elif action == 'edit':
525
- sous_categorie_id = request.form.get('sous_categorie_id')
526
- nom = request.form.get('nom', '').strip()
527
- matiere_id = request.form.get('matiere_id')
528
-
529
- if not sous_categorie_id or not nom or not matiere_id:
530
- flash('Informations incomplètes pour la modification.', 'danger')
531
- else:
532
- sous_categorie = SousCategorie.query.get(sous_categorie_id)
533
- if not sous_categorie:
534
- flash('Sous-catégorie non trouvée.', 'danger')
535
- else:
536
- existing = SousCategorie.query.filter_by(nom=nom, matiere_id=matiere_id).first()
537
- if existing and existing.id != int(sous_categorie_id):
538
- flash(f'Une sous-catégorie avec le nom "{nom}" existe déjà pour cette matière.', 'warning')
539
- else:
540
- sous_categorie.nom = nom
541
- sous_categorie.matiere_id = matiere_id
542
- db.session.commit()
543
- flash(f'Sous-catégorie "{nom}" modifiée avec succès !', 'success')
544
-
545
- elif action == 'delete':
546
- sous_categorie_id = request.form.get('sous_categorie_id')
547
-
548
- if not sous_categorie_id:
549
- flash('ID de sous-catégorie manquant pour la suppression.', 'danger')
550
- else:
551
- sous_categorie = SousCategorie.query.get(sous_categorie_id)
552
- if not sous_categorie:
553
- flash('Sous-catégorie non trouvée.', 'danger')
554
- else:
555
- nom = sous_categorie.nom
556
- db.session.delete(sous_categorie)
557
- db.session.commit()
558
- flash(f'Sous-catégorie "{nom}" supprimée avec succès !', 'success')
559
-
560
- sous_categories = SousCategorie.query.join(Matiere).all()
561
- matieres = Matiere.query.all()
562
- return render_template('admin/sous_categories.html', sous_categories=sous_categories, matieres=matieres)
563
-
564
- # Textes (Content) Management
565
- @admin_bp.route('/textes', methods=['GET', 'POST'])
566
- @admin_required
567
- def textes():
568
- if request.method == 'POST':
569
- action = request.form.get('action')
570
-
571
- if action == 'add':
572
- titre = request.form.get('titre', '').strip()
573
- sous_categorie_id = request.form.get('sous_categorie_id')
574
- contenu = request.form.get('contenu', '').strip()
575
-
576
- if not titre or not sous_categorie_id or not contenu:
577
- flash('Tous les champs sont requis.', 'danger')
578
- else:
579
- new_texte = Texte(
580
- titre=titre,
581
- sous_categorie_id=sous_categorie_id,
582
- contenu=contenu
583
- )
584
- db.session.add(new_texte)
585
- db.session.commit()
586
-
587
- # Parse content into blocks
588
- blocks = parse_content_to_blocks(contenu)
589
- for i, block_data in enumerate(blocks):
590
- new_block = ContentBlock(
591
- texte_id=new_texte.id,
592
- title=block_data['title'],
593
- content=block_data['content'],
594
- order=i
595
- )
596
- db.session.add(new_block)
597
-
598
- db.session.commit()
599
- flash(f'Texte "{titre}" ajouté avec succès !', 'success')
600
- return redirect(url_for('admin_bp.edit_texte', texte_id=new_texte.id))
601
-
602
- elif action == 'delete':
603
- texte_id = request.form.get('texte_id')
604
-
605
- if not texte_id:
606
- flash('ID de texte manquant pour la suppression.', 'danger')
607
- else:
608
- texte = Texte.query.get(texte_id)
609
- if not texte:
610
- flash('Texte non trouvé.', 'danger')
611
- else:
612
- titre = texte.titre
613
- db.session.delete(texte)
614
- db.session.commit()
615
- flash(f'Texte "{titre}" supprimé avec succès !', 'success')
616
-
617
- matieres = Matiere.query.all()
618
- textes = Texte.query.join(SousCategorie).join(Matiere).order_by(Matiere.nom, SousCategorie.nom, Texte.titre).all()
619
-
620
- # Group texts by matiere and sous_categorie for easier display
621
- grouped_textes = {}
622
- for texte in textes:
623
- matiere_id = texte.sous_categorie.matiere.id
624
- if matiere_id not in grouped_textes:
625
- grouped_textes[matiere_id] = {
626
- 'nom': texte.sous_categorie.matiere.nom,
627
- 'color': texte.sous_categorie.matiere.color_code,
628
- 'sous_categories': {}
629
- }
630
-
631
- sous_cat_id = texte.sous_categorie.id
632
- if sous_cat_id not in grouped_textes[matiere_id]['sous_categories']:
633
- grouped_textes[matiere_id]['sous_categories'][sous_cat_id] = {
634
- 'nom': texte.sous_categorie.nom,
635
- 'textes': []
636
- }
637
-
638
- grouped_textes[matiere_id]['sous_categories'][sous_cat_id]['textes'].append({
639
- 'id': texte.id,
640
- 'titre': texte.titre,
641
- 'updated_at': texte.updated_at
642
- })
643
-
644
- sous_categories = SousCategorie.query.all()
645
- return render_template('admin/textes.html', grouped_textes=grouped_textes, matieres=matieres, sous_categories=sous_categories)
646
-
647
- @admin_bp.route('/textes/edit/<int:texte_id>', methods=['GET', 'POST'])
648
- @admin_required
649
- def edit_texte(texte_id):
650
- texte = Texte.query.get_or_404(texte_id)
651
-
652
- if request.method == 'POST':
653
- action = request.form.get('action')
654
-
655
- if action == 'update_basic':
656
- # Basic text info update
657
- titre = request.form.get('titre', '').strip()
658
- sous_categorie_id = request.form.get('sous_categorie_id')
659
-
660
- if not titre or not sous_categorie_id:
661
- flash('Le titre et la sous-catégorie sont requis.', 'danger')
662
- else:
663
- # Save previous content for history
664
- historique = TexteHistorique(
665
- texte_id=texte.id,
666
- contenu_precedent=texte.contenu
667
- )
668
- db.session.add(historique)
669
-
670
- texte.titre = titre
671
- texte.sous_categorie_id = sous_categorie_id
672
-
673
- db.session.commit()
674
- flash('Informations de base mises à jour avec succès !', 'success')
675
-
676
- elif action == 'update_blocks':
677
- # Update content blocks
678
- blocks_data = json.loads(request.form.get('blocks_data', '[]'))
679
-
680
- # Save previous content for history if changes were made
681
- historique = TexteHistorique(
682
- texte_id=texte.id,
683
- contenu_precedent=texte.contenu
684
- )
685
- db.session.add(historique)
686
-
687
- # Update all blocks
688
- # First, remove existing blocks
689
- for block in texte.content_blocks:
690
- db.session.delete(block)
691
-
692
- # Then create new blocks from the submitted data
693
- new_content = []
694
- for i, block_data in enumerate(blocks_data):
695
- new_block = ContentBlock(
696
- texte_id=texte.id,
697
- title=block_data.get('title'),
698
- content=block_data.get('content', ''),
699
- order=i,
700
- image_position=block_data.get('image_position', 'left')
701
- )
702
-
703
- # Set image if provided
704
- image_id = block_data.get('image_id')
705
- if image_id and image_id != 'null' and image_id != 'undefined':
706
- new_block.image_id = image_id
707
-
708
- db.session.add(new_block)
709
-
710
- # Append to full content for the main text field
711
- if new_block.title:
712
- new_content.append(new_block.title)
713
- new_content.append(new_block.content)
714
-
715
- # Update the main content field to match block content
716
- texte.contenu = "\n\n".join(new_content)
717
-
718
- db.session.commit()
719
- flash('Contenu mis à jour avec succès !', 'success')
720
-
721
- elif action == 'upload_image':
722
- if 'image' not in request.files:
723
- return jsonify({'success': False, 'error': 'Aucun fichier trouvé'})
724
-
725
- file = request.files['image']
726
- if file.filename == '':
727
- return jsonify({'success': False, 'error': 'Aucun fichier sélectionné'})
728
-
729
- # Check file type (accept only images)
730
- allowed_mimetypes = ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml']
731
- if file.mimetype not in allowed_mimetypes:
732
- return jsonify({'success': False, 'error': 'Type de fichier non autorisé'})
733
-
734
- # Read file data
735
- file_data = file.read()
736
- if not file_data:
737
- return jsonify({'success': False, 'error': 'Fichier vide'})
738
-
739
- # Create new image record
740
- new_image = Image(
741
- nom_fichier=secure_filename(file.filename),
742
- mime_type=file.mimetype,
743
- data=file_data,
744
- alt_text=request.form.get('alt_text', '')
745
- )
746
- db.session.add(new_image)
747
- db.session.commit()
748
-
749
- # Return image details
750
- image_data = base64.b64encode(new_image.data).decode('utf-8')
751
- return jsonify({
752
- 'success': True,
753
- 'image': {
754
- 'id': new_image.id,
755
- 'filename': new_image.nom_fichier,
756
- 'src': f"data:{new_image.mime_type};base64,{image_data}",
757
- 'alt': new_image.alt_text or "Image illustration"
758
- }
759
- })
760
-
761
- # Get existing content blocks or create them if none exist
762
- if not texte.content_blocks:
763
- # Parse content into blocks
764
- blocks = parse_content_to_blocks(texte.contenu)
765
- for i, block_data in enumerate(blocks):
766
- new_block = ContentBlock(
767
- texte_id=texte.id,
768
- title=block_data['title'],
769
- content=block_data['content'],
770
- order=i
771
- )
772
- db.session.add(new_block)
773
- db.session.commit()
774
-
775
- # Reload the texte to get the new blocks
776
- texte = Texte.query.get(texte_id)
777
-
778
- # Prepare block data for template
779
- blocks = []
780
- for block in texte.content_blocks:
781
- block_data = {
782
- 'id': block.id,
783
- 'title': block.title,
784
- 'content': block.content,
785
- 'order': block.order,
786
- 'image_position': block.image_position,
787
- 'image': None
788
- }
789
-
790
- # Add image data if available
791
- if block.image:
792
- image_data = base64.b64encode(block.image.data).decode('utf-8')
793
- block_data['image'] = {
794
- 'id': block.image.id,
795
- 'src': f"data:{block.image.mime_type};base64,{image_data}",
796
- 'alt': block.image.alt_text or block.title or "Image illustration"
797
- }
798
-
799
- blocks.append(block_data)
800
-
801
- # Get all images for selection
802
- images = Image.query.order_by(Image.uploaded_at.desc()).all()
803
- images_data = []
804
- for image in images:
805
- image_data = base64.b64encode(image.data).decode('utf-8')
806
- images_data.append({
807
- 'id': image.id,
808
- 'filename': image.nom_fichier,
809
- 'src': f"data:{image.mime_type};base64,{image_data}",
810
- 'alt': image.alt_text or "Image illustration"
811
- })
812
-
813
- sous_categories = SousCategorie.query.all()
814
- return render_template('admin/edit_texte.html',
815
- texte=texte,
816
- blocks=blocks,
817
- images=images_data,
818
- sous_categories=sous_categories)
819
-
820
- @admin_bp.route('/historique/<int:texte_id>')
821
- @admin_required
822
- def historique(texte_id):
823
- texte = Texte.query.get_or_404(texte_id)
824
- historiques = TexteHistorique.query.filter_by(texte_id=texte_id).order_by(TexteHistorique.date_modification.desc()).all()
825
-
826
- return render_template('admin/historique.html', texte=texte, historiques=historiques)
827
-
828
- @admin_bp.route('/images', methods=['GET', 'POST'])
829
- @admin_required
830
- def images():
831
- if request.method == 'POST':
832
- action = request.form.get('action')
833
-
834
- if action == 'upload':
835
- if 'image' not in request.files:
836
- flash('Aucun fichier trouvé.', 'danger')
837
- return redirect(request.url)
838
-
839
- file = request.files['image']
840
- if file.filename == '':
841
- flash('Aucun fichier sélectionné.', 'danger')
842
- return redirect(request.url)
843
-
844
- # Check file type (accept only images)
845
- allowed_mimetypes = ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml']
846
- if file.mimetype not in allowed_mimetypes:
847
- flash('Type de fichier non autorisé.', 'danger')
848
- return redirect(request.url)
849
-
850
- # Read file data
851
- file_data = file.read()
852
- if not file_data:
853
- flash('Fichier vide.', 'danger')
854
- return redirect(request.url)
855
-
856
- # Create new image record
857
- alt_text = request.form.get('alt_text', '')
858
- description = request.form.get('description', '')
859
-
860
- new_image = Image(
861
- nom_fichier=secure_filename(file.filename),
862
- mime_type=file.mimetype,
863
- data=file_data,
864
- alt_text=alt_text,
865
- description=description
866
- )
867
- db.session.add(new_image)
868
- db.session.commit()
869
-
870
- flash('Image téléchargée avec succès !', 'success')
871
-
872
- elif action == 'delete':
873
- image_id = request.form.get('image_id')
874
- if image_id:
875
- image = Image.query.get(image_id)
876
- if image:
877
- # Check if image is used in any content block
878
- usage_count = ContentBlock.query.filter_by(image_id=image.id).count()
879
- if usage_count > 0:
880
- flash(f'Cette image est utilisée dans {usage_count} blocs de contenu. Veuillez les modifier avant de supprimer l\'image.', 'warning')
881
- else:
882
- db.session.delete(image)
883
- db.session.commit()
884
- flash('Image supprimée avec succès !', 'success')
885
- else:
886
- flash('Image non trouvée.', 'danger')
887
- else:
888
- flash('ID d\'image manquant.', 'danger')
889
-
890
- elif action == 'update':
891
- image_id = request.form.get('image_id')
892
- alt_text = request.form.get('alt_text', '')
893
- description = request.form.get('description', '')
894
-
895
- if image_id:
896
- image = Image.query.get(image_id)
897
- if image:
898
- image.alt_text = alt_text
899
- image.description = description
900
- db.session.commit()
901
- flash('Informations de l\'image mises à jour avec succès !', 'success')
902
- else:
903
- flash('Image non trouvée.', 'danger')
904
- else:
905
- flash('ID d\'image manquant.', 'danger')
906
-
907
- # Get all images
908
- images = Image.query.order_by(Image.uploaded_at.desc()).all()
909
- images_data = []
910
- for image in images:
911
- image_data = base64.b64encode(image.data).decode('utf-8')
912
- images_data.append({
913
- 'id': image.id,
914
- 'filename': image.nom_fichier,
915
- 'description': image.description,
916
- 'alt_text': image.alt_text,
917
- 'uploaded_at': image.uploaded_at,
918
- 'src': f"data:{image.mime_type};base64,{image_data}"
919
- })
920
-
921
- return render_template('admin/images.html', images=images_data)
922
-
923
- # ---------------------------------------------------------------------------
924
- # Register blueprints and setup database
925
- # ---------------------------------------------------------------------------
926
-
927
- app.register_blueprint(main_bp)
928
- app.register_blueprint(admin_bp)
929
-
930
- # Create tables if they don't exist
931
- with app.app_context():
932
- db.create_all()
933
-
934
- # Application entry point
935
- @app.route('/')
936
- def index():
937
- return redirect(url_for('main_bp.index'))
938
-
939
- # ---------------------------------------------------------------------------
940
- # Serve the app - This is only used when running locally
941
- # ---------------------------------------------------------------------------
942
-
943
- if __name__ == '__main__':
944
- app.run(host=HOST, port=PORT, debug=DEBUG)
945
-
946
- # For Vercel serverless function
947
- def app_handler(environ, start_response):
948
- return app(environ, start_response)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
api/static/css/style.css DELETED
@@ -1,762 +0,0 @@
1
- /* Base Styles & Theme Variables */
2
- :root {
3
- /* Light theme */
4
- --background-color: #f8f9fa;
5
- --text-color: #212529;
6
- --primary-color: #3498db;
7
- --secondary-color: #2ecc71;
8
- --accent-color: #e74c3c;
9
- --muted-color: #95a5a6;
10
- --border-color: #dee2e6;
11
- --card-bg: #ffffff;
12
- --header-bg: #ffffff;
13
- --footer-bg: #f1f1f1;
14
- --shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
15
- --hover-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
16
- --block-bg: #f8f9fa;
17
- --block-border: 1px solid #dee2e6;
18
- --input-bg: #ffffff;
19
- --btn-primary-bg: var(--primary-color);
20
- --btn-primary-text: #ffffff;
21
- --btn-secondary-bg: #6c757d;
22
- --btn-secondary-text: #ffffff;
23
- --modal-bg: #ffffff;
24
- --sidebar-bg: #ffffff; /* Added for consistency */
25
- --sidebar-border: #dee2e6; /* Added */
26
- --sidebar-hover-bg: rgba(52, 152, 219, 0.1); /* Added */
27
- }
28
-
29
- [data-theme="dark"] {
30
- /* Dark theme */
31
- --background-color: #121212;
32
- --text-color: #e0e0e0;
33
- --primary-color: #3498db;
34
- --secondary-color: #2ecc71;
35
- --accent-color: #e74c3c;
36
- --muted-color: #95a5a6;
37
- --border-color: #333333;
38
- --card-bg: #1e1e1e;
39
- --header-bg: #1a1a1a;
40
- --footer-bg: #1a1a1a;
41
- --shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
42
- --hover-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
43
- --block-bg: #1e1e1e;
44
- --block-border: 1px solid #333333;
45
- --input-bg: #2c2c2c;
46
- --btn-primary-bg: var(--primary-color);
47
- --btn-primary-text: #ffffff;
48
- --btn-secondary-bg: #555555;
49
- --btn-secondary-text: #ffffff;
50
- --modal-bg: #1e1e1e;
51
- --sidebar-bg: #1e1e1e; /* Added for dark theme */
52
- --sidebar-border: #333333; /* Added */
53
- --sidebar-hover-bg: rgba(52, 152, 219, 0.2); /* Slightly different hover for dark */
54
- }
55
-
56
- /* General Styles */
57
- body {
58
- background-color: var(--background-color);
59
- color: var(--text-color);
60
- font-family: 'Roboto', 'Segoe UI', 'Arial', sans-serif;
61
- line-height: 1.6;
62
- transition: background-color 0.3s ease, color 0.3s ease;
63
- margin: 0;
64
- padding: 0;
65
- min-height: 100vh;
66
- display: flex;
67
- flex-direction: column;
68
- }
69
-
70
- a {
71
- color: var(--primary-color);
72
- text-decoration: none;
73
- transition: color 0.3s ease;
74
- }
75
-
76
- a:hover {
77
- color: #2980b9;
78
- text-decoration: underline;
79
- }
80
-
81
- /* Header Styles */
82
- .main-header {
83
- background-color: var(--header-bg);
84
- padding: 1rem 0;
85
- box-shadow: var(--shadow);
86
- position: sticky;
87
- top: 0;
88
- z-index: 1030; /* Ensure header is above general content but below modals/sidebars if needed */
89
- transition: background-color 0.3s ease, box-shadow 0.3s ease;
90
- }
91
-
92
- /* MODIFIED: Header Container Layout */
93
- .header-container {
94
- display: flex;
95
- align-items: center;
96
- /* Removed justify-content: space-between; to accommodate burger */
97
- }
98
-
99
- /* NEW: Burger Menu Button */
100
- .burger-menu-button {
101
- background: none;
102
- border: none;
103
- font-size: 1.8rem; /* Match site title size */
104
- color: var(--text-color);
105
- cursor: pointer;
106
- padding: 0.5rem;
107
- margin-right: 1rem; /* Space between burger and title */
108
- line-height: 1;
109
- transition: color 0.3s ease;
110
- }
111
-
112
- .burger-menu-button:hover {
113
- color: var(--primary-color);
114
- }
115
-
116
- /* MODIFIED: Site Title Layout */
117
- .site-title {
118
- flex-grow: 1; /* Allow title to take available space */
119
- text-align: center; /* Center title between burger and actions */
120
- font-size: 1.8rem;
121
- font-weight: bold;
122
- margin: 0; /* Remove default margins */
123
- }
124
-
125
- .site-title a {
126
- color: var(--text-color);
127
- text-decoration: none;
128
- }
129
-
130
- /* Header Actions (Theme toggle, Admin) - Ensure they stay aligned */
131
- .header-actions {
132
- display: flex;
133
- align-items: center;
134
- margin-left: 1rem; /* Space between title and actions */
135
- }
136
-
137
-
138
- /* Theme Toggle */
139
- .theme-toggle {
140
- background: none;
141
- border: none;
142
- cursor: pointer;
143
- color: var(--text-color);
144
- font-size: 1.5rem;
145
- padding: 0.5rem;
146
- transition: color 0.3s ease, transform 0.3s ease;
147
- }
148
-
149
- .theme-toggle:hover {
150
- color: var(--primary-color);
151
- transform: rotate(15deg);
152
- }
153
-
154
- /* Main Content */
155
- .main-content {
156
- flex: 1;
157
- padding: 2rem 0; /* Maintain padding for content area */
158
- /* Removed container adjustments, rely on bootstrap's container inside */
159
- }
160
-
161
-
162
- /* --- NEW: Sidebar Styles --- */
163
-
164
- /* Sidebar Overlay */
165
- .sidebar-overlay {
166
- position: fixed;
167
- top: 0;
168
- left: 0;
169
- width: 100%;
170
- height: 100%;
171
- background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent black */
172
- z-index: 1040; /* Below sidebars, above content */
173
- opacity: 0;
174
- visibility: hidden;
175
- transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out;
176
- }
177
-
178
- .sidebar-overlay.active {
179
- opacity: 1;
180
- visibility: visible;
181
- }
182
-
183
- /* Sidebar Base Styles */
184
- .sidebar {
185
- position: fixed;
186
- top: 0;
187
- left: 0; /* Both sidebars originate from the left */
188
- width: 300px; /* Default width, adjust as needed */
189
- height: 100%;
190
- background-color: var(--sidebar-bg); /* Use theme variable */
191
- box-shadow: var(--shadow);
192
- z-index: 1050; /* Above overlay */
193
- display: flex;
194
- flex-direction: column;
195
- transform: translateX(-100%); /* Initially hidden off-screen to the left */
196
- transition: transform 0.3s ease-in-out;
197
- border-right: 1px solid var(--sidebar-border); /* Use theme variable */
198
- }
199
-
200
- .sidebar.active {
201
- transform: translateX(0); /* Slide in from the left */
202
- }
203
-
204
- /* Sidebar Header */
205
- .sidebar-header {
206
- display: flex;
207
- justify-content: space-between;
208
- align-items: center;
209
- padding: 1rem 1.5rem;
210
- border-bottom: 1px solid var(--sidebar-border); /* Use theme variable */
211
- background-color: var(--header-bg); /* Optional: Match header background */
212
- flex-shrink: 0; /* Prevent header from shrinking */
213
- }
214
-
215
- .sidebar-header h4 {
216
- margin: 0;
217
- font-size: 1.2rem;
218
- font-weight: 600;
219
- flex-grow: 1; /* Allow title to take space */
220
- margin-left: 10px; /* Space after back button if present */
221
- overflow: hidden; /* Prevent long titles from breaking layout */
222
- white-space: nowrap;
223
- text-overflow: ellipsis;
224
- }
225
-
226
- /* Sidebar Back and Close Buttons */
227
- .sidebar-back-button,
228
- .close-sidebar-btn {
229
- background: none;
230
- border: none;
231
- font-size: 1.5rem;
232
- color: var(--muted-color);
233
- cursor: pointer;
234
- padding: 0.25rem;
235
- line-height: 1;
236
- transition: color 0.2s ease;
237
- }
238
- .sidebar-back-button:hover,
239
- .close-sidebar-btn:hover {
240
- color: var(--text-color);
241
- }
242
-
243
- /* Sidebar List (Matières & Sous-catégories) */
244
- .sidebar-list {
245
- list-style: none;
246
- padding: 0;
247
- margin: 0;
248
- flex-grow: 1; /* Take remaining vertical space */
249
- overflow-y: auto; /* Enable vertical scrolling for long lists */
250
- overflow-x: hidden; /* Prevent horizontal scrollbars */
251
- }
252
-
253
- .sidebar-list li {
254
- padding: 0.8rem 1.5rem;
255
- cursor: pointer;
256
- border-bottom: 1px solid var(--sidebar-border); /* Use theme variable */
257
- transition: background-color 0.2s ease;
258
- display: flex;
259
- justify-content: space-between; /* Push icon to the right */
260
- align-items: center;
261
- white-space: nowrap; /* Prevent text wrapping */
262
- }
263
-
264
- .sidebar-list li:last-child {
265
- border-bottom: none; /* Remove border from last item */
266
- }
267
-
268
- .sidebar-list li:hover {
269
- background-color: var(--sidebar-hover-bg); /* Use theme variable */
270
- }
271
-
272
- .sidebar-list li i.fa-chevron-right,
273
- .sidebar-list li i.fa-arrow-left { /* Style for icons inside list items */
274
- color: var(--muted-color);
275
- font-size: 0.9em;
276
- margin-left: 0.5rem; /* Space between text and icon */
277
- }
278
-
279
- /* Optional: Style for active/selected item in sidebar */
280
- .sidebar-list li.active {
281
- background-color: var(--primary-color);
282
- color: white;
283
- }
284
- .sidebar-list li.active i { /* Make icon white too */
285
- color: white;
286
- }
287
-
288
-
289
- /* --- End of Sidebar Styles --- */
290
-
291
-
292
- /* NEW: Initial Instructions Area */
293
- .initial-instructions {
294
- background-color: var(--card-bg);
295
- padding: 2rem;
296
- border-radius: 8px;
297
- box-shadow: var(--shadow);
298
- border: 1px solid var(--border-color);
299
- margin-top: 2rem; /* Adjust as needed */
300
- text-align: center; /* Center content */
301
- }
302
-
303
- /* --- Original Styles (with minor adjustments if needed) --- */
304
-
305
- /* Flash Messages Container */
306
- .flash-container {
307
- position: relative; /* Keep in normal flow */
308
- z-index: 100; /* Below header but above main content */
309
- }
310
-
311
- /* Subject Selection Cards (Original - No longer used on index, but might be elsewhere) */
312
- .subject-card {
313
- background-color: var(--card-bg);
314
- border-radius: 8px;
315
- padding: 1.5rem;
316
- margin-bottom: 1.5rem;
317
- box-shadow: var(--shadow);
318
- transition: box-shadow 0.3s ease, transform 0.3s ease;
319
- cursor: pointer;
320
- }
321
-
322
- .subject-card:hover {
323
- box-shadow: var(--hover-shadow);
324
- transform: translateY(-5px);
325
- }
326
-
327
- .subject-card h3 {
328
- margin-top: 0;
329
- margin-bottom: 1rem;
330
- font-size: 1.5rem;
331
- color: var(--text-color);
332
- }
333
-
334
- /* Subject Color-Coding */
335
- .subject-indicator {
336
- width: 100%;
337
- height: 8px;
338
- border-radius: 4px;
339
- margin-bottom: 1rem;
340
- }
341
-
342
- /* Content Blocks Styling (Used in Content Display Section) */
343
- .content-block {
344
- background-color: var(--block-bg);
345
- border-radius: 8px;
346
- padding: 1.5rem;
347
- margin-bottom: 2rem;
348
- box-shadow: var(--shadow);
349
- border: var(--block-border);
350
- transition: box-shadow 0.3s ease;
351
- overflow: hidden; /* Prevent content spill */
352
- /* border-left: 4px solid transparent; -> Now handled dynamically by JS or remove if not needed */
353
- }
354
-
355
- .content-block:hover {
356
- box-shadow: var(--hover-shadow);
357
- }
358
-
359
- .content-block-title {
360
- font-size: 1.4rem;
361
- font-weight: bold;
362
- margin-top: 0;
363
- margin-bottom: 1rem;
364
- color: var(--text-color);
365
- padding-bottom: 0.5rem;
366
- border-bottom: 2px solid var(--primary-color); /* Default color, JS might override */
367
- }
368
-
369
- .content-block-content {
370
- line-height: 1.7;
371
- }
372
-
373
- /* Image positioning styles */
374
- .block-with-image {
375
- display: flex;
376
- flex-wrap: wrap;
377
- gap: 1.5rem;
378
- align-items: flex-start;
379
- }
380
-
381
- .block-with-image.image-left { flex-direction: row; }
382
- .block-with-image.image-right { flex-direction: row-reverse; }
383
- .block-with-image.image-top { flex-direction: column; }
384
-
385
- .block-image-container {
386
- flex: 0 0 auto;
387
- max-width: 30%; /* Default max width for side images */
388
- margin-bottom: 1rem; /* Spacing below image if wrapped */
389
- }
390
-
391
- .block-with-image.image-top .block-image-container {
392
- max-width: 100%; /* Full width for top images */
393
- margin-bottom: 1.5rem;
394
- }
395
-
396
- .block-image {
397
- width: 100%;
398
- height: auto;
399
- border-radius: 6px;
400
- box-shadow: var(--shadow);
401
- display: block; /* Prevent extra space below image */
402
- }
403
-
404
- .block-content-container {
405
- flex: 1; /* Take remaining space */
406
- min-width: 60%; /* Ensure text has enough space on wider screens */
407
- }
408
-
409
- /* Category Selection UI (Original - No longer used on index) */
410
- .selection-container {
411
- background-color: var(--card-bg);
412
- border-radius: 8px;
413
- padding: 1.5rem;
414
- margin-bottom: 2rem;
415
- box-shadow: var(--shadow);
416
- }
417
-
418
- .selection-title {
419
- font-size: 1.2rem;
420
- font-weight: 600;
421
- margin-bottom: 1rem;
422
- padding-bottom: 0.5rem;
423
- border-bottom: 1px solid var(--border-color);
424
- }
425
-
426
- .selection-list {
427
- list-style: none;
428
- padding: 0;
429
- margin: 0;
430
- }
431
-
432
- .selection-item {
433
- padding: 0.5rem 0.8rem;
434
- margin-bottom: 0.5rem;
435
- border-radius: 4px;
436
- cursor: pointer;
437
- transition: background-color 0.3s ease;
438
- }
439
-
440
- .selection-item:hover {
441
- background-color: rgba(52, 152, 219, 0.1);
442
- }
443
-
444
- .selection-item.active {
445
- background-color: var(--primary-color);
446
- color: white;
447
- }
448
-
449
- /* Content Viewer (Container for title and blocks) */
450
- .content-viewer {
451
- background-color: var(--card-bg);
452
- border-radius: 8px;
453
- padding: 2rem;
454
- box-shadow: var(--shadow);
455
- margin-bottom: 2rem;
456
- }
457
-
458
- .content-title {
459
- font-size: 2rem;
460
- margin-top: 0;
461
- margin-bottom: 1.5rem;
462
- padding-bottom: 0.5rem;
463
- border-bottom: 2px solid var(--primary-color); /* Default color, JS might override */
464
- }
465
-
466
- /* Buttons */
467
- .btn {
468
- display: inline-block;
469
- font-weight: 500;
470
- text-align: center;
471
- white-space: nowrap;
472
- vertical-align: middle;
473
- user-select: none;
474
- border: 1px solid transparent;
475
- padding: 0.5rem 1rem;
476
- font-size: 1rem;
477
- line-height: 1.5;
478
- border-radius: 0.25rem;
479
- transition: all 0.15s ease-in-out;
480
- cursor: pointer;
481
- }
482
-
483
- .btn-primary {
484
- background-color: var(--btn-primary-bg);
485
- color: var(--btn-primary-text);
486
- border-color: var(--btn-primary-bg); /* Added for consistency */
487
- }
488
-
489
- .btn-primary:hover {
490
- background-color: #2980b9; /* Darken primary */
491
- border-color: #2980b9;
492
- color: white;
493
- }
494
-
495
- .btn-secondary {
496
- background-color: var(--btn-secondary-bg);
497
- color: var(--btn-secondary-text);
498
- border-color: var(--btn-secondary-bg); /* Added for consistency */
499
- }
500
-
501
- .btn-secondary:hover {
502
- background-color: #5a6268; /* Darken secondary */
503
- border-color: #5a6268;
504
- color: white;
505
- }
506
-
507
- /* Form Elements */
508
- input, select, textarea {
509
- background-color: var(--input-bg);
510
- color: var(--text-color);
511
- border: 1px solid var(--border-color);
512
- border-radius: 4px;
513
- padding: 0.75rem 1rem;
514
- width: 100%;
515
- transition: border-color 0.3s ease, box-shadow 0.3s ease;
516
- }
517
-
518
- input:focus, select:focus, textarea:focus {
519
- border-color: var(--primary-color);
520
- outline: none;
521
- box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.25);
522
- }
523
-
524
- label {
525
- display: block;
526
- margin-bottom: 0.5rem;
527
- font-weight: 500;
528
- }
529
-
530
- .form-group {
531
- margin-bottom: 1.5rem;
532
- }
533
-
534
- /* Footer */
535
- .main-footer {
536
- background-color: var(--footer-bg);
537
- padding: 2rem 0;
538
- box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.05);
539
- margin-top: auto; /* Push footer to bottom */
540
- flex-shrink: 0; /* Prevent footer from shrinking */
541
- }
542
-
543
- .footer-content {
544
- display: flex;
545
- justify-content: space-between;
546
- flex-wrap: wrap;
547
- gap: 1rem; /* Add gap for wrapping */
548
- }
549
-
550
- .footer-links {
551
- list-style: none;
552
- padding: 0;
553
- margin: 0; /* Reset margin */
554
- display: flex;
555
- flex-wrap: wrap; /* Allow links to wrap */
556
- gap: 1.5rem;
557
- }
558
-
559
- .feedback-form {
560
- margin-top: 1rem;
561
- max-width: 400px; /* Limit width of feedback form */
562
- }
563
-
564
- .feedback-title {
565
- font-size: 1.2rem;
566
- margin-bottom: 0.75rem;
567
- }
568
-
569
- /* Admin specific styles (Keep if used on other pages) */
570
- .admin-container { /* ... Keep original ... */ }
571
- .admin-title { /* ... Keep original ... */ }
572
- .admin-sidebar { /* ... Keep original ... */ }
573
- .admin-nav { /* ... Keep original ... */ }
574
- .admin-nav-item { /* ... Keep original ... */ }
575
- .admin-nav-link { /* ... Keep original ... */ }
576
- .admin-nav-link i { /* ... Keep original ... */ }
577
- .admin-card { /* ... Keep original ... */ }
578
- .admin-card-title { /* ... Keep original ... */ }
579
- .admin-stat { /* ... Keep original ... */ }
580
-
581
- /* Editor styles (Keep if used on other pages) */
582
- .editor-container { /* ... Keep original ... */ }
583
- .block-editor { /* ... Keep original ... */ }
584
- .block-editor-header { /* ... Keep original ... */ }
585
- .block-editor-title { /* ... Keep original ... */ }
586
- .block-editor-actions { /* ... Keep original ... */ }
587
- .block-handle { /* ... Keep original ... */ }
588
- .image-preview { /* ... Keep original ... */ }
589
- .image-gallery { /* ... Keep original ... */ }
590
- .gallery-item { /* ... Keep original ... */ }
591
- .gallery-image { /* ... Keep original ... */ }
592
- .gallery-image-select { /* ... Keep original ... */ }
593
-
594
- /* Login screen (Keep if used) */
595
- .login-container { /* ... Keep original ... */ }
596
- .login-logo { /* ... Keep original ... */ }
597
- .login-title { /* ... Keep original ... */ }
598
-
599
- /* Responsive design adjustments */
600
- @media (max-width: 992px) {
601
- /* No change needed here if using Bootstrap's container correctly */
602
- }
603
-
604
- @media (max-width: 768px) {
605
- /* Content Block Images: Stack vertically */
606
- .block-with-image {
607
- flex-direction: column !important; /* Force column layout */
608
- }
609
- .block-image-container {
610
- max-width: 100%; /* Image takes full width */
611
- margin-bottom: 1.5rem; /* Space below image */
612
- }
613
- .block-content-container {
614
- min-width: 100%; /* Text takes full width */
615
- }
616
-
617
- /* Sidebar width adjustment */
618
- .sidebar {
619
- width: 85%; /* Make sidebar wider on mobile */
620
- max-width: 320px; /* Optional: Set a max width */
621
- }
622
-
623
- /* Optional: Adjust header layout on smaller screens */
624
- .site-title {
625
- font-size: 1.5rem; /* Slightly smaller title */
626
- text-align: left; /* Align title next to burger */
627
- margin-left: 0.5rem; /* Adjust spacing */
628
- }
629
-
630
- .header-actions {
631
- margin-left: auto; /* Push actions to the right */
632
- }
633
- }
634
-
635
-
636
- @media (max-width: 576px) {
637
- .subject-card { /* Style for cards if used elsewhere */
638
- padding: 1rem;
639
- }
640
-
641
- .content-viewer {
642
- padding: 1.5rem; /* Less padding inside content area */
643
- }
644
-
645
- .admin-container {
646
- padding: 1.5rem;
647
- }
648
-
649
- .content-title {
650
- font-size: 1.6rem; /* Smaller main title */
651
- }
652
-
653
- .footer-content {
654
- flex-direction: column; /* Stack footer sections */
655
- align-items: center;
656
- text-align: center;
657
- }
658
- .footer-links {
659
- justify-content: center; /* Center footer links */
660
- margin-top: 0.5rem;
661
- }
662
- .feedback-form {
663
- width: 100%; /* Full width feedback form */
664
- margin-top: 1.5rem;
665
- }
666
- }
667
-
668
- /* Animation keyframes (Keep original) */
669
- @keyframes fadeIn {
670
- from { opacity: 0; }
671
- to { opacity: 1; }
672
- }
673
-
674
- .fade-in {
675
- animation: fadeIn 0.5s ease-in forwards; /* Added forwards to keep state */
676
- }
677
-
678
- @keyframes slideInUp {
679
- from {
680
- transform: translateY(50px);
681
- opacity: 0;
682
- }
683
- to {
684
- transform: translateY(0);
685
- opacity: 1;
686
- }
687
- }
688
-
689
- .slide-in-up {
690
- animation: slideInUp 0.5s ease-out forwards; /* Added forwards */
691
- }
692
-
693
- /* Utility classes (Keep original) */
694
- .text-center { text-align: center; }
695
- .mb-1 { margin-bottom: 0.5rem !important; }
696
- .mb-2 { margin-bottom: 1rem !important; }
697
- .mb-3 { margin-bottom: 1.5rem !important; }
698
- .mb-4 { margin-bottom: 2rem !important; }
699
- .mt-1 { margin-top: 0.5rem !important; }
700
- .mt-2 { margin-top: 1rem !important; }
701
- .mt-3 { margin-top: 1.5rem !important; }
702
- .mt-4 { margin-top: 2rem !important; }
703
- .ms-2 { margin-left: 0.5rem !important; } /* Bootstrap utility */
704
- .float-end { float: right !important; } /* Bootstrap utility */
705
- .d-none { display: none !important; } /* Bootstrap utility */
706
-
707
-
708
- /* Alert Styles (Keep original) */
709
- .alert {
710
- padding: 1rem;
711
- margin-bottom: 1rem;
712
- border-radius: 4px;
713
- border: 1px solid transparent; /* Base border */
714
- }
715
-
716
- .alert-dismissible .btn-close {
717
- padding: 1.25rem 1rem; /* Adjust close button padding */
718
- }
719
-
720
- .alert-success {
721
- color: #0f5132;
722
- background-color: #d1e7dd;
723
- border-color: #badbcc;
724
- }
725
- [data-theme="dark"] .alert-success {
726
- color: #75b798;
727
- background-color: #192c23;
728
- border-color: #254134;
729
- }
730
-
731
- .alert-danger {
732
- color: #842029;
733
- background-color: #f8d7da;
734
- border-color: #f5c2c7;
735
- }
736
- [data-theme="dark"] .alert-danger {
737
- color: #ea868f;
738
- background-color: #341a1d;
739
- border-color: #50272c;
740
- }
741
-
742
- .alert-warning {
743
- color: #664d03;
744
- background-color: #fff3cd;
745
- border-color: #ffecb5;
746
- }
747
- [data-theme="dark"] .alert-warning {
748
- color: #ffda6a;
749
- background-color: #33270a;
750
- border-color: #4d3b0f;
751
- }
752
-
753
- .alert-info {
754
- color: #055160;
755
- background-color: #cff4fc;
756
- border-color: #b6effb;
757
- }
758
- [data-theme="dark"] .alert-info {
759
- color: #6edff6;
760
- background-color: #0f2d36;
761
- border-color: #164350;
762
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
api/static/js/admin.js DELETED
@@ -1,676 +0,0 @@
1
- // Admin JavaScript for the backend management interface
2
-
3
- document.addEventListener('DOMContentLoaded', function() {
4
- // Initialize theme
5
- initTheme();
6
-
7
- // Setup dashboard functionality
8
- setupDashboardCards();
9
-
10
- // Setup admin forms
11
- setupMatiereForm();
12
- setupSousCategorieForm();
13
- setupTexteForm();
14
-
15
- // Setup content block editor
16
- setupContentBlockEditor();
17
-
18
- // Setup image management
19
- setupImageUploader();
20
- setupImageGallery();
21
-
22
- // Setup theme toggle
23
- setupThemeToggle();
24
- });
25
-
26
- // Initialize theme based on user preference
27
- function initTheme() {
28
- const userPreference = localStorage.getItem('theme') || 'light';
29
- document.documentElement.setAttribute('data-theme', userPreference);
30
-
31
- // Update theme icon
32
- updateThemeIcon(userPreference);
33
- }
34
-
35
- // Setup theme toggle functionality
36
- function setupThemeToggle() {
37
- const themeToggle = document.getElementById('theme-toggle');
38
- if (!themeToggle) return;
39
-
40
- themeToggle.addEventListener('click', function() {
41
- const currentTheme = document.documentElement.getAttribute('data-theme');
42
- const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
43
-
44
- // Update theme attribute
45
- document.documentElement.setAttribute('data-theme', newTheme);
46
-
47
- // Save preference to localStorage
48
- localStorage.setItem('theme', newTheme);
49
-
50
- // Update icon
51
- updateThemeIcon(newTheme);
52
-
53
- // Send theme preference to server
54
- saveThemePreference(newTheme);
55
- });
56
- }
57
-
58
- // Update the theme toggle icon based on current theme
59
- function updateThemeIcon(theme) {
60
- const themeToggle = document.getElementById('theme-toggle');
61
- if (!themeToggle) return;
62
-
63
- // Update icon based on theme
64
- if (theme === 'dark') {
65
- themeToggle.innerHTML = '<i class="fas fa-sun"></i>';
66
- themeToggle.setAttribute('title', 'Activer le mode clair');
67
- } else {
68
- themeToggle.innerHTML = '<i class="fas fa-moon"></i>';
69
- themeToggle.setAttribute('title', 'Activer le mode sombre');
70
- }
71
- }
72
-
73
- // Save theme preference to server
74
- function saveThemePreference(theme) {
75
- const formData = new FormData();
76
- formData.append('theme', theme);
77
-
78
- fetch('/set_theme', {
79
- method: 'POST',
80
- body: formData
81
- })
82
- .then(response => response.json())
83
- .then(data => {
84
- console.log('Theme preference saved:', data);
85
- })
86
- .catch(error => {
87
- console.error('Error saving theme preference:', error);
88
- });
89
- }
90
-
91
- // Setup dashboard cards with hover effects
92
- function setupDashboardCards() {
93
- const dashboardCards = document.querySelectorAll('.admin-card');
94
-
95
- dashboardCards.forEach(card => {
96
- card.addEventListener('mouseenter', function() {
97
- this.style.transform = 'translateY(-5px)';
98
- this.style.boxShadow = 'var(--hover-shadow)';
99
- this.style.transition = 'transform 0.3s ease, box-shadow 0.3s ease';
100
- });
101
-
102
- card.addEventListener('mouseleave', function() {
103
- this.style.transform = 'translateY(0)';
104
- this.style.boxShadow = 'var(--shadow)';
105
- });
106
- });
107
- }
108
-
109
- // Setup matiere form functionality
110
- function setupMatiereForm() {
111
- // Show edit form when edit button is clicked
112
- const editButtons = document.querySelectorAll('.edit-matiere-btn');
113
- editButtons.forEach(button => {
114
- button.addEventListener('click', function() {
115
- const matiereId = this.getAttribute('data-id');
116
- const matiereName = this.getAttribute('data-name');
117
- const matiereColor = this.getAttribute('data-color');
118
-
119
- const editForm = document.getElementById('edit-matiere-form');
120
- if (editForm) {
121
- const idInput = editForm.querySelector('input[name="matiere_id"]');
122
- const nameInput = editForm.querySelector('input[name="nom"]');
123
- const colorInput = editForm.querySelector('input[name="color_code"]');
124
-
125
- idInput.value = matiereId;
126
- nameInput.value = matiereName;
127
- colorInput.value = matiereColor;
128
-
129
- // Show the edit form
130
- document.getElementById('add-matiere-section').classList.add('d-none');
131
- document.getElementById('edit-matiere-section').classList.remove('d-none');
132
-
133
- // Scroll to edit form
134
- editForm.scrollIntoView({ behavior: 'smooth' });
135
- }
136
- });
137
- });
138
-
139
- // Cancel edit button
140
- const cancelEditButton = document.getElementById('cancel-edit-matiere');
141
- if (cancelEditButton) {
142
- cancelEditButton.addEventListener('click', function(e) {
143
- e.preventDefault();
144
- document.getElementById('add-matiere-section').classList.remove('d-none');
145
- document.getElementById('edit-matiere-section').classList.add('d-none');
146
- });
147
- }
148
-
149
- // Color picker preview
150
- const colorPickers = document.querySelectorAll('input[type="color"]');
151
- colorPickers.forEach(picker => {
152
- picker.addEventListener('input', function() {
153
- // Find adjacent preview element or create one
154
- let preview = this.nextElementSibling;
155
- if (!preview || !preview.classList.contains('color-preview')) {
156
- preview = document.createElement('span');
157
- preview.className = 'color-preview';
158
- preview.style.display = 'inline-block';
159
- preview.style.width = '24px';
160
- preview.style.height = '24px';
161
- preview.style.borderRadius = '50%';
162
- preview.style.marginLeft = '10px';
163
- this.parentNode.insertBefore(preview, this.nextSibling);
164
- }
165
-
166
- preview.style.backgroundColor = this.value;
167
- });
168
-
169
- // Trigger once to initialize
170
- const event = new Event('input');
171
- picker.dispatchEvent(event);
172
- });
173
- }
174
-
175
- // Setup sous categorie form functionality
176
- function setupSousCategorieForm() {
177
- // Show edit form when edit button is clicked
178
- const editButtons = document.querySelectorAll('.edit-sous-categorie-btn');
179
- editButtons.forEach(button => {
180
- button.addEventListener('click', function() {
181
- const sousCategorieId = this.getAttribute('data-id');
182
- const sousCategorieName = this.getAttribute('data-name');
183
- const matiereId = this.getAttribute('data-matiere-id');
184
-
185
- const editForm = document.getElementById('edit-sous-categorie-form');
186
- if (editForm) {
187
- const idInput = editForm.querySelector('input[name="sous_categorie_id"]');
188
- const nameInput = editForm.querySelector('input[name="nom"]');
189
- const matiereSelect = editForm.querySelector('select[name="matiere_id"]');
190
-
191
- idInput.value = sousCategorieId;
192
- nameInput.value = sousCategorieName;
193
- matiereSelect.value = matiereId;
194
-
195
- // Show the edit form
196
- document.getElementById('add-sous-categorie-section').classList.add('d-none');
197
- document.getElementById('edit-sous-categorie-section').classList.remove('d-none');
198
-
199
- // Scroll to edit form
200
- editForm.scrollIntoView({ behavior: 'smooth' });
201
- }
202
- });
203
- });
204
-
205
- // Cancel edit button
206
- const cancelEditButton = document.getElementById('cancel-edit-sous-categorie');
207
- if (cancelEditButton) {
208
- cancelEditButton.addEventListener('click', function(e) {
209
- e.preventDefault();
210
- document.getElementById('add-sous-categorie-section').classList.remove('d-none');
211
- document.getElementById('edit-sous-categorie-section').classList.add('d-none');
212
- });
213
- }
214
-
215
- // Matiere select filter
216
- const matiereFilterSelect = document.getElementById('matiere-filter');
217
- if (matiereFilterSelect) {
218
- matiereFilterSelect.addEventListener('change', function() {
219
- const selectedMatiereId = this.value;
220
- const sousCategorieRows = document.querySelectorAll('.sous-categorie-row');
221
-
222
- sousCategorieRows.forEach(row => {
223
- if (selectedMatiereId === '' || row.getAttribute('data-matiere-id') === selectedMatiereId) {
224
- row.style.display = '';
225
- } else {
226
- row.style.display = 'none';
227
- }
228
- });
229
- });
230
- }
231
- }
232
-
233
- // Setup texte form functionality
234
- function setupTexteForm() {
235
- // Matiere select change - populate sous-categories
236
- const matiereSelect = document.getElementById('matiere-select');
237
- if (matiereSelect) {
238
- matiereSelect.addEventListener('change', function() {
239
- const matiereId = this.value;
240
- const sousCategorieSelect = document.getElementById('sous-categorie-select');
241
-
242
- if (matiereId && sousCategorieSelect) {
243
- // Clear current options
244
- sousCategorieSelect.innerHTML = '<option value="">Sélectionnez une sous-catégorie</option>';
245
-
246
- // Fetch sous-categories for the selected matiere
247
- fetch(`/get_sous_categories/${matiereId}`)
248
- .then(response => response.json())
249
- .then(data => {
250
- data.forEach(sousCategorie => {
251
- const option = document.createElement('option');
252
- option.value = sousCategorie.id;
253
- option.textContent = sousCategorie.nom;
254
- sousCategorieSelect.appendChild(option);
255
- });
256
- })
257
- .catch(error => {
258
- console.error('Error loading sous-categories:', error);
259
- });
260
- }
261
- });
262
- }
263
- }
264
-
265
- // Setup content block editor
266
- function setupContentBlockEditor() {
267
- const blocksContainer = document.getElementById('blocks-container');
268
- const addBlockButton = document.getElementById('add-block-button');
269
- const saveBlocksButton = document.getElementById('save-blocks-button');
270
-
271
- if (!blocksContainer) return;
272
-
273
- // Add new block
274
- if (addBlockButton) {
275
- addBlockButton.addEventListener('click', function() {
276
- addContentBlock();
277
- });
278
- }
279
-
280
- // Make blocks sortable
281
- if (window.Sortable) {
282
- new Sortable(blocksContainer, {
283
- animation: 150,
284
- handle: '.block-handle',
285
- ghostClass: 'block-ghost',
286
- onEnd: function() {
287
- // Update order numbers
288
- updateBlockOrder();
289
- }
290
- });
291
- }
292
-
293
- // Save blocks
294
- if (saveBlocksButton) {
295
- saveBlocksButton.addEventListener('click', function() {
296
- saveContentBlocks();
297
- });
298
- }
299
-
300
- // Add event listeners for existing blocks
301
- setupExistingBlockControls();
302
- }
303
-
304
- // Setup controls for existing blocks
305
- function setupExistingBlockControls() {
306
- // Setup delete buttons
307
- const deleteButtons = document.querySelectorAll('.delete-block-btn');
308
- deleteButtons.forEach(button => {
309
- button.addEventListener('click', function() {
310
- if (confirm('Êtes-vous sûr de vouloir supprimer ce bloc ?')) {
311
- const blockEditor = this.closest('.block-editor');
312
- blockEditor.remove();
313
- updateBlockOrder();
314
- }
315
- });
316
- });
317
-
318
- // Setup image position selects
319
- const positionSelects = document.querySelectorAll('.image-position-select');
320
- positionSelects.forEach(select => {
321
- select.addEventListener('change', function() {
322
- updateBlockImagePreview(this.closest('.block-editor'));
323
- });
324
- });
325
-
326
- // Setup image selection buttons
327
- const imageSelectButtons = document.querySelectorAll('.select-image-btn');
328
- imageSelectButtons.forEach(button => {
329
- button.addEventListener('click', function() {
330
- const blockEditor = this.closest('.block-editor');
331
- const galleryModal = document.getElementById('image-gallery-modal');
332
-
333
- if (galleryModal) {
334
- // Set current block ID as data attribute for the modal
335
- galleryModal.setAttribute('data-target-block', blockEditor.getAttribute('data-block-id'));
336
-
337
- // Show the modal
338
- const modal = new bootstrap.Modal(galleryModal);
339
- modal.show();
340
- }
341
- });
342
- });
343
-
344
- // Setup image remove buttons
345
- const removeImageButtons = document.querySelectorAll('.remove-image-btn');
346
- removeImageButtons.forEach(button => {
347
- button.addEventListener('click', function() {
348
- const blockEditor = this.closest('.block-editor');
349
- const imageIdInput = blockEditor.querySelector('.block-image-id');
350
- const imagePreview = blockEditor.querySelector('.image-preview');
351
-
352
- if (imageIdInput) {
353
- imageIdInput.value = '';
354
- }
355
-
356
- if (imagePreview) {
357
- imagePreview.src = '';
358
- imagePreview.style.display = 'none';
359
- }
360
-
361
- // Hide remove button
362
- this.style.display = 'none';
363
-
364
- // Show select button
365
- const selectButton = blockEditor.querySelector('.select-image-btn');
366
- if (selectButton) {
367
- selectButton.style.display = 'inline-block';
368
- }
369
- });
370
- });
371
- }
372
-
373
- // Add a new content block to the editor
374
- function addContentBlock(data = null) {
375
- const blocksContainer = document.getElementById('blocks-container');
376
- if (!blocksContainer) return;
377
-
378
- // Generate a unique ID for the block
379
- const blockId = 'block-' + Date.now();
380
-
381
- // Create block HTML
382
- const blockHtml = `
383
- <div class="block-editor" data-block-id="${blockId}">
384
- <div class="block-editor-header">
385
- <div class="d-flex align-items-center">
386
- <span class="block-handle"><i class="fas fa-grip-vertical"></i></span>
387
- <h3 class="block-editor-title">Bloc #${blocksContainer.children.length + 1}</h3>
388
- </div>
389
- <div class="block-editor-actions">
390
- <button type="button" class="btn btn-danger btn-sm delete-block-btn">
391
- <i class="fas fa-trash"></i>
392
- </button>
393
- </div>
394
- </div>
395
- <div class="form-group">
396
- <label for="${blockId}-title">Titre du bloc (optionnel)</label>
397
- <input type="text" class="form-control block-title" id="${blockId}-title" value="${data?.title || ''}">
398
- </div>
399
- <div class="form-group">
400
- <label for="${blockId}-content">Contenu du bloc</label>
401
- <textarea class="form-control block-content" id="${blockId}-content" rows="5">${data?.content || ''}</textarea>
402
- </div>
403
- <div class="form-group">
404
- <label>Image</label>
405
- <div class="d-flex align-items-center mb-2">
406
- <button type="button" class="btn btn-primary btn-sm select-image-btn" style="${data?.image ? 'display:none;' : ''}">
407
- <i class="fas fa-image"></i> Sélectionner une image
408
- </button>
409
- <button type="button" class="btn btn-warning btn-sm remove-image-btn ml-2" style="${data?.image ? '' : 'display:none;'}">
410
- <i class="fas fa-times"></i> Retirer l'image
411
- </button>
412
- </div>
413
- <input type="hidden" class="block-image-id" value="${data?.image?.id || ''}">
414
- <img src="${data?.image?.src || ''}" alt="Preview" class="image-preview mb-2" style="${data?.image ? '' : 'display:none;'}">
415
- <div class="form-group">
416
- <label for="${blockId}-image-position">Position de l'image</label>
417
- <select class="form-control image-position-select" id="${blockId}-image-position">
418
- <option value="left" ${data?.image_position === 'left' ? 'selected' : ''}>Gauche</option>
419
- <option value="right" ${data?.image_position === 'right' ? 'selected' : ''}>Droite</option>
420
- <option value="top" ${data?.image_position === 'top' ? 'selected' : ''}>Haut</option>
421
- </select>
422
- </div>
423
- </div>
424
- </div>
425
- `;
426
-
427
- // Add the block to the container
428
- blocksContainer.insertAdjacentHTML('beforeend', blockHtml);
429
-
430
- // Setup event listeners for the new block
431
- const newBlock = blocksContainer.lastElementChild;
432
-
433
- // Delete button
434
- const deleteButton = newBlock.querySelector('.delete-block-btn');
435
- if (deleteButton) {
436
- deleteButton.addEventListener('click', function() {
437
- if (confirm('Êtes-vous sûr de vouloir supprimer ce bloc ?')) {
438
- newBlock.remove();
439
- updateBlockOrder();
440
- }
441
- });
442
- }
443
-
444
- // Image position select
445
- const positionSelect = newBlock.querySelector('.image-position-select');
446
- if (positionSelect) {
447
- positionSelect.addEventListener('change', function() {
448
- updateBlockImagePreview(newBlock);
449
- });
450
- }
451
-
452
- // Image selection button
453
- const imageSelectButton = newBlock.querySelector('.select-image-btn');
454
- if (imageSelectButton) {
455
- imageSelectButton.addEventListener('click', function() {
456
- const galleryModal = document.getElementById('image-gallery-modal');
457
-
458
- if (galleryModal) {
459
- // Set current block ID as data attribute for the modal
460
- galleryModal.setAttribute('data-target-block', blockId);
461
-
462
- // Show the modal
463
- const modal = new bootstrap.Modal(galleryModal);
464
- modal.show();
465
- }
466
- });
467
- }
468
-
469
- // Image remove button
470
- const removeImageButton = newBlock.querySelector('.remove-image-btn');
471
- if (removeImageButton) {
472
- removeImageButton.addEventListener('click', function() {
473
- const imageIdInput = newBlock.querySelector('.block-image-id');
474
- const imagePreview = newBlock.querySelector('.image-preview');
475
-
476
- if (imageIdInput) {
477
- imageIdInput.value = '';
478
- }
479
-
480
- if (imagePreview) {
481
- imagePreview.src = '';
482
- imagePreview.style.display = 'none';
483
- }
484
-
485
- // Hide remove button
486
- removeImageButton.style.display = 'none';
487
-
488
- // Show select button
489
- if (imageSelectButton) {
490
- imageSelectButton.style.display = 'inline-block';
491
- }
492
- });
493
- }
494
-
495
- // Scroll to the new block
496
- newBlock.scrollIntoView({ behavior: 'smooth' });
497
- }
498
-
499
- // Update block order numbers in the UI
500
- function updateBlockOrder() {
501
- const blocks = document.querySelectorAll('.block-editor');
502
- blocks.forEach((block, index) => {
503
- const titleEl = block.querySelector('.block-editor-title');
504
- if (titleEl) {
505
- titleEl.textContent = `Bloc #${index + 1}`;
506
- }
507
- });
508
- }
509
-
510
- // Update image preview based on position
511
- function updateBlockImagePreview(blockEditor) {
512
- // This function would apply CSS classes to show how the image position
513
- // will look in the frontend
514
- const positionSelect = blockEditor.querySelector('.image-position-select');
515
- const imagePreview = blockEditor.querySelector('.image-preview');
516
-
517
- if (!positionSelect || !imagePreview || imagePreview.style.display === 'none') {
518
- return;
519
- }
520
-
521
- const position = positionSelect.value;
522
-
523
- // Remove existing position classes
524
- imagePreview.classList.remove('position-left', 'position-right', 'position-top');
525
-
526
- // Add the selected position class
527
- imagePreview.classList.add(`position-${position}`);
528
-
529
- // Apply some simple styling to demonstrate the position
530
- switch (position) {
531
- case 'left':
532
- imagePreview.style.float = 'left';
533
- imagePreview.style.marginRight = '15px';
534
- imagePreview.style.marginBottom = '10px';
535
- imagePreview.style.width = '30%';
536
- break;
537
- case 'right':
538
- imagePreview.style.float = 'right';
539
- imagePreview.style.marginLeft = '15px';
540
- imagePreview.style.marginBottom = '10px';
541
- imagePreview.style.width = '30%';
542
- break;
543
- case 'top':
544
- imagePreview.style.float = 'none';
545
- imagePreview.style.marginRight = '0';
546
- imagePreview.style.marginLeft = '0';
547
- imagePreview.style.marginBottom = '15px';
548
- imagePreview.style.width = '100%';
549
- break;
550
- }
551
- }
552
-
553
- // Save content blocks
554
- function saveContentBlocks() {
555
- const blocksContainer = document.getElementById('blocks-container');
556
- const blocksDataInput = document.getElementById('blocks-data');
557
-
558
- if (!blocksContainer || !blocksDataInput) return;
559
-
560
- const blocks = blocksContainer.querySelectorAll('.block-editor');
561
- const blocksData = [];
562
-
563
- blocks.forEach((block, index) => {
564
- const blockId = block.getAttribute('data-block-id');
565
- const title = block.querySelector('.block-title').value;
566
- const content = block.querySelector('.block-content').value;
567
- const imageId = block.querySelector('.block-image-id').value;
568
- const imagePosition = block.querySelector('.image-position-select').value;
569
-
570
- blocksData.push({
571
- id: blockId,
572
- title: title,
573
- content: content,
574
- image_id: imageId,
575
- image_position: imagePosition,
576
- order: index
577
- });
578
- });
579
-
580
- // Set the blocks data as JSON in the hidden input
581
- blocksDataInput.value = JSON.stringify(blocksData);
582
-
583
- // Submit the form
584
- const form = document.getElementById('blocks-form');
585
- if (form) {
586
- form.submit();
587
- }
588
- }
589
-
590
- // Setup image uploader
591
- function setupImageUploader() {
592
- const imageUploadForm = document.getElementById('image-upload-form');
593
- const imageFileInput = document.getElementById('image-file');
594
- const imagePreview = document.getElementById('upload-image-preview');
595
-
596
- if (imageFileInput && imagePreview) {
597
- imageFileInput.addEventListener('change', function() {
598
- if (this.files && this.files[0]) {
599
- const reader = new FileReader();
600
-
601
- reader.onload = function(e) {
602
- imagePreview.src = e.target.result;
603
- imagePreview.style.display = 'block';
604
- };
605
-
606
- reader.readAsDataURL(this.files[0]);
607
- }
608
- });
609
- }
610
-
611
- if (imageUploadForm) {
612
- imageUploadForm.addEventListener('submit', function(e) {
613
- const fileInput = this.querySelector('#image-file');
614
-
615
- if (!fileInput.files || fileInput.files.length === 0) {
616
- e.preventDefault();
617
- alert('Veuillez sélectionner une image.');
618
- }
619
- });
620
- }
621
- }
622
-
623
- // Setup image gallery
624
- function setupImageGallery() {
625
- // Handle image selection from gallery
626
- const galleryItems = document.querySelectorAll('.gallery-item');
627
-
628
- galleryItems.forEach(item => {
629
- item.addEventListener('click', function() {
630
- const imageId = this.getAttribute('data-image-id');
631
- const imageSrc = this.querySelector('img').src;
632
- const galleryModal = document.getElementById('image-gallery-modal');
633
-
634
- if (galleryModal) {
635
- const targetBlockId = galleryModal.getAttribute('data-target-block');
636
- const blockEditor = document.querySelector(`.block-editor[data-block-id="${targetBlockId}"]`);
637
-
638
- if (blockEditor) {
639
- // Update the image ID input
640
- const imageIdInput = blockEditor.querySelector('.block-image-id');
641
- if (imageIdInput) {
642
- imageIdInput.value = imageId;
643
- }
644
-
645
- // Update the image preview
646
- const imagePreview = blockEditor.querySelector('.image-preview');
647
- if (imagePreview) {
648
- imagePreview.src = imageSrc;
649
- imagePreview.style.display = 'block';
650
- }
651
-
652
- // Hide select button and show remove button
653
- const selectButton = blockEditor.querySelector('.select-image-btn');
654
- const removeButton = blockEditor.querySelector('.remove-image-btn');
655
-
656
- if (selectButton) {
657
- selectButton.style.display = 'none';
658
- }
659
-
660
- if (removeButton) {
661
- removeButton.style.display = 'inline-block';
662
- }
663
-
664
- // Update image preview position
665
- updateBlockImagePreview(blockEditor);
666
- }
667
-
668
- // Close the modal
669
- const modal = bootstrap.Modal.getInstance(galleryModal);
670
- if (modal) {
671
- modal.hide();
672
- }
673
- }
674
- });
675
- });
676
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
api/static/js/main.js DELETED
@@ -1,517 +0,0 @@
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
- // ==================================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
api/templates/admin/dashboard.html DELETED
@@ -1,148 +0,0 @@
1
- {% extends 'base.html' %}
2
-
3
- {% block title %}Tableau de bord - Administration{% endblock %}
4
-
5
- {% block styles %}
6
- <style>
7
- .dashboard-stats {
8
- display: grid;
9
- grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
10
- gap: 1.5rem;
11
- margin-bottom: 2rem;
12
- }
13
-
14
- .recent-activity {
15
- background-color: var(--card-bg);
16
- border-radius: 8px;
17
- padding: 1.5rem;
18
- box-shadow: var(--shadow);
19
- }
20
-
21
- .welcome-message {
22
- margin-bottom: 2rem;
23
- padding: 2rem;
24
- background-color: var(--primary-color);
25
- color: white;
26
- border-radius: 8px;
27
- box-shadow: var(--shadow);
28
- }
29
- </style>
30
- {% endblock %}
31
-
32
- {% block content %}
33
- <div class="row">
34
- <div class="col-md-3">
35
- <div class="admin-sidebar">
36
- <h3 class="mb-3">Administration</h3>
37
- <ul class="admin-nav">
38
- <li class="admin-nav-item">
39
- <a href="{{ url_for('admin_bp.dashboard') }}" class="admin-nav-link active">
40
- <i class="fas fa-tachometer-alt"></i> Tableau de bord
41
- </a>
42
- </li>
43
- <li class="admin-nav-item">
44
- <a href="{{ url_for('admin_bp.matieres') }}" class="admin-nav-link">
45
- <i class="fas fa-book"></i> Matières
46
- </a>
47
- </li>
48
- <li class="admin-nav-item">
49
- <a href="{{ url_for('admin_bp.sous_categories') }}" class="admin-nav-link">
50
- <i class="fas fa-list"></i> Sous-catégories
51
- </a>
52
- </li>
53
- <li class="admin-nav-item">
54
- <a href="{{ url_for('admin_bp.textes') }}" class="admin-nav-link">
55
- <i class="fas fa-file-alt"></i> Textes
56
- </a>
57
- </li>
58
- <li class="admin-nav-item">
59
- <a href="{{ url_for('admin_bp.images') }}" class="admin-nav-link">
60
- <i class="fas fa-images"></i> Images
61
- </a>
62
- </li>
63
- <li class="admin-nav-item">
64
- <a href="{{ url_for('admin_bp.logout') }}" class="admin-nav-link">
65
- <i class="fas fa-sign-out-alt"></i> Déconnexion
66
- </a>
67
- </li>
68
- </ul>
69
- </div>
70
- </div>
71
-
72
- <div class="col-md-9">
73
- <div class="welcome-message">
74
- <h2><i class="fas fa-hand-sparkles"></i> Bienvenue dans l'interface d'administration</h2>
75
- <p class="mb-0">Gérez ici l'ensemble des contenus de la plateforme éducative. Utilisez les différentes sections pour ajouter, modifier ou supprimer des contenus.</p>
76
- </div>
77
-
78
- <h2 class="mb-4">Tableau de bord</h2>
79
-
80
- <!-- Stats Cards -->
81
- <div class="dashboard-stats">
82
- <div class="admin-card">
83
- <h3 class="admin-card-title">Matières</h3>
84
- <div class="admin-stat">{{ stats.matieres }}</div>
85
- <p>Nombre total de matières</p>
86
- <a href="{{ url_for('admin_bp.matieres') }}" class="btn btn-primary btn-sm">
87
- <i class="fas fa-eye"></i> Voir
88
- </a>
89
- </div>
90
-
91
- <div class="admin-card">
92
- <h3 class="admin-card-title">Sous-catégories</h3>
93
- <div class="admin-stat">{{ stats.sous_categories }}</div>
94
- <p>Nombre total de sous-catégories</p>
95
- <a href="{{ url_for('admin_bp.sous_categories') }}" class="btn btn-primary btn-sm">
96
- <i class="fas fa-eye"></i> Voir
97
- </a>
98
- </div>
99
-
100
- <div class="admin-card">
101
- <h3 class="admin-card-title">Textes</h3>
102
- <div class="admin-stat">{{ stats.textes }}</div>
103
- <p>Nombre total de textes</p>
104
- <a href="{{ url_for('admin_bp.textes') }}" class="btn btn-primary btn-sm">
105
- <i class="fas fa-eye"></i> Voir
106
- </a>
107
- </div>
108
-
109
- <div class="admin-card">
110
- <h3 class="admin-card-title">Images</h3>
111
- <div class="admin-stat">{{ stats.images }}</div>
112
- <p>Nombre total d'images</p>
113
- <a href="{{ url_for('admin_bp.images') }}" class="btn btn-primary btn-sm">
114
- <i class="fas fa-eye"></i> Voir
115
- </a>
116
- </div>
117
- </div>
118
-
119
- <!-- Recent Activity -->
120
- <div class="recent-activity">
121
- <h3 class="mb-3">Textes récemment modifiés</h3>
122
- {% if recent_textes %}
123
- <ul class="list-group">
124
- {% for texte in recent_textes %}
125
- <li class="list-group-item d-flex justify-content-between align-items-center">
126
- <div>
127
- <h5 class="mb-1">{{ texte.titre }}</h5>
128
- <small>{{ texte.updated_at.strftime('%d/%m/%Y à %H:%M') }}</small>
129
- </div>
130
- <a href="{{ url_for('admin_bp.edit_texte', texte_id=texte.id) }}" class="btn btn-sm btn-primary">
131
- <i class="fas fa-edit"></i> Éditer
132
- </a>
133
- </li>
134
- {% endfor %}
135
- </ul>
136
- {% else %}
137
- <div class="alert alert-info">
138
- Aucun texte n'a été modifié récemment.
139
- </div>
140
- {% endif %}
141
- </div>
142
- </div>
143
- </div>
144
- {% endblock %}
145
-
146
- {% block scripts %}
147
- <script src="{{ url_for('static', filename='js/admin.js') }}"></script>
148
- {% endblock %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
api/templates/admin/edit_texte.html DELETED
@@ -1,282 +0,0 @@
1
- {% extends 'base.html' %}
2
-
3
- {% block title %}Éditer {{ texte.titre }} - Administration{% endblock %}
4
-
5
- {% block styles %}
6
- <style>
7
- .block-editor {
8
- position: relative;
9
- transition: all 0.3s ease;
10
- }
11
-
12
- .block-ghost {
13
- opacity: 0.5;
14
- background: var(--primary-color);
15
- }
16
-
17
- .block-handle {
18
- cursor: move;
19
- color: var(--muted-color);
20
- }
21
-
22
- .image-position-example {
23
- padding: 10px;
24
- border: 1px dashed var(--border-color);
25
- margin-top: 10px;
26
- border-radius: 4px;
27
- }
28
-
29
- .block-image-container {
30
- margin-bottom: 15px;
31
- }
32
-
33
- .image-preview {
34
- max-height: 150px;
35
- object-fit: contain;
36
- }
37
-
38
- .gallery-image {
39
- transition: transform 0.2s;
40
- cursor: pointer;
41
- }
42
-
43
- .gallery-image:hover {
44
- transform: scale(1.05);
45
- }
46
- </style>
47
- {% endblock %}
48
-
49
- {% block content %}
50
- <div class="row">
51
- <div class="col-md-3">
52
- <div class="admin-sidebar">
53
- <h3 class="mb-3">Administration</h3>
54
- <ul class="admin-nav">
55
- <li class="admin-nav-item">
56
- <a href="{{ url_for('admin_bp.dashboard') }}" class="admin-nav-link">
57
- <i class="fas fa-tachometer-alt"></i> Tableau de bord
58
- </a>
59
- </li>
60
- <li class="admin-nav-item">
61
- <a href="{{ url_for('admin_bp.matieres') }}" class="admin-nav-link">
62
- <i class="fas fa-book"></i> Matières
63
- </a>
64
- </li>
65
- <li class="admin-nav-item">
66
- <a href="{{ url_for('admin_bp.sous_categories') }}" class="admin-nav-link">
67
- <i class="fas fa-list"></i> Sous-catégories
68
- </a>
69
- </li>
70
- <li class="admin-nav-item">
71
- <a href="{{ url_for('admin_bp.textes') }}" class="admin-nav-link active">
72
- <i class="fas fa-file-alt"></i> Textes
73
- </a>
74
- </li>
75
- <li class="admin-nav-item">
76
- <a href="{{ url_for('admin_bp.images') }}" class="admin-nav-link">
77
- <i class="fas fa-images"></i> Images
78
- </a>
79
- </li>
80
- <li class="admin-nav-item">
81
- <a href="{{ url_for('admin_bp.logout') }}" class="admin-nav-link">
82
- <i class="fas fa-sign-out-alt"></i> Déconnexion
83
- </a>
84
- </li>
85
- </ul>
86
- </div>
87
- </div>
88
-
89
- <div class="col-md-9">
90
- <div class="admin-container">
91
- <h2 class="admin-title">Éditer la méthodologie : {{ texte.titre }}</h2>
92
-
93
- <div class="mb-4">
94
- <a href="{{ url_for('admin_bp.textes') }}" class="btn btn-secondary mb-3">
95
- <i class="fas fa-arrow-left"></i> Retour à la liste
96
- </a>
97
- <a href="{{ url_for('admin_bp.historique', texte_id=texte.id) }}" class="btn btn-info mb-3">
98
- <i class="fas fa-history"></i> Voir l'historique
99
- </a>
100
- </div>
101
-
102
- <!-- Basic Information Form -->
103
- <div class="card mb-4">
104
- <div class="card-header">
105
- <h4>Informations de base</h4>
106
- </div>
107
- <div class="card-body">
108
- <form method="POST" action="{{ url_for('admin_bp.edit_texte', texte_id=texte.id) }}">
109
- <input type="hidden" name="action" value="update_basic">
110
-
111
- <div class="form-group mb-3">
112
- <label for="titre">Titre</label>
113
- <input type="text" class="form-control" id="titre" name="titre" value="{{ texte.titre }}" required>
114
- </div>
115
-
116
- <div class="form-group mb-3">
117
- <label for="sous_categorie_id">Sous-catégorie</label>
118
- <select class="form-control" id="sous_categorie_id" name="sous_categorie_id" required>
119
- {% for sc in sous_categories %}
120
- <option value="{{ sc.id }}" {% if sc.id == texte.sous_categorie_id %}selected{% endif %}>
121
- {{ sc.matiere.nom }} - {{ sc.nom }}
122
- </option>
123
- {% endfor %}
124
- </select>
125
- </div>
126
-
127
- <button type="submit" class="btn btn-primary">
128
- <i class="fas fa-save"></i> Mettre à jour les informations
129
- </button>
130
- </form>
131
- </div>
132
- </div>
133
-
134
- <!-- Content Blocks Editor -->
135
- <div class="card mb-4">
136
- <div class="card-header d-flex justify-content-between align-items-center">
137
- <h4>Blocs de contenu</h4>
138
- <button id="add-block-button" class="btn btn-success">
139
- <i class="fas fa-plus"></i> Ajouter un bloc
140
- </button>
141
- </div>
142
- <div class="card-body">
143
- <p class="text-muted mb-4">
144
- Organisez votre contenu en blocs distincts. Chaque bloc peut contenir un titre, du texte et une image.
145
- Vous pouvez réorganiser les blocs en les faisant glisser.
146
- </p>
147
-
148
- <form id="blocks-form" method="POST" action="{{ url_for('admin_bp.edit_texte', texte_id=texte.id) }}">
149
- <input type="hidden" name="action" value="update_blocks">
150
- <input type="hidden" id="blocks-data" name="blocks_data" value="">
151
-
152
- <div id="blocks-container">
153
- {% for block in blocks %}
154
- <div class="block-editor mb-4" data-block-id="{{ block.id }}">
155
- <div class="block-editor-header">
156
- <div class="d-flex align-items-center">
157
- <span class="block-handle"><i class="fas fa-grip-vertical"></i></span>
158
- <h3 class="block-editor-title">Bloc #{{ loop.index }}</h3>
159
- </div>
160
- <div class="block-editor-actions">
161
- <button type="button" class="btn btn-danger btn-sm delete-block-btn">
162
- <i class="fas fa-trash"></i>
163
- </button>
164
- </div>
165
- </div>
166
- <div class="form-group mb-3">
167
- <label for="block-{{ block.id }}-title">Titre du bloc (optionnel)</label>
168
- <input type="text" class="form-control block-title" id="block-{{ block.id }}-title" value="{{ block.title or '' }}">
169
- </div>
170
- <div class="form-group mb-3">
171
- <label for="block-{{ block.id }}-content">Contenu du bloc</label>
172
- <textarea class="form-control block-content" id="block-{{ block.id }}-content" rows="5">{{ block.content or '' }}</textarea>
173
- </div>
174
- <div class="form-group">
175
- <label>Image</label>
176
- <div class="d-flex align-items-center mb-2">
177
- <button type="button" class="btn btn-primary btn-sm select-image-btn" {% if block.image %}style="display:none;"{% endif %}>
178
- <i class="fas fa-image"></i> Sélectionner une image
179
- </button>
180
- <button type="button" class="btn btn-warning btn-sm remove-image-btn ml-2" {% if not block.image %}style="display:none;"{% endif %}>
181
- <i class="fas fa-times"></i> Retirer l'image
182
- </button>
183
- </div>
184
- <input type="hidden" class="block-image-id" value="{{ block.image.id if block.image else '' }}">
185
-
186
- {% if block.image %}
187
- <div class="block-image-container">
188
- <img src="{{ block.image.src }}" alt="{{ block.image.alt }}" class="image-preview">
189
- </div>
190
- {% else %}
191
- <img src="" alt="Preview" class="image-preview" style="display:none;">
192
- {% endif %}
193
-
194
- <div class="form-group mt-3">
195
- <label for="block-{{ block.id }}-image-position">Position de l'image</label>
196
- <select class="form-control image-position-select" id="block-{{ block.id }}-image-position">
197
- <option value="left" {% if block.image_position == 'left' %}selected{% endif %}>Gauche</option>
198
- <option value="right" {% if block.image_position == 'right' %}selected{% endif %}>Droite</option>
199
- <option value="top" {% if block.image_position == 'top' %}selected{% endif %}>Haut</option>
200
- </select>
201
- </div>
202
- </div>
203
- </div>
204
- {% endfor %}
205
- </div>
206
-
207
- <div class="mt-4 text-center">
208
- <button type="button" id="save-blocks-button" class="btn btn-primary btn-lg">
209
- <i class="fas fa-save"></i> Enregistrer les modifications
210
- </button>
211
- </div>
212
- </form>
213
- </div>
214
- </div>
215
-
216
- <!-- Image Upload Section -->
217
- <div class="card mb-4">
218
- <div class="card-header">
219
- <h4>Ajouter une nouvelle image</h4>
220
- </div>
221
- <div class="card-body">
222
- <form method="POST" action="{{ url_for('admin_bp.edit_texte', texte_id=texte.id) }}" enctype="multipart/form-data">
223
- <input type="hidden" name="action" value="upload_image">
224
-
225
- <div class="form-group mb-3">
226
- <label for="image">Sélectionner une image</label>
227
- <input type="file" class="form-control" id="image" name="image" accept="image/*" required>
228
- </div>
229
-
230
- <div class="form-group mb-3">
231
- <label for="alt_text">Texte alternatif (pour l'accessibilité)</label>
232
- <input type="text" class="form-control" id="alt_text" name="alt_text" placeholder="Description de l'image">
233
- </div>
234
-
235
- <button type="submit" class="btn btn-success">
236
- <i class="fas fa-upload"></i> Télécharger l'image
237
- </button>
238
- </form>
239
- </div>
240
- </div>
241
- </div>
242
- </div>
243
- </div>
244
-
245
- <!-- Image Gallery Modal -->
246
- <div class="modal fade" id="image-gallery-modal" tabindex="-1" aria-labelledby="imageGalleryModalLabel" aria-hidden="true">
247
- <div class="modal-dialog modal-lg">
248
- <div class="modal-content">
249
- <div class="modal-header">
250
- <h5 class="modal-title" id="imageGalleryModalLabel">Sélectionner une image</h5>
251
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
252
- </div>
253
- <div class="modal-body">
254
- <div class="row">
255
- {% for image in images %}
256
- <div class="col-md-3 col-sm-4 col-6 mb-3">
257
- <div class="gallery-item" data-image-id="{{ image.id }}">
258
- <img src="{{ image.src }}" alt="{{ image.alt }}" class="gallery-image img-fluid">
259
- </div>
260
- </div>
261
- {% else %}
262
- <div class="col-12">
263
- <div class="alert alert-info">
264
- Aucune image disponible. Veuillez en télécharger une.
265
- </div>
266
- </div>
267
- {% endfor %}
268
- </div>
269
- </div>
270
- <div class="modal-footer">
271
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
272
- </div>
273
- </div>
274
- </div>
275
- </div>
276
- {% endblock %}
277
-
278
- {% block scripts %}
279
- <!-- Include Sortable.js for drag and drop functionality -->
280
- <script src="https://cdn.jsdelivr.net/npm/sortablejs@1.14.0/Sortable.min.js"></script>
281
- <script src="{{ url_for('static', filename='js/admin.js') }}"></script>
282
- {% endblock %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
api/templates/admin/historique.html DELETED
@@ -1,171 +0,0 @@
1
- {% extends 'base.html' %}
2
-
3
- {% block title %}Historique des modifications - {{ texte.titre }} - Administration{% endblock %}
4
-
5
- {% block styles %}
6
- <style>
7
- .history-entry {
8
- position: relative;
9
- padding: 20px;
10
- border-left: 3px solid var(--primary-color);
11
- margin-bottom: 20px;
12
- background-color: var(--card-bg);
13
- border-radius: 8px;
14
- box-shadow: var(--shadow);
15
- }
16
-
17
- .history-date {
18
- position: absolute;
19
- top: 10px;
20
- right: 15px;
21
- font-size: 0.9rem;
22
- color: var(--muted-color);
23
- }
24
-
25
- .history-content {
26
- background-color: var(--block-bg);
27
- padding: 15px;
28
- border-radius: 8px;
29
- margin-top: 15px;
30
- white-space: pre-wrap;
31
- max-height: 300px;
32
- overflow-y: auto;
33
- border: 1px solid var(--border-color);
34
- }
35
-
36
- .empty-history {
37
- text-align: center;
38
- padding: 50px 20px;
39
- background-color: var(--block-bg);
40
- border-radius: 8px;
41
- margin-top: 20px;
42
- }
43
-
44
- .timeline-container {
45
- position: relative;
46
- margin-left: 20px;
47
- }
48
-
49
- .timeline-line {
50
- position: absolute;
51
- left: 0;
52
- top: 0;
53
- bottom: 0;
54
- width: 3px;
55
- background-color: var(--border-color);
56
- }
57
-
58
- .timeline-dot {
59
- position: absolute;
60
- left: -8px;
61
- top: 20px;
62
- width: 18px;
63
- height: 18px;
64
- border-radius: 50%;
65
- background-color: var(--primary-color);
66
- z-index: 2;
67
- }
68
- </style>
69
- {% endblock %}
70
-
71
- {% block content %}
72
- <div class="row">
73
- <div class="col-md-3">
74
- <div class="admin-sidebar">
75
- <h3 class="mb-3">Administration</h3>
76
- <ul class="admin-nav">
77
- <li class="admin-nav-item">
78
- <a href="{{ url_for('admin_bp.dashboard') }}" class="admin-nav-link">
79
- <i class="fas fa-tachometer-alt"></i> Tableau de bord
80
- </a>
81
- </li>
82
- <li class="admin-nav-item">
83
- <a href="{{ url_for('admin_bp.matieres') }}" class="admin-nav-link">
84
- <i class="fas fa-book"></i> Matières
85
- </a>
86
- </li>
87
- <li class="admin-nav-item">
88
- <a href="{{ url_for('admin_bp.sous_categories') }}" class="admin-nav-link">
89
- <i class="fas fa-list"></i> Sous-catégories
90
- </a>
91
- </li>
92
- <li class="admin-nav-item">
93
- <a href="{{ url_for('admin_bp.textes') }}" class="admin-nav-link active">
94
- <i class="fas fa-file-alt"></i> Textes
95
- </a>
96
- </li>
97
- <li class="admin-nav-item">
98
- <a href="{{ url_for('admin_bp.images') }}" class="admin-nav-link">
99
- <i class="fas fa-images"></i> Images
100
- </a>
101
- </li>
102
- <li class="admin-nav-item">
103
- <a href="{{ url_for('admin_bp.logout') }}" class="admin-nav-link">
104
- <i class="fas fa-sign-out-alt"></i> Déconnexion
105
- </a>
106
- </li>
107
- </ul>
108
- </div>
109
- </div>
110
-
111
- <div class="col-md-9">
112
- <div class="admin-container">
113
- <h2 class="admin-title">Historique des modifications : {{ texte.titre }}</h2>
114
-
115
- <div class="mb-4">
116
- <a href="{{ url_for('admin_bp.edit_texte', texte_id=texte.id) }}" class="btn btn-secondary">
117
- <i class="fas fa-arrow-left"></i> Retour à l'éditeur
118
- </a>
119
- <a href="{{ url_for('admin_bp.textes') }}" class="btn btn-secondary">
120
- <i class="fas fa-list"></i> Liste des textes
121
- </a>
122
- </div>
123
-
124
- <div class="card mb-4">
125
- <div class="card-header">
126
- <h4><i class="fas fa-history"></i> Versions précédentes</h4>
127
- </div>
128
- <div class="card-body">
129
- {% if historiques %}
130
- <div class="timeline-container">
131
- <div class="timeline-line"></div>
132
-
133
- <!-- Current version -->
134
- <div class="history-entry">
135
- <div class="timeline-dot"></div>
136
- <h5>Version actuelle</h5>
137
- <span class="history-date">
138
- <i class="fas fa-clock"></i> {{ texte.updated_at.strftime('%d/%m/%Y à %H:%M') }}
139
- </span>
140
- <div class="history-content">{{ texte.contenu }}</div>
141
- </div>
142
-
143
- <!-- Previous versions -->
144
- {% for historique in historiques %}
145
- <div class="history-entry">
146
- <div class="timeline-dot"></div>
147
- <h5>Version antérieure #{{ loop.index }}</h5>
148
- <span class="history-date">
149
- <i class="fas fa-clock"></i> {{ historique.date_modification.strftime('%d/%m/%Y à %H:%M') }}
150
- </span>
151
- <div class="history-content">{{ historique.contenu_precedent }}</div>
152
- </div>
153
- {% endfor %}
154
- </div>
155
- {% else %}
156
- <div class="empty-history">
157
- <i class="fas fa-info-circle fa-3x mb-3"></i>
158
- <h4>Aucun historique disponible</h4>
159
- <p>Ce texte n'a pas encore été modifié depuis sa création.</p>
160
- </div>
161
- {% endif %}
162
- </div>
163
- </div>
164
- </div>
165
- </div>
166
- </div>
167
- {% endblock %}
168
-
169
- {% block scripts %}
170
- <script src="{{ url_for('static', filename='js/admin.js') }}"></script>
171
- {% endblock %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
api/templates/admin/images.html DELETED
@@ -1,282 +0,0 @@
1
- {% extends 'base.html' %}
2
-
3
- {% block title %}Gestion des Images - Administration{% endblock %}
4
-
5
- {% block styles %}
6
- <style>
7
- .image-card {
8
- position: relative;
9
- background-color: var(--card-bg);
10
- border-radius: 8px;
11
- padding: 10px;
12
- margin-bottom: 20px;
13
- box-shadow: var(--shadow);
14
- transition: transform 0.3s ease, box-shadow 0.3s ease;
15
- }
16
-
17
- .image-card:hover {
18
- transform: translateY(-5px);
19
- box-shadow: var(--hover-shadow);
20
- }
21
-
22
- .image-container {
23
- position: relative;
24
- width: 100%;
25
- height: 200px;
26
- margin-bottom: 15px;
27
- overflow: hidden;
28
- border-radius: 6px;
29
- background-color: var(--block-bg);
30
- }
31
-
32
- .image-preview {
33
- width: 100%;
34
- height: 100%;
35
- object-fit: contain;
36
- }
37
-
38
- .image-info {
39
- padding: 10px 0;
40
- }
41
-
42
- .image-actions {
43
- display: flex;
44
- justify-content: space-between;
45
- margin-top: 10px;
46
- }
47
-
48
- .image-upload-preview {
49
- max-width: 100%;
50
- max-height: 200px;
51
- margin-top: 15px;
52
- border-radius: 6px;
53
- display: none;
54
- }
55
-
56
- .image-date {
57
- font-size: 0.8rem;
58
- color: var(--muted-color);
59
- }
60
-
61
- .image-detail-modal img {
62
- max-width: 100%;
63
- max-height: 500px;
64
- }
65
-
66
- .image-filter {
67
- margin-bottom: 20px;
68
- }
69
- </style>
70
- {% endblock %}
71
-
72
- {% block content %}
73
- <div class="row">
74
- <div class="col-md-3">
75
- <div class="admin-sidebar">
76
- <h3 class="mb-3">Administration</h3>
77
- <ul class="admin-nav">
78
- <li class="admin-nav-item">
79
- <a href="{{ url_for('admin_bp.dashboard') }}" class="admin-nav-link">
80
- <i class="fas fa-tachometer-alt"></i> Tableau de bord
81
- </a>
82
- </li>
83
- <li class="admin-nav-item">
84
- <a href="{{ url_for('admin_bp.matieres') }}" class="admin-nav-link">
85
- <i class="fas fa-book"></i> Matières
86
- </a>
87
- </li>
88
- <li class="admin-nav-item">
89
- <a href="{{ url_for('admin_bp.sous_categories') }}" class="admin-nav-link">
90
- <i class="fas fa-list"></i> Sous-catégories
91
- </a>
92
- </li>
93
- <li class="admin-nav-item">
94
- <a href="{{ url_for('admin_bp.textes') }}" class="admin-nav-link">
95
- <i class="fas fa-file-alt"></i> Textes
96
- </a>
97
- </li>
98
- <li class="admin-nav-item">
99
- <a href="{{ url_for('admin_bp.images') }}" class="admin-nav-link active">
100
- <i class="fas fa-images"></i> Images
101
- </a>
102
- </li>
103
- <li class="admin-nav-item">
104
- <a href="{{ url_for('admin_bp.logout') }}" class="admin-nav-link">
105
- <i class="fas fa-sign-out-alt"></i> Déconnexion
106
- </a>
107
- </li>
108
- </ul>
109
- </div>
110
- </div>
111
-
112
- <div class="col-md-9">
113
- <div class="admin-container">
114
- <h2 class="admin-title">Gestion des Images</h2>
115
-
116
- <!-- Upload Image Section -->
117
- <div class="card mb-4">
118
- <div class="card-header">
119
- <h4><i class="fas fa-upload"></i> Télécharger une nouvelle image</h4>
120
- </div>
121
- <div class="card-body">
122
- <form id="image-upload-form" method="POST" action="{{ url_for('admin_bp.images') }}" enctype="multipart/form-data">
123
- <input type="hidden" name="action" value="upload">
124
-
125
- <div class="row">
126
- <div class="col-md-6">
127
- <div class="form-group mb-3">
128
- <label for="image-file">Sélectionner une image</label>
129
- <input type="file" class="form-control" id="image-file" name="image" accept="image/*" required>
130
- </div>
131
-
132
- <div class="form-group mb-3">
133
- <label for="alt_text">Texte alternatif (pour l'accessibilité)</label>
134
- <input type="text" class="form-control" id="alt_text" name="alt_text" placeholder="Description de l'image">
135
- </div>
136
-
137
- <div class="form-group mb-3">
138
- <label for="description">Description (optionnelle)</label>
139
- <textarea class="form-control" id="description" name="description" rows="3" placeholder="Description ou notes sur l'image"></textarea>
140
- </div>
141
- </div>
142
- <div class="col-md-6 d-flex align-items-center justify-content-center">
143
- <img id="upload-image-preview" class="image-upload-preview" src="#" alt="Aperçu">
144
- </div>
145
- </div>
146
-
147
- <button type="submit" class="btn btn-primary">
148
- <i class="fas fa-upload"></i> Télécharger
149
- </button>
150
- </form>
151
- </div>
152
- </div>
153
-
154
- <!-- Image Gallery -->
155
- <div class="card">
156
- <div class="card-header">
157
- <h4><i class="fas fa-images"></i> Bibliothèque d'images</h4>
158
- </div>
159
- <div class="card-body">
160
- <!-- Filter -->
161
- <div class="image-filter mb-4">
162
- <div class="input-group">
163
- <span class="input-group-text"><i class="fas fa-search"></i></span>
164
- <input type="text" id="image-search" class="form-control" placeholder="Rechercher par nom ou description...">
165
- </div>
166
- </div>
167
-
168
- {% if images %}
169
- <div class="row" id="image-gallery">
170
- {% for image in images %}
171
- <div class="col-lg-4 col-md-6 mb-4">
172
- <div class="image-card">
173
- <div class="image-container">
174
- <img src="{{ image.src }}" alt="{{ image.alt_text or 'Image' }}" class="image-preview">
175
- </div>
176
- <div class="image-info">
177
- <h5 class="mb-1" title="{{ image.filename }}">
178
- {{ image.filename|truncate(20) }}
179
- </h5>
180
- <p class="image-date mb-1">
181
- <i class="fas fa-calendar-alt"></i>
182
- {{ image.uploaded_at.strftime('%d/%m/%Y') }}
183
- </p>
184
- <p class="text-muted small mb-0">
185
- {{ image.description|default('Aucune description')|truncate(50) }}
186
- </p>
187
- </div>
188
- <div class="image-actions">
189
- <button class="btn btn-sm btn-info" data-bs-toggle="modal" data-bs-target="#imageModal{{ image.id }}">
190
- <i class="fas fa-eye"></i> Détails
191
- </button>
192
- <form method="POST" action="{{ url_for('admin_bp.images') }}" onsubmit="return confirm('Êtes-vous sûr de vouloir supprimer cette image ?')">
193
- <input type="hidden" name="action" value="delete">
194
- <input type="hidden" name="image_id" value="{{ image.id }}">
195
- <button type="submit" class="btn btn-sm btn-danger">
196
- <i class="fas fa-trash"></i>
197
- </button>
198
- </form>
199
- </div>
200
- </div>
201
-
202
- <!-- Image Modal -->
203
- <div class="modal fade image-detail-modal" id="imageModal{{ image.id }}" tabindex="-1" aria-labelledby="imageModalLabel{{ image.id }}" aria-hidden="true">
204
- <div class="modal-dialog modal-lg">
205
- <div class="modal-content">
206
- <div class="modal-header">
207
- <h5 class="modal-title" id="imageModalLabel{{ image.id }}">{{ image.filename }}</h5>
208
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
209
- </div>
210
- <div class="modal-body">
211
- <div class="text-center mb-4">
212
- <img src="{{ image.src }}" alt="{{ image.alt_text or 'Image' }}" class="img-fluid">
213
- </div>
214
-
215
- <form method="POST" action="{{ url_for('admin_bp.images') }}">
216
- <input type="hidden" name="action" value="update">
217
- <input type="hidden" name="image_id" value="{{ image.id }}">
218
-
219
- <div class="form-group mb-3">
220
- <label>Téléchargée le</label>
221
- <input type="text" class="form-control" value="{{ image.uploaded_at.strftime('%d/%m/%Y à %H:%M') }}" readonly>
222
- </div>
223
-
224
- <div class="form-group mb-3">
225
- <label for="alt_text{{ image.id }}">Texte alternatif</label>
226
- <input type="text" class="form-control" id="alt_text{{ image.id }}" name="alt_text" value="{{ image.alt_text or '' }}">
227
- </div>
228
-
229
- <div class="form-group mb-3">
230
- <label for="description{{ image.id }}">Description</label>
231
- <textarea class="form-control" id="description{{ image.id }}" name="description" rows="3">{{ image.description or '' }}</textarea>
232
- </div>
233
-
234
- <button type="submit" class="btn btn-primary">
235
- <i class="fas fa-save"></i> Mettre à jour
236
- </button>
237
- </form>
238
- </div>
239
- </div>
240
- </div>
241
- </div>
242
- </div>
243
- {% endfor %}
244
- </div>
245
- {% else %}
246
- <div class="alert alert-info">
247
- Aucune image n'a été téléchargée. Utilisez le formulaire ci-dessus pour ajouter votre première image.
248
- </div>
249
- {% endif %}
250
- </div>
251
- </div>
252
- </div>
253
- </div>
254
- </div>
255
- {% endblock %}
256
-
257
- {% block scripts %}
258
- <script src="{{ url_for('static', filename='js/admin.js') }}"></script>
259
- <script>
260
- // Image search functionality
261
- document.addEventListener('DOMContentLoaded', function() {
262
- const searchInput = document.getElementById('image-search');
263
- if (!searchInput) return;
264
-
265
- searchInput.addEventListener('input', function() {
266
- const searchTerm = this.value.toLowerCase();
267
- const imageCards = document.querySelectorAll('.image-card');
268
-
269
- imageCards.forEach(card => {
270
- const filename = card.querySelector('h5').textContent.toLowerCase();
271
- const description = card.querySelector('.text-muted').textContent.toLowerCase();
272
-
273
- if (filename.includes(searchTerm) || description.includes(searchTerm)) {
274
- card.closest('.col-lg-4').style.display = '';
275
- } else {
276
- card.closest('.col-lg-4').style.display = 'none';
277
- }
278
- });
279
- });
280
- });
281
- </script>
282
- {% endblock %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
api/templates/admin/login.html DELETED
@@ -1,87 +0,0 @@
1
- {% extends 'base.html' %}
2
-
3
- {% block title %}Connexion - Administration{% endblock %}
4
-
5
- {% block styles %}
6
- <style>
7
- .login-container {
8
- max-width: 500px;
9
- margin: 50px auto;
10
- padding: 30px;
11
- background-color: var(--card-bg);
12
- border-radius: 10px;
13
- box-shadow: var(--shadow);
14
- }
15
-
16
- .login-header {
17
- text-align: center;
18
- margin-bottom: 30px;
19
- }
20
-
21
- .login-icon {
22
- font-size: 60px;
23
- color: var(--primary-color);
24
- margin-bottom: 20px;
25
- }
26
-
27
- .login-title {
28
- font-size: 24px;
29
- margin-bottom: 10px;
30
- }
31
-
32
- .login-subtitle {
33
- color: var(--muted-color);
34
- }
35
-
36
- .login-form {
37
- margin-top: 20px;
38
- }
39
-
40
- .form-floating {
41
- margin-bottom: 20px;
42
- }
43
-
44
- .login-footer {
45
- text-align: center;
46
- margin-top: 20px;
47
- color: var(--muted-color);
48
- }
49
- </style>
50
- {% endblock %}
51
-
52
- {% block content %}
53
- <div class="login-container">
54
- <div class="login-header">
55
- <div class="login-icon">
56
- <i class="fas fa-user-shield"></i>
57
- </div>
58
- <h1 class="login-title">Espace d'administration</h1>
59
- <p class="login-subtitle">Connectez-vous pour gérer le contenu</p>
60
- </div>
61
-
62
- <form method="POST" action="{{ url_for('admin_bp.login') }}" class="login-form">
63
- <div class="form-floating mb-3">
64
- <input type="text" class="form-control" id="username" name="username" placeholder="Nom d'utilisateur" required>
65
- <label for="username">Nom d'utilisateur</label>
66
- </div>
67
-
68
- <div class="form-floating mb-4">
69
- <input type="password" class="form-control" id="password" name="password" placeholder="Mot de passe" required>
70
- <label for="password">Mot de passe</label>
71
- </div>
72
-
73
- <div class="d-grid">
74
- <button type="submit" class="btn btn-primary btn-lg">
75
- <i class="fas fa-sign-in-alt me-2"></i> Se connecter
76
- </button>
77
- </div>
78
- </form>
79
-
80
- <div class="login-footer">
81
- <p>Seuls les administrateurs autorisés peuvent accéder à cette section.</p>
82
- <a href="{{ url_for('main_bp.index') }}">
83
- <i class="fas fa-arrow-left me-1"></i> Retour à l'accueil
84
- </a>
85
- </div>
86
- </div>
87
- {% endblock %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
api/templates/admin/matieres.html DELETED
@@ -1,213 +0,0 @@
1
- {% extends 'base.html' %}
2
-
3
- {% block title %}Gestion des Matières - Administration{% endblock %}
4
-
5
- {% block styles %}
6
- <style>
7
- .color-preview {
8
- display: inline-block;
9
- width: 24px;
10
- height: 24px;
11
- border-radius: 50%;
12
- margin-left: 10px;
13
- }
14
-
15
- .color-badge {
16
- display: inline-block;
17
- width: 20px;
18
- height: 20px;
19
- border-radius: 4px;
20
- margin-right: 10px;
21
- }
22
-
23
- .matiere-card {
24
- background-color: var(--card-bg);
25
- border-radius: 8px;
26
- padding: 16px;
27
- margin-bottom: 16px;
28
- box-shadow: var(--shadow);
29
- transition: transform 0.3s ease, box-shadow 0.3s ease;
30
- border-left: 5px solid #ddd;
31
- }
32
-
33
- .matiere-card:hover {
34
- transform: translateY(-5px);
35
- box-shadow: var(--hover-shadow);
36
- }
37
-
38
- .matiere-actions {
39
- display: flex;
40
- justify-content: flex-end;
41
- gap: 8px;
42
- }
43
- </style>
44
- {% endblock %}
45
-
46
- {% block content %}
47
- <div class="row">
48
- <div class="col-md-3">
49
- <div class="admin-sidebar">
50
- <h3 class="mb-3">Administration</h3>
51
- <ul class="admin-nav">
52
- <li class="admin-nav-item">
53
- <a href="{{ url_for('admin_bp.dashboard') }}" class="admin-nav-link">
54
- <i class="fas fa-tachometer-alt"></i> Tableau de bord
55
- </a>
56
- </li>
57
- <li class="admin-nav-item">
58
- <a href="{{ url_for('admin_bp.matieres') }}" class="admin-nav-link active">
59
- <i class="fas fa-book"></i> Matières
60
- </a>
61
- </li>
62
- <li class="admin-nav-item">
63
- <a href="{{ url_for('admin_bp.sous_categories') }}" class="admin-nav-link">
64
- <i class="fas fa-list"></i> Sous-catégories
65
- </a>
66
- </li>
67
- <li class="admin-nav-item">
68
- <a href="{{ url_for('admin_bp.textes') }}" class="admin-nav-link">
69
- <i class="fas fa-file-alt"></i> Textes
70
- </a>
71
- </li>
72
- <li class="admin-nav-item">
73
- <a href="{{ url_for('admin_bp.images') }}" class="admin-nav-link">
74
- <i class="fas fa-images"></i> Images
75
- </a>
76
- </li>
77
- <li class="admin-nav-item">
78
- <a href="{{ url_for('admin_bp.logout') }}" class="admin-nav-link">
79
- <i class="fas fa-sign-out-alt"></i> Déconnexion
80
- </a>
81
- </li>
82
- </ul>
83
- </div>
84
- </div>
85
-
86
- <div class="col-md-9">
87
- <div class="admin-container">
88
- <h2 class="admin-title">Gestion des Matières</h2>
89
-
90
- <div class="row mb-4">
91
- <!-- Add Matiere Section -->
92
- <div id="add-matiere-section" class="col-md-6">
93
- <div class="card">
94
- <div class="card-header">
95
- <h4><i class="fas fa-plus-circle"></i> Ajouter une matière</h4>
96
- </div>
97
- <div class="card-body">
98
- <form method="POST" action="{{ url_for('admin_bp.matieres') }}">
99
- <input type="hidden" name="action" value="add">
100
-
101
- <div class="form-group mb-3">
102
- <label for="nom">Nom de la matière</label>
103
- <input type="text" class="form-control" id="nom" name="nom" required>
104
- </div>
105
-
106
- <div class="form-group mb-3">
107
- <label for="color_code">Couleur</label>
108
- <div class="input-group">
109
- <input type="color" class="form-control form-control-color" id="color_code" name="color_code" value="#3498db">
110
- <!-- Color preview will be added by JS -->
111
- </div>
112
- </div>
113
-
114
- <button type="submit" class="btn btn-primary">
115
- <i class="fas fa-save"></i> Ajouter
116
- </button>
117
- </form>
118
- </div>
119
- </div>
120
- </div>
121
-
122
- <!-- Edit Matiere Section (initially hidden) -->
123
- <div id="edit-matiere-section" class="col-md-6 d-none">
124
- <div class="card">
125
- <div class="card-header">
126
- <h4><i class="fas fa-edit"></i> Modifier une matière</h4>
127
- </div>
128
- <div class="card-body">
129
- <form id="edit-matiere-form" method="POST" action="{{ url_for('admin_bp.matieres') }}">
130
- <input type="hidden" name="action" value="edit">
131
- <input type="hidden" name="matiere_id" value="">
132
-
133
- <div class="form-group mb-3">
134
- <label for="edit-nom">Nom de la matière</label>
135
- <input type="text" class="form-control" id="edit-nom" name="nom" required>
136
- </div>
137
-
138
- <div class="form-group mb-3">
139
- <label for="edit-color_code">Couleur</label>
140
- <div class="input-group">
141
- <input type="color" class="form-control form-control-color" id="edit-color_code" name="color_code" value="#3498db">
142
- <!-- Color preview will be added by JS -->
143
- </div>
144
- </div>
145
-
146
- <div class="btn-group">
147
- <button type="submit" class="btn btn-primary">
148
- <i class="fas fa-save"></i> Mettre à jour
149
- </button>
150
- <button type="button" id="cancel-edit-matiere" class="btn btn-secondary">
151
- <i class="fas fa-times"></i> Annuler
152
- </button>
153
- </div>
154
- </form>
155
- </div>
156
- </div>
157
- </div>
158
- </div>
159
-
160
- <!-- List of Matieres -->
161
- <div class="card">
162
- <div class="card-header">
163
- <h4><i class="fas fa-list"></i> Liste des matières</h4>
164
- </div>
165
- <div class="card-body">
166
- {% if matieres %}
167
- <div class="row">
168
- {% for matiere in matieres %}
169
- <div class="col-md-6 mb-3">
170
- <div class="matiere-card" style="border-left-color: {{ matiere.color_code }};">
171
- <div class="d-flex justify-content-between align-items-center mb-2">
172
- <h5 class="mb-0">
173
- <span class="color-badge" style="background-color: {{ matiere.color_code }};"></span>
174
- {{ matiere.nom }}
175
- </h5>
176
- </div>
177
- <div class="small mb-2">
178
- <span class="badge bg-secondary">{{ matiere.sous_categories|length }} sous-catégories</span>
179
- </div>
180
- <div class="matiere-actions">
181
- <button class="btn btn-sm btn-primary edit-matiere-btn"
182
- data-id="{{ matiere.id }}"
183
- data-name="{{ matiere.nom }}"
184
- data-color="{{ matiere.color_code }}">
185
- <i class="fas fa-edit"></i> Modifier
186
- </button>
187
- <form method="POST" action="{{ url_for('admin_bp.matieres') }}" style="display: inline-block" onsubmit="return confirm('Êtes-vous sûr de vouloir supprimer cette matière ? Toutes les sous-catégories et textes associés seront également supprimés.')">
188
- <input type="hidden" name="action" value="delete">
189
- <input type="hidden" name="matiere_id" value="{{ matiere.id }}">
190
- <button type="submit" class="btn btn-sm btn-danger">
191
- <i class="fas fa-trash"></i> Supprimer
192
- </button>
193
- </form>
194
- </div>
195
- </div>
196
- </div>
197
- {% endfor %}
198
- </div>
199
- {% else %}
200
- <div class="alert alert-info">
201
- Aucune matière n'a été ajoutée. Utilisez le formulaire ci-dessus pour créer votre première matière.
202
- </div>
203
- {% endif %}
204
- </div>
205
- </div>
206
- </div>
207
- </div>
208
- </div>
209
- {% endblock %}
210
-
211
- {% block scripts %}
212
- <script src="{{ url_for('static', filename='js/admin.js') }}"></script>
213
- {% endblock %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
api/templates/admin/sous_categories.html DELETED
@@ -1,230 +0,0 @@
1
- {% extends 'base.html' %}
2
-
3
- {% block title %}Gestion des Sous-catégories - Administration{% endblock %}
4
-
5
- {% block styles %}
6
- <style>
7
- .sous-categorie-row {
8
- transition: background-color 0.3s ease;
9
- }
10
-
11
- .sous-categorie-row:hover {
12
- background-color: rgba(52, 152, 219, 0.1);
13
- }
14
-
15
- .color-badge {
16
- display: inline-block;
17
- width: 16px;
18
- height: 16px;
19
- border-radius: 50%;
20
- margin-right: 8px;
21
- }
22
-
23
- .filter-container {
24
- background-color: var(--block-bg);
25
- padding: 15px;
26
- border-radius: 8px;
27
- margin-bottom: 20px;
28
- }
29
-
30
- .matiere-container {
31
- margin-top: 30px;
32
- border-left: 3px solid var(--primary-color);
33
- padding-left: 15px;
34
- }
35
- </style>
36
- {% endblock %}
37
-
38
- {% block content %}
39
- <div class="row">
40
- <div class="col-md-3">
41
- <div class="admin-sidebar">
42
- <h3 class="mb-3">Administration</h3>
43
- <ul class="admin-nav">
44
- <li class="admin-nav-item">
45
- <a href="{{ url_for('admin_bp.dashboard') }}" class="admin-nav-link">
46
- <i class="fas fa-tachometer-alt"></i> Tableau de bord
47
- </a>
48
- </li>
49
- <li class="admin-nav-item">
50
- <a href="{{ url_for('admin_bp.matieres') }}" class="admin-nav-link">
51
- <i class="fas fa-book"></i> Matières
52
- </a>
53
- </li>
54
- <li class="admin-nav-item">
55
- <a href="{{ url_for('admin_bp.sous_categories') }}" class="admin-nav-link active">
56
- <i class="fas fa-list"></i> Sous-catégories
57
- </a>
58
- </li>
59
- <li class="admin-nav-item">
60
- <a href="{{ url_for('admin_bp.textes') }}" class="admin-nav-link">
61
- <i class="fas fa-file-alt"></i> Textes
62
- </a>
63
- </li>
64
- <li class="admin-nav-item">
65
- <a href="{{ url_for('admin_bp.images') }}" class="admin-nav-link">
66
- <i class="fas fa-images"></i> Images
67
- </a>
68
- </li>
69
- <li class="admin-nav-item">
70
- <a href="{{ url_for('admin_bp.logout') }}" class="admin-nav-link">
71
- <i class="fas fa-sign-out-alt"></i> Déconnexion
72
- </a>
73
- </li>
74
- </ul>
75
- </div>
76
- </div>
77
-
78
- <div class="col-md-9">
79
- <div class="admin-container">
80
- <h2 class="admin-title">Gestion des Sous-catégories</h2>
81
-
82
- <div class="row mb-4">
83
- <!-- Add Sous-Categorie Section -->
84
- <div id="add-sous-categorie-section" class="col-md-6">
85
- <div class="card">
86
- <div class="card-header">
87
- <h4><i class="fas fa-plus-circle"></i> Ajouter une sous-catégorie</h4>
88
- </div>
89
- <div class="card-body">
90
- <form method="POST" action="{{ url_for('admin_bp.sous_categories') }}">
91
- <input type="hidden" name="action" value="add">
92
-
93
- <div class="form-group mb-3">
94
- <label for="matiere_id">Matière</label>
95
- <select class="form-control" id="matiere_id" name="matiere_id" required>
96
- <option value="">Sélectionnez une matière</option>
97
- {% for matiere in matieres %}
98
- <option value="{{ matiere.id }}">{{ matiere.nom }}</option>
99
- {% endfor %}
100
- </select>
101
- </div>
102
-
103
- <div class="form-group mb-3">
104
- <label for="nom">Nom de la sous-catégorie</label>
105
- <input type="text" class="form-control" id="nom" name="nom" required>
106
- </div>
107
-
108
- <button type="submit" class="btn btn-primary">
109
- <i class="fas fa-save"></i> Ajouter
110
- </button>
111
- </form>
112
- </div>
113
- </div>
114
- </div>
115
-
116
- <!-- Edit Sous-Categorie Section (initially hidden) -->
117
- <div id="edit-sous-categorie-section" class="col-md-6 d-none">
118
- <div class="card">
119
- <div class="card-header">
120
- <h4><i class="fas fa-edit"></i> Modifier une sous-catégorie</h4>
121
- </div>
122
- <div class="card-body">
123
- <form id="edit-sous-categorie-form" method="POST" action="{{ url_for('admin_bp.sous_categories') }}">
124
- <input type="hidden" name="action" value="edit">
125
- <input type="hidden" name="sous_categorie_id" value="">
126
-
127
- <div class="form-group mb-3">
128
- <label for="edit-matiere_id">Matière</label>
129
- <select class="form-control" id="edit-matiere_id" name="matiere_id" required>
130
- {% for matiere in matieres %}
131
- <option value="{{ matiere.id }}">{{ matiere.nom }}</option>
132
- {% endfor %}
133
- </select>
134
- </div>
135
-
136
- <div class="form-group mb-3">
137
- <label for="edit-nom">Nom de la sous-catégorie</label>
138
- <input type="text" class="form-control" id="edit-nom" name="nom" required>
139
- </div>
140
-
141
- <div class="btn-group">
142
- <button type="submit" class="btn btn-primary">
143
- <i class="fas fa-save"></i> Mettre à jour
144
- </button>
145
- <button type="button" id="cancel-edit-sous-categorie" class="btn btn-secondary">
146
- <i class="fas fa-times"></i> Annuler
147
- </button>
148
- </div>
149
- </form>
150
- </div>
151
- </div>
152
- </div>
153
- </div>
154
-
155
- <!-- Filter by Matiere -->
156
- <div class="filter-container mb-4">
157
- <div class="row align-items-center">
158
- <div class="col-md-3">
159
- <label for="matiere-filter" class="form-label mb-0"><strong>Filtrer par matière :</strong></label>
160
- </div>
161
- <div class="col-md-9">
162
- <select id="matiere-filter" class="form-select">
163
- <option value="">Toutes les matières</option>
164
- {% for matiere in matieres %}
165
- <option value="{{ matiere.id }}">{{ matiere.nom }}</option>
166
- {% endfor %}
167
- </select>
168
- </div>
169
- </div>
170
- </div>
171
-
172
- <!-- List of Sous-Categories -->
173
- <div class="card">
174
- <div class="card-header">
175
- <h4><i class="fas fa-list"></i> Liste des sous-catégories</h4>
176
- </div>
177
- <div class="card-body">
178
- {% if sous_categories %}
179
- <div class="table-responsive">
180
- <table class="table table-hover">
181
- <thead>
182
- <tr>
183
- <th>Matière</th>
184
- <th>Sous-catégorie</th>
185
- <th>Actions</th>
186
- </tr>
187
- </thead>
188
- <tbody>
189
- {% for sous_categorie in sous_categories %}
190
- <tr class="sous-categorie-row" data-matiere-id="{{ sous_categorie.matiere.id }}">
191
- <td>
192
- <span class="color-badge" style="background-color: {{ sous_categorie.matiere.color_code }};"></span>
193
- {{ sous_categorie.matiere.nom }}
194
- </td>
195
- <td>{{ sous_categorie.nom }}</td>
196
- <td>
197
- <button class="btn btn-sm btn-primary edit-sous-categorie-btn"
198
- data-id="{{ sous_categorie.id }}"
199
- data-name="{{ sous_categorie.nom }}"
200
- data-matiere-id="{{ sous_categorie.matiere.id }}">
201
- <i class="fas fa-edit"></i> Modifier
202
- </button>
203
- <form method="POST" action="{{ url_for('admin_bp.sous_categories') }}" style="display: inline-block" onsubmit="return confirm('Êtes-vous sûr de vouloir supprimer cette sous-catégorie ? Tous les textes associés seront également supprimés.')">
204
- <input type="hidden" name="action" value="delete">
205
- <input type="hidden" name="sous_categorie_id" value="{{ sous_categorie.id }}">
206
- <button type="submit" class="btn btn-sm btn-danger">
207
- <i class="fas fa-trash"></i> Supprimer
208
- </button>
209
- </form>
210
- </td>
211
- </tr>
212
- {% endfor %}
213
- </tbody>
214
- </table>
215
- </div>
216
- {% else %}
217
- <div class="alert alert-info">
218
- Aucune sous-catégorie n'a été ajoutée. Utilisez le formulaire ci-dessus pour créer votre première sous-catégorie.
219
- </div>
220
- {% endif %}
221
- </div>
222
- </div>
223
- </div>
224
- </div>
225
- </div>
226
- {% endblock %}
227
-
228
- {% block scripts %}
229
- <script src="{{ url_for('static', filename='js/admin.js') }}"></script>
230
- {% endblock %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
api/templates/admin/textes.html DELETED
@@ -1,230 +0,0 @@
1
- {% extends 'base.html' %}
2
-
3
- {% block title %}Gestion des Textes - Administration{% endblock %}
4
-
5
- {% block styles %}
6
- <style>
7
- .texte-card {
8
- background-color: var(--card-bg);
9
- border-radius: 8px;
10
- padding: 16px;
11
- margin-bottom: 16px;
12
- box-shadow: var(--shadow);
13
- transition: transform 0.3s ease, box-shadow 0.3s ease;
14
- border-left: 5px solid var(--primary-color);
15
- }
16
-
17
- .texte-card:hover {
18
- transform: translateY(-5px);
19
- box-shadow: var(--hover-shadow);
20
- }
21
-
22
- .texte-actions {
23
- display: flex;
24
- justify-content: flex-end;
25
- gap: 8px;
26
- margin-top: 10px;
27
- }
28
-
29
- .accordion-button:not(.collapsed) {
30
- background-color: var(--block-bg);
31
- color: var(--text-color);
32
- }
33
-
34
- .matiere-header {
35
- padding: 10px 15px;
36
- margin: 10px 0;
37
- border-radius: 5px;
38
- color: white;
39
- }
40
-
41
- .sous-categorie-header {
42
- padding: 8px 15px;
43
- border-radius: 5px;
44
- background-color: var(--block-bg);
45
- margin: 10px 0;
46
- }
47
-
48
- .date-info {
49
- font-size: 12px;
50
- color: var(--muted-color);
51
- }
52
- </style>
53
- {% endblock %}
54
-
55
- {% block content %}
56
- <div class="row">
57
- <div class="col-md-3">
58
- <div class="admin-sidebar">
59
- <h3 class="mb-3">Administration</h3>
60
- <ul class="admin-nav">
61
- <li class="admin-nav-item">
62
- <a href="{{ url_for('admin_bp.dashboard') }}" class="admin-nav-link">
63
- <i class="fas fa-tachometer-alt"></i> Tableau de bord
64
- </a>
65
- </li>
66
- <li class="admin-nav-item">
67
- <a href="{{ url_for('admin_bp.matieres') }}" class="admin-nav-link">
68
- <i class="fas fa-book"></i> Matières
69
- </a>
70
- </li>
71
- <li class="admin-nav-item">
72
- <a href="{{ url_for('admin_bp.sous_categories') }}" class="admin-nav-link">
73
- <i class="fas fa-list"></i> Sous-catégories
74
- </a>
75
- </li>
76
- <li class="admin-nav-item">
77
- <a href="{{ url_for('admin_bp.textes') }}" class="admin-nav-link active">
78
- <i class="fas fa-file-alt"></i> Textes
79
- </a>
80
- </li>
81
- <li class="admin-nav-item">
82
- <a href="{{ url_for('admin_bp.images') }}" class="admin-nav-link">
83
- <i class="fas fa-images"></i> Images
84
- </a>
85
- </li>
86
- <li class="admin-nav-item">
87
- <a href="{{ url_for('admin_bp.logout') }}" class="admin-nav-link">
88
- <i class="fas fa-sign-out-alt"></i> Déconnexion
89
- </a>
90
- </li>
91
- </ul>
92
- </div>
93
- </div>
94
-
95
- <div class="col-md-9">
96
- <div class="admin-container">
97
- <h2 class="admin-title">Gestion des Textes</h2>
98
-
99
- <!-- Add New Text Button -->
100
- <div class="mb-4">
101
- <button class="btn btn-primary" type="button" data-bs-toggle="collapse" data-bs-target="#addTextCollapse" aria-expanded="false" aria-controls="addTextCollapse">
102
- <i class="fas fa-plus-circle"></i> Ajouter un nouveau texte
103
- </button>
104
- </div>
105
-
106
- <!-- Add Text Form (Collapsible) -->
107
- <div class="collapse mb-4" id="addTextCollapse">
108
- <div class="card">
109
- <div class="card-header">
110
- <h4><i class="fas fa-plus-circle"></i> Nouveau texte</h4>
111
- </div>
112
- <div class="card-body">
113
- <form method="POST" action="{{ url_for('admin_bp.textes') }}">
114
- <input type="hidden" name="action" value="add">
115
-
116
- <div class="form-group mb-3">
117
- <label for="titre">Titre</label>
118
- <input type="text" class="form-control" id="titre" name="titre" required>
119
- </div>
120
-
121
- <div class="row mb-3">
122
- <div class="col-md-6">
123
- <div class="form-group">
124
- <label for="matiere-select">Matière</label>
125
- <select class="form-control" id="matiere-select" required>
126
- <option value="">Sélectionnez une matière</option>
127
- {% for matiere in matieres %}
128
- <option value="{{ matiere.id }}">{{ matiere.nom }}</option>
129
- {% endfor %}
130
- </select>
131
- </div>
132
- </div>
133
- <div class="col-md-6">
134
- <div class="form-group">
135
- <label for="sous-categorie-select">Sous-catégorie</label>
136
- <select class="form-control" id="sous-categorie-select" name="sous_categorie_id" required>
137
- <option value="">Sélectionnez d'abord une matière</option>
138
- </select>
139
- </div>
140
- </div>
141
- </div>
142
-
143
- <div class="form-group mb-3">
144
- <label for="contenu">Contenu</label>
145
- <textarea class="form-control" id="contenu" name="contenu" rows="10" required></textarea>
146
- <small class="form-text text-muted">
147
- Séparez les paragraphes par des lignes vides. Les titres des blocs peuvent être indiqués sur une ligne séparée et seront automatiquement détectés.
148
- </small>
149
- </div>
150
-
151
- <button type="submit" class="btn btn-primary">
152
- <i class="fas fa-save"></i> Créer et éditer
153
- </button>
154
- </form>
155
- </div>
156
- </div>
157
- </div>
158
-
159
- <!-- List of Textes -->
160
- <div class="card">
161
- <div class="card-header">
162
- <h4><i class="fas fa-file-alt"></i> Liste des textes</h4>
163
- </div>
164
- <div class="card-body">
165
- {% if grouped_textes %}
166
- <div class="accordion" id="textesAccordion">
167
- {% for matiere_id, matiere_data in grouped_textes.items() %}
168
- <div class="accordion-item mb-3">
169
- <h2 class="accordion-header" id="heading{{ matiere_id }}">
170
- <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapse{{ matiere_id }}" aria-expanded="false" aria-controls="collapse{{ matiere_id }}" style="background-color: {{ matiere_data.color }}; color: white;">
171
- <i class="fas fa-book me-2"></i> {{ matiere_data.nom }}
172
- </button>
173
- </h2>
174
- <div id="collapse{{ matiere_id }}" class="accordion-collapse collapse" aria-labelledby="heading{{ matiere_id }}" data-bs-parent="#textesAccordion">
175
- <div class="accordion-body">
176
- {% for sous_cat_id, sous_cat_data in matiere_data.sous_categories.items() %}
177
- <div class="sous-categorie-group">
178
- <div class="sous-categorie-header" style="border-left: 5px solid {{ matiere_data.color }};">
179
- <i class="fas fa-list-ul me-2"></i> {{ sous_cat_data.nom }}
180
- </div>
181
-
182
- <div class="row">
183
- {% for texte in sous_cat_data.textes %}
184
- <div class="col-md-6 mb-3">
185
- <div class="texte-card" style="border-left-color: {{ matiere_data.color }};">
186
- <h5>{{ texte.titre }}</h5>
187
- <p class="date-info">
188
- Dernière mise à jour: {{ texte.updated_at.strftime('%d/%m/%Y à %H:%M') }}
189
- </p>
190
- <div class="texte-actions">
191
- <a href="{{ url_for('admin_bp.edit_texte', texte_id=texte.id) }}" class="btn btn-sm btn-primary">
192
- <i class="fas fa-edit"></i> Éditer
193
- </a>
194
- <a href="{{ url_for('admin_bp.historique', texte_id=texte.id) }}" class="btn btn-sm btn-info">
195
- <i class="fas fa-history"></i> Historique
196
- </a>
197
- <form method="POST" action="{{ url_for('admin_bp.textes') }}" onsubmit="return confirm('Êtes-vous sûr de vouloir supprimer ce texte ?')">
198
- <input type="hidden" name="action" value="delete">
199
- <input type="hidden" name="texte_id" value="{{ texte.id }}">
200
- <button type="submit" class="btn btn-sm btn-danger">
201
- <i class="fas fa-trash"></i>
202
- </button>
203
- </form>
204
- </div>
205
- </div>
206
- </div>
207
- {% endfor %}
208
- </div>
209
- </div>
210
- {% endfor %}
211
- </div>
212
- </div>
213
- </div>
214
- {% endfor %}
215
- </div>
216
- {% else %}
217
- <div class="alert alert-info">
218
- Aucun texte n'a été ajouté. Utilisez le formulaire ci-dessus pour créer votre premier texte.
219
- </div>
220
- {% endif %}
221
- </div>
222
- </div>
223
- </div>
224
- </div>
225
- </div>
226
- {% endblock %}
227
-
228
- {% block scripts %}
229
- <script src="{{ url_for('static', filename='js/admin.js') }}"></script>
230
- {% endblock %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
api/templates/base.html DELETED
@@ -1,141 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="fr" data-theme="light">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>{% block title %}Plateforme Éducative{% endblock %}</title>
7
-
8
- <!-- Bootstrap CSS from CDN -->
9
- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
10
-
11
- <!-- Font Awesome for icons -->
12
- <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
13
-
14
- <!-- Google Fonts -->
15
- <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
16
-
17
- <!-- Custom CSS -->
18
- <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
19
-
20
- <!-- Additional CSS specific to templates -->
21
- {% block styles %}{% endblock %}
22
- </head>
23
- <body>
24
- <!-- Header -->
25
- <header class="main-header">
26
- <div class="container header-container">
27
- <!-- Burger Menu Button -->
28
- <button id="burger-menu" class="burger-menu-button" title="Ouvrir le menu">
29
- <i class="fas fa-bars"></i>
30
- </button>
31
-
32
- <h1 class="site-title">
33
- <a href="{{ url_for('main_bp.index') }}">Méthodologies</a>
34
- </h1>
35
-
36
- <div class="header-actions">
37
- <!-- Theme toggle button -->
38
- <button id="theme-toggle" class="theme-toggle" title="Changer de thème">
39
- <i class="fas fa-moon"></i>
40
- </button>
41
-
42
- <!-- Admin link if admin is logged in -->
43
- {% if session.get('admin_logged_in') %}
44
- <a href="{{ url_for('admin_bp.dashboard') }}" class="btn btn-primary btn-sm ms-2">
45
- <i class="fas fa-cog"></i> Gestion
46
- </a>
47
- {% endif %}
48
- </div>
49
- </div>
50
- </header>
51
-
52
- <!-- Sidebar Overlay -->
53
- <div id="sidebar-overlay" class="sidebar-overlay"></div>
54
-
55
- <!-- Sidebar 1: Matières -->
56
- <nav id="sidebar-matieres" class="sidebar">
57
- <div class="sidebar-header">
58
- <h4>Matières</h4>
59
- <button class="close-sidebar-btn" data-sidebar-id="sidebar-matieres" title="Fermer le menu">×</button>
60
- </div>
61
- <ul id="matieres-list-sidebar" class="sidebar-list">
62
- {# Assure-toi de passer la variable 'matieres' depuis ta route Flask #}
63
- {# Example: return render_template('index.html', matieres=get_all_matieres()) #}
64
- {% if matieres %}
65
- {% for matiere in matieres %}
66
- {# Utilise |e pour échapper les caractères spéciaux dans le nom pour l'attribut data #}
67
- <li data-matiere-id="{{ matiere.id }}" data-matiere-nom="{{ matiere.nom | e }}">
68
- {{ matiere.nom }}
69
- <i class="fas fa-chevron-right float-end"></i>
70
- </li>
71
- {% endfor %}
72
- {% else %}
73
- <li>Aucune matière disponible.</li>
74
- {% endif %}
75
- </ul>
76
- </nav>
77
-
78
- <!-- Sidebar 2: Sous-catégories -->
79
- <nav id="sidebar-sous-categories" class="sidebar">
80
- <div class="sidebar-header">
81
- <button id="sidebar-back-button" class="sidebar-back-button" title="Retour aux matières">
82
- <i class="fas fa-arrow-left"></i>
83
- </button>
84
- <h4 id="sidebar-sous-categories-title">Sous-catégories</h4>
85
- <button class="close-sidebar-btn" data-sidebar-id="sidebar-sous-categories" title="Fermer le menu">×</button>
86
- </div>
87
- <ul id="sous-categories-list-sidebar" class="sidebar-list">
88
- <!-- Les sous-catégories seront chargées ici par JavaScript -->
89
- <li>Chargement...</li>
90
- </ul>
91
- </nav>
92
-
93
- <!-- Flash messages -->
94
- <div class="container mt-3 flash-container">
95
- {% with messages = get_flashed_messages(with_categories=true) %}
96
- {% if messages %}
97
- {% for category, message in messages %}
98
- <div class="alert alert-{{ category if category != 'message' else 'info' }} alert-dismissible fade show" role="alert">
99
- {{ message }}
100
- <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
101
- </div>
102
- {% endfor %}
103
- {% endif %}
104
- {% endwith %}
105
- </div>
106
-
107
- <!-- Main content -->
108
- <main class="main-content">
109
- <div class="container">
110
- {% block content %}{% endblock %}
111
- </div>
112
- </main>
113
-
114
- <!-- Footer -->
115
- <footer class="main-footer">
116
- <div class="container footer-content">
117
-
118
-
119
- <!-- Feedback form -->
120
- <div class="feedback-form">
121
- <h4 class="feedback-title">Votre avis nous intéresse</h4>
122
- <form id="feedback-form" action="{{ url_for('main_bp.submit_feedback') }}" method="POST">
123
- <div class="form-group mb-2">
124
- <textarea id="feedback-message" name="message" class="form-control" rows="2" placeholder="Laissez-nous un message..." required></textarea>
125
- </div>
126
- <button type="submit" class="btn btn-primary">Envoyer</button>
127
- </form>
128
- </div>
129
- </div>
130
- </footer>
131
-
132
- <!-- Bootstrap JS Bundle with Popper -->
133
- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
134
-
135
- <!-- Main JavaScript -->
136
- <script src="{{ url_for('static', filename='js/main.js') }}"></script>
137
-
138
- <!-- Additional JavaScript -->
139
- {% block scripts %}{% endblock %}
140
- </body>
141
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
api/templates/index.html DELETED
@@ -1,40 +0,0 @@
1
- {% extends 'base.html' %}
2
-
3
- {% block title %}Accueil - Méthodologies{% endblock %}
4
-
5
- {% block content %}
6
-
7
- <!-- Initial Instructions -->
8
- {# Cette section est visible par défaut lorsque la page se charge #}
9
- {# Elle sera cachée par JavaScript une fois qu'un contenu est sélectionné via les sidebars #}
10
- <div id="initial-instructions" class="initial-instructions text-center mt-5">
11
- <i class="fas fa-info-circle fa-3x text-muted mb-3"></i>
12
- <h3>Bienvenue !</h3>
13
- <p class="lead">Utilisez le menu <i class="fas fa-bars"></i> en haut à gauche pour naviguer.</p>
14
- <p>Sélectionnez une matière, puis une sous-catégorie pour afficher le contenu ici.</p>
15
- </div>
16
-
17
- <!-- Content Display Section -->
18
- {# Cette section est cachée par défaut ('d-none') #}
19
- {# Elle sera rendue visible et remplie par JavaScript lorsque l'utilisateur sélectionne une sous-catégorie #}
20
- <section id="content-section" class="mb-5 d-none">
21
- <div class="content-viewer">
22
- <h2 id="content-title" class="content-title">
23
- <!-- Le titre du contenu sera chargé ici par JavaScript -->
24
- </h2>
25
- <div id="content-blocks" class="content-blocks">
26
- <!-- Les blocs de contenu seront chargés ici par JavaScript -->
27
- </div>
28
- </div>
29
- </section>
30
-
31
- {% endblock %}
32
-
33
- {% block scripts %}
34
- {# Si tu as du JavaScript spécifique à cette page (peu probable maintenant que tout est dans main.js), tu peux l'ajouter ici #}
35
- {# Par exemple : <script src="{{ url_for('static', filename='js/index_specific.js') }}"></script> #}
36
- {% endblock %}
37
-
38
- {% block styles %}
39
- {# Si tu as du CSS spécifique à cette page, tu peux l'ajouter ici #}
40
- {% endblock %}