Spaces:
Sleeping
Sleeping
89192
Browse files- __pycache__/config.cpython-312.pyc +0 -0
- api/index.py +948 -0
- api/static/css/style.css +683 -0
- api/static/js/admin.js +676 -0
- api/static/js/main.js +393 -0
- api/templates/admin/dashboard.html +148 -0
- api/templates/admin/edit_texte.html +282 -0
- api/templates/admin/historique.html +171 -0
- api/templates/admin/images.html +282 -0
- api/templates/admin/login.html +87 -0
- api/templates/admin/matieres.html +213 -0
- api/templates/admin/sous_categories.html +230 -0
- api/templates/admin/textes.html +230 -0
- api/templates/base.html +102 -0
- api/templates/index.html +79 -0
- app.py +8 -0
- app/__init__.py +0 -28
- app/__pycache__/__init__.cpython-312.pyc +0 -0
- app/__pycache__/admin.cpython-312.pyc +0 -0
- app/__pycache__/models.cpython-312.pyc +0 -0
- app/__pycache__/views.cpython-312.pyc +0 -0
- app/admin.py +0 -70
- app/models.py +0 -34
- app/static/css/style.css +0 -585
- app/templates/base.html +0 -44
- app/templates/index.html +0 -19
- app/templates/matiere.html +0 -34
- app/templates/sous_categorie.html +0 -35
- app/templates/texte.html +0 -15
- app/views.py +0 -28
- config.py +0 -17
- main.py +10 -0
- migrations/README +0 -1
- migrations/__pycache__/env.cpython-312.pyc +0 -0
- migrations/alembic.ini +0 -50
- migrations/env.py +0 -113
- migrations/script.py.mako +0 -24
- migrations/versions/80f91a17b1db_initial_migration.py +0 -112
- migrations/versions/__pycache__/80f91a17b1db_initial_migration.cpython-312.pyc +0 -0
- run.py +0 -6
__pycache__/config.cpython-312.pyc
DELETED
|
Binary file (667 Bytes)
|
|
|
api/index.py
ADDED
|
@@ -0,0 +1,948 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,683 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
}
|
| 25 |
+
|
| 26 |
+
[data-theme="dark"] {
|
| 27 |
+
/* Dark theme */
|
| 28 |
+
--background-color: #121212;
|
| 29 |
+
--text-color: #e0e0e0;
|
| 30 |
+
--primary-color: #3498db;
|
| 31 |
+
--secondary-color: #2ecc71;
|
| 32 |
+
--accent-color: #e74c3c;
|
| 33 |
+
--muted-color: #95a5a6;
|
| 34 |
+
--border-color: #333333;
|
| 35 |
+
--card-bg: #1e1e1e;
|
| 36 |
+
--header-bg: #1a1a1a;
|
| 37 |
+
--footer-bg: #1a1a1a;
|
| 38 |
+
--shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
|
| 39 |
+
--hover-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
|
| 40 |
+
--block-bg: #1e1e1e;
|
| 41 |
+
--block-border: 1px solid #333333;
|
| 42 |
+
--input-bg: #2c2c2c;
|
| 43 |
+
--btn-primary-bg: var(--primary-color);
|
| 44 |
+
--btn-primary-text: #ffffff;
|
| 45 |
+
--btn-secondary-bg: #555555;
|
| 46 |
+
--btn-secondary-text: #ffffff;
|
| 47 |
+
--modal-bg: #1e1e1e;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
/* General Styles */
|
| 51 |
+
body {
|
| 52 |
+
background-color: var(--background-color);
|
| 53 |
+
color: var(--text-color);
|
| 54 |
+
font-family: 'Roboto', 'Segoe UI', 'Arial', sans-serif;
|
| 55 |
+
line-height: 1.6;
|
| 56 |
+
transition: background-color 0.3s ease, color 0.3s ease;
|
| 57 |
+
margin: 0;
|
| 58 |
+
padding: 0;
|
| 59 |
+
min-height: 100vh;
|
| 60 |
+
display: flex;
|
| 61 |
+
flex-direction: column;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
a {
|
| 65 |
+
color: var(--primary-color);
|
| 66 |
+
text-decoration: none;
|
| 67 |
+
transition: color 0.3s ease;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
a:hover {
|
| 71 |
+
color: #2980b9;
|
| 72 |
+
text-decoration: underline;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
/* Header Styles */
|
| 76 |
+
.main-header {
|
| 77 |
+
background-color: var(--header-bg);
|
| 78 |
+
padding: 1rem 0;
|
| 79 |
+
box-shadow: var(--shadow);
|
| 80 |
+
position: sticky;
|
| 81 |
+
top: 0;
|
| 82 |
+
z-index: 1000;
|
| 83 |
+
transition: background-color 0.3s ease, box-shadow 0.3s ease;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.header-container {
|
| 87 |
+
display: flex;
|
| 88 |
+
justify-content: space-between;
|
| 89 |
+
align-items: center;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.site-title {
|
| 93 |
+
font-size: 1.8rem;
|
| 94 |
+
font-weight: bold;
|
| 95 |
+
margin: 0;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
.site-title a {
|
| 99 |
+
color: var(--text-color);
|
| 100 |
+
text-decoration: none;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
/* Theme Toggle */
|
| 104 |
+
.theme-toggle {
|
| 105 |
+
background: none;
|
| 106 |
+
border: none;
|
| 107 |
+
cursor: pointer;
|
| 108 |
+
color: var(--text-color);
|
| 109 |
+
font-size: 1.5rem;
|
| 110 |
+
padding: 0.5rem;
|
| 111 |
+
transition: color 0.3s ease, transform 0.3s ease;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
.theme-toggle:hover {
|
| 115 |
+
color: var(--primary-color);
|
| 116 |
+
transform: rotate(15deg);
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
/* Main Content */
|
| 120 |
+
.main-content {
|
| 121 |
+
flex: 1;
|
| 122 |
+
padding: 2rem 0;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
/* Subject Selection Cards */
|
| 126 |
+
.subject-card {
|
| 127 |
+
background-color: var(--card-bg);
|
| 128 |
+
border-radius: 8px;
|
| 129 |
+
padding: 1.5rem;
|
| 130 |
+
margin-bottom: 1.5rem;
|
| 131 |
+
box-shadow: var(--shadow);
|
| 132 |
+
transition: box-shadow 0.3s ease, transform 0.3s ease;
|
| 133 |
+
cursor: pointer;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
.subject-card:hover {
|
| 137 |
+
box-shadow: var(--hover-shadow);
|
| 138 |
+
transform: translateY(-5px);
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.subject-card h3 {
|
| 142 |
+
margin-top: 0;
|
| 143 |
+
margin-bottom: 1rem;
|
| 144 |
+
font-size: 1.5rem;
|
| 145 |
+
color: var(--text-color);
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
/* Subject Color-Coding */
|
| 149 |
+
.subject-indicator {
|
| 150 |
+
width: 100%;
|
| 151 |
+
height: 8px;
|
| 152 |
+
border-radius: 4px;
|
| 153 |
+
margin-bottom: 1rem;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
/* Content Blocks Styling */
|
| 157 |
+
.content-block {
|
| 158 |
+
background-color: var(--block-bg);
|
| 159 |
+
border-radius: 8px;
|
| 160 |
+
padding: 1.5rem;
|
| 161 |
+
margin-bottom: 2rem;
|
| 162 |
+
box-shadow: var(--shadow);
|
| 163 |
+
border: var(--block-border);
|
| 164 |
+
transition: box-shadow 0.3s ease;
|
| 165 |
+
overflow: hidden;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.content-block:hover {
|
| 169 |
+
box-shadow: var(--hover-shadow);
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
.content-block-title {
|
| 173 |
+
font-size: 1.4rem;
|
| 174 |
+
font-weight: bold;
|
| 175 |
+
margin-top: 0;
|
| 176 |
+
margin-bottom: 1rem;
|
| 177 |
+
color: var(--text-color);
|
| 178 |
+
padding-bottom: 0.5rem;
|
| 179 |
+
border-bottom: 2px solid var(--primary-color);
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.content-block-content {
|
| 183 |
+
line-height: 1.7;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
/* Image positioning styles */
|
| 187 |
+
.block-with-image {
|
| 188 |
+
display: flex;
|
| 189 |
+
flex-wrap: wrap;
|
| 190 |
+
gap: 1.5rem;
|
| 191 |
+
align-items: flex-start;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.block-with-image.image-left {
|
| 195 |
+
flex-direction: row;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.block-with-image.image-right {
|
| 199 |
+
flex-direction: row-reverse;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
.block-with-image.image-top {
|
| 203 |
+
flex-direction: column;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
.block-image-container {
|
| 207 |
+
flex: 0 0 auto;
|
| 208 |
+
max-width: 30%;
|
| 209 |
+
margin-bottom: 1rem;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
.block-with-image.image-top .block-image-container {
|
| 213 |
+
max-width: 100%;
|
| 214 |
+
margin-bottom: 1.5rem;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
.block-image {
|
| 218 |
+
width: 100%;
|
| 219 |
+
height: auto;
|
| 220 |
+
border-radius: 6px;
|
| 221 |
+
box-shadow: var(--shadow);
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
.block-content-container {
|
| 225 |
+
flex: 1;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
@media (max-width: 768px) {
|
| 229 |
+
.block-with-image {
|
| 230 |
+
flex-direction: column !important;
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
.block-image-container {
|
| 234 |
+
max-width: 100%;
|
| 235 |
+
margin-bottom: 1.5rem;
|
| 236 |
+
}
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
/* Category Selection UI */
|
| 240 |
+
.selection-container {
|
| 241 |
+
background-color: var(--card-bg);
|
| 242 |
+
border-radius: 8px;
|
| 243 |
+
padding: 1.5rem;
|
| 244 |
+
margin-bottom: 2rem;
|
| 245 |
+
box-shadow: var(--shadow);
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
.selection-title {
|
| 249 |
+
font-size: 1.2rem;
|
| 250 |
+
font-weight: 600;
|
| 251 |
+
margin-bottom: 1rem;
|
| 252 |
+
padding-bottom: 0.5rem;
|
| 253 |
+
border-bottom: 1px solid var(--border-color);
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
.selection-list {
|
| 257 |
+
list-style: none;
|
| 258 |
+
padding: 0;
|
| 259 |
+
margin: 0;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
.selection-item {
|
| 263 |
+
padding: 0.5rem 0.8rem;
|
| 264 |
+
margin-bottom: 0.5rem;
|
| 265 |
+
border-radius: 4px;
|
| 266 |
+
cursor: pointer;
|
| 267 |
+
transition: background-color 0.3s ease;
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
.selection-item:hover {
|
| 271 |
+
background-color: rgba(52, 152, 219, 0.1);
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
.selection-item.active {
|
| 275 |
+
background-color: var(--primary-color);
|
| 276 |
+
color: white;
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
/* Content Viewer */
|
| 280 |
+
.content-viewer {
|
| 281 |
+
background-color: var(--card-bg);
|
| 282 |
+
border-radius: 8px;
|
| 283 |
+
padding: 2rem;
|
| 284 |
+
box-shadow: var(--shadow);
|
| 285 |
+
margin-bottom: 2rem;
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
.content-title {
|
| 289 |
+
font-size: 2rem;
|
| 290 |
+
margin-top: 0;
|
| 291 |
+
margin-bottom: 1.5rem;
|
| 292 |
+
padding-bottom: 0.5rem;
|
| 293 |
+
border-bottom: 2px solid var(--primary-color);
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
/* Buttons */
|
| 297 |
+
.btn {
|
| 298 |
+
display: inline-block;
|
| 299 |
+
font-weight: 500;
|
| 300 |
+
text-align: center;
|
| 301 |
+
white-space: nowrap;
|
| 302 |
+
vertical-align: middle;
|
| 303 |
+
user-select: none;
|
| 304 |
+
border: 1px solid transparent;
|
| 305 |
+
padding: 0.5rem 1rem;
|
| 306 |
+
font-size: 1rem;
|
| 307 |
+
line-height: 1.5;
|
| 308 |
+
border-radius: 0.25rem;
|
| 309 |
+
transition: all 0.15s ease-in-out;
|
| 310 |
+
cursor: pointer;
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
.btn-primary {
|
| 314 |
+
background-color: var(--btn-primary-bg);
|
| 315 |
+
color: var(--btn-primary-text);
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
.btn-primary:hover {
|
| 319 |
+
background-color: #2980b9;
|
| 320 |
+
color: white;
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
.btn-secondary {
|
| 324 |
+
background-color: var(--btn-secondary-bg);
|
| 325 |
+
color: var(--btn-secondary-text);
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
.btn-secondary:hover {
|
| 329 |
+
background-color: #5a6268;
|
| 330 |
+
color: white;
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
/* Form Elements */
|
| 334 |
+
input, select, textarea {
|
| 335 |
+
background-color: var(--input-bg);
|
| 336 |
+
color: var(--text-color);
|
| 337 |
+
border: 1px solid var(--border-color);
|
| 338 |
+
border-radius: 4px;
|
| 339 |
+
padding: 0.75rem 1rem;
|
| 340 |
+
width: 100%;
|
| 341 |
+
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
input:focus, select:focus, textarea:focus {
|
| 345 |
+
border-color: var(--primary-color);
|
| 346 |
+
outline: none;
|
| 347 |
+
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.25);
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
label {
|
| 351 |
+
display: block;
|
| 352 |
+
margin-bottom: 0.5rem;
|
| 353 |
+
font-weight: 500;
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
.form-group {
|
| 357 |
+
margin-bottom: 1.5rem;
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
/* Footer */
|
| 361 |
+
.main-footer {
|
| 362 |
+
background-color: var(--footer-bg);
|
| 363 |
+
padding: 2rem 0;
|
| 364 |
+
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.05);
|
| 365 |
+
margin-top: auto;
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
.footer-content {
|
| 369 |
+
display: flex;
|
| 370 |
+
justify-content: space-between;
|
| 371 |
+
flex-wrap: wrap;
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
.footer-links {
|
| 375 |
+
list-style: none;
|
| 376 |
+
padding: 0;
|
| 377 |
+
display: flex;
|
| 378 |
+
gap: 1.5rem;
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
.feedback-form {
|
| 382 |
+
margin-top: 1rem;
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
.feedback-title {
|
| 386 |
+
font-size: 1.2rem;
|
| 387 |
+
margin-bottom: 0.75rem;
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
/* Admin specific styles */
|
| 391 |
+
.admin-container {
|
| 392 |
+
background-color: var(--card-bg);
|
| 393 |
+
border-radius: 8px;
|
| 394 |
+
padding: 2rem;
|
| 395 |
+
box-shadow: var(--shadow);
|
| 396 |
+
margin-bottom: 2rem;
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
.admin-title {
|
| 400 |
+
margin-top: 0;
|
| 401 |
+
margin-bottom: 1.5rem;
|
| 402 |
+
font-size: 1.8rem;
|
| 403 |
+
color: var(--text-color);
|
| 404 |
+
border-bottom: 2px solid var(--primary-color);
|
| 405 |
+
padding-bottom: 0.5rem;
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
.admin-sidebar {
|
| 409 |
+
background-color: var(--block-bg);
|
| 410 |
+
border-radius: 8px;
|
| 411 |
+
padding: 1.5rem;
|
| 412 |
+
margin-bottom: 2rem;
|
| 413 |
+
box-shadow: var(--shadow);
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
.admin-nav {
|
| 417 |
+
list-style: none;
|
| 418 |
+
padding: 0;
|
| 419 |
+
margin: 0;
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
.admin-nav-item {
|
| 423 |
+
margin-bottom: 0.5rem;
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
.admin-nav-link {
|
| 427 |
+
display: block;
|
| 428 |
+
padding: 0.75rem 1rem;
|
| 429 |
+
border-radius: 4px;
|
| 430 |
+
color: var(--text-color);
|
| 431 |
+
text-decoration: none;
|
| 432 |
+
transition: background-color 0.3s ease, color 0.3s ease;
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
.admin-nav-link:hover, .admin-nav-link.active {
|
| 436 |
+
background-color: var(--primary-color);
|
| 437 |
+
color: white;
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
.admin-nav-link i {
|
| 441 |
+
margin-right: 0.5rem;
|
| 442 |
+
width: 20px;
|
| 443 |
+
text-align: center;
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
.admin-card {
|
| 447 |
+
background-color: var(--block-bg);
|
| 448 |
+
border-radius: 8px;
|
| 449 |
+
padding: 1.5rem;
|
| 450 |
+
margin-bottom: 1.5rem;
|
| 451 |
+
box-shadow: var(--shadow);
|
| 452 |
+
border-left: 4px solid var(--primary-color);
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
.admin-card-title {
|
| 456 |
+
font-size: 1.2rem;
|
| 457 |
+
margin-top: 0;
|
| 458 |
+
margin-bottom: 1rem;
|
| 459 |
+
color: var(--text-color);
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
.admin-stat {
|
| 463 |
+
font-size: 2.5rem;
|
| 464 |
+
font-weight: bold;
|
| 465 |
+
margin-bottom: 0.5rem;
|
| 466 |
+
color: var(--primary-color);
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
/* Editor styles */
|
| 470 |
+
.editor-container {
|
| 471 |
+
background-color: var(--block-bg);
|
| 472 |
+
border-radius: 8px;
|
| 473 |
+
padding: 1.5rem;
|
| 474 |
+
margin-bottom: 2rem;
|
| 475 |
+
box-shadow: var(--shadow);
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
.block-editor {
|
| 479 |
+
background-color: var(--card-bg);
|
| 480 |
+
border-radius: 8px;
|
| 481 |
+
padding: 1.5rem;
|
| 482 |
+
margin-bottom: 1.5rem;
|
| 483 |
+
box-shadow: var(--shadow);
|
| 484 |
+
border: var(--block-border);
|
| 485 |
+
position: relative;
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
.block-editor-header {
|
| 489 |
+
display: flex;
|
| 490 |
+
justify-content: space-between;
|
| 491 |
+
align-items: center;
|
| 492 |
+
margin-bottom: 1rem;
|
| 493 |
+
padding-bottom: 0.5rem;
|
| 494 |
+
border-bottom: 1px solid var(--border-color);
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
.block-editor-title {
|
| 498 |
+
font-size: 1.2rem;
|
| 499 |
+
font-weight: 600;
|
| 500 |
+
margin: 0;
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
.block-editor-actions {
|
| 504 |
+
display: flex;
|
| 505 |
+
gap: 0.5rem;
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
.block-handle {
|
| 509 |
+
cursor: move;
|
| 510 |
+
padding: 0.25rem 0.5rem;
|
| 511 |
+
margin-right: 0.5rem;
|
| 512 |
+
background-color: var(--border-color);
|
| 513 |
+
border-radius: 4px;
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
.image-preview {
|
| 517 |
+
max-width: 100%;
|
| 518 |
+
height: auto;
|
| 519 |
+
margin-top: 1rem;
|
| 520 |
+
border-radius: 6px;
|
| 521 |
+
box-shadow: var(--shadow);
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
.image-gallery {
|
| 525 |
+
display: grid;
|
| 526 |
+
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
| 527 |
+
gap: 1rem;
|
| 528 |
+
margin-top: 1rem;
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
.gallery-item {
|
| 532 |
+
position: relative;
|
| 533 |
+
border-radius: 6px;
|
| 534 |
+
overflow: hidden;
|
| 535 |
+
box-shadow: var(--shadow);
|
| 536 |
+
cursor: pointer;
|
| 537 |
+
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
.gallery-item:hover {
|
| 541 |
+
transform: scale(1.05);
|
| 542 |
+
box-shadow: var(--hover-shadow);
|
| 543 |
+
}
|
| 544 |
+
|
| 545 |
+
.gallery-image {
|
| 546 |
+
width: 100%;
|
| 547 |
+
height: 120px;
|
| 548 |
+
object-fit: cover;
|
| 549 |
+
display: block;
|
| 550 |
+
}
|
| 551 |
+
|
| 552 |
+
.gallery-image-select {
|
| 553 |
+
position: absolute;
|
| 554 |
+
top: 5px;
|
| 555 |
+
right: 5px;
|
| 556 |
+
background-color: rgba(0, 0, 0, 0.5);
|
| 557 |
+
color: white;
|
| 558 |
+
border: none;
|
| 559 |
+
border-radius: 50%;
|
| 560 |
+
width: 30px;
|
| 561 |
+
height: 30px;
|
| 562 |
+
display: flex;
|
| 563 |
+
align-items: center;
|
| 564 |
+
justify-content: center;
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
/* Login screen */
|
| 568 |
+
.login-container {
|
| 569 |
+
max-width: 400px;
|
| 570 |
+
margin: 3rem auto;
|
| 571 |
+
background-color: var(--card-bg);
|
| 572 |
+
border-radius: 8px;
|
| 573 |
+
padding: 2rem;
|
| 574 |
+
box-shadow: var(--shadow);
|
| 575 |
+
}
|
| 576 |
+
|
| 577 |
+
.login-logo {
|
| 578 |
+
text-align: center;
|
| 579 |
+
margin-bottom: 2rem;
|
| 580 |
+
}
|
| 581 |
+
|
| 582 |
+
.login-title {
|
| 583 |
+
font-size: 1.8rem;
|
| 584 |
+
text-align: center;
|
| 585 |
+
margin-bottom: 2rem;
|
| 586 |
+
}
|
| 587 |
+
|
| 588 |
+
/* Responsive design adjustments */
|
| 589 |
+
@media (max-width: 992px) {
|
| 590 |
+
.container {
|
| 591 |
+
max-width: 100%;
|
| 592 |
+
padding-left: 1rem;
|
| 593 |
+
padding-right: 1rem;
|
| 594 |
+
}
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
@media (max-width: 576px) {
|
| 598 |
+
.subject-card {
|
| 599 |
+
padding: 1rem;
|
| 600 |
+
}
|
| 601 |
+
|
| 602 |
+
.content-viewer {
|
| 603 |
+
padding: 1.5rem;
|
| 604 |
+
}
|
| 605 |
+
|
| 606 |
+
.admin-container {
|
| 607 |
+
padding: 1.5rem;
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
.content-title {
|
| 611 |
+
font-size: 1.6rem;
|
| 612 |
+
}
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
/* Animation keyframes */
|
| 616 |
+
@keyframes fadeIn {
|
| 617 |
+
from { opacity: 0; }
|
| 618 |
+
to { opacity: 1; }
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
.fade-in {
|
| 622 |
+
animation: fadeIn 0.5s ease-in;
|
| 623 |
+
}
|
| 624 |
+
|
| 625 |
+
@keyframes slideInUp {
|
| 626 |
+
from {
|
| 627 |
+
transform: translateY(50px);
|
| 628 |
+
opacity: 0;
|
| 629 |
+
}
|
| 630 |
+
to {
|
| 631 |
+
transform: translateY(0);
|
| 632 |
+
opacity: 1;
|
| 633 |
+
}
|
| 634 |
+
}
|
| 635 |
+
|
| 636 |
+
.slide-in-up {
|
| 637 |
+
animation: slideInUp 0.5s ease-out;
|
| 638 |
+
}
|
| 639 |
+
|
| 640 |
+
/* Utility classes */
|
| 641 |
+
.text-center {
|
| 642 |
+
text-align: center;
|
| 643 |
+
}
|
| 644 |
+
|
| 645 |
+
.mb-1 { margin-bottom: 0.5rem; }
|
| 646 |
+
.mb-2 { margin-bottom: 1rem; }
|
| 647 |
+
.mb-3 { margin-bottom: 1.5rem; }
|
| 648 |
+
.mb-4 { margin-bottom: 2rem; }
|
| 649 |
+
|
| 650 |
+
.mt-1 { margin-top: 0.5rem; }
|
| 651 |
+
.mt-2 { margin-top: 1rem; }
|
| 652 |
+
.mt-3 { margin-top: 1.5rem; }
|
| 653 |
+
.mt-4 { margin-top: 2rem; }
|
| 654 |
+
|
| 655 |
+
.alert {
|
| 656 |
+
padding: 1rem;
|
| 657 |
+
margin-bottom: 1rem;
|
| 658 |
+
border-radius: 4px;
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
.alert-success {
|
| 662 |
+
background-color: rgba(46, 204, 113, 0.2);
|
| 663 |
+
color: #2ecc71;
|
| 664 |
+
border: 1px solid rgba(46, 204, 113, 0.3);
|
| 665 |
+
}
|
| 666 |
+
|
| 667 |
+
.alert-danger {
|
| 668 |
+
background-color: rgba(231, 76, 60, 0.2);
|
| 669 |
+
color: #e74c3c;
|
| 670 |
+
border: 1px solid rgba(231, 76, 60, 0.3);
|
| 671 |
+
}
|
| 672 |
+
|
| 673 |
+
.alert-warning {
|
| 674 |
+
background-color: rgba(241, 196, 15, 0.2);
|
| 675 |
+
color: #f1c40f;
|
| 676 |
+
border: 1px solid rgba(241, 196, 15, 0.3);
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
.alert-info {
|
| 680 |
+
background-color: rgba(52, 152, 219, 0.2);
|
| 681 |
+
color: #3498db;
|
| 682 |
+
border: 1px solid rgba(52, 152, 219, 0.3);
|
| 683 |
+
}
|
api/static/js/admin.js
ADDED
|
@@ -0,0 +1,676 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Main application JavaScript for the frontend
|
| 2 |
+
|
| 3 |
+
// Wait for the DOM to be loaded before executing
|
| 4 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 5 |
+
// Initialize theme
|
| 6 |
+
initTheme();
|
| 7 |
+
|
| 8 |
+
// Setup interactive elements
|
| 9 |
+
setupSubjectSelection();
|
| 10 |
+
setupCategorySelection();
|
| 11 |
+
setupTextSelection();
|
| 12 |
+
setupThemeToggle();
|
| 13 |
+
|
| 14 |
+
// Setup feedback form submission
|
| 15 |
+
const feedbackForm = document.getElementById('feedback-form');
|
| 16 |
+
if (feedbackForm) {
|
| 17 |
+
feedbackForm.addEventListener('submit', function(e) {
|
| 18 |
+
const feedbackMessage = document.getElementById('feedback-message');
|
| 19 |
+
if (!feedbackMessage.value.trim()) {
|
| 20 |
+
e.preventDefault();
|
| 21 |
+
alert('Veuillez entrer un message avant d\'envoyer votre feedback.');
|
| 22 |
+
}
|
| 23 |
+
});
|
| 24 |
+
}
|
| 25 |
+
});
|
| 26 |
+
|
| 27 |
+
// Initialize theme based on user preference
|
| 28 |
+
function initTheme() {
|
| 29 |
+
const userPreference = localStorage.getItem('theme') || 'light';
|
| 30 |
+
document.documentElement.setAttribute('data-theme', userPreference);
|
| 31 |
+
|
| 32 |
+
// Update theme icon
|
| 33 |
+
updateThemeIcon(userPreference);
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// Setup theme toggle functionality
|
| 37 |
+
function setupThemeToggle() {
|
| 38 |
+
const themeToggle = document.getElementById('theme-toggle');
|
| 39 |
+
if (!themeToggle) return;
|
| 40 |
+
|
| 41 |
+
themeToggle.addEventListener('click', function() {
|
| 42 |
+
const currentTheme = document.documentElement.getAttribute('data-theme');
|
| 43 |
+
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
| 44 |
+
|
| 45 |
+
// Update theme attribute
|
| 46 |
+
document.documentElement.setAttribute('data-theme', newTheme);
|
| 47 |
+
|
| 48 |
+
// Save preference to localStorage
|
| 49 |
+
localStorage.setItem('theme', newTheme);
|
| 50 |
+
|
| 51 |
+
// Update icon
|
| 52 |
+
updateThemeIcon(newTheme);
|
| 53 |
+
|
| 54 |
+
// Send theme preference to server
|
| 55 |
+
saveThemePreference(newTheme);
|
| 56 |
+
});
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
// Update the theme toggle icon based on current theme
|
| 60 |
+
function updateThemeIcon(theme) {
|
| 61 |
+
const themeToggle = document.getElementById('theme-toggle');
|
| 62 |
+
if (!themeToggle) return;
|
| 63 |
+
|
| 64 |
+
// Update icon based on theme
|
| 65 |
+
if (theme === 'dark') {
|
| 66 |
+
themeToggle.innerHTML = '<i class="fas fa-sun"></i>';
|
| 67 |
+
themeToggle.setAttribute('title', 'Activer le mode clair');
|
| 68 |
+
} else {
|
| 69 |
+
themeToggle.innerHTML = '<i class="fas fa-moon"></i>';
|
| 70 |
+
themeToggle.setAttribute('title', 'Activer le mode sombre');
|
| 71 |
+
}
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
// Save theme preference to server
|
| 75 |
+
function saveThemePreference(theme) {
|
| 76 |
+
const formData = new FormData();
|
| 77 |
+
formData.append('theme', theme);
|
| 78 |
+
|
| 79 |
+
fetch('/set_theme', {
|
| 80 |
+
method: 'POST',
|
| 81 |
+
body: formData
|
| 82 |
+
})
|
| 83 |
+
.then(response => response.json())
|
| 84 |
+
.then(data => {
|
| 85 |
+
console.log('Theme preference saved:', data);
|
| 86 |
+
})
|
| 87 |
+
.catch(error => {
|
| 88 |
+
console.error('Error saving theme preference:', error);
|
| 89 |
+
});
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
// Setup subject selection functionality
|
| 93 |
+
function setupSubjectSelection() {
|
| 94 |
+
const subjectCards = document.querySelectorAll('.subject-card');
|
| 95 |
+
const subjectSelect = document.getElementById('matiere-select');
|
| 96 |
+
|
| 97 |
+
// Handle subject card clicks
|
| 98 |
+
subjectCards.forEach(card => {
|
| 99 |
+
card.addEventListener('click', function() {
|
| 100 |
+
const matiereId = this.getAttribute('data-matiere-id');
|
| 101 |
+
|
| 102 |
+
// Update select element if it exists
|
| 103 |
+
if (subjectSelect) {
|
| 104 |
+
subjectSelect.value = matiereId;
|
| 105 |
+
// Trigger change event to load subcategories
|
| 106 |
+
const event = new Event('change');
|
| 107 |
+
subjectSelect.dispatchEvent(event);
|
| 108 |
+
} else {
|
| 109 |
+
loadSubCategories(matiereId);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
// Highlight the selected card
|
| 113 |
+
subjectCards.forEach(c => c.classList.remove('active'));
|
| 114 |
+
this.classList.add('active');
|
| 115 |
+
|
| 116 |
+
// Show the categories section
|
| 117 |
+
const categoriesSection = document.getElementById('sous-categories-section');
|
| 118 |
+
if (categoriesSection) {
|
| 119 |
+
categoriesSection.classList.remove('d-none');
|
| 120 |
+
categoriesSection.scrollIntoView({ behavior: 'smooth' });
|
| 121 |
+
}
|
| 122 |
+
});
|
| 123 |
+
});
|
| 124 |
+
|
| 125 |
+
// Handle subject select change
|
| 126 |
+
if (subjectSelect) {
|
| 127 |
+
subjectSelect.addEventListener('change', function() {
|
| 128 |
+
const matiereId = this.value;
|
| 129 |
+
if (matiereId) {
|
| 130 |
+
loadSubCategories(matiereId);
|
| 131 |
+
|
| 132 |
+
// Show the categories section
|
| 133 |
+
const categoriesSection = document.getElementById('sous-categories-section');
|
| 134 |
+
if (categoriesSection) {
|
| 135 |
+
categoriesSection.classList.remove('d-none');
|
| 136 |
+
}
|
| 137 |
+
}
|
| 138 |
+
});
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
// Load subcategories for the selected subject
|
| 143 |
+
function loadSubCategories(matiereId) {
|
| 144 |
+
fetch(`/get_sous_categories/${matiereId}`)
|
| 145 |
+
.then(response => response.json())
|
| 146 |
+
.then(data => {
|
| 147 |
+
// Update subcategories list
|
| 148 |
+
const sousCategoriesList = document.getElementById('sous-categories-list');
|
| 149 |
+
if (sousCategoriesList) {
|
| 150 |
+
sousCategoriesList.innerHTML = '';
|
| 151 |
+
|
| 152 |
+
data.forEach(category => {
|
| 153 |
+
const item = document.createElement('li');
|
| 154 |
+
item.className = 'selection-item';
|
| 155 |
+
item.setAttribute('data-category-id', category.id);
|
| 156 |
+
item.textContent = category.nom;
|
| 157 |
+
|
| 158 |
+
// Add click event
|
| 159 |
+
item.addEventListener('click', function() {
|
| 160 |
+
const categoryId = this.getAttribute('data-category-id');
|
| 161 |
+
loadTextes(categoryId);
|
| 162 |
+
|
| 163 |
+
// Highlight the selected category
|
| 164 |
+
const items = sousCategoriesList.querySelectorAll('.selection-item');
|
| 165 |
+
items.forEach(i => i.classList.remove('active'));
|
| 166 |
+
this.classList.add('active');
|
| 167 |
+
|
| 168 |
+
// Show the texts section
|
| 169 |
+
const textesSection = document.getElementById('textes-section');
|
| 170 |
+
if (textesSection) {
|
| 171 |
+
textesSection.classList.remove('d-none');
|
| 172 |
+
}
|
| 173 |
+
});
|
| 174 |
+
|
| 175 |
+
sousCategoriesList.appendChild(item);
|
| 176 |
+
});
|
| 177 |
+
|
| 178 |
+
// Show the subcategories section if it's hidden
|
| 179 |
+
const sousCategoriesSection = document.getElementById('sous-categories-section');
|
| 180 |
+
if (sousCategoriesSection) {
|
| 181 |
+
sousCategoriesSection.classList.remove('d-none');
|
| 182 |
+
}
|
| 183 |
+
}
|
| 184 |
+
})
|
| 185 |
+
.catch(error => {
|
| 186 |
+
console.error('Error loading subcategories:', error);
|
| 187 |
+
});
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
// Setup category selection functionality
|
| 191 |
+
function setupCategorySelection() {
|
| 192 |
+
const categorySelect = document.getElementById('sous-categorie-select');
|
| 193 |
+
|
| 194 |
+
if (categorySelect) {
|
| 195 |
+
categorySelect.addEventListener('change', function() {
|
| 196 |
+
const categoryId = this.value;
|
| 197 |
+
if (categoryId) {
|
| 198 |
+
loadTextes(categoryId);
|
| 199 |
+
|
| 200 |
+
// Show the texts section
|
| 201 |
+
const textesSection = document.getElementById('textes-section');
|
| 202 |
+
if (textesSection) {
|
| 203 |
+
textesSection.classList.remove('d-none');
|
| 204 |
+
}
|
| 205 |
+
}
|
| 206 |
+
});
|
| 207 |
+
}
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
// Load texts for the selected category
|
| 211 |
+
function loadTextes(categoryId) {
|
| 212 |
+
fetch(`/get_textes/${categoryId}`)
|
| 213 |
+
.then(response => response.json())
|
| 214 |
+
.then(data => {
|
| 215 |
+
// Update texts list
|
| 216 |
+
const textesList = document.getElementById('textes-list');
|
| 217 |
+
if (textesList) {
|
| 218 |
+
textesList.innerHTML = '';
|
| 219 |
+
|
| 220 |
+
data.forEach(texte => {
|
| 221 |
+
const item = document.createElement('li');
|
| 222 |
+
item.className = 'selection-item';
|
| 223 |
+
item.setAttribute('data-texte-id', texte.id);
|
| 224 |
+
item.textContent = texte.titre;
|
| 225 |
+
|
| 226 |
+
// Add click event
|
| 227 |
+
item.addEventListener('click', function() {
|
| 228 |
+
const texteId = this.getAttribute('data-texte-id');
|
| 229 |
+
displayTexte(texteId);
|
| 230 |
+
|
| 231 |
+
// Highlight the selected text
|
| 232 |
+
const items = textesList.querySelectorAll('.selection-item');
|
| 233 |
+
items.forEach(i => i.classList.remove('active'));
|
| 234 |
+
this.classList.add('active');
|
| 235 |
+
});
|
| 236 |
+
|
| 237 |
+
textesList.appendChild(item);
|
| 238 |
+
});
|
| 239 |
+
|
| 240 |
+
// Show the texts section
|
| 241 |
+
const textesSection = document.getElementById('textes-section');
|
| 242 |
+
if (textesSection) {
|
| 243 |
+
textesSection.classList.remove('d-none');
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
// Hide the content section since no text is selected yet
|
| 247 |
+
const contentSection = document.getElementById('content-section');
|
| 248 |
+
if (contentSection) {
|
| 249 |
+
contentSection.classList.add('d-none');
|
| 250 |
+
}
|
| 251 |
+
}
|
| 252 |
+
})
|
| 253 |
+
.catch(error => {
|
| 254 |
+
console.error('Error loading texts:', error);
|
| 255 |
+
});
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
// Setup text selection functionality
|
| 259 |
+
function setupTextSelection() {
|
| 260 |
+
const texteSelect = document.getElementById('texte-select');
|
| 261 |
+
|
| 262 |
+
if (texteSelect) {
|
| 263 |
+
texteSelect.addEventListener('change', function() {
|
| 264 |
+
const texteId = this.value;
|
| 265 |
+
if (texteId) {
|
| 266 |
+
displayTexte(texteId);
|
| 267 |
+
}
|
| 268 |
+
});
|
| 269 |
+
}
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
// Display the selected texte with content blocks
|
| 273 |
+
function displayTexte(texteId) {
|
| 274 |
+
fetch(`/get_texte/${texteId}`)
|
| 275 |
+
.then(response => response.json())
|
| 276 |
+
.then(data => {
|
| 277 |
+
const contentSection = document.getElementById('content-section');
|
| 278 |
+
const contentTitle = document.getElementById('content-title');
|
| 279 |
+
const contentBlocks = document.getElementById('content-blocks');
|
| 280 |
+
|
| 281 |
+
if (contentSection && contentTitle && contentBlocks) {
|
| 282 |
+
// Update content title
|
| 283 |
+
contentTitle.textContent = data.titre;
|
| 284 |
+
|
| 285 |
+
// Update content theme color based on matiere color
|
| 286 |
+
if (data.color_code) {
|
| 287 |
+
// Apply color to title underline
|
| 288 |
+
contentTitle.style.borderBottomColor = data.color_code;
|
| 289 |
+
|
| 290 |
+
// Apply color to all block titles
|
| 291 |
+
const style = document.createElement('style');
|
| 292 |
+
style.id = 'dynamic-block-styles';
|
| 293 |
+
const existingStyle = document.getElementById('dynamic-block-styles');
|
| 294 |
+
if (existingStyle) {
|
| 295 |
+
existingStyle.remove();
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
style.textContent = `
|
| 299 |
+
.content-block-title {
|
| 300 |
+
border-bottom-color: ${data.color_code} !important;
|
| 301 |
+
}
|
| 302 |
+
.content-block {
|
| 303 |
+
border-left: 4px solid ${data.color_code} !important;
|
| 304 |
+
}
|
| 305 |
+
`;
|
| 306 |
+
document.head.appendChild(style);
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
// Clear existing content
|
| 310 |
+
contentBlocks.innerHTML = '';
|
| 311 |
+
|
| 312 |
+
// Create content blocks
|
| 313 |
+
if (data.blocks && data.blocks.length > 0) {
|
| 314 |
+
data.blocks.forEach(block => {
|
| 315 |
+
// Create block container
|
| 316 |
+
const blockDiv = document.createElement('div');
|
| 317 |
+
blockDiv.className = 'content-block fade-in';
|
| 318 |
+
|
| 319 |
+
// Check if block has an image
|
| 320 |
+
if (block.image) {
|
| 321 |
+
blockDiv.classList.add('block-with-image');
|
| 322 |
+
blockDiv.classList.add(`image-${block.image_position || 'left'}`);
|
| 323 |
+
|
| 324 |
+
// Create image container
|
| 325 |
+
const imageDiv = document.createElement('div');
|
| 326 |
+
imageDiv.className = 'block-image-container';
|
| 327 |
+
|
| 328 |
+
// Create image element
|
| 329 |
+
const imageEl = document.createElement('img');
|
| 330 |
+
imageEl.className = 'block-image';
|
| 331 |
+
imageEl.src = block.image.src;
|
| 332 |
+
imageEl.alt = block.image.alt || 'Illustration';
|
| 333 |
+
|
| 334 |
+
imageDiv.appendChild(imageEl);
|
| 335 |
+
blockDiv.appendChild(imageDiv);
|
| 336 |
+
|
| 337 |
+
// Create content container
|
| 338 |
+
const contentDiv = document.createElement('div');
|
| 339 |
+
contentDiv.className = 'block-content-container';
|
| 340 |
+
|
| 341 |
+
// Add block title if present
|
| 342 |
+
if (block.title) {
|
| 343 |
+
const titleEl = document.createElement('h3');
|
| 344 |
+
titleEl.className = 'content-block-title';
|
| 345 |
+
titleEl.textContent = block.title;
|
| 346 |
+
contentDiv.appendChild(titleEl);
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
// Add block content
|
| 350 |
+
const contentEl = document.createElement('div');
|
| 351 |
+
contentEl.className = 'content-block-content';
|
| 352 |
+
contentEl.innerHTML = block.content.replace(/\n/g, '<br>');
|
| 353 |
+
contentDiv.appendChild(contentEl);
|
| 354 |
+
|
| 355 |
+
blockDiv.appendChild(contentDiv);
|
| 356 |
+
} else {
|
| 357 |
+
// No image - simple block
|
| 358 |
+
|
| 359 |
+
// Add block title if present
|
| 360 |
+
if (block.title) {
|
| 361 |
+
const titleEl = document.createElement('h3');
|
| 362 |
+
titleEl.className = 'content-block-title';
|
| 363 |
+
titleEl.textContent = block.title;
|
| 364 |
+
blockDiv.appendChild(titleEl);
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
// Add block content
|
| 368 |
+
const contentEl = document.createElement('div');
|
| 369 |
+
contentEl.className = 'content-block-content';
|
| 370 |
+
contentEl.innerHTML = block.content.replace(/\n/g, '<br>');
|
| 371 |
+
blockDiv.appendChild(contentEl);
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
// Add the block to the content area
|
| 375 |
+
contentBlocks.appendChild(blockDiv);
|
| 376 |
+
});
|
| 377 |
+
} else {
|
| 378 |
+
// Fallback to regular content if no blocks
|
| 379 |
+
const blockDiv = document.createElement('div');
|
| 380 |
+
blockDiv.className = 'content-block';
|
| 381 |
+
blockDiv.innerHTML = data.contenu.replace(/\n/g, '<br>');
|
| 382 |
+
contentBlocks.appendChild(blockDiv);
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
// Show the content section
|
| 386 |
+
contentSection.classList.remove('d-none');
|
| 387 |
+
contentSection.scrollIntoView({ behavior: 'smooth' });
|
| 388 |
+
}
|
| 389 |
+
})
|
| 390 |
+
.catch(error => {
|
| 391 |
+
console.error('Error loading texte:', error);
|
| 392 |
+
});
|
| 393 |
+
}
|
api/templates/admin/dashboard.html
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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/[email protected]/Sortable.min.js"></script>
|
| 281 |
+
<script src="{{ url_for('static', filename='js/admin.js') }}"></script>
|
| 282 |
+
{% endblock %}
|
api/templates/admin/historique.html
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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/[email protected]/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 |
+
<h1 class="site-title">
|
| 28 |
+
<a href="{{ url_for('main_bp.index') }}">Méthodologies</a>
|
| 29 |
+
</h1>
|
| 30 |
+
|
| 31 |
+
<div class="header-actions">
|
| 32 |
+
<!-- Theme toggle button -->
|
| 33 |
+
<button id="theme-toggle" class="theme-toggle" title="Changer de thème">
|
| 34 |
+
<i class="fas fa-moon"></i>
|
| 35 |
+
</button>
|
| 36 |
+
|
| 37 |
+
<!-- Admin link if admin is logged in -->
|
| 38 |
+
{% if session.get('admin_logged_in') %}
|
| 39 |
+
<a href="{{ url_for('admin_bp.dashboard') }}" class="btn btn-primary btn-sm ms-2">
|
| 40 |
+
<i class="fas fa-cog"></i> Gestion
|
| 41 |
+
</a>
|
| 42 |
+
{% endif %}
|
| 43 |
+
</div>
|
| 44 |
+
</div>
|
| 45 |
+
</header>
|
| 46 |
+
|
| 47 |
+
<!-- Flash messages -->
|
| 48 |
+
<div class="container mt-3">
|
| 49 |
+
{% with messages = get_flashed_messages(with_categories=true) %}
|
| 50 |
+
{% if messages %}
|
| 51 |
+
{% for category, message in messages %}
|
| 52 |
+
<div class="alert alert-{{ category if category != 'message' else 'info' }} alert-dismissible fade show" role="alert">
|
| 53 |
+
{{ message }}
|
| 54 |
+
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
| 55 |
+
</div>
|
| 56 |
+
{% endfor %}
|
| 57 |
+
{% endif %}
|
| 58 |
+
{% endwith %}
|
| 59 |
+
</div>
|
| 60 |
+
|
| 61 |
+
<!-- Main content -->
|
| 62 |
+
<main class="main-content">
|
| 63 |
+
<div class="container">
|
| 64 |
+
{% block content %}{% endblock %}
|
| 65 |
+
</div>
|
| 66 |
+
</main>
|
| 67 |
+
|
| 68 |
+
<!-- Footer -->
|
| 69 |
+
<footer class="main-footer">
|
| 70 |
+
<div class="container footer-content">
|
| 71 |
+
<div>
|
| 72 |
+
<p>© {{ year|default(2023) }} Méthodologies - Tous droits réservés</p>
|
| 73 |
+
<ul class="footer-links">
|
| 74 |
+
<li><a href="#">À propos</a></li>
|
| 75 |
+
<li><a href="#">Contact</a></li>
|
| 76 |
+
<li><a href="#">Mentions légales</a></li>
|
| 77 |
+
</ul>
|
| 78 |
+
</div>
|
| 79 |
+
|
| 80 |
+
<!-- Feedback form -->
|
| 81 |
+
<div class="feedback-form">
|
| 82 |
+
<h4 class="feedback-title">Votre avis nous intéresse</h4>
|
| 83 |
+
<form id="feedback-form" action="{{ url_for('main_bp.submit_feedback') }}" method="POST">
|
| 84 |
+
<div class="form-group mb-2">
|
| 85 |
+
<textarea id="feedback-message" name="message" class="form-control" rows="2" placeholder="Laissez-nous un message..."></textarea>
|
| 86 |
+
</div>
|
| 87 |
+
<button type="submit" class="btn btn-primary">Envoyer</button>
|
| 88 |
+
</form>
|
| 89 |
+
</div>
|
| 90 |
+
</div>
|
| 91 |
+
</footer>
|
| 92 |
+
|
| 93 |
+
<!-- Bootstrap JS Bundle with Popper -->
|
| 94 |
+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
|
| 95 |
+
|
| 96 |
+
<!-- Main JavaScript -->
|
| 97 |
+
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
| 98 |
+
|
| 99 |
+
<!-- Additional JavaScript -->
|
| 100 |
+
{% block scripts %}{% endblock %}
|
| 101 |
+
</body>
|
| 102 |
+
</html>
|
api/templates/index.html
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends 'base.html' %}
|
| 2 |
+
|
| 3 |
+
{% block title %}Accueil - Méthodologies{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="row">
|
| 7 |
+
<div class="col-12">
|
| 8 |
+
<h2 class="mb-4">Bienvenue sur la plateforme de méthodologies</h2>
|
| 9 |
+
<p class="lead mb-4">Sélectionnez une matière pour découvrir des méthodologies adaptées à vos besoins.</p>
|
| 10 |
+
</div>
|
| 11 |
+
</div>
|
| 12 |
+
|
| 13 |
+
<!-- Subject Selection Section -->
|
| 14 |
+
<section id="matieres-section" class="mb-5 slide-in-up">
|
| 15 |
+
<h3 class="mb-3">Matières</h3>
|
| 16 |
+
<div class="row">
|
| 17 |
+
{% for matiere in matieres %}
|
| 18 |
+
<div class="col-md-4 mb-4">
|
| 19 |
+
<div class="subject-card" data-matiere-id="{{ matiere.id }}">
|
| 20 |
+
<div class="subject-indicator" style="background-color: {{ matiere.color_code }};"></div>
|
| 21 |
+
<h3>{{ matiere.nom }}</h3>
|
| 22 |
+
<p>Cliquez pour explorer les méthodologies dans cette matière</p>
|
| 23 |
+
<div class="text-end">
|
| 24 |
+
<span class="btn btn-sm" style="background-color: {{ matiere.color_code }}; color: white;">
|
| 25 |
+
<i class="fas fa-arrow-right"></i>
|
| 26 |
+
</span>
|
| 27 |
+
</div>
|
| 28 |
+
</div>
|
| 29 |
+
</div>
|
| 30 |
+
{% else %}
|
| 31 |
+
<div class="col-12">
|
| 32 |
+
<div class="alert alert-info">
|
| 33 |
+
Aucune matière n'est disponible pour le moment.
|
| 34 |
+
</div>
|
| 35 |
+
</div>
|
| 36 |
+
{% endfor %}
|
| 37 |
+
</div>
|
| 38 |
+
</section>
|
| 39 |
+
|
| 40 |
+
<!-- Categories Section (initially hidden) -->
|
| 41 |
+
<section id="sous-categories-section" class="mb-5 d-none fade-in">
|
| 42 |
+
<h3 class="mb-3">Types de méthodologies</h3>
|
| 43 |
+
<div class="row">
|
| 44 |
+
<div class="col-md-6 col-lg-4">
|
| 45 |
+
<div class="selection-container">
|
| 46 |
+
<h4 class="selection-title">Sélectionnez un type</h4>
|
| 47 |
+
<ul id="sous-categories-list" class="selection-list">
|
| 48 |
+
<!-- Sous catégories will be loaded here -->
|
| 49 |
+
</ul>
|
| 50 |
+
</div>
|
| 51 |
+
</div>
|
| 52 |
+
</div>
|
| 53 |
+
</section>
|
| 54 |
+
|
| 55 |
+
<!-- Texts Section (initially hidden) -->
|
| 56 |
+
<section id="textes-section" class="mb-5 d-none fade-in">
|
| 57 |
+
<h3 class="mb-3">Méthodologies</h3>
|
| 58 |
+
<div class="row">
|
| 59 |
+
<div class="col-md-6 col-lg-4">
|
| 60 |
+
<div class="selection-container">
|
| 61 |
+
<h4 class="selection-title">Sélectionnez une méthodologie</h4>
|
| 62 |
+
<ul id="textes-list" class="selection-list">
|
| 63 |
+
<!-- Textes will be loaded here -->
|
| 64 |
+
</ul>
|
| 65 |
+
</div>
|
| 66 |
+
</div>
|
| 67 |
+
</div>
|
| 68 |
+
</section>
|
| 69 |
+
|
| 70 |
+
<!-- Content Display Section (initially hidden) -->
|
| 71 |
+
<section id="content-section" class="mb-5 d-none fade-in">
|
| 72 |
+
<div class="content-viewer">
|
| 73 |
+
<h2 id="content-title" class="content-title"><!-- Title will be populated here --></h2>
|
| 74 |
+
<div id="content-blocks" class="content-blocks">
|
| 75 |
+
<!-- Content blocks will be populated here -->
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
</section>
|
| 79 |
+
{% endblock %}
|
app.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from api.index import app, db
|
| 2 |
+
|
| 3 |
+
# Create database tables if they don't exist
|
| 4 |
+
with app.app_context():
|
| 5 |
+
db.create_all()
|
| 6 |
+
|
| 7 |
+
if __name__ == '__main__':
|
| 8 |
+
app.run(debug=True)
|
app/__init__.py
DELETED
|
@@ -1,28 +0,0 @@
|
|
| 1 |
-
from flask import Flask
|
| 2 |
-
from flask_sqlalchemy import SQLAlchemy
|
| 3 |
-
from flask_admin import Admin
|
| 4 |
-
from flask_ckeditor import CKEditor
|
| 5 |
-
from flask_migrate import Migrate
|
| 6 |
-
from config import Config
|
| 7 |
-
|
| 8 |
-
db = SQLAlchemy()
|
| 9 |
-
migrate = Migrate() # Initialisation de Flask-Migrate
|
| 10 |
-
admin = Admin(name='Mon Projet', template_mode='bootstrap3')
|
| 11 |
-
ckeditor = CKEditor()
|
| 12 |
-
|
| 13 |
-
def create_app(config_class=Config):
|
| 14 |
-
app = Flask(__name__)
|
| 15 |
-
app.config.from_object(config_class)
|
| 16 |
-
|
| 17 |
-
db.init_app(app)
|
| 18 |
-
migrate.init_app(app, db)
|
| 19 |
-
admin.init_app(app)
|
| 20 |
-
ckeditor.init_app(app)
|
| 21 |
-
|
| 22 |
-
from app.admin import bp as custom_admin_bp #
|
| 23 |
-
app.register_blueprint(custom_admin_bp)
|
| 24 |
-
|
| 25 |
-
from app.views import bp as main_bp
|
| 26 |
-
app.register_blueprint(main_bp)
|
| 27 |
-
|
| 28 |
-
return app
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/__pycache__/__init__.cpython-312.pyc
DELETED
|
Binary file (1.34 kB)
|
|
|
app/__pycache__/admin.cpython-312.pyc
DELETED
|
Binary file (4.47 kB)
|
|
|
app/__pycache__/models.cpython-312.pyc
DELETED
|
Binary file (2.91 kB)
|
|
|
app/__pycache__/views.cpython-312.pyc
DELETED
|
Binary file (2.47 kB)
|
|
|
app/admin.py
DELETED
|
@@ -1,70 +0,0 @@
|
|
| 1 |
-
from flask import Blueprint
|
| 2 |
-
from flask_admin.contrib.sqla import ModelView
|
| 3 |
-
from flask_admin import BaseView, expose
|
| 4 |
-
from app import db, admin
|
| 5 |
-
from app.models import Matiere, SousCategorie, Texte
|
| 6 |
-
from flask_ckeditor import CKEditorField
|
| 7 |
-
from wtforms import StringField, TextAreaField
|
| 8 |
-
from bleach import clean
|
| 9 |
-
from bs4 import BeautifulSoup
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
bp = Blueprint('custom_admin', __name__, url_prefix='/admin')
|
| 13 |
-
|
| 14 |
-
def sanitize_html(html_content):
|
| 15 |
-
# TRÈS PERMISSIF - UNIQUEMENT POUR LE TEST
|
| 16 |
-
return clean(html_content, tags=[], attributes={}, strip=False)
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
class MatiereView(ModelView):
|
| 21 |
-
column_list = ('nom', 'sous_categories') # Colonnes à afficher dans la liste
|
| 22 |
-
form_columns = ('nom',)
|
| 23 |
-
|
| 24 |
-
class SousCategorieView(ModelView):
|
| 25 |
-
column_list = ('nom', 'matiere')
|
| 26 |
-
form_columns = ('nom', 'matiere')
|
| 27 |
-
#form_overrides = dict(nom=StringField)
|
| 28 |
-
form_args = { # Amélioration de la sélection de la matière
|
| 29 |
-
'matiere': {
|
| 30 |
-
'query_factory': lambda: Matiere.query.order_by(func.lower(Matiere.nom)) #Tri insensible à la casse
|
| 31 |
-
}
|
| 32 |
-
}
|
| 33 |
-
def on_model_change(self, form, model, is_created):
|
| 34 |
-
# Vérification de l'unicité (nom, matiere_id) *avant* l'insertion/mise à jour
|
| 35 |
-
if is_created:
|
| 36 |
-
existing = SousCategorie.query.filter(
|
| 37 |
-
func.lower(SousCategorie.nom) == func.lower(form.nom.data),
|
| 38 |
-
SousCategorie.matiere_id == form.matiere.data.id
|
| 39 |
-
).first()
|
| 40 |
-
if existing:
|
| 41 |
-
raise ValueError("Cette sous-catégorie existe déjà pour cette matière.")
|
| 42 |
-
else: #Mise à jour
|
| 43 |
-
existing = SousCategorie.query.filter(
|
| 44 |
-
func.lower(SousCategorie.nom) == func.lower(form.nom.data),
|
| 45 |
-
SousCategorie.matiere_id == form.matiere.data.id,
|
| 46 |
-
SousCategorie.id != model.id
|
| 47 |
-
).first()
|
| 48 |
-
|
| 49 |
-
if existing:
|
| 50 |
-
raise ValueError("Cette sous-catégorie existe déjà pour cette matière.")
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
class TexteView(ModelView):
|
| 54 |
-
column_list = ('titre', 'sous_categorie')
|
| 55 |
-
form_columns = ('titre', 'contenu', 'sous_categorie')
|
| 56 |
-
form_overrides = dict(contenu=CKEditorField)
|
| 57 |
-
form_args = {
|
| 58 |
-
'sous_categorie': {
|
| 59 |
-
'query_factory': lambda: SousCategorie.query.join(Matiere).order_by(func.lower(Matiere.nom), func.lower(SousCategorie.nom))
|
| 60 |
-
}
|
| 61 |
-
}
|
| 62 |
-
|
| 63 |
-
def on_model_change(self, form, model, is_created):
|
| 64 |
-
model.contenu = sanitize_html(form.contenu.data)
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
admin.add_view(MatiereView(Matiere, db.session))
|
| 69 |
-
admin.add_view(SousCategorieView(SousCategorie, db.session))
|
| 70 |
-
admin.add_view(TexteView(Texte, db.session))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/models.py
DELETED
|
@@ -1,34 +0,0 @@
|
|
| 1 |
-
from app import db
|
| 2 |
-
from sqlalchemy import func # pour lower()
|
| 3 |
-
|
| 4 |
-
class Matiere(db.Model):
|
| 5 |
-
id = db.Column(db.Integer, primary_key=True)
|
| 6 |
-
nom = db.Column(db.String(64), unique=True, nullable=False)
|
| 7 |
-
sous_categories = db.relationship('SousCategorie', backref='matiere', lazy='dynamic', cascade="all, delete-orphan")
|
| 8 |
-
|
| 9 |
-
def __repr__(self):
|
| 10 |
-
return f'<Matiere {self.nom}>'
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
class SousCategorie(db.Model):
|
| 14 |
-
id = db.Column(db.Integer, primary_key=True)
|
| 15 |
-
nom = db.Column(db.String(64), nullable=False)
|
| 16 |
-
matiere_id = db.Column(db.Integer, db.ForeignKey('matiere.id'), nullable=False)
|
| 17 |
-
textes = db.relationship('Texte', backref='sous_categorie', lazy='dynamic', cascade="all, delete-orphan")
|
| 18 |
-
# Contrainte d'unicité composite
|
| 19 |
-
__table_args__ = (
|
| 20 |
-
db.UniqueConstraint('nom', 'matiere_id', name='_nom_matiere_uc'),
|
| 21 |
-
)
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
def __repr__(self):
|
| 25 |
-
return f'<SousCategorie {self.nom}>'
|
| 26 |
-
|
| 27 |
-
class Texte(db.Model):
|
| 28 |
-
id = db.Column(db.Integer, primary_key=True)
|
| 29 |
-
titre = db.Column(db.String(128), nullable=False)
|
| 30 |
-
contenu = db.Column(db.Text, nullable=False)
|
| 31 |
-
sous_categorie_id = db.Column(db.Integer, db.ForeignKey('sous_categorie.id'), nullable=False)
|
| 32 |
-
|
| 33 |
-
def __repr__(self):
|
| 34 |
-
return f'<Texte {self.titre}>'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/static/css/style.css
DELETED
|
@@ -1,585 +0,0 @@
|
|
| 1 |
-
/* Style pour la page des matières */
|
| 2 |
-
.page-title {
|
| 3 |
-
text-align: center;
|
| 4 |
-
margin: 2rem 0 3rem;
|
| 5 |
-
font-size: 2.5rem;
|
| 6 |
-
color: #2c3e50;
|
| 7 |
-
position: relative;
|
| 8 |
-
}
|
| 9 |
-
|
| 10 |
-
.page-title::after {
|
| 11 |
-
content: '';
|
| 12 |
-
position: absolute;
|
| 13 |
-
bottom: -10px;
|
| 14 |
-
left: 50%;
|
| 15 |
-
transform: translateX(-50%);
|
| 16 |
-
width: 60px;
|
| 17 |
-
height: 3px;
|
| 18 |
-
background: #3498db;
|
| 19 |
-
border-radius: 2px;
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
.subjects-grid {
|
| 23 |
-
display: grid;
|
| 24 |
-
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
| 25 |
-
gap: 1.5rem;
|
| 26 |
-
padding: 1rem;
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
.subject-card {
|
| 30 |
-
background: white;
|
| 31 |
-
border-radius: 12px;
|
| 32 |
-
overflow: hidden;
|
| 33 |
-
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 34 |
-
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
| 35 |
-
position: relative;
|
| 36 |
-
}
|
| 37 |
-
|
| 38 |
-
.subject-card:hover {
|
| 39 |
-
transform: translateY(-5px);
|
| 40 |
-
box-shadow: 0 8px 15px rgba(52, 152, 219, 0.2);
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
.subject-link {
|
| 44 |
-
display: flex;
|
| 45 |
-
justify-content: space-between;
|
| 46 |
-
align-items: center;
|
| 47 |
-
padding: 1.5rem;
|
| 48 |
-
text-decoration: none;
|
| 49 |
-
color: inherit;
|
| 50 |
-
position: relative;
|
| 51 |
-
z-index: 1;
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
.subject-link::after {
|
| 55 |
-
content: '';
|
| 56 |
-
position: absolute;
|
| 57 |
-
top: 0;
|
| 58 |
-
left: 0;
|
| 59 |
-
width: 100%;
|
| 60 |
-
height: 100%;
|
| 61 |
-
background: linear-gradient(135deg, #46a1dd 0%, #2980b9 100%);
|
| 62 |
-
opacity: 0;
|
| 63 |
-
transition: opacity 0.3s ease;
|
| 64 |
-
z-index: -1;
|
| 65 |
-
}
|
| 66 |
-
|
| 67 |
-
.subject-link:hover {
|
| 68 |
-
color: white;
|
| 69 |
-
}
|
| 70 |
-
|
| 71 |
-
.subject-link:hover::after {
|
| 72 |
-
opacity: 1;
|
| 73 |
-
}
|
| 74 |
-
|
| 75 |
-
.subject-name {
|
| 76 |
-
font-size: 1.2rem;
|
| 77 |
-
font-weight: 600;
|
| 78 |
-
transition: color 0.3s ease;
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
.subject-arrow {
|
| 82 |
-
font-size: 1.5rem;
|
| 83 |
-
opacity: 0;
|
| 84 |
-
transform: translateX(-10px);
|
| 85 |
-
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 86 |
-
}
|
| 87 |
-
|
| 88 |
-
.subject-link:hover .subject-arrow {
|
| 89 |
-
opacity: 1;
|
| 90 |
-
transform: translateX(0);
|
| 91 |
-
}
|
| 92 |
-
|
| 93 |
-
.no-subjects {
|
| 94 |
-
text-align: center;
|
| 95 |
-
color: #95a5a6;
|
| 96 |
-
font-size: 1.2rem;
|
| 97 |
-
padding: 2rem;
|
| 98 |
-
grid-column: 1 / -1;
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
/* Animation d'apparition */
|
| 102 |
-
@keyframes cardEntrance {
|
| 103 |
-
from {
|
| 104 |
-
opacity: 0;
|
| 105 |
-
transform: translateY(20px);
|
| 106 |
-
}
|
| 107 |
-
to {
|
| 108 |
-
opacity: 1;
|
| 109 |
-
transform: translateY(0);
|
| 110 |
-
}
|
| 111 |
-
}
|
| 112 |
-
|
| 113 |
-
.subject-card {
|
| 114 |
-
animation: cardEntrance 0.6s ease forwards;
|
| 115 |
-
animation-delay: calc(var(--index) * 0.1s);
|
| 116 |
-
}
|
| 117 |
-
|
| 118 |
-
@media (max-width: 768px) {
|
| 119 |
-
.subjects-grid {
|
| 120 |
-
grid-template-columns: 1fr;
|
| 121 |
-
}
|
| 122 |
-
|
| 123 |
-
.subject-link {
|
| 124 |
-
padding: 1rem;
|
| 125 |
-
}
|
| 126 |
-
}
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
/* Page matière */
|
| 130 |
-
.subject-container {
|
| 131 |
-
max-width: 1200px;
|
| 132 |
-
margin: 2rem auto;
|
| 133 |
-
padding: 0 1rem;
|
| 134 |
-
}
|
| 135 |
-
|
| 136 |
-
.subject-header {
|
| 137 |
-
text-align: center;
|
| 138 |
-
margin-bottom: 3rem;
|
| 139 |
-
position: relative;
|
| 140 |
-
}
|
| 141 |
-
|
| 142 |
-
.subject-title {
|
| 143 |
-
font-size: 2.5rem;
|
| 144 |
-
color: #2c3e50;
|
| 145 |
-
margin-bottom: 1rem;
|
| 146 |
-
position: relative;
|
| 147 |
-
display: inline-block;
|
| 148 |
-
}
|
| 149 |
-
|
| 150 |
-
.subject-header-decoration {
|
| 151 |
-
height: 4px;
|
| 152 |
-
width: 80px;
|
| 153 |
-
background: #3498db;
|
| 154 |
-
margin: 0 auto;
|
| 155 |
-
border-radius: 2px;
|
| 156 |
-
position: relative;
|
| 157 |
-
animation: headerLine 1s ease-out;
|
| 158 |
-
}
|
| 159 |
-
|
| 160 |
-
.subcategories-section {
|
| 161 |
-
background: white;
|
| 162 |
-
padding: 2rem;
|
| 163 |
-
border-radius: 12px;
|
| 164 |
-
box-shadow: 0 4px 20px rgba(0,0,0,0.05);
|
| 165 |
-
}
|
| 166 |
-
|
| 167 |
-
.section-title {
|
| 168 |
-
color: #4a5568;
|
| 169 |
-
font-size: 1.5rem;
|
| 170 |
-
margin-bottom: 2rem;
|
| 171 |
-
padding-bottom: 0.5rem;
|
| 172 |
-
border-bottom: 2px solid #e2e8f0;
|
| 173 |
-
}
|
| 174 |
-
|
| 175 |
-
.subcategories-grid {
|
| 176 |
-
display: grid;
|
| 177 |
-
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
| 178 |
-
gap: 1.5rem;
|
| 179 |
-
}
|
| 180 |
-
|
| 181 |
-
.subcategory-card {
|
| 182 |
-
background: white;
|
| 183 |
-
border-radius: 8px;
|
| 184 |
-
padding: 1.5rem;
|
| 185 |
-
text-decoration: none;
|
| 186 |
-
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 187 |
-
border: 1px solid #e2e8f0;
|
| 188 |
-
position: relative;
|
| 189 |
-
overflow: hidden;
|
| 190 |
-
}
|
| 191 |
-
|
| 192 |
-
.subcategory-card:hover {
|
| 193 |
-
transform: translateY(-3px);
|
| 194 |
-
box-shadow: 0 10px 15px rgba(52, 152, 219, 0.15);
|
| 195 |
-
border-color: #3498db;
|
| 196 |
-
}
|
| 197 |
-
|
| 198 |
-
.subcategory-content {
|
| 199 |
-
display: flex;
|
| 200 |
-
justify-content: space-between;
|
| 201 |
-
align-items: center;
|
| 202 |
-
}
|
| 203 |
-
|
| 204 |
-
.subcategory-name {
|
| 205 |
-
color: #2c3e50;
|
| 206 |
-
font-weight: 500;
|
| 207 |
-
font-size: 1.1rem;
|
| 208 |
-
transition: color 0.3s ease;
|
| 209 |
-
}
|
| 210 |
-
|
| 211 |
-
.arrow-icon {
|
| 212 |
-
width: 24px;
|
| 213 |
-
height: 24px;
|
| 214 |
-
opacity: 0;
|
| 215 |
-
transform: translateX(-10px);
|
| 216 |
-
transition: all 0.3s ease;
|
| 217 |
-
color: #3498db;
|
| 218 |
-
}
|
| 219 |
-
|
| 220 |
-
.subcategory-card:hover .arrow-icon {
|
| 221 |
-
opacity: 1;
|
| 222 |
-
transform: translateX(0);
|
| 223 |
-
}
|
| 224 |
-
|
| 225 |
-
.no-subcategories {
|
| 226 |
-
text-align: center;
|
| 227 |
-
padding: 3rem;
|
| 228 |
-
grid-column: 1 / -1;
|
| 229 |
-
}
|
| 230 |
-
|
| 231 |
-
.empty-icon {
|
| 232 |
-
width: 60px;
|
| 233 |
-
height: 60px;
|
| 234 |
-
margin-bottom: 1rem;
|
| 235 |
-
color: #cbd5e0;
|
| 236 |
-
}
|
| 237 |
-
|
| 238 |
-
.no-subcategories p {
|
| 239 |
-
color: #a0aec0;
|
| 240 |
-
font-size: 1.1rem;
|
| 241 |
-
}
|
| 242 |
-
|
| 243 |
-
/* Animations */
|
| 244 |
-
@keyframes headerLine {
|
| 245 |
-
from { width: 0; opacity: 0; }
|
| 246 |
-
to { width: 80px; opacity: 1; }
|
| 247 |
-
}
|
| 248 |
-
|
| 249 |
-
@media (max-width: 768px) {
|
| 250 |
-
.subject-container {
|
| 251 |
-
padding: 0;
|
| 252 |
-
}
|
| 253 |
-
|
| 254 |
-
.subcategories-section {
|
| 255 |
-
padding: 1.5rem;
|
| 256 |
-
}
|
| 257 |
-
|
| 258 |
-
.subject-title {
|
| 259 |
-
font-size: 2rem;
|
| 260 |
-
}
|
| 261 |
-
}
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
/*sous catégorie*/
|
| 265 |
-
/* Page textes */
|
| 266 |
-
.content-container {
|
| 267 |
-
max-width: 1200px;
|
| 268 |
-
margin: 2rem auto;
|
| 269 |
-
padding: 0 1.5rem;
|
| 270 |
-
}
|
| 271 |
-
|
| 272 |
-
.header-section {
|
| 273 |
-
text-align: center;
|
| 274 |
-
margin-bottom: 3rem;
|
| 275 |
-
}
|
| 276 |
-
|
| 277 |
-
.main-title {
|
| 278 |
-
font-size: 2.3rem;
|
| 279 |
-
color: #2c3e50;
|
| 280 |
-
margin-bottom: 0.5rem;
|
| 281 |
-
}
|
| 282 |
-
|
| 283 |
-
.title-underline {
|
| 284 |
-
width: 60px;
|
| 285 |
-
height: 3px;
|
| 286 |
-
background: linear-gradient(135deg, #46a1dd 0%, #2980b9 100%);
|
| 287 |
-
margin: 0 auto;
|
| 288 |
-
border-radius: 2px;
|
| 289 |
-
}
|
| 290 |
-
|
| 291 |
-
.texts-section {
|
| 292 |
-
background: #fff;
|
| 293 |
-
padding: 2rem;
|
| 294 |
-
border-radius: 12px;
|
| 295 |
-
box-shadow: 0 4px 20px rgba(0,0,0,0.05);
|
| 296 |
-
}
|
| 297 |
-
|
| 298 |
-
.section-subtitle {
|
| 299 |
-
color: #4a5568;
|
| 300 |
-
font-size: 1.4rem;
|
| 301 |
-
margin-bottom: 2rem;
|
| 302 |
-
padding-bottom: 0.75rem;
|
| 303 |
-
border-bottom: 2px solid #edf2f7;
|
| 304 |
-
}
|
| 305 |
-
|
| 306 |
-
.texts-grid {
|
| 307 |
-
display: grid;
|
| 308 |
-
gap: 1.2rem;
|
| 309 |
-
}
|
| 310 |
-
|
| 311 |
-
.text-card {
|
| 312 |
-
display: flex;
|
| 313 |
-
justify-content: space-between;
|
| 314 |
-
align-items: center;
|
| 315 |
-
padding: 1.5rem;
|
| 316 |
-
background: #fff;
|
| 317 |
-
border-radius: 8px;
|
| 318 |
-
border: 1px solid #e2e8f0;
|
| 319 |
-
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 320 |
-
text-decoration: none;
|
| 321 |
-
position: relative;
|
| 322 |
-
}
|
| 323 |
-
|
| 324 |
-
.text-card:hover {
|
| 325 |
-
transform: translateY(-3px);
|
| 326 |
-
box-shadow: 0 5px 15px rgba(52, 152, 219, 0.15);
|
| 327 |
-
border-color: #3498db;
|
| 328 |
-
}
|
| 329 |
-
|
| 330 |
-
.text-title {
|
| 331 |
-
color: #2c3e50;
|
| 332 |
-
font-size: 1.1rem;
|
| 333 |
-
margin-bottom: 0.5rem;
|
| 334 |
-
}
|
| 335 |
-
|
| 336 |
-
.text-meta {
|
| 337 |
-
display: flex;
|
| 338 |
-
gap: 1rem;
|
| 339 |
-
font-size: 0.9rem;
|
| 340 |
-
color: #718096;
|
| 341 |
-
}
|
| 342 |
-
|
| 343 |
-
.meta-item i {
|
| 344 |
-
margin-right: 0.4rem;
|
| 345 |
-
color: #3498db;
|
| 346 |
-
}
|
| 347 |
-
|
| 348 |
-
.card-arrow {
|
| 349 |
-
color: #3498db;
|
| 350 |
-
opacity: 0;
|
| 351 |
-
}
|
| 352 |
-
|
| 353 |
-
/* le texte, cours*/
|
| 354 |
-
|
| 355 |
-
/* Page texte */
|
| 356 |
-
.text-container {
|
| 357 |
-
max-width: 800px;
|
| 358 |
-
margin: 2rem auto;
|
| 359 |
-
padding: 0 1.5rem;
|
| 360 |
-
}
|
| 361 |
-
|
| 362 |
-
.text-header {
|
| 363 |
-
text-align: center;
|
| 364 |
-
margin-bottom: 3rem;
|
| 365 |
-
}
|
| 366 |
-
|
| 367 |
-
.text-title {
|
| 368 |
-
font-size: 2.4rem;
|
| 369 |
-
color: #2c3e50;
|
| 370 |
-
margin-bottom: 1rem;
|
| 371 |
-
line-height: 1.3;
|
| 372 |
-
background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
|
| 373 |
-
-webkit-background-clip: text;
|
| 374 |
-
background-clip: text;
|
| 375 |
-
-webkit-text-fill-color: transparent;
|
| 376 |
-
display: inline-block;
|
| 377 |
-
}
|
| 378 |
-
|
| 379 |
-
.text-meta {
|
| 380 |
-
display: flex;
|
| 381 |
-
gap: 1.5rem;
|
| 382 |
-
justify-content: center;
|
| 383 |
-
color: #718096;
|
| 384 |
-
font-size: 0.95rem;
|
| 385 |
-
}
|
| 386 |
-
|
| 387 |
-
.meta-item i {
|
| 388 |
-
margin-right: 0.5rem;
|
| 389 |
-
color: #3498db;
|
| 390 |
-
}
|
| 391 |
-
|
| 392 |
-
.texte-contenu {
|
| 393 |
-
content: '';
|
| 394 |
-
position: absolute;
|
| 395 |
-
top: 0;
|
| 396 |
-
left: 0;
|
| 397 |
-
width: 100%;
|
| 398 |
-
height: 100%;
|
| 399 |
-
background: linear-gradient(135deg, #46a1dd 0%, #2980b9 100%);
|
| 400 |
-
opacity: 0;
|
| 401 |
-
transition: opacity 0.3s ease;
|
| 402 |
-
z-index: -1;
|
| 403 |
-
box-shadow: 0 4px 20px rgba(0,0,0,0.05);
|
| 404 |
-
animation: fadeIn 0.6s ease-out;
|
| 405 |
-
}
|
| 406 |
-
|
| 407 |
-
/* Styles de contenu riche */
|
| 408 |
-
.texte-contenu h2,
|
| 409 |
-
.texte-contenu h3 {
|
| 410 |
-
color: #2c3e50;
|
| 411 |
-
margin: 2rem 0 1rem;
|
| 412 |
-
}
|
| 413 |
-
|
| 414 |
-
.texte-contenu h2 {
|
| 415 |
-
font-size: 1.6rem;
|
| 416 |
-
border-bottom: 2px solid #e2e8f0;
|
| 417 |
-
padding-bottom: 0.5rem;
|
| 418 |
-
}
|
| 419 |
-
|
| 420 |
-
.texte-contenu h3 {
|
| 421 |
-
font-size: 1.4rem;
|
| 422 |
-
}
|
| 423 |
-
|
| 424 |
-
.texte-contenu p {
|
| 425 |
-
margin-bottom: 1.5rem;
|
| 426 |
-
}
|
| 427 |
-
|
| 428 |
-
.texte-contenu img {
|
| 429 |
-
max-width: 100%;
|
| 430 |
-
height: auto;
|
| 431 |
-
border-radius: 8px;
|
| 432 |
-
margin: 1.5rem 0;
|
| 433 |
-
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
| 434 |
-
}
|
| 435 |
-
|
| 436 |
-
.texte-contenu ul,
|
| 437 |
-
.texte-contenu ol {
|
| 438 |
-
margin: 1.5rem 0;
|
| 439 |
-
padding-left: 2rem;
|
| 440 |
-
}
|
| 441 |
-
|
| 442 |
-
.texte-contenu li {
|
| 443 |
-
margin-bottom: 0.8rem;
|
| 444 |
-
}
|
| 445 |
-
|
| 446 |
-
.texte-contenu blockquote {
|
| 447 |
-
border-left: 4px solid #3498db;
|
| 448 |
-
margin: 1.5rem 0;
|
| 449 |
-
padding: 1rem 1.5rem;
|
| 450 |
-
background: #f8f9fa;
|
| 451 |
-
border-radius: 0 6px 6px 0;
|
| 452 |
-
}
|
| 453 |
-
|
| 454 |
-
/* additif */
|
| 455 |
-
|
| 456 |
-
/* Header */
|
| 457 |
-
.main-navbar {
|
| 458 |
-
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
| 459 |
-
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.08);
|
| 460 |
-
padding: 0.8rem 1rem;
|
| 461 |
-
position: relative;
|
| 462 |
-
z-index: 1000;
|
| 463 |
-
}
|
| 464 |
-
|
| 465 |
-
.navbar-brand {
|
| 466 |
-
font-weight: 700;
|
| 467 |
-
color: #2c3e50 !important;
|
| 468 |
-
font-size: 1.4rem;
|
| 469 |
-
display: flex;
|
| 470 |
-
align-items: center;
|
| 471 |
-
transition: transform 0.3s ease;
|
| 472 |
-
}
|
| 473 |
-
|
| 474 |
-
.navbar-brand:hover {
|
| 475 |
-
transform: translateX(5px);
|
| 476 |
-
}
|
| 477 |
-
|
| 478 |
-
.navbar-brand::after {
|
| 479 |
-
content: "";
|
| 480 |
-
display: inline-block;
|
| 481 |
-
width: 2px;
|
| 482 |
-
height: 24px;
|
| 483 |
-
background: #3498db;
|
| 484 |
-
margin-left: 1rem;
|
| 485 |
-
transform: skew(-15deg);
|
| 486 |
-
}
|
| 487 |
-
|
| 488 |
-
.nav-link {
|
| 489 |
-
color: #4a5568 !important;
|
| 490 |
-
font-weight: 500;
|
| 491 |
-
padding: 0.5rem 1.2rem !important;
|
| 492 |
-
border-radius: 8px;
|
| 493 |
-
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 494 |
-
position: relative;
|
| 495 |
-
}
|
| 496 |
-
|
| 497 |
-
.nav-link:hover {
|
| 498 |
-
color: #3498db !important;
|
| 499 |
-
background: rgba(52, 152, 219, 0.08);
|
| 500 |
-
}
|
| 501 |
-
|
| 502 |
-
.nav-link.admin-link {
|
| 503 |
-
background: rgba(52, 152, 219, 0.1);
|
| 504 |
-
margin-left: 1rem;
|
| 505 |
-
}
|
| 506 |
-
|
| 507 |
-
.nav-link.admin-link:hover {
|
| 508 |
-
background: rgba(52, 152, 219, 0.2);
|
| 509 |
-
}
|
| 510 |
-
|
| 511 |
-
.nav-link.admin-link::before {
|
| 512 |
-
content: "\f023";
|
| 513 |
-
font-family: "Font Awesome 5 Free";
|
| 514 |
-
font-weight: 900;
|
| 515 |
-
margin-right: 0.5rem;
|
| 516 |
-
}
|
| 517 |
-
|
| 518 |
-
/* Footer */
|
| 519 |
-
.main-footer {
|
| 520 |
-
background: #2c3e50;
|
| 521 |
-
color: #ecf0f1;
|
| 522 |
-
padding: 1rem 0; /* Réduc pour pc */
|
| 523 |
-
margin-top: auto;
|
| 524 |
-
border-top: 3px solid #3498db;
|
| 525 |
-
}
|
| 526 |
-
|
| 527 |
-
.main-footer .social-links {
|
| 528 |
-
display: flex;
|
| 529 |
-
justify-content: center;
|
| 530 |
-
gap: 1.5rem;
|
| 531 |
-
margin: 1.5rem 0;
|
| 532 |
-
}
|
| 533 |
-
|
| 534 |
-
.main-footer .social-links a {
|
| 535 |
-
color: #bdc3c7;
|
| 536 |
-
font-size: 1.4rem;
|
| 537 |
-
transition: all 0.3s ease;
|
| 538 |
-
}
|
| 539 |
-
|
| 540 |
-
.main-footer .social-links a:hover {
|
| 541 |
-
color: #3498db;
|
| 542 |
-
transform: translateY(-3px);
|
| 543 |
-
}
|
| 544 |
-
|
| 545 |
-
.main-footer .copyright {
|
| 546 |
-
font-size: 0.9rem;
|
| 547 |
-
opacity: 0.8;
|
| 548 |
-
margin-top: 1rem;
|
| 549 |
-
}
|
| 550 |
-
|
| 551 |
-
/* Responsive */
|
| 552 |
-
@media (max-width: 768px) {
|
| 553 |
-
.main-footer {
|
| 554 |
-
padding: 0.75rem 0; /* Réduc pour mobile */
|
| 555 |
-
}
|
| 556 |
-
}
|
| 557 |
-
|
| 558 |
-
.nav-link {
|
| 559 |
-
padding: 0.5rem !important;
|
| 560 |
-
}
|
| 561 |
-
|
| 562 |
-
.main-footer {
|
| 563 |
-
padding: 1.5rem 0;
|
| 564 |
-
}
|
| 565 |
-
}
|
| 566 |
-
.copyright {
|
| 567 |
-
font-size: 0.9rem;
|
| 568 |
-
opacity: 0.8;
|
| 569 |
-
margin-top: 1rem;
|
| 570 |
-
text-align: center;
|
| 571 |
-
}
|
| 572 |
-
|
| 573 |
-
.text-meta {
|
| 574 |
-
display: flex;
|
| 575 |
-
gap: 1.5rem;
|
| 576 |
-
margin-top: 0.8rem;
|
| 577 |
-
}
|
| 578 |
-
|
| 579 |
-
.meta-item {
|
| 580 |
-
display: flex;
|
| 581 |
-
align-items: center;
|
| 582 |
-
gap: 0.5rem;
|
| 583 |
-
font-size: 0.9rem;
|
| 584 |
-
color: #6c757d;
|
| 585 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/templates/base.html
DELETED
|
@@ -1,44 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="fr">
|
| 3 |
-
<head>
|
| 4 |
-
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
| 5 |
-
<meta charset="UTF-8">
|
| 6 |
-
<title>{% block title %}Mon Projet{% endblock %}</title>
|
| 7 |
-
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
| 8 |
-
<!-- Bootstrap CSS -->
|
| 9 |
-
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
|
| 10 |
-
{% block head %}{% endblock %}
|
| 11 |
-
</head>
|
| 12 |
-
<body class="d-flex flex-column min-vh-100">
|
| 13 |
-
|
| 14 |
-
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
| 15 |
-
<a class="navbar-brand" href="{{ url_for('main.index') }}">Mariam Ai</a>
|
| 16 |
-
<ul class="navbar-nav ml-auto"> <!-- ml-auto pour aligner à droite -->
|
| 17 |
-
|
| 18 |
-
</ul>
|
| 19 |
-
</nav>
|
| 20 |
-
|
| 21 |
-
<div class="container mt-4 flex-grow-1">
|
| 22 |
-
{% block content %}{% endblock %}
|
| 23 |
-
</div> <!-- End of main container -->
|
| 24 |
-
|
| 25 |
-
<footer class="main-footer">
|
| 26 |
-
<div class="container">
|
| 27 |
-
<div class="social-links">
|
| 28 |
-
<a href="https://whatsapp.com/channel/0029VazlRNk9WtBuQuAzUU2d"><i class="fab fa-whatsapp"></i></a>
|
| 29 |
-
<a href="https://www.tiktok.com/@mariamai241"><i class="fab fa-tiktok"></i></a>
|
| 30 |
-
</div>
|
| 31 |
-
<div class="copyright">
|
| 32 |
-
©2025 Mariam Ai. Tous droits réservés.
|
| 33 |
-
</div>
|
| 34 |
-
</div>
|
| 35 |
-
</footer>
|
| 36 |
-
|
| 37 |
-
<!-- Bootstrap JS, Popper.js, and jQuery (Optional, but often needed) -->
|
| 38 |
-
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
|
| 39 |
-
<script src="https://cdn.jsdelivr.net/npm/@popperjs/[email protected]/dist/umd/popper.min.js"></script>
|
| 40 |
-
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
|
| 41 |
-
{% block scripts %}{% endblock %}
|
| 42 |
-
</body>
|
| 43 |
-
</html>
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/templates/index.html
DELETED
|
@@ -1,19 +0,0 @@
|
|
| 1 |
-
{% extends "base.html" %}
|
| 2 |
-
{% block content %}
|
| 3 |
-
<div class="container">
|
| 4 |
-
<h1 class="page-title">Matières</h1>
|
| 5 |
-
|
| 6 |
-
<div class="subjects-grid">
|
| 7 |
-
{% for matiere in matieres %}
|
| 8 |
-
<div class="subject-card">
|
| 9 |
-
<a class="subject-link" href="{{ url_for('main.matiere', matiere_id=matiere.id) }}">
|
| 10 |
-
<span class="subject-name">{{ matiere.nom }}</span>
|
| 11 |
-
<span class="subject-arrow">→</span>
|
| 12 |
-
</a>
|
| 13 |
-
</div>
|
| 14 |
-
{% else %}
|
| 15 |
-
<p class="no-subjects">Aucune matière disponible</p>
|
| 16 |
-
{% endfor %}
|
| 17 |
-
</div>
|
| 18 |
-
</div>
|
| 19 |
-
{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/templates/matiere.html
DELETED
|
@@ -1,34 +0,0 @@
|
|
| 1 |
-
{% extends "base.html" %}
|
| 2 |
-
|
| 3 |
-
{% block content %}
|
| 4 |
-
<div class="subject-container">
|
| 5 |
-
<div class="subject-header">
|
| 6 |
-
<h1 class="subject-title">{{ matiere.nom }}</h1>
|
| 7 |
-
<div class="subject-header-decoration"></div>
|
| 8 |
-
</div>
|
| 9 |
-
|
| 10 |
-
<div class="subcategories-section">
|
| 11 |
-
<h2 class="section-title">Sous-catégories</h2>
|
| 12 |
-
|
| 13 |
-
<div class="subcategories-grid">
|
| 14 |
-
{% for sous_categorie in sous_categories %}
|
| 15 |
-
<a href="{{ url_for('main.sous_categorie', sous_categorie_id=sous_categorie.id) }}" class="subcategory-card">
|
| 16 |
-
<div class="subcategory-content">
|
| 17 |
-
<span class="subcategory-name">{{ sous_categorie.nom }}</span>
|
| 18 |
-
<svg class="arrow-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 19 |
-
<path stroke-linecap="round" stroke-linejoin="round" d="M14 5l7 7m0 0l-7 7m7-7H3"/>
|
| 20 |
-
</svg>
|
| 21 |
-
</div>
|
| 22 |
-
</a>
|
| 23 |
-
{% else %}
|
| 24 |
-
<div class="no-subcategories">
|
| 25 |
-
<svg class="empty-icon" viewBox="0 0 24 24">
|
| 26 |
-
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2M12 3a4 4 0 1 0 0 8 4 4 0 0 0 0-8z"/>
|
| 27 |
-
</svg>
|
| 28 |
-
<p>Aucune sous-catégorie disponible</p>
|
| 29 |
-
</div>
|
| 30 |
-
{% endfor %}
|
| 31 |
-
</div>
|
| 32 |
-
</div>
|
| 33 |
-
</div>
|
| 34 |
-
{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/templates/sous_categorie.html
DELETED
|
@@ -1,35 +0,0 @@
|
|
| 1 |
-
{% extends "base.html" %}
|
| 2 |
-
|
| 3 |
-
{% block content %}
|
| 4 |
-
<div class="content-container">
|
| 5 |
-
<div class="header-section">
|
| 6 |
-
<h1 class="main-title">{{ sous_categorie.nom }}</h1>
|
| 7 |
-
<div class="title-underline"></div>
|
| 8 |
-
</div>
|
| 9 |
-
|
| 10 |
-
<div class="texts-section">
|
| 11 |
-
<h2 class="section-subtitle">Textes</h2>
|
| 12 |
-
|
| 13 |
-
<div class="texts-grid">
|
| 14 |
-
{% for texte in textes %}
|
| 15 |
-
<a href="{{ url_for('main.texte', texte_id=texte.id) }}" class="text-card">
|
| 16 |
-
<div class="card-content">
|
| 17 |
-
<h3 class="text-title">{{ texte.titre }}</h3>
|
| 18 |
-
<div class="text-meta">
|
| 19 |
-
<!-- Ajoutez des métadonnées si disponibles -->
|
| 20 |
-
<span class="meta-item"><i class="fas fa-calendar-alt"></i> Date</span>
|
| 21 |
-
<span class="meta-item"><i class="fas fa-user-edit"></i> Auteur</span>
|
| 22 |
-
</div>
|
| 23 |
-
</div>
|
| 24 |
-
<i class="fas fa-arrow-right card-arrow"></i>
|
| 25 |
-
</a>
|
| 26 |
-
{% else %}
|
| 27 |
-
<div class="empty-state">
|
| 28 |
-
<i class="fas fa-book-open empty-icon"></i>
|
| 29 |
-
<p>Aucun texte disponible dans cette sous-catégorie</p>
|
| 30 |
-
</div>
|
| 31 |
-
{% endfor %}
|
| 32 |
-
</div>
|
| 33 |
-
</div>
|
| 34 |
-
</div>
|
| 35 |
-
{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/templates/texte.html
DELETED
|
@@ -1,15 +0,0 @@
|
|
| 1 |
-
{% extends "base.html" %}
|
| 2 |
-
|
| 3 |
-
{% block content %}
|
| 4 |
-
<div class="container">
|
| 5 |
-
<h1>{{ texte.titre }}</h1>
|
| 6 |
-
<p class="author">
|
| 7 |
-
|
| 8 |
-
Auteur : Admin
|
| 9 |
-
|
| 10 |
-
</p>
|
| 11 |
-
<div>
|
| 12 |
-
{{ texte.contenu|safe }}
|
| 13 |
-
</div>
|
| 14 |
-
</div>
|
| 15 |
-
{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/views.py
DELETED
|
@@ -1,28 +0,0 @@
|
|
| 1 |
-
from flask import Blueprint, render_template, abort
|
| 2 |
-
from app.models import Matiere, SousCategorie, Texte
|
| 3 |
-
from sqlalchemy import func
|
| 4 |
-
|
| 5 |
-
bp = Blueprint('main', __name__)
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
@bp.route('/')
|
| 9 |
-
def index():
|
| 10 |
-
matieres = Matiere.query.order_by(func.lower(Matiere.nom)).all()
|
| 11 |
-
return render_template('index.html', matieres=matieres)
|
| 12 |
-
|
| 13 |
-
@bp.route('/matiere/<int:matiere_id>')
|
| 14 |
-
def matiere(matiere_id):
|
| 15 |
-
matiere = Matiere.query.get_or_404(matiere_id)
|
| 16 |
-
sous_categories = matiere.sous_categories.order_by(func.lower(SousCategorie.nom)).all() # Tri insensible
|
| 17 |
-
return render_template('matiere.html', matiere=matiere, sous_categories=sous_categories)
|
| 18 |
-
|
| 19 |
-
@bp.route('/sous_categorie/<int:sous_categorie_id>')
|
| 20 |
-
def sous_categorie(sous_categorie_id):
|
| 21 |
-
sous_categorie = SousCategorie.query.get_or_404(sous_categorie_id)
|
| 22 |
-
textes = sous_categorie.textes.order_by(Texte.titre).all() # Tri par titre
|
| 23 |
-
return render_template('sous_categorie.html', sous_categorie=sous_categorie, textes=textes)
|
| 24 |
-
|
| 25 |
-
@bp.route('/texte/<int:texte_id>')
|
| 26 |
-
def texte(texte_id):
|
| 27 |
-
texte = Texte.query.get_or_404(texte_id)
|
| 28 |
-
return render_template('texte.html', texte=texte)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
config.py
DELETED
|
@@ -1,17 +0,0 @@
|
|
| 1 |
-
import os
|
| 2 |
-
|
| 3 |
-
class Config:
|
| 4 |
-
SECRET_KEY = os.getenv("SECRET_KEY", "changeme") # Utilise une variable d'environnement, avec une valeur par défaut à changer
|
| 5 |
-
|
| 6 |
-
SQLALCHEMY_DATABASE_URI =os.getenv("DATABASE_URL")
|
| 7 |
-
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
| 8 |
-
|
| 9 |
-
SQLALCHEMY_ENGINE_OPTIONS = {
|
| 10 |
-
# Recycle connections after 280 seconds (less than potential 5-min server timeout)
|
| 11 |
-
'pool_recycle': 280,
|
| 12 |
-
# Optionally, enable pre-ping for extra robustness (recommended)
|
| 13 |
-
'pool_pre_ping': True
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
FLASK_ADMIN_SWATCH = 'cerulean' # Optionnel, permet de changer le style facilement
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
main.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from api.index import app, db
|
| 3 |
+
|
| 4 |
+
# Create database tables if they don't exist
|
| 5 |
+
with app.app_context():
|
| 6 |
+
db.create_all()
|
| 7 |
+
|
| 8 |
+
if __name__ == '__main__':
|
| 9 |
+
port = int(os.environ.get("PORT", 5000))
|
| 10 |
+
app.run(host="0.0.0.0", port=port)
|
migrations/README
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
Single-database configuration for Flask.
|
|
|
|
|
|
migrations/__pycache__/env.cpython-312.pyc
DELETED
|
Binary file (4.5 kB)
|
|
|
migrations/alembic.ini
DELETED
|
@@ -1,50 +0,0 @@
|
|
| 1 |
-
# A generic, single database configuration.
|
| 2 |
-
|
| 3 |
-
[alembic]
|
| 4 |
-
# template used to generate migration files
|
| 5 |
-
# file_template = %%(rev)s_%%(slug)s
|
| 6 |
-
|
| 7 |
-
# set to 'true' to run the environment during
|
| 8 |
-
# the 'revision' command, regardless of autogenerate
|
| 9 |
-
# revision_environment = false
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
# Logging configuration
|
| 13 |
-
[loggers]
|
| 14 |
-
keys = root,sqlalchemy,alembic,flask_migrate
|
| 15 |
-
|
| 16 |
-
[handlers]
|
| 17 |
-
keys = console
|
| 18 |
-
|
| 19 |
-
[formatters]
|
| 20 |
-
keys = generic
|
| 21 |
-
|
| 22 |
-
[logger_root]
|
| 23 |
-
level = WARN
|
| 24 |
-
handlers = console
|
| 25 |
-
qualname =
|
| 26 |
-
|
| 27 |
-
[logger_sqlalchemy]
|
| 28 |
-
level = WARN
|
| 29 |
-
handlers =
|
| 30 |
-
qualname = sqlalchemy.engine
|
| 31 |
-
|
| 32 |
-
[logger_alembic]
|
| 33 |
-
level = INFO
|
| 34 |
-
handlers =
|
| 35 |
-
qualname = alembic
|
| 36 |
-
|
| 37 |
-
[logger_flask_migrate]
|
| 38 |
-
level = INFO
|
| 39 |
-
handlers =
|
| 40 |
-
qualname = flask_migrate
|
| 41 |
-
|
| 42 |
-
[handler_console]
|
| 43 |
-
class = StreamHandler
|
| 44 |
-
args = (sys.stderr,)
|
| 45 |
-
level = NOTSET
|
| 46 |
-
formatter = generic
|
| 47 |
-
|
| 48 |
-
[formatter_generic]
|
| 49 |
-
format = %(levelname)-5.5s [%(name)s] %(message)s
|
| 50 |
-
datefmt = %H:%M:%S
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
migrations/env.py
DELETED
|
@@ -1,113 +0,0 @@
|
|
| 1 |
-
import logging
|
| 2 |
-
from logging.config import fileConfig
|
| 3 |
-
|
| 4 |
-
from flask import current_app
|
| 5 |
-
|
| 6 |
-
from alembic import context
|
| 7 |
-
|
| 8 |
-
# this is the Alembic Config object, which provides
|
| 9 |
-
# access to the values within the .ini file in use.
|
| 10 |
-
config = context.config
|
| 11 |
-
|
| 12 |
-
# Interpret the config file for Python logging.
|
| 13 |
-
# This line sets up loggers basically.
|
| 14 |
-
fileConfig(config.config_file_name)
|
| 15 |
-
logger = logging.getLogger('alembic.env')
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
def get_engine():
|
| 19 |
-
try:
|
| 20 |
-
# this works with Flask-SQLAlchemy<3 and Alchemical
|
| 21 |
-
return current_app.extensions['migrate'].db.get_engine()
|
| 22 |
-
except (TypeError, AttributeError):
|
| 23 |
-
# this works with Flask-SQLAlchemy>=3
|
| 24 |
-
return current_app.extensions['migrate'].db.engine
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
def get_engine_url():
|
| 28 |
-
try:
|
| 29 |
-
return get_engine().url.render_as_string(hide_password=False).replace(
|
| 30 |
-
'%', '%%')
|
| 31 |
-
except AttributeError:
|
| 32 |
-
return str(get_engine().url).replace('%', '%%')
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
# add your model's MetaData object here
|
| 36 |
-
# for 'autogenerate' support
|
| 37 |
-
# from myapp import mymodel
|
| 38 |
-
# target_metadata = mymodel.Base.metadata
|
| 39 |
-
config.set_main_option('sqlalchemy.url', get_engine_url())
|
| 40 |
-
target_db = current_app.extensions['migrate'].db
|
| 41 |
-
|
| 42 |
-
# other values from the config, defined by the needs of env.py,
|
| 43 |
-
# can be acquired:
|
| 44 |
-
# my_important_option = config.get_main_option("my_important_option")
|
| 45 |
-
# ... etc.
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
def get_metadata():
|
| 49 |
-
if hasattr(target_db, 'metadatas'):
|
| 50 |
-
return target_db.metadatas[None]
|
| 51 |
-
return target_db.metadata
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
def run_migrations_offline():
|
| 55 |
-
"""Run migrations in 'offline' mode.
|
| 56 |
-
|
| 57 |
-
This configures the context with just a URL
|
| 58 |
-
and not an Engine, though an Engine is acceptable
|
| 59 |
-
here as well. By skipping the Engine creation
|
| 60 |
-
we don't even need a DBAPI to be available.
|
| 61 |
-
|
| 62 |
-
Calls to context.execute() here emit the given string to the
|
| 63 |
-
script output.
|
| 64 |
-
|
| 65 |
-
"""
|
| 66 |
-
url = config.get_main_option("sqlalchemy.url")
|
| 67 |
-
context.configure(
|
| 68 |
-
url=url, target_metadata=get_metadata(), literal_binds=True
|
| 69 |
-
)
|
| 70 |
-
|
| 71 |
-
with context.begin_transaction():
|
| 72 |
-
context.run_migrations()
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
def run_migrations_online():
|
| 76 |
-
"""Run migrations in 'online' mode.
|
| 77 |
-
|
| 78 |
-
In this scenario we need to create an Engine
|
| 79 |
-
and associate a connection with the context.
|
| 80 |
-
|
| 81 |
-
"""
|
| 82 |
-
|
| 83 |
-
# this callback is used to prevent an auto-migration from being generated
|
| 84 |
-
# when there are no changes to the schema
|
| 85 |
-
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
|
| 86 |
-
def process_revision_directives(context, revision, directives):
|
| 87 |
-
if getattr(config.cmd_opts, 'autogenerate', False):
|
| 88 |
-
script = directives[0]
|
| 89 |
-
if script.upgrade_ops.is_empty():
|
| 90 |
-
directives[:] = []
|
| 91 |
-
logger.info('No changes in schema detected.')
|
| 92 |
-
|
| 93 |
-
conf_args = current_app.extensions['migrate'].configure_args
|
| 94 |
-
if conf_args.get("process_revision_directives") is None:
|
| 95 |
-
conf_args["process_revision_directives"] = process_revision_directives
|
| 96 |
-
|
| 97 |
-
connectable = get_engine()
|
| 98 |
-
|
| 99 |
-
with connectable.connect() as connection:
|
| 100 |
-
context.configure(
|
| 101 |
-
connection=connection,
|
| 102 |
-
target_metadata=get_metadata(),
|
| 103 |
-
**conf_args
|
| 104 |
-
)
|
| 105 |
-
|
| 106 |
-
with context.begin_transaction():
|
| 107 |
-
context.run_migrations()
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
if context.is_offline_mode():
|
| 111 |
-
run_migrations_offline()
|
| 112 |
-
else:
|
| 113 |
-
run_migrations_online()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
migrations/script.py.mako
DELETED
|
@@ -1,24 +0,0 @@
|
|
| 1 |
-
"""${message}
|
| 2 |
-
|
| 3 |
-
Revision ID: ${up_revision}
|
| 4 |
-
Revises: ${down_revision | comma,n}
|
| 5 |
-
Create Date: ${create_date}
|
| 6 |
-
|
| 7 |
-
"""
|
| 8 |
-
from alembic import op
|
| 9 |
-
import sqlalchemy as sa
|
| 10 |
-
${imports if imports else ""}
|
| 11 |
-
|
| 12 |
-
# revision identifiers, used by Alembic.
|
| 13 |
-
revision = ${repr(up_revision)}
|
| 14 |
-
down_revision = ${repr(down_revision)}
|
| 15 |
-
branch_labels = ${repr(branch_labels)}
|
| 16 |
-
depends_on = ${repr(depends_on)}
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
def upgrade():
|
| 20 |
-
${upgrades if upgrades else "pass"}
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
def downgrade():
|
| 24 |
-
${downgrades if downgrades else "pass"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
migrations/versions/80f91a17b1db_initial_migration.py
DELETED
|
@@ -1,112 +0,0 @@
|
|
| 1 |
-
"""Initial migration
|
| 2 |
-
|
| 3 |
-
Revision ID: 80f91a17b1db
|
| 4 |
-
Revises:
|
| 5 |
-
Create Date: 2025-02-17 12:20:44.860732
|
| 6 |
-
|
| 7 |
-
"""
|
| 8 |
-
from alembic import op
|
| 9 |
-
import sqlalchemy as sa
|
| 10 |
-
from sqlalchemy.dialects import postgresql
|
| 11 |
-
|
| 12 |
-
# revision identifiers, used by Alembic.
|
| 13 |
-
revision = '80f91a17b1db'
|
| 14 |
-
down_revision = None
|
| 15 |
-
branch_labels = None
|
| 16 |
-
depends_on = None
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
def upgrade():
|
| 20 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 21 |
-
op.create_table('sous_categorie',
|
| 22 |
-
sa.Column('id', sa.Integer(), nullable=False),
|
| 23 |
-
sa.Column('nom', sa.String(length=64), nullable=False),
|
| 24 |
-
sa.Column('matiere_id', sa.Integer(), nullable=False),
|
| 25 |
-
sa.ForeignKeyConstraint(['matiere_id'], ['matiere.id'], ),
|
| 26 |
-
sa.PrimaryKeyConstraint('id'),
|
| 27 |
-
sa.UniqueConstraint('nom', 'matiere_id', name='_nom_matiere_uc')
|
| 28 |
-
)
|
| 29 |
-
op.create_table('texte',
|
| 30 |
-
sa.Column('id', sa.Integer(), nullable=False),
|
| 31 |
-
sa.Column('titre', sa.String(length=128), nullable=False),
|
| 32 |
-
sa.Column('contenu', sa.Text(), nullable=False),
|
| 33 |
-
sa.Column('sous_categorie_id', sa.Integer(), nullable=False),
|
| 34 |
-
sa.ForeignKeyConstraint(['sous_categorie_id'], ['sous_categorie.id'], ),
|
| 35 |
-
sa.PrimaryKeyConstraint('id')
|
| 36 |
-
)
|
| 37 |
-
op.drop_table('cours')
|
| 38 |
-
op.drop_table('subjects')
|
| 39 |
-
op.drop_table('texts')
|
| 40 |
-
op.drop_table('commentaire')
|
| 41 |
-
op.drop_table('categories')
|
| 42 |
-
op.drop_table('categorie')
|
| 43 |
-
with op.batch_alter_table('matiere', schema=None) as batch_op:
|
| 44 |
-
batch_op.alter_column('nom',
|
| 45 |
-
existing_type=sa.VARCHAR(length=255),
|
| 46 |
-
type_=sa.String(length=64),
|
| 47 |
-
existing_nullable=False)
|
| 48 |
-
batch_op.create_unique_constraint(None, ['nom'])
|
| 49 |
-
batch_op.drop_column('description')
|
| 50 |
-
|
| 51 |
-
# ### end Alembic commands ###
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
def downgrade():
|
| 55 |
-
# ### commands auto generated by Alembic - please adjust! ###
|
| 56 |
-
with op.batch_alter_table('matiere', schema=None) as batch_op:
|
| 57 |
-
batch_op.add_column(sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True))
|
| 58 |
-
batch_op.drop_constraint(None, type_='unique')
|
| 59 |
-
batch_op.alter_column('nom',
|
| 60 |
-
existing_type=sa.String(length=64),
|
| 61 |
-
type_=sa.VARCHAR(length=255),
|
| 62 |
-
existing_nullable=False)
|
| 63 |
-
|
| 64 |
-
op.create_table('categorie',
|
| 65 |
-
sa.Column('id', sa.INTEGER(), server_default=sa.text("nextval('categorie_id_seq'::regclass)"), autoincrement=True, nullable=False),
|
| 66 |
-
sa.Column('nom', sa.VARCHAR(length=255), autoincrement=False, nullable=False),
|
| 67 |
-
sa.Column('matiere_id', sa.INTEGER(), autoincrement=False, nullable=False),
|
| 68 |
-
sa.ForeignKeyConstraint(['matiere_id'], ['matiere.id'], name='fk_matiere', ondelete='CASCADE'),
|
| 69 |
-
sa.PrimaryKeyConstraint('id', name='categorie_pkey'),
|
| 70 |
-
postgresql_ignore_search_path=False
|
| 71 |
-
)
|
| 72 |
-
op.create_table('categories',
|
| 73 |
-
sa.Column('id', sa.INTEGER(), server_default=sa.text("nextval('categories_id_seq'::regclass)"), autoincrement=True, nullable=False),
|
| 74 |
-
sa.Column('name', sa.VARCHAR(length=100), autoincrement=False, nullable=False),
|
| 75 |
-
sa.Column('subject_id', sa.INTEGER(), autoincrement=False, nullable=False),
|
| 76 |
-
sa.ForeignKeyConstraint(['subject_id'], ['subjects.id'], name='categories_subject_id_fkey'),
|
| 77 |
-
sa.PrimaryKeyConstraint('id', name='categories_pkey'),
|
| 78 |
-
postgresql_ignore_search_path=False
|
| 79 |
-
)
|
| 80 |
-
op.create_table('commentaire',
|
| 81 |
-
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
|
| 82 |
-
sa.Column('contenu', sa.TEXT(), autoincrement=False, nullable=False),
|
| 83 |
-
sa.Column('auteur', sa.VARCHAR(length=255), autoincrement=False, nullable=False),
|
| 84 |
-
sa.Column('date', postgresql.TIMESTAMP(), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True),
|
| 85 |
-
sa.Column('cours_id', sa.INTEGER(), autoincrement=False, nullable=False),
|
| 86 |
-
sa.ForeignKeyConstraint(['cours_id'], ['cours.id'], name='fk_cours', ondelete='CASCADE'),
|
| 87 |
-
sa.PrimaryKeyConstraint('id', name='commentaire_pkey')
|
| 88 |
-
)
|
| 89 |
-
op.create_table('texts',
|
| 90 |
-
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
|
| 91 |
-
sa.Column('title', sa.VARCHAR(length=150), autoincrement=False, nullable=True),
|
| 92 |
-
sa.Column('content', sa.TEXT(), autoincrement=False, nullable=False),
|
| 93 |
-
sa.Column('category_id', sa.INTEGER(), autoincrement=False, nullable=False),
|
| 94 |
-
sa.ForeignKeyConstraint(['category_id'], ['categories.id'], name='texts_category_id_fkey'),
|
| 95 |
-
sa.PrimaryKeyConstraint('id', name='texts_pkey')
|
| 96 |
-
)
|
| 97 |
-
op.create_table('subjects',
|
| 98 |
-
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
|
| 99 |
-
sa.Column('name', sa.VARCHAR(length=100), autoincrement=False, nullable=False),
|
| 100 |
-
sa.PrimaryKeyConstraint('id', name='subjects_pkey')
|
| 101 |
-
)
|
| 102 |
-
op.create_table('cours',
|
| 103 |
-
sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False),
|
| 104 |
-
sa.Column('titre', sa.VARCHAR(length=255), autoincrement=False, nullable=False),
|
| 105 |
-
sa.Column('description', sa.TEXT(), autoincrement=False, nullable=True),
|
| 106 |
-
sa.Column('categorie_id', sa.INTEGER(), autoincrement=False, nullable=False),
|
| 107 |
-
sa.ForeignKeyConstraint(['categorie_id'], ['categorie.id'], name='fk_categorie', ondelete='CASCADE'),
|
| 108 |
-
sa.PrimaryKeyConstraint('id', name='cours_pkey')
|
| 109 |
-
)
|
| 110 |
-
op.drop_table('texte')
|
| 111 |
-
op.drop_table('sous_categorie')
|
| 112 |
-
# ### end Alembic commands ###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
migrations/versions/__pycache__/80f91a17b1db_initial_migration.cpython-312.pyc
DELETED
|
Binary file (8.15 kB)
|
|
|
run.py
DELETED
|
@@ -1,6 +0,0 @@
|
|
| 1 |
-
from app import create_app
|
| 2 |
-
import os
|
| 3 |
-
app = create_app()
|
| 4 |
-
|
| 5 |
-
if __name__ == '__main__':
|
| 6 |
-
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 5000)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|