test redesign gesthub
This commit is contained in:
@@ -58,7 +58,7 @@ services:
|
|||||||
image: quay.io/keycloak/keycloak:22.0.5
|
image: quay.io/keycloak/keycloak:22.0.5
|
||||||
command:
|
command:
|
||||||
- start-dev
|
- start-dev
|
||||||
- --hostname=keycloak.ninolbt.com
|
- --hostname=localhost
|
||||||
- --hostname-strict=false
|
- --hostname-strict=false
|
||||||
- --hostname-strict-https=false
|
- --hostname-strict-https=false
|
||||||
- --proxy=edge
|
- --proxy=edge
|
||||||
|
|||||||
145
web/app.py
145
web/app.py
@@ -1,29 +1,51 @@
|
|||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
import json
|
import json
|
||||||
from flask import Flask, redirect, url_for, jsonify, session, render_template
|
# J'ai ajouté 'request' aux imports
|
||||||
|
from flask import Flask, redirect, url_for, jsonify, session, render_template, request
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from authlib.integrations.flask_client import OAuth
|
from authlib.integrations.flask_client import OAuth
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
ANNOUNCE_FILE = os.path.join(os.path.dirname(__file__), "annonces.json")
|
ANNOUNCE_FILE = os.path.join(os.path.dirname(__file__), "annonces.json")
|
||||||
|
# Ta config DB actuelle
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://flaskuser:flaskpass@mariadb/flaskdb'
|
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://flaskuser:flaskpass@mariadb/flaskdb'
|
||||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||||
app.secret_key = os.environ.get("SECRET_KEY", "dev-key")
|
app.secret_key = os.environ.get("SECRET_KEY", "dev-key")
|
||||||
|
|
||||||
db = SQLAlchemy(app)
|
db = SQLAlchemy(app)
|
||||||
|
|
||||||
# Configuration de Authlib
|
# --- MODELE DE DONNEES POUR LES BLOCS ---
|
||||||
oauth = OAuth(app)
|
class Block(db.Model):
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
block_type = db.Column(db.String(50)) # ex: 'iframe', 'buttons', 'html'
|
||||||
|
column_name = db.Column(db.String(20)) # ex: 'left', 'center', 'right'
|
||||||
|
position = db.Column(db.Integer) # pour l'ordre (0, 1, 2...)
|
||||||
|
data = db.Column(db.Text) # Contenu JSON (url, titre, etc.)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return {
|
||||||
|
"id": self.id,
|
||||||
|
"type": self.block_type,
|
||||||
|
"column": self.column_name,
|
||||||
|
"position": self.position,
|
||||||
|
"data": json.loads(self.data) if self.data else {}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Création des tables si elles n'existent pas
|
||||||
|
with app.app_context():
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
# ... (Ici, garde ta configuration OAUTH et tes routes Login/Logout/Auth inchangées) ...
|
||||||
|
# ... (Garde aussi tes fonctions load_announces, save_announces etc.) ...
|
||||||
|
|
||||||
|
oauth = OAuth(app)
|
||||||
keycloak = oauth.register(
|
keycloak = oauth.register(
|
||||||
name='keycloak',
|
name='keycloak',
|
||||||
client_id='flask-app',
|
client_id='flask-app',
|
||||||
client_secret='T5G5jzCBiphnBNh9uuj0f6YNc9HrP8r4',
|
client_secret='T5G5jzCBiphnBNh9uuj0f6YNc9HrP8r4',
|
||||||
server_metadata_url='https://keycloak.ninolbt.com/realms/gesthub/.well-known/openid-configuration',
|
server_metadata_url='https://keycloak.ninolbt.com/realms/gesthub/.well-known/openid-configuration',
|
||||||
client_kwargs={
|
client_kwargs={'scope': 'openid profile email'}
|
||||||
'scope': 'openid profile email',
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
@@ -31,7 +53,6 @@ def index():
|
|||||||
user = session.get('user')
|
user = session.get('user')
|
||||||
if user:
|
if user:
|
||||||
return render_template('view/index.html', user=user)
|
return render_template('view/index.html', user=user)
|
||||||
|
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for('login'))
|
||||||
|
|
||||||
@app.route('/login')
|
@app.route('/login')
|
||||||
@@ -48,88 +69,84 @@ def auth():
|
|||||||
userinfo = keycloak.parse_id_token(token, nonce=nonce)
|
userinfo = keycloak.parse_id_token(token, nonce=nonce)
|
||||||
session['user'] = userinfo
|
session['user'] = userinfo
|
||||||
session["id_token"] = token.get("id_token")
|
session["id_token"] = token.get("id_token")
|
||||||
app.logger.debug(f"User info: {userinfo}")
|
|
||||||
return redirect('/')
|
return redirect('/')
|
||||||
|
|
||||||
@app.route("/logout")
|
@app.route("/logout")
|
||||||
def logout():
|
def logout():
|
||||||
id_token = session.get("id_token")
|
id_token = session.get("id_token")
|
||||||
print("ID Token Hint:", id_token)
|
|
||||||
session.clear()
|
session.clear()
|
||||||
return redirect(
|
return redirect(
|
||||||
f"https://keycloak.ninolbt.com/realms/gesthub/protocol/openid-connect/logout"
|
f"https://keycloak.ninolbt.com/realms/gesthub/protocol/openid-connect/logout"
|
||||||
f"?post_logout_redirect_uri=https://dashboard.ninolbt.com"
|
f"?post_logout_redirect_uri=https://dashboard.ninolbt.com"
|
||||||
f"&id_token_hint={id_token}"
|
f"&id_token_hint={id_token}"
|
||||||
)
|
)
|
||||||
def load_announces():
|
|
||||||
try:
|
|
||||||
with open(ANNOUNCE_FILE, "r", encoding="utf-8") as f:
|
|
||||||
return json.load(f)
|
|
||||||
except (FileNotFoundError, json.JSONDecodeError):
|
|
||||||
return []
|
|
||||||
|
|
||||||
def save_announces(announces):
|
# --- API LAYOUT (Gestion des Blocs) ---
|
||||||
with open(ANNOUNCE_FILE, "w", encoding="utf-8") as f:
|
|
||||||
json.dump(announces, f, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
def get_next_id(announces):
|
@app.route('/api/layout', methods=['GET'])
|
||||||
return max((a.get("id", 0) for a in announces), default=0) + 1
|
def get_layout():
|
||||||
|
# Récupère tous les blocs triés par position
|
||||||
|
blocks = Block.query.order_by(Block.position).all()
|
||||||
|
return jsonify([b.to_dict() for b in blocks])
|
||||||
|
|
||||||
@app.route("/api/annonces", methods=["GET"])
|
@app.route('/api/layout/save', methods=['POST'])
|
||||||
def get_announces():
|
def save_layout():
|
||||||
return jsonify(load_announces())
|
# Sauvegarde l'ordre et la colonne après un drag & drop
|
||||||
|
|
||||||
@app.route("/api/annonces", methods=["POST"])
|
|
||||||
def create_annonce():
|
|
||||||
user = session.get("user")
|
user = session.get("user")
|
||||||
if not user or "/admin" not in user.get("groups", []):
|
if not user or "/admin" not in user.get("groups", []):
|
||||||
return jsonify({"error": "unauthorized"}), 403
|
return jsonify({"error": "Unauthorized"}), 403
|
||||||
|
|
||||||
|
layout_data = request.json # Liste de {id, column, position}
|
||||||
|
|
||||||
|
for item in layout_data:
|
||||||
|
block = Block.query.get(item['id'])
|
||||||
|
if block:
|
||||||
|
block.column_name = item['column']
|
||||||
|
block.position = item['position']
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify({"status": "saved"})
|
||||||
|
|
||||||
|
@app.route('/api/block/add', methods=['POST'])
|
||||||
|
def add_block():
|
||||||
|
user = session.get("user")
|
||||||
|
if not user or "/admin" not in user.get("groups", []):
|
||||||
|
return jsonify({"error": "Unauthorized"}), 403
|
||||||
|
|
||||||
data = request.json
|
data = request.json
|
||||||
if not data.get("text"):
|
new_block = Block(
|
||||||
return jsonify({"error": "missing text"}), 400
|
block_type=data.get('type'),
|
||||||
|
column_name=data.get('column', 'center'),
|
||||||
|
position=99, # Ajoute à la fin par défaut
|
||||||
|
data=json.dumps(data.get('data', {}))
|
||||||
|
)
|
||||||
|
db.session.add(new_block)
|
||||||
|
db.session.commit()
|
||||||
|
return jsonify(new_block.to_dict())
|
||||||
|
|
||||||
announces = load_announces()
|
@app.route('/api/block/<int:block_id>', methods=['DELETE'])
|
||||||
new_announce = {
|
def delete_block(block_id):
|
||||||
"id": get_next_id(announces),
|
|
||||||
"text": data["text"],
|
|
||||||
"author": user.get("preferred_username", "admin")
|
|
||||||
}
|
|
||||||
announces.append(new_announce)
|
|
||||||
save_announces(announces)
|
|
||||||
return jsonify({"status": "ok", "announce": new_announce})
|
|
||||||
|
|
||||||
@app.route("/api/annonces/<int:annonce_id>", methods=["DELETE"])
|
|
||||||
def delete_annonce(annonce_id):
|
|
||||||
user = session.get("user")
|
user = session.get("user")
|
||||||
if not user or "/admin" not in user.get("groups", []):
|
if not user or "/admin" not in user.get("groups", []):
|
||||||
return jsonify({"error": "unauthorized"}), 403
|
return jsonify({"error": "Unauthorized"}), 403
|
||||||
announces = load_announces()
|
|
||||||
announces = [a for a in announces if a["id"] != annonce_id]
|
|
||||||
save_announces(announces)
|
|
||||||
return jsonify({"status": "deleted"})
|
|
||||||
|
|
||||||
@app.route("/api/annonces/<int:annonce_id>", methods=["PUT"])
|
block = Block.query.get(block_id)
|
||||||
def edit_annonce(annonce_id):
|
if block:
|
||||||
user = session.get("user")
|
db.session.delete(block)
|
||||||
if not user or "/admin" not in user.get("groups", []):
|
db.session.commit()
|
||||||
return jsonify({"error": "unauthorized"}), 403
|
return jsonify({"status": "deleted"})
|
||||||
data = request.json
|
return jsonify({"error": "not found"}), 404
|
||||||
announces = load_announces()
|
|
||||||
found = False
|
# --- API ANNONCES (Tes routes existantes) ---
|
||||||
for a in announces:
|
# ... (Colle ici tes routes /api/annonces existantes, elles sont très bien) ...
|
||||||
if a["id"] == annonce_id:
|
# ... (N'oublie pas la route /api/is_admin) ...
|
||||||
a["text"] = data.get("text", a["text"])
|
|
||||||
found = True
|
|
||||||
if not found:
|
|
||||||
return jsonify({"error": "not found"}), 404
|
|
||||||
save_announces(announces)
|
|
||||||
return jsonify({"status": "updated"})
|
|
||||||
|
|
||||||
@app.route("/api/is_admin")
|
@app.route("/api/is_admin")
|
||||||
def is_admin():
|
def is_admin():
|
||||||
user = session.get("user")
|
user = session.get("user")
|
||||||
|
# Sécurité : si pas de user, renvoie false
|
||||||
|
if not user: return jsonify({"admin": False})
|
||||||
return jsonify({"admin": "/admin" in user.get("groups", [])})
|
return jsonify({"admin": "/admin" in user.get("groups", [])})
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(host='0.0.0.0', debug=True)
|
app.run(host='0.0.0.0', debug=True)
|
||||||
65
web/init_db.py
Normal file
65
web/init_db.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
from app import app, db, Block
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Configuration des widgets actuels extraits de ton index.html
|
||||||
|
initial_blocks = [
|
||||||
|
# 1. Colonne GAUCHE : Le Planning
|
||||||
|
Block(
|
||||||
|
block_type='iframe',
|
||||||
|
column_name='left',
|
||||||
|
position=0,
|
||||||
|
data=json.dumps({
|
||||||
|
"title": "Planning / Agenda",
|
||||||
|
"url": "https://mattermost.ninolbt.com/boards/team/8xj6d4ukwigk7rznqi3w339x7e/b3kmbqfwd33dmdy9g9g3ezaoxza/vu4nuxhf73ircznrkrcgzonno8a",
|
||||||
|
"height": "100%",
|
||||||
|
"styleClass": "planning"
|
||||||
|
})
|
||||||
|
),
|
||||||
|
# 2. Colonne CENTRALE : Le Trello (Board)
|
||||||
|
Block(
|
||||||
|
block_type='iframe',
|
||||||
|
column_name='center',
|
||||||
|
position=0,
|
||||||
|
data=json.dumps({
|
||||||
|
"title": "",
|
||||||
|
"url": "https://mattermost.ninolbt.com/boards/team/8xj6d4ukwigk7rznqi3w339x7e/b3kmbqfwd33dmdy9g9g3ezaoxza/va5xp53m6spbi8qo6qnng7711me",
|
||||||
|
"height": "680",
|
||||||
|
"styleClass": "trello"
|
||||||
|
})
|
||||||
|
),
|
||||||
|
# 3. Colonne DROITE : Les boutons
|
||||||
|
Block(
|
||||||
|
block_type='buttons',
|
||||||
|
column_name='right',
|
||||||
|
position=0,
|
||||||
|
data=json.dumps({
|
||||||
|
"links": [
|
||||||
|
{"label": "Projet", "url": "#"},
|
||||||
|
{"label": "GDD Global", "url": "#"},
|
||||||
|
{"label": "Bible 3D Art", "url": "#"},
|
||||||
|
{"label": "Bible GameDev", "url": "#"},
|
||||||
|
{"label": "Réglement du HUB", "url": "#"}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
def init_data():
|
||||||
|
with app.app_context():
|
||||||
|
# Crée les tables si elles n'existent pas encore
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
# Vérifie si la DB est déjà remplie pour éviter les doublons
|
||||||
|
if Block.query.first():
|
||||||
|
print("La base de données contient déjà des blocs.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("Injection des widgets par défaut...")
|
||||||
|
for block in initial_blocks:
|
||||||
|
db.session.add(block)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
print("Terminé ! Tes widgets sont en base de données.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
init_data()
|
||||||
@@ -1,153 +1,155 @@
|
|||||||
|
// ... (Garde tout ton code existant pour le Dark mode, Menu, Contact, etc.) ...
|
||||||
|
|
||||||
// mode dark pour les gens qui ne sortent pas de chez eux voir la lumiere du jours
|
// --- GESTION DU LAYOUT DYNAMIQUE ---
|
||||||
document.getElementById("toggle-darkmode").onclick = () => {
|
|
||||||
document.body.classList.toggle("dark");
|
|
||||||
};
|
|
||||||
|
|
||||||
// profil
|
|
||||||
document.querySelector(".profile-menu").addEventListener("click", (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
document.querySelector(".profile-menu").classList.toggle("active");
|
|
||||||
});
|
|
||||||
|
|
||||||
// notif
|
|
||||||
document.querySelector(".notification").addEventListener("click", (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
document.querySelector(".notification").classList.toggle("active");
|
|
||||||
});
|
|
||||||
|
|
||||||
// syteme contact
|
|
||||||
const contactOptions = document.querySelectorAll('.contact-option:not(.expandable)');
|
|
||||||
const contactModal = document.getElementById('contactModal');
|
|
||||||
const recipientName = document.getElementById('recipient-name');
|
|
||||||
const cancelBtn = document.getElementById('cancel-message');
|
|
||||||
const sendBtn = document.getElementById('send-message');
|
|
||||||
const dropdown = document.querySelector('.dropdown');
|
|
||||||
const nestedDropdowns = document.querySelectorAll('.nested-dropdown');
|
|
||||||
|
|
||||||
//menu princ
|
|
||||||
dropdown.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
const content = dropdown.querySelector('.dropdown-content');
|
|
||||||
content.style.display = content.style.display === 'block' ? 'none' : 'block';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Gestion des sous-menus
|
|
||||||
nestedDropdowns.forEach(dropdown => {
|
|
||||||
dropdown.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
const nestedContent = dropdown.querySelector('.nested-content');
|
|
||||||
nestedContent.style.display = nestedContent.style.display === 'block' ? 'none' : 'block';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fermer les menus quand on clique ailleurs
|
|
||||||
document.addEventListener('click', () => {
|
|
||||||
document.querySelector('.dropdown-content').style.display = 'none';
|
|
||||||
document.querySelectorAll('.nested-content').forEach(el => {
|
|
||||||
el.style.display = 'none';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
contactOptions.forEach(option => {
|
|
||||||
option.addEventListener('click', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (option.dataset.role) {
|
|
||||||
recipientName.textContent = option.dataset.role;
|
|
||||||
contactModal.style.display = 'block';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelector('.close-modal').addEventListener('click', () => {
|
|
||||||
contactModal.style.display = 'none';
|
|
||||||
});
|
|
||||||
|
|
||||||
cancelBtn.addEventListener('click', () => {
|
|
||||||
contactModal.style.display = 'none';
|
|
||||||
});
|
|
||||||
|
|
||||||
sendBtn.addEventListener('click', () => {
|
|
||||||
const message = document.getElementById('message-content').value;
|
|
||||||
if (message.trim() !== '') {
|
|
||||||
contactModal.style.display = 'none';
|
|
||||||
document.getElementById('message-content').value = '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fermer la modale si on clique en dehors
|
|
||||||
window.addEventListener('click', (e) => {
|
|
||||||
if (e.target === contactModal) {
|
|
||||||
contactModal.style.display = 'none';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Gestion des annonces
|
let isPageAdmin = false;
|
||||||
|
|
||||||
let isAdmin = false;
|
// 1. Vérification Admin et Initialisation
|
||||||
|
fetch("/api/is_admin")
|
||||||
fetch("/api/is_admin")
|
.then(res => res.json())
|
||||||
.then(res => res.json())
|
.then(data => {
|
||||||
.then(data => {
|
isPageAdmin = data.admin;
|
||||||
isAdmin = data.admin;
|
if (isPageAdmin) {
|
||||||
if (isAdmin) {
|
document.getElementById("admin-panel").style.display = "block";
|
||||||
document.getElementById("admin-tools").style.display = "block";
|
document.body.classList.add('admin-active');
|
||||||
}
|
initDragAndDrop();
|
||||||
});
|
|
||||||
|
// Afficher aussi les outils d'annonces si tu veux garder ça
|
||||||
fetch("/api/annonces")
|
const tools = document.getElementById("admin-tools");
|
||||||
.then(res => res.json())
|
if(tools) tools.style.display = "block";
|
||||||
.then(data => {
|
|
||||||
const container = document.getElementById("annonces-container");
|
|
||||||
container.innerHTML = "";
|
|
||||||
|
|
||||||
data.forEach(item => {
|
|
||||||
const div = document.createElement("div");
|
|
||||||
div.className = "postit";
|
|
||||||
div.dataset.id = item.id;
|
|
||||||
div.innerHTML = `
|
|
||||||
<span class="annonce-text">${item.text}</span>
|
|
||||||
<br><small>${item.author}</small>
|
|
||||||
`;
|
|
||||||
|
|
||||||
if (isAdmin) {
|
|
||||||
const editBtn = document.createElement("button");
|
|
||||||
editBtn.textContent = "✏️";
|
|
||||||
editBtn.onclick = () => editAnnonce(item.id, item.text);
|
|
||||||
div.appendChild(editBtn);
|
|
||||||
|
|
||||||
const delBtn = document.createElement("button");
|
|
||||||
delBtn.textContent = "🗑️";
|
|
||||||
delBtn.onclick = () => deleteAnnonce(item.id);
|
|
||||||
div.appendChild(delBtn);
|
|
||||||
}
|
|
||||||
|
|
||||||
container.appendChild(div);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function submitAnnonce() {
|
|
||||||
const txt = document.getElementById("annonce-text").value;
|
|
||||||
fetch("/api/annonces", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ text: txt })
|
|
||||||
}).then(() => location.reload());
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteAnnonce(id) {
|
|
||||||
fetch(`/api/annonces/${id}`, {
|
|
||||||
method: "DELETE"
|
|
||||||
}).then(() => location.reload());
|
|
||||||
}
|
|
||||||
|
|
||||||
function editAnnonce(id, oldText) {
|
|
||||||
const newText = prompt("Modifier l'annonce :", oldText);
|
|
||||||
if (newText && newText !== oldText) {
|
|
||||||
fetch(`/api/annonces/${id}`, {
|
|
||||||
method: "PUT",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ text: newText })
|
|
||||||
}).then(() => location.reload());
|
|
||||||
}
|
}
|
||||||
}
|
loadLayout(); // Charger les blocs
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Chargement des blocs depuis l'API
|
||||||
|
function loadLayout() {
|
||||||
|
fetch('/api/layout')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(blocks => {
|
||||||
|
// Vider les colonnes
|
||||||
|
['left', 'center', 'right'].forEach(id => document.getElementById(id).innerHTML = '');
|
||||||
|
|
||||||
|
// Remettre le titre au centre (optionnel si tu veux qu'il soit un bloc aussi)
|
||||||
|
document.getElementById('center').innerHTML = '<h1 class="main-title">Gesthub</h1>';
|
||||||
|
|
||||||
|
blocks.forEach(block => {
|
||||||
|
const col = document.getElementById(block.column);
|
||||||
|
if (col) {
|
||||||
|
const el = createBlockElement(block);
|
||||||
|
col.appendChild(el);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Création du HTML d'un bloc
|
||||||
|
function createBlockElement(block) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'draggable-item';
|
||||||
|
div.dataset.id = block.id;
|
||||||
|
div.style.marginBottom = "1rem";
|
||||||
|
div.style.background = block.type === 'buttons' ? 'transparent' : '';
|
||||||
|
|
||||||
|
// Barre de drag (visible seulement en admin via CSS)
|
||||||
|
let html = `<div class="drag-handle">:: Deplacer :: <span onclick="deleteBlock(${block.id})" style="color:red;cursor:pointer;float:right">✖</span></div>`;
|
||||||
|
|
||||||
|
if (block.type === 'iframe') {
|
||||||
|
html += `
|
||||||
|
<div class="trello" style="padding: 10px; height: auto;">
|
||||||
|
${block.data.title ? `<h3>${block.data.title}</h3>` : ''}
|
||||||
|
<iframe src="${block.data.url}" width="100%" height="${block.data.height || 400}" frameborder="0"></iframe>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (block.type === 'buttons') {
|
||||||
|
// block.data.links doit être un tableau d'objets {label, url}
|
||||||
|
let btns = '';
|
||||||
|
if(block.data.links) {
|
||||||
|
block.data.links.forEach(link => {
|
||||||
|
btns += `<button class="simple-btn" onclick="window.open('${link.url}', '_blank')">${link.label}</button>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
html += `<div class="button-container">${btns}</div>`;
|
||||||
|
} else if (block.type === 'html') {
|
||||||
|
html += `<div class="postit">${block.data.content}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.innerHTML = html;
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Initialisation Drag & Drop (SortableJS)
|
||||||
|
function initDragAndDrop() {
|
||||||
|
const containers = [document.getElementById('left'), document.getElementById('center'), document.getElementById('right')];
|
||||||
|
|
||||||
|
containers.forEach(container => {
|
||||||
|
new Sortable(container, {
|
||||||
|
group: 'shared', // Permet de déplacer entre les colonnes
|
||||||
|
handle: '.drag-handle', // On ne peut attraper que par la poignée
|
||||||
|
animation: 150,
|
||||||
|
onEnd: function (evt) {
|
||||||
|
saveLayoutPosition();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Sauvegarde de la position
|
||||||
|
function saveLayoutPosition() {
|
||||||
|
const layout = [];
|
||||||
|
['left', 'center', 'right'].forEach(colId => {
|
||||||
|
const col = document.getElementById(colId);
|
||||||
|
const items = col.querySelectorAll('.draggable-item');
|
||||||
|
items.forEach((item, index) => {
|
||||||
|
layout.push({
|
||||||
|
id: item.dataset.id,
|
||||||
|
column: colId,
|
||||||
|
position: index
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
fetch('/api/layout/save', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(layout)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Ajouter un Widget (Logique simple pour l'instant)
|
||||||
|
window.addNewWidget = function(type) {
|
||||||
|
let data = {};
|
||||||
|
|
||||||
|
if (type === 'iframe') {
|
||||||
|
const url = prompt("URL de l'iframe (Mattermost/Trello) :");
|
||||||
|
const height = prompt("Hauteur (ex: 400) :", "400");
|
||||||
|
if (!url) return;
|
||||||
|
data = { url: url, height: height, title: "Nouveau Widget" };
|
||||||
|
} else if (type === 'buttons') {
|
||||||
|
// Pour faire simple, on ajoute un bouton par défaut, tu pourras éditer plus tard
|
||||||
|
data = {
|
||||||
|
links: [
|
||||||
|
{ label: "Nouveau Bouton", url: "https://google.com" }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
} else if (type === 'html') {
|
||||||
|
const text = prompt("Texte du Post-it :");
|
||||||
|
data = { content: text };
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/api/block/add', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ type: type, column: 'center', data: data })
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(block => {
|
||||||
|
location.reload(); // Recharge pour afficher le nouveau bloc proprement
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.deleteBlock = function(id) {
|
||||||
|
if(confirm("Supprimer ce bloc ?")) {
|
||||||
|
fetch(`/api/block/${id}`, { method: 'DELETE' })
|
||||||
|
.then(() => location.reload());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,114 +6,49 @@
|
|||||||
<title>Gesthub</title>
|
<title>Gesthub</title>
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='assets/css/index.css') }}" />
|
<link rel="stylesheet" href="{{ url_for('static', filename='assets/css/index.css') }}" />
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" />
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" />
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.15.0/Sortable.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<button id="toggle-darkmode">🌓</button>
|
<button id="toggle-darkmode">🌓</button>
|
||||||
|
|
||||||
<header>
|
|
||||||
<div class="notification">
|
|
||||||
<span class="bell">🔔</span>
|
|
||||||
<div class="notification-dropdown">
|
|
||||||
<p>📌 Rendu le 03.02.2026</p>
|
|
||||||
<p>📝 Nouvelle tâche ajoutée</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="nav-link">
|
|
||||||
<a href="https://discord.com" target="_blank">Accéder au serveur Discord</a>
|
|
||||||
|
|
||||||
<a href="https://mattermost.ninolbt.com/boards/team/8xj6d4ukwigk7rznqi3w339x7e/b3kmbqfwd33dmdy9g9g3ezaoxza/va5xp53m6spbi8qo6qnng7711me" target="_blank">Board</a>
|
|
||||||
|
|
||||||
<a href="https://mattermost.ninolbt.com/boards/team/8xj6d4ukwigk7rznqi3w339x7e/b3kmbqfwd33dmdy9g9g3ezaoxza/va5xp53m6spbi8qo6qnng7711me" target="_blank">Board</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="nav-link dropdown">
|
|
||||||
<span>Contacter</span>
|
|
||||||
<div class="dropdown-content">
|
|
||||||
<a href="#" class="contact-option" data-role="LEAD HUB">LEAD HUB</a>
|
|
||||||
<a href="#" class="contact-option" data-role="CO LEAD HUB">CO LEAD HUB</a>
|
|
||||||
<div class="nested-dropdown">
|
|
||||||
<a href="#" class="contact-option expandable">CHEF PROJET <i class="fa-solid fa-caret-down"></i></a>
|
|
||||||
<div class="nested-content">
|
|
||||||
<a href="#" class="contact-option" data-role="Y. Conception">Y. Conception</a>
|
|
||||||
<a href="#" class="contact-option" data-role="Y.Comm">Y.Comm</a>
|
|
||||||
<a href="#" class="contact-option" data-role="Y.dev">Y.dev</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<a href="https://mattermost.ninolbt.com/gesthub/channels/town-square" target="_blank">Chat</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="profile-menu">
|
|
||||||
<span id="profile-name">{{ user['preferred_username'] }} <i class="fa-solid fa-caret-down"></i></span>
|
|
||||||
<div class="profile-dropdown">
|
|
||||||
<!-- <a class="simple-btn" href="" type="button">Profil</a> -->
|
|
||||||
<a class="simple-btn" href="/logout" type="button">Déconnexion</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- contact -->
|
|
||||||
<div id="contactModal" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<span class="close-modal">×</span>
|
|
||||||
<h3 id="modal-recipient">Envoyer un message à <span id="recipient-name"></span></h3>
|
|
||||||
<textarea id="message-content" placeholder="Écrivez votre message ici..."></textarea>
|
|
||||||
<div class="modal-buttons">
|
|
||||||
<button id="cancel-message" class="btn-cancel">Annuler</button>
|
|
||||||
<button id="send-message" class="btn-send">Envoyer</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<!-- clonne gauche pr widget planning -->
|
<aside class="left-column" id="left">
|
||||||
<aside class="left-column">
|
</aside>
|
||||||
<div class="planning">Planning / Agenda
|
|
||||||
<iframe
|
|
||||||
src="https://mattermost.ninolbt.com/boards/team/8xj6d4ukwigk7rznqi3w339x7e/b3kmbqfwd33dmdy9g9g3ezaoxza/vu4nuxhf73ircznrkrcgzonno8a"
|
|
||||||
style="width: 100%; height: 100%; border: none;"
|
|
||||||
allowfullscreen
|
|
||||||
></iframe>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<!-- Centre pr widget trello -->
|
<section class="center-column" id="center">
|
||||||
<section class="center-column">
|
<h1 class="main-title">Gesthub</h1>
|
||||||
<h1 class="main-title">Gesthub</h1>
|
</section>
|
||||||
<div class="trello">
|
|
||||||
<iframe src="https://mattermost.ninolbt.com/boards/team/8xj6d4ukwigk7rznqi3w339x7e/b3kmbqfwd33dmdy9g9g3ezaoxza/va5xp53m6spbi8qo6qnng7711me" width="100%" height="680" frameborder="0"></iframe>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- colonne droite pr partie info user je vais changer ca dans cet emplacement
|
<aside class="right-column" id="right">
|
||||||
je vais metrre des boutons et autres features comme des doc etc ou jsp
|
</aside>
|
||||||
je m en occupe -->
|
</main>
|
||||||
<!-- Colonne droite avec boutons simples -->
|
|
||||||
<aside class="right-column">
|
|
||||||
<div class="button-container">
|
|
||||||
<button class="simple-btn">Projet</button>
|
|
||||||
<button class="simple-btn">GDD Global</button>
|
|
||||||
<button class="simple-btn">Bible 3D Art</button>
|
|
||||||
<button class="simple-btn">Bible GameDev</button>
|
|
||||||
<button class="simple-btn">Réglement du HUB</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Bloc annonces admin
|
<div id="admin-panel" style="display:none;">
|
||||||
<div id="admin-announcements" style="margin-top: 2rem;">
|
<div class="admin-toolbar">
|
||||||
<h3 style="margin-bottom: 1rem;">📌 Annonces</h3>
|
<span>🛠 Mode Édition</span>
|
||||||
<div id="annonces-container"></div>
|
<button onclick="addNewWidget('iframe')">+ Iframe</button>
|
||||||
|
<button onclick="addNewWidget('buttons')">+ Boutons</button>
|
||||||
Formulaire admin caché par défaut-
|
<button onclick="addNewWidget('html')">+ Texte/HTML</button>
|
||||||
<div id="admin-tools" style="display: none; margin-top: 1rem;">
|
|
||||||
<textarea id="annonce-text" placeholder="Nouvelle annonce..." style="width: 100%; padding: 0.5rem; margin-bottom: 0.5rem;"></textarea>
|
|
||||||
<button onclick="submitAnnonce()" style="width: 100%;">📤 Publier</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</aside> -->
|
|
||||||
|
|
||||||
</main>
|
<style>
|
||||||
|
/* Petit style rapide pour la toolbar admin */
|
||||||
|
#admin-panel {
|
||||||
|
position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
|
||||||
|
background: #222; padding: 10px 20px; border-radius: 30px;
|
||||||
|
box-shadow: 0 5px 15px rgba(0,0,0,0.3); z-index: 999;
|
||||||
|
}
|
||||||
|
.admin-toolbar button {
|
||||||
|
background: #4CAF50; border: none; color: white; padding: 5px 10px;
|
||||||
|
border-radius: 5px; cursor: pointer; margin-left: 10px;
|
||||||
|
}
|
||||||
|
.drag-handle { cursor: move; padding: 5px; background: rgba(0,0,0,0.1); text-align: center; color: #666; font-size: 12px; margin-bottom: 5px; display: none;}
|
||||||
|
.admin-active .drag-handle { display: block; }
|
||||||
|
.admin-active .draggable-item { border: 2px dashed #ccc; min-height: 50px; }
|
||||||
|
</style>
|
||||||
|
|
||||||
<script src="{{ url_for('static', filename='assets/js/index.js') }}"></script>
|
<script src="{{ url_for('static', filename='assets/js/index.js') }}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user