Spaces:
Running
Running
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)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|