test redesign gesthub
This commit is contained in:
@@ -58,7 +58,7 @@ services:
|
||||
image: quay.io/keycloak/keycloak:22.0.5
|
||||
command:
|
||||
- start-dev
|
||||
- --hostname=keycloak.ninolbt.com
|
||||
- --hostname=localhost
|
||||
- --hostname-strict=false
|
||||
- --hostname-strict-https=false
|
||||
- --proxy=edge
|
||||
|
||||
141
web/app.py
141
web/app.py
@@ -1,29 +1,51 @@
|
||||
import os
|
||||
import uuid
|
||||
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 authlib.integrations.flask_client import OAuth
|
||||
|
||||
app = Flask(__name__)
|
||||
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_TRACK_MODIFICATIONS'] = False
|
||||
app.secret_key = os.environ.get("SECRET_KEY", "dev-key")
|
||||
|
||||
db = SQLAlchemy(app)
|
||||
|
||||
# Configuration de Authlib
|
||||
oauth = OAuth(app)
|
||||
# --- MODELE DE DONNEES POUR LES BLOCS ---
|
||||
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(
|
||||
name='keycloak',
|
||||
client_id='flask-app',
|
||||
client_secret='T5G5jzCBiphnBNh9uuj0f6YNc9HrP8r4',
|
||||
server_metadata_url='https://keycloak.ninolbt.com/realms/gesthub/.well-known/openid-configuration',
|
||||
client_kwargs={
|
||||
'scope': 'openid profile email',
|
||||
}
|
||||
client_kwargs={'scope': 'openid profile email'}
|
||||
)
|
||||
|
||||
@app.route('/')
|
||||
@@ -31,7 +53,6 @@ def index():
|
||||
user = session.get('user')
|
||||
if user:
|
||||
return render_template('view/index.html', user=user)
|
||||
|
||||
return redirect(url_for('login'))
|
||||
|
||||
@app.route('/login')
|
||||
@@ -48,88 +69,84 @@ def auth():
|
||||
userinfo = keycloak.parse_id_token(token, nonce=nonce)
|
||||
session['user'] = userinfo
|
||||
session["id_token"] = token.get("id_token")
|
||||
app.logger.debug(f"User info: {userinfo}")
|
||||
return redirect('/')
|
||||
|
||||
@app.route("/logout")
|
||||
def logout():
|
||||
id_token = session.get("id_token")
|
||||
print("ID Token Hint:", id_token)
|
||||
session.clear()
|
||||
return redirect(
|
||||
f"https://keycloak.ninolbt.com/realms/gesthub/protocol/openid-connect/logout"
|
||||
f"?post_logout_redirect_uri=https://dashboard.ninolbt.com"
|
||||
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):
|
||||
with open(ANNOUNCE_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(announces, f, ensure_ascii=False, indent=2)
|
||||
# --- API LAYOUT (Gestion des Blocs) ---
|
||||
|
||||
def get_next_id(announces):
|
||||
return max((a.get("id", 0) for a in announces), default=0) + 1
|
||||
@app.route('/api/layout', methods=['GET'])
|
||||
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"])
|
||||
def get_announces():
|
||||
return jsonify(load_announces())
|
||||
|
||||
@app.route("/api/annonces", methods=["POST"])
|
||||
def create_annonce():
|
||||
@app.route('/api/layout/save', methods=['POST'])
|
||||
def save_layout():
|
||||
# Sauvegarde l'ordre et la colonne après un drag & drop
|
||||
user = session.get("user")
|
||||
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
|
||||
if not data.get("text"):
|
||||
return jsonify({"error": "missing text"}), 400
|
||||
new_block = Block(
|
||||
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()
|
||||
new_announce = {
|
||||
"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):
|
||||
@app.route('/api/block/<int:block_id>', methods=['DELETE'])
|
||||
def delete_block(block_id):
|
||||
user = session.get("user")
|
||||
if not user or "/admin" not in user.get("groups", []):
|
||||
return jsonify({"error": "unauthorized"}), 403
|
||||
announces = load_announces()
|
||||
announces = [a for a in announces if a["id"] != annonce_id]
|
||||
save_announces(announces)
|
||||
return jsonify({"error": "Unauthorized"}), 403
|
||||
|
||||
block = Block.query.get(block_id)
|
||||
if block:
|
||||
db.session.delete(block)
|
||||
db.session.commit()
|
||||
return jsonify({"status": "deleted"})
|
||||
|
||||
@app.route("/api/annonces/<int:annonce_id>", methods=["PUT"])
|
||||
def edit_annonce(annonce_id):
|
||||
user = session.get("user")
|
||||
if not user or "/admin" not in user.get("groups", []):
|
||||
return jsonify({"error": "unauthorized"}), 403
|
||||
data = request.json
|
||||
announces = load_announces()
|
||||
found = False
|
||||
for a in announces:
|
||||
if a["id"] == annonce_id:
|
||||
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"})
|
||||
|
||||
# --- API ANNONCES (Tes routes existantes) ---
|
||||
# ... (Colle ici tes routes /api/annonces existantes, elles sont très bien) ...
|
||||
# ... (N'oublie pas la route /api/is_admin) ...
|
||||
|
||||
@app.route("/api/is_admin")
|
||||
def is_admin():
|
||||
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", [])})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
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
|
||||
document.getElementById("toggle-darkmode").onclick = () => {
|
||||
document.body.classList.toggle("dark");
|
||||
};
|
||||
// --- GESTION DU LAYOUT DYNAMIQUE ---
|
||||
|
||||
// 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 isAdmin = false;
|
||||
let isPageAdmin = false;
|
||||
|
||||
// 1. Vérification Admin et Initialisation
|
||||
fetch("/api/is_admin")
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
isAdmin = data.admin;
|
||||
if (isAdmin) {
|
||||
document.getElementById("admin-tools").style.display = "block";
|
||||
isPageAdmin = data.admin;
|
||||
if (isPageAdmin) {
|
||||
document.getElementById("admin-panel").style.display = "block";
|
||||
document.body.classList.add('admin-active');
|
||||
initDragAndDrop();
|
||||
|
||||
// Afficher aussi les outils d'annonces si tu veux garder ça
|
||||
const tools = document.getElementById("admin-tools");
|
||||
if(tools) tools.style.display = "block";
|
||||
}
|
||||
loadLayout(); // Charger les blocs
|
||||
});
|
||||
|
||||
fetch("/api/annonces")
|
||||
// 2. Chargement des blocs depuis l'API
|
||||
function loadLayout() {
|
||||
fetch('/api/layout')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
const container = document.getElementById("annonces-container");
|
||||
container.innerHTML = "";
|
||||
.then(blocks => {
|
||||
// Vider les colonnes
|
||||
['left', 'center', 'right'].forEach(id => document.getElementById(id).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>
|
||||
// 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>
|
||||
`;
|
||||
|
||||
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);
|
||||
} 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>`;
|
||||
}
|
||||
|
||||
container.appendChild(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
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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());
|
||||
fetch('/api/layout/save', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(layout)
|
||||
});
|
||||
}
|
||||
|
||||
function deleteAnnonce(id) {
|
||||
fetch(`/api/annonces/${id}`, {
|
||||
method: "DELETE"
|
||||
}).then(() => location.reload());
|
||||
// 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 };
|
||||
}
|
||||
|
||||
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());
|
||||
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>
|
||||
<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" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.15.0/Sortable.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<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>
|
||||
<!-- clonne gauche pr widget planning -->
|
||||
<aside class="left-column">
|
||||
<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 class="left-column" id="left">
|
||||
</aside>
|
||||
|
||||
<!-- Centre pr widget trello -->
|
||||
<section class="center-column">
|
||||
<section class="center-column" id="center">
|
||||
<h1 class="main-title">Gesthub</h1>
|
||||
<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
|
||||
je vais metrre des boutons et autres features comme des doc etc ou jsp
|
||||
je m en occupe -->
|
||||
<!-- 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-announcements" style="margin-top: 2rem;">
|
||||
<h3 style="margin-bottom: 1rem;">📌 Annonces</h3>
|
||||
<div id="annonces-container"></div>
|
||||
|
||||
Formulaire admin caché par défaut-
|
||||
<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>
|
||||
</aside> -->
|
||||
|
||||
<aside class="right-column" id="right">
|
||||
</aside>
|
||||
</main>
|
||||
|
||||
<div id="admin-panel" style="display:none;">
|
||||
<div class="admin-toolbar">
|
||||
<span>🛠 Mode Édition</span>
|
||||
<button onclick="addNewWidget('iframe')">+ Iframe</button>
|
||||
<button onclick="addNewWidget('buttons')">+ Boutons</button>
|
||||
<button onclick="addNewWidget('html')">+ Texte/HTML</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user