Ajout de la gestion des jeux gratuits et des invitations Discord. Création des modèles FreeGame et DiscordInvite, mise à jour de la base de données et des fichiers de configuration. Intégration de nouvelles fonctionnalités pour synchroniser et révoquer les invitations, ainsi que l'ajout d'un flux RSS pour les jeux gratuits. Amélioration de l'interface d'administration avec des statistiques et des options de gestion des serveurs.
This commit is contained in:
@@ -5,6 +5,17 @@
|
||||
<p>Configurez les tokens Discord, les notifications Humble Bundle et l'API Twitch.</p>
|
||||
|
||||
<h2>Discord</h2>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}" style="padding: 10px; margin: 10px 0; border-radius: 5px; {% if category == 'error' %}background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb;{% else %}background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb;{% endif %}">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form action="{{ url_for('updateConfiguration') }}" method="POST">
|
||||
<fieldset>
|
||||
<legend>API Discord</legend>
|
||||
@@ -43,6 +54,87 @@
|
||||
</small>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Auto-Role</legend>
|
||||
<label for="autorole_enable">
|
||||
<input type="checkbox" name="autorole_enable" {% if configuration.getValue('autorole_enable') %}checked="checked"{% endif %}>
|
||||
Attribuer automatiquement un rôle aux nouveaux membres
|
||||
</label>
|
||||
|
||||
<label for="autorole_role_id">Rôle à attribuer</label>
|
||||
{% if roles|length > 1 %}
|
||||
<div class="tabs tabs-autorole">
|
||||
{% for guild_data in roles %}
|
||||
<button type="button" class="tab-button-autorole" onclick="openTabAutorole(event, 'autorole-guild-{{guild_data.guild_id}}')" {% if loop.first %}id="defaultOpenAutorole"{% endif %}>
|
||||
{{ guild_data.guild_name }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% for guild_data in roles %}
|
||||
<div id="autorole-guild-{{guild_data.guild_id}}" class="tab-content-autorole" {% if not loop.first %}style="display: none;"{% endif %}>
|
||||
<select name="autorole_role_id">
|
||||
<option value="">-- Aucun rôle --</option>
|
||||
{% for role in guild_data.roles %}
|
||||
<option value="{{role.id}}" {% if configuration.getIntValue('autorole_role_id')==role.id %}selected="selected"{% endif %}>
|
||||
{% if role.color.value != 0 %}⬤{% else %}○{% endif %} {{role.name}}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<small>
|
||||
<strong>Note :</strong> Le bot doit avoir la permission "Gérer les rôles" et son rôle doit être positionné plus haut que le rôle à attribuer dans la hiérarchie des rôles.
|
||||
</small>
|
||||
|
||||
<script>
|
||||
function openTabAutorole(evt, tabName) {
|
||||
var i, tabcontent, tabbuttons;
|
||||
tabcontent = document.getElementsByClassName("tab-content-autorole");
|
||||
for (i = 0; i < tabcontent.length; i++) {
|
||||
tabcontent[i].style.display = "none";
|
||||
}
|
||||
tabbuttons = document.getElementsByClassName("tab-button-autorole");
|
||||
for (i = 0; i < tabbuttons.length; i++) {
|
||||
tabbuttons[i].className = tabbuttons[i].className.replace(" active", "");
|
||||
}
|
||||
document.getElementById(tabName).style.display = "block";
|
||||
evt.currentTarget.className += " active";
|
||||
}
|
||||
document.getElementById("defaultOpenAutorole")?.click();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.tabs-autorole {
|
||||
overflow: hidden;
|
||||
border-bottom: 2px solid #ccc;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.tab-button-autorole {
|
||||
background-color: #f1f1f1;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
padding: 10px 20px;
|
||||
transition: 0.3s;
|
||||
font-size: 14px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
.tab-button-autorole:hover {
|
||||
background-color: #ddd;
|
||||
}
|
||||
.tab-button-autorole.active {
|
||||
background-color: #ccc;
|
||||
font-weight: bold;
|
||||
}
|
||||
.tab-content-autorole {
|
||||
animation: fadeEffect 0.3s;
|
||||
}
|
||||
</style>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Messages de départ</legend>
|
||||
<label for="leave_enable">
|
||||
@@ -237,4 +329,42 @@
|
||||
|
||||
<input type="Submit" value="Enregistrer la configuration Humble Bundle">
|
||||
</form>
|
||||
|
||||
<h2>⚠️ Gestion des serveurs</h2>
|
||||
<form action="{{ url_for('leaveGuild') }}" method="POST" onsubmit="return confirmLeave();">
|
||||
<fieldset>
|
||||
<legend>Quitter un serveur Discord</legend>
|
||||
<p style="color: #856404; background-color: #fff3cd; border: 1px solid #ffeeba; padding: 10px; border-radius: 5px;">
|
||||
<strong>⚠️ Attention :</strong> Cette action est irréversible ! Le bot quittera définitivement le serveur sélectionné.
|
||||
Pour rejoindre à nouveau, vous devrez ré-inviter le bot.
|
||||
</p>
|
||||
|
||||
{% if guilds and guilds|length > 0 %}
|
||||
<label for="guild_id">Sélectionner un serveur à quitter</label>
|
||||
<select name="guild_id" id="guild_id" required>
|
||||
<option value="">-- Choisir un serveur --</option>
|
||||
{% for guild in guilds %}
|
||||
<option value="{{ guild.id }}">{{ guild.name }} ({{ guild.member_count }} membres)</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label for="confirm_leave" style="margin-top: 15px;">
|
||||
<input type="checkbox" name="confirm_leave" id="confirm_leave" value="1" required>
|
||||
Je confirme vouloir faire quitter le bot de ce serveur
|
||||
</label>
|
||||
|
||||
<input type="submit" value="🚪 Quitter le serveur" style="background-color: #dc3545; border-color: #dc3545;" />
|
||||
{% else %}
|
||||
<p>Aucun serveur Discord connecté ou le bot n'est pas encore démarré.</p>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
function confirmLeave() {
|
||||
var select = document.getElementById('guild_id');
|
||||
var serverName = select.options[select.selectedIndex].text;
|
||||
return confirm('Êtes-vous VRAIMENT sûr de vouloir quitter le serveur "' + serverName + '" ?\n\nCette action est IRRÉVERSIBLE !');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,381 @@
|
||||
{% extends "template.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1>🎮 Jeux Gratuits</h1>
|
||||
<p>Gérez les notifications de jeux gratuits via le flux RSS LootScraper. Activez le module, choisissez les sources à suivre et décidez quels jeux partager avec votre communauté.</p>
|
||||
|
||||
<h2>Configuration</h2>
|
||||
<form action="{{ url_for('updateFreeGamesConfig') }}" method="POST">
|
||||
<fieldset>
|
||||
<legend>Paramètres généraux</legend>
|
||||
|
||||
<label for="freegames_enable">
|
||||
<input type="checkbox" name="freegames_enable" id="freegames_enable" {% if configuration.getValue('freegames_enable') %}checked="checked"{% endif %}>
|
||||
Activer le module Jeux Gratuits
|
||||
</label>
|
||||
|
||||
<label for="freegames_auto_notify">
|
||||
<input type="checkbox" name="freegames_auto_notify" id="freegames_auto_notify" {% if configuration.getValue('freegames_auto_notify') %}checked="checked"{% endif %}>
|
||||
Notification automatique des nouveaux jeux
|
||||
</label>
|
||||
<small>Si activé, les nouveaux jeux des sources sélectionnées seront automatiquement partagés</small>
|
||||
|
||||
<label for="freegames_channel">Canal de notification</label>
|
||||
<select name="freegames_channel" id="freegames_channel">
|
||||
<option value="">-- Sélectionner un canal --</option>
|
||||
{% for channel in channels %}
|
||||
<option value="{{channel.id}}" {% if configuration.getIntValue('freegames_channel')==channel.id %}selected="selected"{% endif %}>
|
||||
{{channel.name}}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Mentions</legend>
|
||||
|
||||
<label for="freegames_mention_type">Type de mention</label>
|
||||
<select name="freegames_mention_type" id="freegames_mention_type" onchange="toggleRoleSelect()">
|
||||
<option value="none" {% if configuration.getValue('freegames_mention_type') == 'none' or not configuration.getValue('freegames_mention_type') %}selected{% endif %}>Aucune mention</option>
|
||||
<option value="here" {% if configuration.getValue('freegames_mention_type') == 'here' %}selected{% endif %}>@here</option>
|
||||
<option value="everyone" {% if configuration.getValue('freegames_mention_type') == 'everyone' %}selected{% endif %}>@everyone</option>
|
||||
<option value="role" {% if configuration.getValue('freegames_mention_type') == 'role' %}selected{% endif %}>Rôle spécifique</option>
|
||||
</select>
|
||||
|
||||
<div id="role-select-container" style="{% if configuration.getValue('freegames_mention_type') != 'role' %}display: none;{% endif %}">
|
||||
<label for="freegames_mention_role">Rôle à mentionner</label>
|
||||
{% for guild_data in roles %}
|
||||
<select name="freegames_mention_role" id="freegames_mention_role">
|
||||
<option value="">-- Sélectionner un rôle --</option>
|
||||
{% for role in guild_data.roles %}
|
||||
<option value="{{role.id}}" {% if configuration.getIntValue('freegames_mention_role')==role.id %}selected="selected"{% endif %}>
|
||||
{% if role.color.value != 0 %}●{% else %}○{% endif %} {{role.name}}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Sources à suivre</legend>
|
||||
<small>Sélectionnez les plateformes dont vous souhaitez recevoir les offres. Si aucune n'est sélectionnée, toutes les sources seront incluses.</small>
|
||||
|
||||
{% set enabled_sources = (configuration.getValue('freegames_sources') or '').split(',') %}
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; margin-top: 15px;">
|
||||
{% for key, name in sources.items() %}
|
||||
<label style="display: flex; align-items: center; gap: 8px;">
|
||||
<input type="checkbox" name="freegames_sources" value="{{key}}" {% if key in enabled_sources or not configuration.getValue('freegames_sources') %}checked{% endif %}>
|
||||
{{name}}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<input type="submit" value="💾 Enregistrer la configuration">
|
||||
</form>
|
||||
|
||||
<h2>Jeux disponibles <span class="badge">{{pending_count}} en attente</span></h2>
|
||||
|
||||
<div style="margin-bottom: 20px;">
|
||||
<button type="button" onclick="refreshGames()" class="btn-secondary">🔄 Rafraîchir le flux RSS</button>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 15px;">
|
||||
<label for="filter-source">Filtrer par source :</label>
|
||||
<select id="filter-source" onchange="filterGames()">
|
||||
<option value="all">Toutes les sources</option>
|
||||
{% for key, name in sources.items() %}
|
||||
<option value="{{name}}">{{name}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label for="filter-status" style="margin-left: 20px;">Filtrer par statut :</label>
|
||||
<select id="filter-status" onchange="filterGames()">
|
||||
<option value="all">Tous</option>
|
||||
<option value="pending">En attente</option>
|
||||
<option value="notified">Notifiés</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="games-list">
|
||||
{% for game in games %}
|
||||
<div class="game-card" data-source="{{game.source}}" data-status="{% if game.notified %}notified{% else %}pending{% endif %}">
|
||||
<div class="game-header">
|
||||
{% if game.image_url %}
|
||||
<img src="{{game.image_url}}" alt="{{game.title}}" class="game-thumb">
|
||||
{% else %}
|
||||
<div class="game-thumb-placeholder">🎮</div>
|
||||
{% endif %}
|
||||
<div class="game-info">
|
||||
<h3>{{game.title}}</h3>
|
||||
<span class="source-badge">{{game.source}}</span>
|
||||
{% if game.notified %}
|
||||
<span class="status-badge notified">✓ Notifié</span>
|
||||
{% else %}
|
||||
<span class="status-badge pending">⏳ En attente</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if game.description %}
|
||||
<p class="game-description">{{game.description[:150]}}{% if game.description|length > 150 %}...{% endif %}</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="game-meta">
|
||||
{% if game.valid_to %}
|
||||
<span title="Date de fin">⏰ Jusqu'au {{game.valid_to.strftime('%d/%m/%Y')}}</span>
|
||||
{% endif %}
|
||||
<a href="{{game.url}}" target="_blank" rel="noopener">🔗 Voir l'offre</a>
|
||||
</div>
|
||||
|
||||
<div class="game-actions">
|
||||
{% if not game.notified %}
|
||||
<button type="button" onclick="notifyGame({{game.id}}, this)" class="btn-primary">📢 Notifier</button>
|
||||
<button type="button" onclick="markNotified({{game.id}}, this)" class="btn-secondary">✓ Marquer comme notifié</button>
|
||||
{% else %}
|
||||
<button type="button" onclick="resetGame({{game.id}}, this)" class="btn-secondary">↩️ Réinitialiser</button>
|
||||
{% endif %}
|
||||
<button type="button" onclick="deleteGame({{game.id}}, this)" class="btn-danger">🗑️ Supprimer</button>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>Aucun jeu gratuit trouvé. Cliquez sur "Rafraîchir le flux RSS" pour récupérer les dernières offres.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.badge {
|
||||
background: #5865F2;
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.games-list {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.game-card {
|
||||
background: var(--color-bg-secondary, #f5f5f5);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
border: 1px solid var(--color-border, #ddd);
|
||||
}
|
||||
|
||||
.game-card[data-status="notified"] {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.game-header {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.game-thumb {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.game-thumb-placeholder {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: var(--color-bg-tertiary, #e0e0e0);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.game-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.game-info h3 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.source-badge {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8em;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.status-badge.pending {
|
||||
background: #FF9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-badge.notified {
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.game-description {
|
||||
color: var(--color-text-secondary, #666);
|
||||
font-size: 0.9em;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.game-meta {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
font-size: 0.85em;
|
||||
color: var(--color-text-secondary, #666);
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.game-meta a {
|
||||
color: #5865F2;
|
||||
}
|
||||
|
||||
.game-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.game-actions button {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.game-actions button:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #5865F2;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #747F8D;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #ED4245;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function toggleRoleSelect() {
|
||||
const mentionType = document.getElementById('freegames_mention_type').value;
|
||||
const roleContainer = document.getElementById('role-select-container');
|
||||
roleContainer.style.display = mentionType === 'role' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function filterGames() {
|
||||
const sourceFilter = document.getElementById('filter-source').value;
|
||||
const statusFilter = document.getElementById('filter-status').value;
|
||||
const cards = document.querySelectorAll('.game-card');
|
||||
|
||||
cards.forEach(card => {
|
||||
const source = card.dataset.source;
|
||||
const status = card.dataset.status;
|
||||
|
||||
const sourceMatch = sourceFilter === 'all' || source === sourceFilter;
|
||||
const statusMatch = statusFilter === 'all' || status === statusFilter;
|
||||
|
||||
card.classList.toggle('hidden', !(sourceMatch && statusMatch));
|
||||
});
|
||||
}
|
||||
|
||||
function refreshGames() {
|
||||
fetch('/freegames/refresh', { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
alert(data.message);
|
||||
if (data.new_games > 0) {
|
||||
location.reload();
|
||||
}
|
||||
})
|
||||
.catch(e => alert('Erreur: ' + e));
|
||||
}
|
||||
|
||||
function notifyGame(id, btn) {
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⏳ Envoi...';
|
||||
|
||||
fetch('/freegames/notify/' + id, { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Erreur: ' + data.message);
|
||||
btn.disabled = false;
|
||||
btn.textContent = '📢 Notifier';
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
alert('Erreur: ' + e);
|
||||
btn.disabled = false;
|
||||
btn.textContent = '📢 Notifier';
|
||||
});
|
||||
}
|
||||
|
||||
function markNotified(id, btn) {
|
||||
fetch('/freegames/mark-notified/' + id, { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
}
|
||||
})
|
||||
.catch(e => alert('Erreur: ' + e));
|
||||
}
|
||||
|
||||
function resetGame(id, btn) {
|
||||
fetch('/freegames/reset/' + id, { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
}
|
||||
})
|
||||
.catch(e => alert('Erreur: ' + e));
|
||||
}
|
||||
|
||||
function deleteGame(id, btn) {
|
||||
if (!confirm('Supprimer ce jeu de la liste ?')) return;
|
||||
|
||||
fetch('/freegames/delete/' + id, { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
btn.closest('.game-card').remove();
|
||||
}
|
||||
})
|
||||
.catch(e => alert('Erreur: ' + e));
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
{% block content %}
|
||||
<h1>Humeurs de Mamie</h1>
|
||||
<p>Définissez les statuts Discord qui changeront automatiquement toutes les 10 minutes pour donner de la personnalité à votre bot.</p>
|
||||
|
||||
<div class="info-box">
|
||||
<h3>📝 Variables disponibles</h3>
|
||||
<p>Vous pouvez utiliser ces variables dans vos humeurs, elles seront remplacées automatiquement :</p>
|
||||
<ul>
|
||||
<li><code>{servers}</code> — Nombre de serveurs gérés par le bot</li>
|
||||
<li><code>{members}</code> — Nombre total de membres sur tous les serveurs</li>
|
||||
<li><code>{channels}</code> — Nombre total de salons sur tous les serveurs</li>
|
||||
</ul>
|
||||
<p><em>Exemple : "Je surveille {members} personnes sur {servers} serveurs 👀"</em></p>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
+158
-4
@@ -1,7 +1,161 @@
|
||||
{% extends "template.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Bienvenue sur l'interface d'administration de Mamie.</h1>
|
||||
<p>Nous devons définir ce que nous souhaitons afficher sur la page d'accueil. Peut-être l'historique des dernières
|
||||
modifications ? de la modération ?</p>
|
||||
{% endblock %}
|
||||
<h1>Tableau de bord</h1>
|
||||
<p>Bienvenue sur l'interface d'administration de Mamie Henriette.</p>
|
||||
|
||||
<!-- Services -->
|
||||
<section class="dashboard-section">
|
||||
<h2>🔌 Services</h2>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card {{ 'online' if stats.discord_connected else 'offline' }}">
|
||||
<div class="stat-icon">🤖</div>
|
||||
<div class="stat-content">
|
||||
<span class="stat-label">Discord</span>
|
||||
<span class="stat-value">{{ 'Connecté' if stats.discord_connected else 'Déconnecté' }}</span>
|
||||
{% if stats.discord_connected and stats.discord_bot_name %}
|
||||
<span class="stat-sub">{{ stats.discord_bot_name }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="stat-status {{ 'online' if stats.discord_connected else 'offline' }}"></div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card {{ 'online' if stats.twitch_connected else 'offline' }}">
|
||||
<div class="stat-icon">📺</div>
|
||||
<div class="stat-content">
|
||||
<span class="stat-label">Twitch</span>
|
||||
<span class="stat-value">{{ 'Connecté' if stats.twitch_connected else 'Déconnecté' }}</span>
|
||||
{% if stats.twitch_connected and stats.twitch_channel %}
|
||||
<span class="stat-sub">#{{ stats.twitch_channel }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="stat-status {{ 'online' if stats.twitch_connected else 'offline' }}"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Statistiques Discord -->
|
||||
{% if stats.discord_connected %}
|
||||
<section class="dashboard-section">
|
||||
<h2>📊 Statistiques Discord</h2>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">🏠</div>
|
||||
<div class="stat-content">
|
||||
<span class="stat-label">Serveurs</span>
|
||||
<span class="stat-value big">{{ stats.discord_guilds }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">👥</div>
|
||||
<div class="stat-content">
|
||||
<span class="stat-label">Membres</span>
|
||||
<span class="stat-value big">{{ stats.discord_members }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">💬</div>
|
||||
<div class="stat-content">
|
||||
<span class="stat-label">Salons</span>
|
||||
<span class="stat-value big">{{ stats.discord_channels }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Envoyer une annonce -->
|
||||
<section class="dashboard-section">
|
||||
<h2>📢 Envoyer une annonce</h2>
|
||||
<form id="announce-form" class="announce-form-inline">
|
||||
<div class="announce-row">
|
||||
<input type="text" id="message-input" name="message" placeholder="Votre message..." required>
|
||||
<select id="channel-select" name="channel_id" required>
|
||||
<option value="">Salon</option>
|
||||
{% for channel in channels %}
|
||||
<option value="{{ channel.id }}">#{{ channel.name }} ({{ channel.guild_name }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit" id="send-btn">
|
||||
<span class="btn-text">📨 Envoyer</span>
|
||||
<span class="btn-loading" style="display: none;">⏳</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="announce-result" class="announce-result" style="display: none;"></div>
|
||||
</form>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<!-- Fonctionnalités -->
|
||||
<section class="dashboard-section">
|
||||
<h2>⚡ Fonctionnalités</h2>
|
||||
<div class="cogs-grid">
|
||||
{% for cog_name, enabled in stats.cogs_enabled.items() %}
|
||||
<div class="cog-item {{ 'enabled' if enabled else 'disabled' }}">
|
||||
<span class="cog-status">{{ '✅' if enabled else '❌' }}</span>
|
||||
<span class="cog-name">{{ cog_name }}</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted">Aucune information disponible. Le bot Discord n'est peut-être pas encore connecté.</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% if stats.last_update %}
|
||||
<p class="last-update">Dernière mise à jour : {{ stats.last_update.strftime('%d/%m/%Y à %H:%M:%S') }}</p>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
document.getElementById('announce-form')?.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const btn = document.getElementById('send-btn');
|
||||
const btnText = btn.querySelector('.btn-text');
|
||||
const btnLoading = btn.querySelector('.btn-loading');
|
||||
const result = document.getElementById('announce-result');
|
||||
|
||||
const channelId = document.getElementById('channel-select').value;
|
||||
const message = document.getElementById('message-input').value;
|
||||
|
||||
// UI loading
|
||||
btn.disabled = true;
|
||||
btnText.style.display = 'none';
|
||||
btnLoading.style.display = 'inline';
|
||||
result.style.display = 'none';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/send-message', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
channel_id: channelId,
|
||||
message: message
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
result.style.display = 'block';
|
||||
if (data.success) {
|
||||
result.className = 'announce-result success';
|
||||
result.textContent = '✅ ' + data.message;
|
||||
document.getElementById('message-input').value = '';
|
||||
} else {
|
||||
result.className = 'announce-result error';
|
||||
result.textContent = '❌ ' + data.error;
|
||||
}
|
||||
} catch (error) {
|
||||
result.style.display = 'block';
|
||||
result.className = 'announce-result error';
|
||||
result.textContent = '❌ Erreur de connexion';
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btnText.style.display = 'inline';
|
||||
btnLoading.style.display = 'none';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -3,61 +3,175 @@
|
||||
{% block content %}
|
||||
<h1>Modération Discord</h1>
|
||||
|
||||
<p>
|
||||
Historique des actions de modération effectuées sur le serveur Discord.
|
||||
<p>Historique des actions de modération effectuées sur le serveur Discord. Le bot enregistre automatiquement les avertissements, exclusions et bannissements.</p>
|
||||
|
||||
Le bot enregistre automatiquement les avertissements, exclusions et bannissements.
|
||||
<!-- Navigation entre les sections -->
|
||||
<div class="moderation-nav">
|
||||
<a href="{{ url_for('moderation') }}" class="nav-btn {{ '' if show_invites else 'active' }}">📋 Événements de modération</a>
|
||||
<a href="{{ url_for('moderation_invitations') }}" class="nav-btn {{ 'active' if show_invites else '' }}">🔗 Invitations</a>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Commande</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>!averto @utilisateur raison</strong><br><small>Alias : !warn, !av, !avertissement</small></td>
|
||||
<td>Avertit un utilisateur et enregistre l'avertissement dans la base de données</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>!delaverto id</strong><br><small>Alias : !removewarn, !delwarn</small></td>
|
||||
<td>Retire un avertissement en utilisant son numéro d'ID</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>!warnings</strong> ou <strong>!warnings @utilisateur</strong><br><small>Alias : !listevent, !listwarn</small></td>
|
||||
<td>Affiche la liste des événements de modération (tous ou pour un utilisateur spécifique)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>!inspect @utilisateur</strong> ou <strong>!inspect id</strong></td>
|
||||
<td>Affiche des informations détaillées sur un utilisateur : création du compte, date d'arrivée, historique de modération</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>!kick @utilisateur raison</strong></td>
|
||||
<td>Expulse un utilisateur du serveur</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>!ban @utilisateur raison</strong></td>
|
||||
<td>Bannit définitivement un utilisateur du serveur</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>!unban discord_id</strong> ou <strong>!unban #sanction_id raison</strong></td>
|
||||
<td>Révoque le bannissement d'un utilisateur et lui envoie une invitation</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>!banlist</strong></td>
|
||||
<td>Affiche la liste des utilisateurs actuellement bannis du serveur</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>!aide</strong><br><small>Alias : !help</small></td>
|
||||
<td>Affiche l'aide avec toutes les commandes disponibles</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</p>
|
||||
<!-- Section Statistiques -->
|
||||
<div class="dashboard-section">
|
||||
<h2>📊 Statistiques</h2>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">📋</div>
|
||||
<div class="stat-content">
|
||||
<span class="stat-label">Total des sanctions</span>
|
||||
<span class="stat-value big">{{ stats.total }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">⏰</div>
|
||||
<div class="stat-content">
|
||||
<span class="stat-label">Dernières 24h</span>
|
||||
<span class="stat-value big">{{ stats.recent_24h }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">📅</div>
|
||||
<div class="stat-content">
|
||||
<span class="stat-label">7 derniers jours</span>
|
||||
<span class="stat-value big">{{ stats.recent_7d }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">📆</div>
|
||||
<div class="stat-content">
|
||||
<span class="stat-label">30 derniers jours</span>
|
||||
<span class="stat-value big">{{ stats.recent_30d }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section Types de sanctions et Top modérateurs -->
|
||||
<div class="moderation-insights">
|
||||
<div class="insight-card">
|
||||
<h3>🛡️ Top Modérateurs</h3>
|
||||
{% if stats.top_moderators %}
|
||||
<div class="leaderboard">
|
||||
{% for mod_name, count in stats.top_moderators %}
|
||||
<div class="leaderboard-item">
|
||||
<span class="rank">{{ loop.index }}</span>
|
||||
<span class="name">{{ mod_name }}</span>
|
||||
<span class="count">{{ count }} sanction{{ 's' if count > 1 else '' }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted">Aucune donnée disponible</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="insight-card">
|
||||
<h3>⚖️ Types de sanctions</h3>
|
||||
{% if stats.type_counts %}
|
||||
<div class="type-badges">
|
||||
{% for type_name, count in stats.type_counts.items() %}
|
||||
<div class="type-badge {{ type_name | lower }}">
|
||||
<span class="type-name">{{ type_name }}</span>
|
||||
<span class="type-count">{{ count }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted">Aucune donnée disponible</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="insight-card">
|
||||
<h3>⚠️ Utilisateurs les plus sanctionnés</h3>
|
||||
{% if stats.top_sanctioned %}
|
||||
<div class="leaderboard">
|
||||
{% for username, count in stats.top_sanctioned %}
|
||||
<div class="leaderboard-item warning">
|
||||
<span class="rank">{{ loop.index }}</span>
|
||||
<span class="name">{{ username }}</span>
|
||||
<span class="count">{{ count }} sanction{{ 's' if count > 1 else '' }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted">Aucune donnée disponible</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section Commandes (collapsible) -->
|
||||
<details class="commands-section">
|
||||
<summary>
|
||||
<span class="summary-icon">📖</span>
|
||||
<span>Commandes de modération disponibles</span>
|
||||
</summary>
|
||||
<div class="commands-grid">
|
||||
<div class="command-card">
|
||||
<div class="command-header">
|
||||
<code>!averto @utilisateur raison</code>
|
||||
</div>
|
||||
<p>Avertit un utilisateur et enregistre l'avertissement</p>
|
||||
<small>Alias : !warn, !av, !avertissement</small>
|
||||
</div>
|
||||
<div class="command-card">
|
||||
<div class="command-header">
|
||||
<code>!delaverto id</code>
|
||||
</div>
|
||||
<p>Retire un avertissement par son ID</p>
|
||||
<small>Alias : !removewarn, !delwarn</small>
|
||||
</div>
|
||||
<div class="command-card">
|
||||
<div class="command-header">
|
||||
<code>!warnings [@utilisateur]</code>
|
||||
</div>
|
||||
<p>Liste les événements de modération</p>
|
||||
<small>Alias : !listevent, !listwarn</small>
|
||||
</div>
|
||||
<div class="command-card">
|
||||
<div class="command-header">
|
||||
<code>!inspect @utilisateur</code>
|
||||
</div>
|
||||
<p>Informations détaillées sur un utilisateur</p>
|
||||
<small>Aussi : !inspect id</small>
|
||||
</div>
|
||||
<div class="command-card">
|
||||
<div class="command-header">
|
||||
<code>!kick @utilisateur raison</code>
|
||||
</div>
|
||||
<p>Expulse un utilisateur du serveur</p>
|
||||
</div>
|
||||
<div class="command-card">
|
||||
<div class="command-header">
|
||||
<code>!ban @utilisateur raison</code>
|
||||
</div>
|
||||
<p>Bannit définitivement un utilisateur</p>
|
||||
</div>
|
||||
<div class="command-card">
|
||||
<div class="command-header">
|
||||
<code>!unban discord_id</code>
|
||||
</div>
|
||||
<p>Révoque un bannissement</p>
|
||||
<small>Aussi : !unban #sanction_id raison</small>
|
||||
</div>
|
||||
<div class="command-card">
|
||||
<div class="command-header">
|
||||
<code>!banlist</code>
|
||||
</div>
|
||||
<p>Liste des utilisateurs bannis</p>
|
||||
</div>
|
||||
<div class="command-card">
|
||||
<div class="command-header">
|
||||
<code>!aide</code>
|
||||
</div>
|
||||
<p>Affiche toutes les commandes</p>
|
||||
<small>Alias : !help</small>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{% if not event %}
|
||||
<h2>Événements de modération</h2>
|
||||
<h2>📜 Historique des événements</h2>
|
||||
{% if events %}
|
||||
<table class="moderation">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -73,24 +187,31 @@
|
||||
<tbody>
|
||||
{% for mod_event in events %}
|
||||
<tr>
|
||||
<td>{{ mod_event.type }}</td>
|
||||
<td><span class="event-type {{ mod_event.type | lower if mod_event.type else '' }}">{{ mod_event.type }}</span></td>
|
||||
<td>{{ mod_event.username }}</td>
|
||||
<td>{{ mod_event.discord_id }}</td>
|
||||
<td><code>{{ mod_event.discord_id }}</code></td>
|
||||
<td>{{ mod_event.created_at.strftime('%d/%m/%Y %H:%M') if mod_event.created_at else 'N/A' }}</td>
|
||||
<td>{{ mod_event.reason }}</td>
|
||||
<td>{{ mod_event.staff_name }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('open_edit_moderation_event', event_id = mod_event.id) }}" class="icon">✐</a>
|
||||
<a href="{{ url_for('delete_moderation_event', event_id = mod_event.id) }}" onclick="return confirm('Êtes-vous sûr de vouloir supprimer cet événement ?')" class="icon">🗑</a>
|
||||
<a href="{{ url_for('open_edit_moderation_event', event_id = mod_event.id) }}" class="icon" title="Modifier">✐</a>
|
||||
<a href="{{ url_for('delete_moderation_event', event_id = mod_event.id) }}" onclick="return confirm('Êtes-vous sûr de vouloir supprimer cet événement ?')" class="icon" title="Supprimer">🗑</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">🎉</div>
|
||||
<p>Aucun événement de modération enregistré</p>
|
||||
<small>Le serveur est sage !</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if event %}
|
||||
<h2>Editer un événement</h2>
|
||||
<h2>✏️ Editer un événement</h2>
|
||||
<form action="{{ url_for('update_moderation_event', event_id = event.id) }}" method="POST">
|
||||
<label for="type">Type</label>
|
||||
<input name="type" type="text" value="{{ event.type }}" disabled />
|
||||
@@ -107,4 +228,142 @@
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% if show_invites %}
|
||||
<!-- Section Invitations Discord -->
|
||||
<h2>🔗 Invitations Discord</h2>
|
||||
|
||||
<div class="invites-actions">
|
||||
<a href="{{ url_for('sync_invitations') }}" class="btn btn-primary">🔄 Synchroniser les invitations</a>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="show-revoked" {{ 'checked' if show_revoked else '' }} onchange="toggleRevoked()">
|
||||
Afficher les invitations révoquées
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{% if invite_stats %}
|
||||
<!-- Statistiques des invitations -->
|
||||
<div class="dashboard-section">
|
||||
<h3>📊 Statistiques des invitations</h3>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">🔗</div>
|
||||
<div class="stat-content">
|
||||
<span class="stat-label">Invitations actives</span>
|
||||
<span class="stat-value big">{{ invite_stats.total }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">👥</div>
|
||||
<div class="stat-content">
|
||||
<span class="stat-label">Utilisations totales</span>
|
||||
<span class="stat-value big">{{ invite_stats.total_uses }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">♾️</div>
|
||||
<div class="stat-content">
|
||||
<span class="stat-label">Permanentes</span>
|
||||
<span class="stat-value big">{{ invite_stats.permanent }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon">⏳</div>
|
||||
<div class="stat-content">
|
||||
<span class="stat-label">Temporaires</span>
|
||||
<span class="stat-value big">{{ invite_stats.temporary }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if invite_stats.top_inviters %}
|
||||
<div class="moderation-insights">
|
||||
<div class="insight-card">
|
||||
<h3>🏆 Top inviteurs</h3>
|
||||
<div class="leaderboard">
|
||||
{% for inviter_name, uses in invite_stats.top_inviters %}
|
||||
<div class="leaderboard-item">
|
||||
<span class="rank">{{ loop.index }}</span>
|
||||
<span class="name">{{ inviter_name }}</span>
|
||||
<span class="count">{{ uses }} membre{{ 's' if uses > 1 else '' }} invité{{ 's' if uses > 1 else '' }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<!-- Liste des invitations -->
|
||||
{% if invites %}
|
||||
<table class="moderation invites-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Code</th>
|
||||
<th>Canal</th>
|
||||
<th>Créateur</th>
|
||||
<th>Utilisations</th>
|
||||
<th>Max utilisations</th>
|
||||
<th>Expiration</th>
|
||||
<th>Créée le</th>
|
||||
<th>Statut</th>
|
||||
<th>#</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for invite in invites %}
|
||||
<tr class="{{ 'revoked' if invite.revoked else '' }} {{ 'expired' if invite.is_expired else '' }}">
|
||||
<td><code>{{ invite.code }}</code></td>
|
||||
<td>{{ invite.channel_name or 'N/A' }}</td>
|
||||
<td>{{ invite.inviter_name or 'Inconnu' }}</td>
|
||||
<td>{{ invite.uses or 0 }}</td>
|
||||
<td>{{ invite.max_uses if invite.max_uses > 0 else '∞' }}</td>
|
||||
<td>
|
||||
{% if invite.max_age == 0 %}
|
||||
<span class="badge permanent">Jamais</span>
|
||||
{% elif invite.expires_at %}
|
||||
{{ invite.expires_at.strftime('%d/%m/%Y %H:%M') }}
|
||||
{% else %}
|
||||
N/A
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ invite.created_at.strftime('%d/%m/%Y %H:%M') if invite.created_at else 'N/A' }}</td>
|
||||
<td>
|
||||
{% if invite.revoked %}
|
||||
<span class="badge revoked">Révoquée</span>
|
||||
{% elif invite.is_expired %}
|
||||
<span class="badge expired">Expirée</span>
|
||||
{% elif invite.max_uses > 0 and invite.uses >= invite.max_uses %}
|
||||
<span class="badge used">Max atteint</span>
|
||||
{% else %}
|
||||
<span class="badge active">Active</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if not invite.revoked %}
|
||||
<a href="{{ url_for('revoke_invitation', invite_code=invite.code) }}"
|
||||
onclick="return confirm('Êtes-vous sûr de vouloir révoquer l\'invitation {{ invite.code }} ?')"
|
||||
class="icon" title="Révoquer">🗑</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">🔗</div>
|
||||
<p>Aucune invitation trouvée</p>
|
||||
<small>Cliquez sur "Synchroniser les invitations" pour récupérer les invitations depuis Discord</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
function toggleRevoked() {
|
||||
const showRevoked = document.getElementById('show-revoked').checked;
|
||||
window.location.href = '{{ url_for("moderation_invitations") }}?show_revoked=' + showRevoked;
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<title>Mamie Henriette</title>
|
||||
<link rel="stylesheet" href="/static/css/mvp.css" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/static/css/style.css" />
|
||||
<link rel="icon" href="/static/ico/favicon.ico" type="image/x-icon">
|
||||
<link rel="shortcut icon" href="/static/ico/favicon.ico" type="image/x-icon">
|
||||
@@ -16,18 +18,56 @@
|
||||
<body>
|
||||
<header>
|
||||
<nav>
|
||||
<a href="/"><img src="/static/ico/favicon.ico"></a>
|
||||
<ul>
|
||||
<li><a href="/live-alert">Alerte live</a></li>
|
||||
<li><a href="/commandes">Commandes</a></li>
|
||||
<li><a href="/humeurs">Humeurs</a></li>
|
||||
<li><a href="/moderation">Modération</a></li>
|
||||
<li><a href="/protondb">ProtonDB</a></li>
|
||||
<li><a href="/configurations">Configurations</a></li>
|
||||
<a href="/" class="nav-logo">
|
||||
<img src="/static/ico/favicon.ico" alt="Mamie Henriette">
|
||||
<span>Mamie Henriette</span>
|
||||
</a>
|
||||
|
||||
<input type="checkbox" id="nav-toggle" class="nav-toggle">
|
||||
<label for="nav-toggle" class="nav-toggle-label">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</label>
|
||||
|
||||
<ul class="nav-menu">
|
||||
<li class="has-submenu">
|
||||
<a href="#">Discord</a>
|
||||
<ul class="submenu">
|
||||
<li><a href="/commandes">📝 Commandes</a></li>
|
||||
<li><a href="/humeurs">😊 Humeurs</a></li>
|
||||
<li><a href="/moderation">🛡️ Modération</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="has-submenu">
|
||||
<a href="#">Twitch</a>
|
||||
<ul class="submenu">
|
||||
<li><a href="/live-alert">📺 Alerte Live</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="has-submenu">
|
||||
<a href="#">Outils</a>
|
||||
<ul class="submenu">
|
||||
<li><a href="/freegames">🎮 Jeux Gratuits</a></li>
|
||||
<li><a href="/protondb">🐧 ProtonDB</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/configurations">⚙️ Config</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="flash-messages">
|
||||
{% for category, message in messages %}
|
||||
<div class="flash-message {{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
<footer>
|
||||
|
||||
Reference in New Issue
Block a user