Spaces:
Sleeping
Sleeping
Delete api
Browse files- api/index.py +0 -948
- api/static/css/style.css +0 -762
- api/static/js/admin.js +0 -676
- api/static/js/main.js +0 -517
- api/templates/admin/dashboard.html +0 -148
- api/templates/admin/edit_texte.html +0 -282
- api/templates/admin/historique.html +0 -171
- api/templates/admin/images.html +0 -282
- api/templates/admin/login.html +0 -87
- api/templates/admin/matieres.html +0 -213
- api/templates/admin/sous_categories.html +0 -230
- api/templates/admin/textes.html +0 -230
- api/templates/base.html +0 -141
- api/templates/index.html +0 -40
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 %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|