Merge branch 'twitchbot' into main
Intégration de toutes les fonctionnalités développées dans la branche twitchbot : - Système de modération Twitch complet - Filtre de liens intelligent - Notifications d'événements Twitch - Système Freeloot Discord - Salons automatiques Discord - Authentification utilisateur - Gestion des utilisateurs - Interfaces d'administration étendues Conflits résolus en faveur des versions de twitchbot pour : - templates/commandes.html - templates/configurations.html - templates/humeurs.html - templates/index.html - templates/live-alert.html - templates/moderation.html - templates/protondb.html - templates/template.html Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
{% extends "template.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-md mx-auto py-12 text-center">
|
||||
<h1 class="text-2xl font-bold text-slate-800 dark:text-white mb-2">Accès refusé</h1>
|
||||
<p class="text-slate-600 dark:text-slate-400 mb-6">Vous n'avez pas les droits nécessaires pour accéder à cette page.</p>
|
||||
<a href="{{ url_for('index') }}" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-primary-600 dark:bg-primary-500 text-white font-medium hover:bg-primary-700 dark:hover:bg-primary-600 transition-colors">Retour à l'accueil</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,188 @@
|
||||
{% extends "template.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-8">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="p-3 bg-purple-100 dark:bg-purple-900/30 rounded-lg">
|
||||
<svg class="w-6 h-6 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5.882V19.24a1.76 1.76 0 01-3.417.592l-2.147-6.15M18 13a3 3 0 100-6M5.436 13.683A4.001 4.001 0 017 6h1.832c4.1 0 7.625-1.234 9.168-3v14c-1.543-1.766-5.067-3-9.168-3H7a3.988 3.988 0 01-1.564-.317z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">Annonces Twitch</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400">Messages automatiques périodiques dans le chat</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Configurez des messages envoyés automatiquement dans le chat Twitch.
|
||||
L'annonce est envoyée uniquement si le temps est écoulé ET si le nombre minimum de messages a été atteint.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{% if not announcement %}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Annonces configurées</h2>
|
||||
</div>
|
||||
|
||||
{% if announcements %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700/50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Nom</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Message</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Temps</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Min. messages</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Dernier envoi</th>
|
||||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{% for ann in announcements %}
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/30 transition-colors">
|
||||
<td class="px-6 py-4">
|
||||
<span class="font-medium text-gray-900 dark:text-white">{{ ann.name }}</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="text-gray-600 dark:text-gray-300 text-sm max-w-xs truncate block">{{ ann.text[:60] }}{% if ann.text|length > 60 %}...{% endif %}</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400">
|
||||
{{ ann.periodicity }} min
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400">
|
||||
{{ ann.min_chat_messages }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
{% if ann.last_sent %}
|
||||
{{ ann.last_sent.strftime('%d/%m %H:%M') }}
|
||||
{% else %}
|
||||
<span class="italic">Jamais</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<a href="{{ url_for('toggleAnnouncement', id=ann.id) }}"
|
||||
class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
||||
title="{{ 'Désactiver' if ann.enable else 'Activer' }}">
|
||||
{% if ann.enable %}
|
||||
<svg class="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
{% else %}
|
||||
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
{% endif %}
|
||||
</a>
|
||||
<a href="{{ url_for('resetAnnouncement', id=ann.id) }}"
|
||||
class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
||||
title="Remettre le compteur à zéro">
|
||||
<svg class="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="{{ url_for('openEditAnnouncement', id=ann.id) }}"
|
||||
class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
||||
title="Modifier">
|
||||
<svg class="w-5 h-5 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="{{ url_for('delAnnouncement', id=ann.id) }}"
|
||||
onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette annonce ?')"
|
||||
class="p-2 rounded-lg hover:bg-red-100 dark:hover:bg-red-900/30 transition-colors"
|
||||
title="Supprimer">
|
||||
<svg class="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="px-6 py-12 text-center">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5.882V19.24a1.76 1.76 0 01-3.417.592l-2.147-6.15M18 13a3 3 0 100-6M5.436 13.683A4.001 4.001 0 017 6h1.832c4.1 0 7.625-1.234 9.168-3v14c-1.543-1.766-5.067-3-9.168-3H7a3.988 3.988 0 01-1.564-.317z"></path>
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">Aucune annonce</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Commencez par créer votre première annonce automatique.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-6">
|
||||
{{ 'Modifier l\'annonce' if announcement else 'Ajouter une annonce' }}
|
||||
</h2>
|
||||
|
||||
<form action="{{ url_for('submitEditAnnouncement', id=announcement.id) if announcement else url_for('addAnnouncement') }}" method="POST" class="space-y-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Nom de l'annonce
|
||||
</label>
|
||||
<input type="text" name="name" id="name" required maxlength="64"
|
||||
value="{{ announcement.name if announcement else '' }}"
|
||||
placeholder="Ex: Règles du chat"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-colors">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="periodicity" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Temps entre les annonces (minutes)
|
||||
</label>
|
||||
<input type="number" name="periodicity" id="periodicity" required min="1" max="1440"
|
||||
value="{{ announcement.periodicity if announcement else 10 }}"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-colors">
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">1 min à 1440 min (24h)</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="min_chat_messages" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Messages minimum entre annonces
|
||||
</label>
|
||||
<input type="number" name="min_chat_messages" id="min_chat_messages" required min="0" max="1000"
|
||||
value="{{ announcement.min_chat_messages if announcement else 0 }}"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-colors">
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">0 = pas de minimum</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="text" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Message
|
||||
</label>
|
||||
<textarea name="text" id="text" required maxlength="500" rows="4"
|
||||
placeholder="Le message qui sera envoyé dans le chat..."
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-colors resize-none">{{ announcement.text if announcement else '' }}</textarea>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Maximum 500 caractères</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<button type="submit"
|
||||
class="px-6 py-2 bg-purple-600 hover:bg-purple-700 text-white font-medium rounded-lg transition-colors focus:ring-2 focus:ring-purple-500 focus:ring-offset-2">
|
||||
{{ 'Enregistrer' if announcement else 'Ajouter' }}
|
||||
</button>
|
||||
{% if announcement %}
|
||||
<a href="{{ url_for('openAnnouncements') }}"
|
||||
class="px-6 py-2 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 font-medium rounded-lg transition-colors">
|
||||
Annuler
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
+106
-77
@@ -1,99 +1,128 @@
|
||||
{% extends "template.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-semibold text-slate-800 dark:text-white mb-1">Commandes</h1>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400">
|
||||
Gérez les commandes personnalisées du bot. Ces commandes peuvent être activées sur Discord et/ou Twitch.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden mb-6">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead>
|
||||
<tr class="bg-slate-50 dark:bg-slate-700/50 border-b border-slate-200 dark:border-slate-700">
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Commande</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Réponse</th>
|
||||
<th class="px-4 py-3 text-center text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Discord</th>
|
||||
<th class="px-4 py-3 text-center text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Twitch</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-200 dark:divide-slate-700">
|
||||
{% for commande in commandes %}
|
||||
<tr class="hover:bg-slate-50 dark:hover:bg-slate-700/30 transition-colors">
|
||||
<td class="px-4 py-3">
|
||||
<code class="px-1.5 py-0.5 bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded text-xs font-mono">{{ commande.trigger }}</code>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-slate-600 dark:text-slate-400 text-sm max-w-xs">
|
||||
<div class="line-clamp-2">{{ commande.response }}</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
<a href="{{ url_for('toggle_discord_commande', commande_id = commande.id) }}" class="inline-flex" title="{{ 'Désactiver' if commande.discord_enable else 'Activer' }}">
|
||||
{% if commande.discord_enable %}
|
||||
<span class="w-5 h-5 text-green-600 dark:text-green-500">✓</span>
|
||||
{% else %}
|
||||
<span class="w-5 h-5 text-slate-400">–</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-center">
|
||||
<a href="{{ url_for('toggle_twitch_commande', commande_id = commande.id) }}" class="inline-flex" title="{{ 'Désactiver' if commande.twitch_enable else 'Activer' }}">
|
||||
{% if commande.twitch_enable %}
|
||||
<span class="w-5 h-5 text-green-600 dark:text-green-500">✓</span>
|
||||
{% else %}
|
||||
<span class="w-5 h-5 text-slate-400">–</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<a href="{{ url_for('delete_commande', commande_id = commande.id) }}" onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette commande ?')" class="text-sm text-slate-500 hover:text-red-600 dark:hover:text-red-400 transition-colors">
|
||||
Supprimer
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="5" class="px-4 py-8 text-center text-sm text-slate-500 dark:text-slate-400">
|
||||
Aucune commande configurée
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-4">Commandes de Mamie</h1>
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<p class="text-gray-700 dark:text-gray-300">
|
||||
Gérez les commandes personnalisées du bot. Ces commandes peuvent être activées sur Discord et/ou Twitch selon vos besoins.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 p-5">
|
||||
<h2 class="text-lg font-medium text-slate-800 dark:text-white mb-5">Ajouter une commande</h2>
|
||||
<div class="mb-8">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Liste des commandes</h2>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Commande</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Réponse</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Discord</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Twitch</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Permission Twitch</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{% for commande in commandes %}
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<code class="px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded text-purple-600 dark:text-purple-400 font-mono">{{ commande.trigger }}</code>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-gray-600 dark:text-gray-400 max-w-md truncate">{{ commande.response }}</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
<a href="{{ url_for('toggle_discord_commande', commande_id = commande.id) }}"
|
||||
class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors inline-block"
|
||||
title="{{ 'Désactiver sur Discord' if commande.discord_enable else 'Activer sur Discord' }}">
|
||||
{{ '✅' if commande.discord_enable else '❌' }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
<a href="{{ url_for('toggle_twitch_commande', commande_id = commande.id) }}"
|
||||
class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors inline-block"
|
||||
title="{{ 'Désactiver sur Twitch' if commande.twitch_enable else 'Activer sur Twitch' }}">
|
||||
{{ '✅' if commande.twitch_enable else '❌' }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
{% if commande.twitch_enable %}
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300">
|
||||
{{ twitch_permissions.get(commande.twitch_permission or 'viewer', 'Tous') }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="text-gray-400 dark:text-gray-500">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
<a href="{{ url_for('delete_commande', commande_id = commande.id) }}"
|
||||
onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette commande ?')"
|
||||
class="p-2 rounded-lg hover:bg-red-100 dark:hover:bg-red-900/30 transition-colors text-red-600 dark:text-red-400 inline-block"
|
||||
title="Supprimer">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="6" class="px-6 py-8 text-center text-gray-500 dark:text-gray-400">
|
||||
Aucune commande configurée. Ajoutez-en une ci-dessous.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-6">Ajouter une commande</h2>
|
||||
|
||||
<form action="{{ url_for('add_commande') }}" method="POST" class="space-y-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="trigger" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Commande</label>
|
||||
<input type="text" name="trigger" id="trigger" placeholder="!macommande" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
|
||||
<label for="trigger" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Commande</label>
|
||||
<input name="trigger" id="trigger" type="text" required
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all"
|
||||
placeholder="!macommande"/>
|
||||
</div>
|
||||
<div class="flex items-end gap-6">
|
||||
<label class="flex items-center gap-3 cursor-pointer">
|
||||
<input type="checkbox" name="discord_enable" checked class="w-4 h-4 text-slate-600 bg-slate-100 dark:bg-slate-700 border-slate-300 dark:border-slate-600 rounded focus:ring-slate-500 focus:ring-2">
|
||||
<span class="text-sm text-slate-700 dark:text-slate-300">Discord</span>
|
||||
|
||||
<div class="flex flex-wrap items-end gap-6">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input name="discord_enable" type="checkbox" checked
|
||||
class="w-5 h-5 rounded border-gray-300 dark:border-gray-600 text-purple-600 focus:ring-purple-500 dark:bg-gray-700">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Discord</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-3 cursor-pointer">
|
||||
<input type="checkbox" name="twitch_enable" class="w-4 h-4 text-slate-600 bg-slate-100 dark:bg-slate-700 border-slate-300 dark:border-slate-600 rounded focus:ring-slate-500 focus:ring-2">
|
||||
<span class="text-sm text-slate-700 dark:text-slate-300">Twitch</span>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input name="twitch_enable" type="checkbox" id="twitch_enable_checkbox"
|
||||
class="w-5 h-5 rounded border-gray-300 dark:border-gray-600 text-purple-600 focus:ring-purple-500 dark:bg-gray-700">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Twitch</span>
|
||||
</label>
|
||||
<div class="w-full sm:w-auto">
|
||||
<label for="twitch_permission" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Permission Twitch (qui peut utiliser la commande)</label>
|
||||
<select name="twitch_permission" id="twitch_permission"
|
||||
class="w-full sm:w-48 px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 focus:border-transparent">
|
||||
{% for value, label in twitch_permissions.items() %}
|
||||
<option value="{{ value }}">{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="response" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Réponse</label>
|
||||
<textarea name="response" id="response" rows="4" placeholder="Le message que le bot enverra..." class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all resize-none"></textarea>
|
||||
<label for="response" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Réponse</label>
|
||||
<textarea name="response" id="response" rows="4" required
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all resize-y"
|
||||
placeholder="La réponse que le bot enverra..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<button type="submit" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600 text-white text-sm font-medium rounded-lg transition-colors">
|
||||
Ajouter
|
||||
<div>
|
||||
<button type="submit"
|
||||
class="px-6 py-2.5 bg-purple-600 hover:bg-purple-700 text-white font-medium rounded-lg transition-colors focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
|
||||
Ajouter la commande
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,251 +1,341 @@
|
||||
{% extends "template.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-semibold text-slate-800 dark:text-white mb-1">Configurations</h1>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400">
|
||||
Paramètres Discord, Twitch et Humble Bundle.
|
||||
</p>
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="mb-4 space-y-2">
|
||||
{% for category, msg in messages %}
|
||||
<div class="p-4 rounded-lg {{ 'bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 text-green-800 dark:text-green-200' if category == 'success' else 'bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 text-amber-800 dark:text-amber-200' }}">
|
||||
{{ msg }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-4">Configuration de Mamie</h1>
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<p class="text-gray-700 dark:text-gray-300">
|
||||
Configurez les tokens Discord, les notifications Humble Bundle et l'API Twitch.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 mb-6 overflow-hidden">
|
||||
<div class="px-5 py-4 border-b border-slate-200 dark:border-slate-700">
|
||||
<h2 class="text-lg font-medium text-slate-800 dark:text-white">Discord</h2>
|
||||
</div>
|
||||
|
||||
<form action="{{ url_for('updateConfiguration') }}" method="POST" class="p-5 space-y-6">
|
||||
<div>
|
||||
<label for="discord_token" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Token Discord</label>
|
||||
<input type="password" name="discord_token" id="discord_token" placeholder="Votre token Discord (caché)" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
|
||||
<p class="mt-1 text-xs text-amber-600 dark:text-amber-400">Nécessite un redémarrage après modification</p>
|
||||
<div class="space-y-8">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<svg class="w-6 h-6 text-indigo-500" fill="currentColor" viewBox="0 0 24 24"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Discord</h2>
|
||||
</div>
|
||||
|
||||
<div class="pt-4 border-t border-slate-200 dark:border-slate-700">
|
||||
<h3 class="text-sm font-medium text-slate-800 dark:text-white mb-4">Messages de bienvenue</h3>
|
||||
|
||||
<label class="flex items-center gap-3 cursor-pointer mb-4">
|
||||
<input type="checkbox" name="welcome_enable" {% if configuration.getValue('welcome_enable') %}checked{% endif %} class="w-4 h-4 text-slate-600 bg-slate-100 dark:bg-slate-700 border-slate-300 dark:border-slate-600 rounded focus:ring-slate-500">
|
||||
<span class="text-sm text-slate-700 dark:text-slate-300">Activer le message de bienvenue</span>
|
||||
</label>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
|
||||
<form action="{{ url_for('updateConfiguration') }}" method="POST" class="space-y-6">
|
||||
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4 space-y-4">
|
||||
<h3 class="font-medium text-gray-800 dark:text-gray-200">API Discord</h3>
|
||||
<div>
|
||||
<label for="welcome_channel_id" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Canal de bienvenue</label>
|
||||
<select name="welcome_channel_id" id="welcome_channel_id" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
|
||||
{% for channel in channels %}
|
||||
<option value="{{ channel.id }}" {% if configuration.getIntValue('welcome_channel_id') == channel.id %}selected{% endif %}>{{ channel.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label for="discord_token" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Token Discord (caché)</label>
|
||||
<input name="discord_token" id="discord_token" type="password"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all"
|
||||
placeholder="Votre token Discord"/>
|
||||
<p class="mt-1 text-xs text-amber-600 dark:text-amber-400">Nécessite un redémarrage après modification</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="welcome_message" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Message personnalisé</label>
|
||||
<textarea name="welcome_message" id="welcome_message" rows="2" placeholder="Bienvenue {member.mention} sur le serveur !" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all resize-none">{{ configuration.getValue('welcome_message') }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 bg-slate-50 dark:bg-slate-700/50 rounded-lg p-3">
|
||||
<p class="text-xs font-medium text-slate-600 dark:text-slate-400 mb-2">Variables :</p>
|
||||
<div class="flex flex-wrap gap-2 text-xs">
|
||||
<code class="px-1.5 py-0.5 bg-slate-200 dark:bg-slate-600 rounded">{member.mention}</code>
|
||||
<code class="px-1.5 py-0.5 bg-slate-200 dark:bg-slate-600 rounded">{member.name}</code>
|
||||
<code class="px-1.5 py-0.5 bg-slate-200 dark:bg-slate-600 rounded">{server.name}</code>
|
||||
<code class="px-1.5 py-0.5 bg-slate-200 dark:bg-slate-600 rounded">{server.member_count}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-4 border-t border-slate-200 dark:border-slate-700">
|
||||
<h3 class="text-sm font-medium text-slate-800 dark:text-white mb-4">Messages de départ</h3>
|
||||
|
||||
<label class="flex items-center gap-3 cursor-pointer mb-4">
|
||||
<input type="checkbox" name="leave_enable" {% if configuration.getValue('leave_enable') %}checked{% endif %} class="w-4 h-4 text-slate-600 bg-slate-100 dark:bg-slate-700 border-slate-300 dark:border-slate-600 rounded focus:ring-slate-500">
|
||||
<span class="text-sm text-slate-700 dark:text-slate-300">Activer le message de départ</span>
|
||||
</label>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label for="leave_channel_id" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Canal de départ</label>
|
||||
<select name="leave_channel_id" id="leave_channel_id" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
|
||||
{% for channel in channels %}
|
||||
<option value="{{ channel.id }}" {% if configuration.getIntValue('leave_channel_id') == channel.id %}selected{% endif %}>{{ channel.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="leave_message" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Message personnalisé</label>
|
||||
<textarea name="leave_message" id="leave_message" rows="2" placeholder="{member.mention} a quitté le serveur." class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all resize-none">{{ configuration.getValue('leave_message') }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-4 border-t border-slate-200 dark:border-slate-700">
|
||||
<h3 class="text-sm font-medium text-slate-800 dark:text-white mb-4">Modération</h3>
|
||||
|
||||
<div class="space-y-3 mb-4">
|
||||
<label class="flex items-center gap-3 cursor-pointer">
|
||||
<input type="checkbox" name="moderation_enable" {% if configuration.getValue('moderation_enable') %}checked{% endif %} class="w-4 h-4 text-slate-600 bg-slate-100 dark:bg-slate-700 border-slate-300 dark:border-slate-600 rounded focus:ring-slate-500">
|
||||
<span class="text-sm text-slate-700 dark:text-slate-300">Activer les commandes d'avertissement</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-3 cursor-pointer">
|
||||
<input type="checkbox" name="moderation_ban_enable" {% if configuration.getValue('moderation_ban_enable') %}checked{% endif %} class="w-4 h-4 text-slate-600 bg-slate-100 dark:bg-slate-700 border-slate-300 dark:border-slate-600 rounded focus:ring-slate-500">
|
||||
<span class="text-sm text-slate-700 dark:text-slate-300">Activer les commandes de bannissement</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-3 cursor-pointer">
|
||||
<input type="checkbox" name="moderation_kick_enable" {% if configuration.getValue('moderation_kick_enable') %}checked{% endif %} class="w-4 h-4 text-slate-600 bg-slate-100 dark:bg-slate-700 border-slate-300 dark:border-slate-600 rounded focus:ring-slate-500">
|
||||
<span class="text-sm text-slate-700 dark:text-slate-300">Activer la commande d'expulsion</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label for="moderation_log_channel_id" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Canal de logs</label>
|
||||
<select name="moderation_log_channel_id" id="moderation_log_channel_id" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
|
||||
{% for channel in channels %}
|
||||
<option value="{{ channel.id }}" {% if configuration.getIntValue('moderation_log_channel_id') == channel.id %}selected{% endif %}>{{ channel.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="moderation_embed_delete_delay" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Délai suppression (sec)</label>
|
||||
<input type="number" name="moderation_embed_delete_delay" id="moderation_embed_delete_delay" value="{{ configuration.getValue('moderation_embed_delete_delay') or '0' }}" min="0" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-3">Rôles Staff autorisés</label>
|
||||
{% set selected_roles = (configuration.getValue('moderation_staff_role_ids') or '').split(',') %}
|
||||
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4 space-y-4">
|
||||
<h3 class="font-medium text-gray-800 dark:text-gray-200">Messages de bienvenue</h3>
|
||||
|
||||
{% if roles|length > 1 %}
|
||||
<div class="flex flex-wrap gap-2 mb-3">
|
||||
{% for guild_data in roles %}
|
||||
<button type="button" onclick="openTab(event, 'guild-{{ guild_data.guild_id }}')" class="tab-button px-3 py-1.5 text-sm rounded-lg bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-600 transition-colors {% if loop.first %}active bg-slate-200 dark:bg-slate-600{% endif %}">
|
||||
{{ guild_data.guild_name }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" name="welcome_enable" {% if configuration.getValue('welcome_enable') %}checked{% endif %}
|
||||
class="w-5 h-5 rounded border-gray-300 dark:border-gray-600 text-indigo-600 focus:ring-indigo-500 dark:bg-gray-700">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Activer le message de bienvenue pour les nouveaux membres</span>
|
||||
</label>
|
||||
|
||||
{% for guild_data in roles %}
|
||||
<div id="guild-{{ guild_data.guild_id }}" class="tab-content {% if not loop.first %}hidden{% endif %}">
|
||||
<div class="max-h-48 overflow-y-auto bg-slate-50 dark:bg-slate-700/50 rounded-lg p-3 space-y-1">
|
||||
{% for role in guild_data.roles %}
|
||||
<label class="flex items-center gap-3 cursor-pointer p-2 rounded hover:bg-slate-100 dark:hover:bg-slate-600/50 transition-colors">
|
||||
<input type="checkbox" name="moderation_staff_role_ids" value="{{ role.id }}" {% if role.id|string in selected_roles %}checked{% endif %} class="w-4 h-4 text-slate-600 bg-slate-100 dark:bg-slate-700 border-slate-300 dark:border-slate-600 rounded focus:ring-slate-500">
|
||||
{% if role.color.value != 0 %}
|
||||
<span style="color:#{{ '%06x' % role.color.value }}">●</span>
|
||||
{% else %}
|
||||
<span class="text-slate-400">○</span>
|
||||
{% endif %}
|
||||
<span class="text-sm text-slate-700 dark:text-slate-300">{{ role.name }}</span>
|
||||
</label>
|
||||
<div>
|
||||
<label for="welcome_channel_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Canal de bienvenue</label>
|
||||
<select name="welcome_channel_id" id="welcome_channel_id"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all">
|
||||
{% for channel in channels %}
|
||||
<option value="{{channel.id}}" {% if configuration.getIntValue('welcome_channel_id')==channel.id %}selected{% endif %}>{{channel.name}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="welcome_message" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Message personnalisé</label>
|
||||
<textarea name="welcome_message" id="welcome_message" rows="3"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all resize-y"
|
||||
placeholder="Bienvenue {member.mention} sur le serveur !">{{ configuration.getValue('welcome_message') }}</textarea>
|
||||
<div class="mt-2 text-xs text-gray-500 dark:text-gray-400 space-y-1">
|
||||
<p><code class="px-1 bg-gray-200 dark:bg-gray-600 rounded">{member.mention}</code> Mentionne l'utilisateur</p>
|
||||
<p><code class="px-1 bg-gray-200 dark:bg-gray-600 rounded">{member.name}</code> Nom d'utilisateur</p>
|
||||
<p><code class="px-1 bg-gray-200 dark:bg-gray-600 rounded">{server.name}</code> Nom du serveur</p>
|
||||
<p><code class="px-1 bg-gray-200 dark:bg-gray-600 rounded">{server.member_count}</code> Nombre de membres</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pt-4 border-t border-slate-200 dark:border-slate-700">
|
||||
<button type="submit" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600 text-white text-sm font-medium rounded-lg transition-colors">
|
||||
Enregistrer
|
||||
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4 space-y-4">
|
||||
<h3 class="font-medium text-gray-800 dark:text-gray-200">Messages de départ</h3>
|
||||
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" name="leave_enable" {% if configuration.getValue('leave_enable') %}checked{% endif %}
|
||||
class="w-5 h-5 rounded border-gray-300 dark:border-gray-600 text-indigo-600 focus:ring-indigo-500 dark:bg-gray-700">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Activer le message quand un membre quitte le serveur</span>
|
||||
</label>
|
||||
|
||||
<div>
|
||||
<label for="leave_channel_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Canal de départ</label>
|
||||
<select name="leave_channel_id" id="leave_channel_id"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all">
|
||||
{% for channel in channels %}
|
||||
<option value="{{channel.id}}" {% if configuration.getIntValue('leave_channel_id')==channel.id %}selected{% endif %}>{{channel.name}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="leave_message" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Message personnalisé</label>
|
||||
<textarea name="leave_message" id="leave_message" rows="3"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all resize-y"
|
||||
placeholder="{member.name} a quitté le serveur.">{{ configuration.getValue('leave_message') }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="auto-rooms" class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4 space-y-4">
|
||||
<h3 class="font-medium text-gray-800 dark:text-gray-200">Auto Rooms (salons vocaux temporaires)</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Quand un membre rejoint le canal vocal configuré ci-dessous, un salon vocal temporaire est créé. Le message de configuration avec les réactions apparaît dans la <strong>partie texte du vocal</strong> (onglet Discussion à droite quand on ouvre le salon). Seul le propriétaire peut réagir.
|
||||
</p>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" name="auto_rooms_enable" {% if configuration.getValue('auto_rooms_enable') %}checked{% endif %}
|
||||
class="w-5 h-5 rounded border-gray-300 dark:border-gray-600 text-indigo-600 focus:ring-indigo-500 dark:bg-gray-700">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Activer les Auto Rooms</span>
|
||||
</label>
|
||||
<div>
|
||||
<label for="auto_rooms_channel_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Canal vocal à rejoindre pour créer un salon</label>
|
||||
<select name="auto_rooms_channel_id" id="auto_rooms_channel_id"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all">
|
||||
<option value="">— Choisir un canal vocal —</option>
|
||||
{% for channel in voice_channels %}
|
||||
<option value="{{ channel.id }}" {% if configuration.getIntValue('auto_rooms_channel_id') == channel.id %}selected{% endif %}>{{ channel.name }} (vocal)</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Ex. « + Créer votre salon » — les membres qui rejoignent ce canal obtiennent un salon vocal dédié.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4 space-y-4">
|
||||
<h3 class="font-medium text-gray-800 dark:text-gray-200">Modération</h3>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" name="moderation_enable" {% if configuration.getValue('moderation_enable') %}checked{% endif %}
|
||||
class="w-5 h-5 rounded border-gray-300 dark:border-gray-600 text-indigo-600 focus:ring-indigo-500 dark:bg-gray-700">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Activer les commandes d'avertissement (!warn, !unwarn, !inspect)</span>
|
||||
</label>
|
||||
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" name="moderation_ban_enable" {% if configuration.getValue('moderation_ban_enable') %}checked{% endif %}
|
||||
class="w-5 h-5 rounded border-gray-300 dark:border-gray-600 text-indigo-600 focus:ring-indigo-500 dark:bg-gray-700">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Activer les commandes de bannissement (!ban, !unban)</span>
|
||||
</label>
|
||||
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" name="moderation_kick_enable" {% if configuration.getValue('moderation_kick_enable') %}checked{% endif %}
|
||||
class="w-5 h-5 rounded border-gray-300 dark:border-gray-600 text-indigo-600 focus:ring-indigo-500 dark:bg-gray-700">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Activer la commande d'expulsion (!kick)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="moderation_log_channel_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Canal de logs de modération</label>
|
||||
<select name="moderation_log_channel_id" id="moderation_log_channel_id"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all">
|
||||
{% for channel in channels %}
|
||||
<option value="{{channel.id}}" {% if configuration.getIntValue('moderation_log_channel_id')==channel.id %}selected{% endif %}>{{channel.name}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Toutes les actions de modération seront notifiées dans ce canal</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Rôles Staff autorisés</label>
|
||||
{% set selected_roles = (configuration.getValue('moderation_staff_role_ids') or '').split(',') %}
|
||||
|
||||
{% if roles|length > 1 %}
|
||||
<div class="flex flex-wrap gap-1 border-b border-gray-200 dark:border-gray-600 mb-3">
|
||||
{% for guild_data in roles %}
|
||||
<button type="button" class="tab-button px-4 py-2 text-sm font-medium rounded-t-lg transition-colors
|
||||
{% if loop.first %}bg-gray-200 dark:bg-gray-600 text-gray-900 dark:text-white{% else %}bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600{% endif %}"
|
||||
onclick="openTab(event, 'guild-{{guild_data.guild_id}}')" {% if loop.first %}id="defaultOpen"{% endif %}>
|
||||
{{ guild_data.guild_name }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% for guild_data in roles %}
|
||||
<div id="guild-{{guild_data.guild_id}}" class="tab-content {% if not loop.first %}hidden{% endif %}">
|
||||
<div class="max-h-64 overflow-y-auto border border-gray-200 dark:border-gray-600 rounded-lg p-3 space-y-2">
|
||||
{% for role in guild_data.roles %}
|
||||
<label class="flex items-center gap-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600 p-1 rounded">
|
||||
<input type="checkbox" name="moderation_staff_role_ids" value="{{role.id}}"
|
||||
{% if role.id|string in selected_roles %}checked{% endif %}
|
||||
class="w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-indigo-600 focus:ring-indigo-500 dark:bg-gray-700">
|
||||
{% if role.color.value != 0 %}
|
||||
<span style="color:#{{ '%06x' % role.color.value }}">●</span>
|
||||
{% else %}
|
||||
<span class="text-gray-400">○</span>
|
||||
{% endif %}
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{role.name}}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">Sélectionnez les rôles qui peuvent utiliser les commandes de modération</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="moderation_embed_delete_delay" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Délai de suppression des embeds (secondes)</label>
|
||||
<input name="moderation_embed_delete_delay" id="moderation_embed_delete_delay" type="number" min="0"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-all"
|
||||
value="{{ configuration.getValue('moderation_embed_delete_delay') or '0' }}" placeholder="0"/>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Mettre 0 pour ne pas supprimer automatiquement</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit"
|
||||
class="px-6 py-2.5 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-lg transition-colors focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
|
||||
Enregistrer la configuration Discord
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 mb-6 overflow-hidden">
|
||||
<div class="px-5 py-4 border-b border-slate-200 dark:border-slate-700">
|
||||
<h2 class="text-lg font-medium text-slate-800 dark:text-white">API Twitch</h2>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<form action="{{ url_for('updateConfiguration') }}" method="POST" class="p-5 space-y-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="twitch_client_id" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Client ID</label>
|
||||
<input type="text" name="twitch_client_id" id="twitch_client_id" value="{{ configuration.getValue('twitch_client_id') }}" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
|
||||
</div>
|
||||
<div>
|
||||
<label for="twitch_client_secret" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Client Secret</label>
|
||||
<input type="text" name="twitch_client_secret" id="twitch_client_secret" value="{{ configuration.getValue('twitch_client_secret') }}" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<svg class="w-6 h-6 text-purple-500" fill="currentColor" viewBox="0 0 24 24"><path d="M11.571 4.714h1.715v5.143H11.57l-.002-5.143zm3.43 0H16.714v5.143H15V4.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0H6zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714v9.429z"/></svg>
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">API Twitch</h2>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="twitch_channel" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Chaîne à rejoindre</label>
|
||||
<input type="text" name="twitch_channel" id="twitch_channel" value="{{ configuration.getValue('twitch_channel') }}" placeholder="#machinTruc" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
|
||||
</div>
|
||||
<form action="{{ url_for('updateConfiguration') }}" method="POST" class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="twitch_client_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Client ID</label>
|
||||
<input name="twitch_client_id" id="twitch_client_id" type="text"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all"
|
||||
value="{{ configuration.getValue('twitch_client_id') }}"/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="twitch_client_secret" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Client Secret</label>
|
||||
<input name="twitch_client_secret" id="twitch_client_secret" type="text"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all"
|
||||
value="{{ configuration.getValue('twitch_client_secret') }}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="twitch_channel" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Chaîne à rejoindre</label>
|
||||
<input name="twitch_channel" id="twitch_channel" type="text"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all"
|
||||
placeholder="#machinTruc"
|
||||
value="{{ configuration.getValue('twitch_channel') }}"/>
|
||||
</div>
|
||||
|
||||
{% if configuration.getValue('twitch_client_secret') and configuration.getValue('twitch_client_id') %}
|
||||
<div>
|
||||
<a href="{{ url_for('twitchRequestToken') }}" class="text-sm text-slate-600 dark:text-slate-400 hover:underline">
|
||||
Obtenir token et refresh token
|
||||
</a>
|
||||
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4 space-y-4">
|
||||
<h3 class="font-medium text-gray-800 dark:text-gray-200">Fonctionnalités du bot Twitch</h3>
|
||||
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" name="twitch_commands_enable" {% if configuration.getValue('twitch_commands_enable') %}checked{% endif %}
|
||||
class="w-5 h-5 rounded border-gray-300 dark:border-gray-600 text-purple-600 focus:ring-purple-500 dark:bg-gray-700">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Activer les commandes personnalisées (!commande)</span>
|
||||
</label>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 ml-7">Les commandes configurées dans la page "Commandes" seront actives dans le chat Twitch</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<button type="submit"
|
||||
class="px-6 py-2.5 bg-purple-600 hover:bg-purple-700 text-white font-medium rounded-lg transition-colors focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
|
||||
Enregistrer la configuration Twitch
|
||||
</button>
|
||||
<a href="{{ url_for('twitchConfigurationHelp') }}" class="text-purple-600 dark:text-purple-400 hover:underline text-sm">Aide</a>
|
||||
</div>
|
||||
|
||||
{% if configuration.getValue('twitch_client_secret') and configuration.getValue('twitch_client_id') %}
|
||||
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<a href="{{ url_for('twitchRequestToken') }}"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 rounded-lg hover:bg-purple-200 dark:hover:bg-purple-900/50 transition-colors text-sm font-medium">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"></path></svg>
|
||||
Obtenir token et refresh token
|
||||
</a>
|
||||
|
||||
<div class="mt-4 grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Access Token</label>
|
||||
<input type="text" readonly
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-100 dark:bg-gray-600 text-gray-700 dark:text-gray-300"
|
||||
value="{{ configuration.getValue('twitch_access_token') }}"/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Refresh Token</label>
|
||||
<input type="text" readonly
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-100 dark:bg-gray-600 text-gray-700 dark:text-gray-300"
|
||||
value="{{ configuration.getValue('twitch_refresh_token') }}"/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-amber-600 dark:text-amber-400">Nécessite un redémarrage après l'obtention des Tokens.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<svg class="w-6 h-6 text-orange-500" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Humble Bundle</h2>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="twitch_access_token" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Access Token</label>
|
||||
<input type="text" name="twitch_access_token" id="twitch_access_token" value="{{ configuration.getValue('twitch_access_token') }}" readonly class="w-full px-3 py-2 bg-slate-100 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-500 dark:text-slate-400 font-mono">
|
||||
<form action="{{ url_for('updateConfiguration') }}" method="POST" class="space-y-4">
|
||||
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
Humble Bundle propose régulièrement des bundles de jeux vidéo à des prix réduits. Activez les notifications pour recevoir automatiquement les nouveaux packs disponibles sur votre serveur Discord.
|
||||
</p>
|
||||
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" name="humble_bundle_enable" {% if configuration.getValue('humble_bundle_enable') %}checked{% endif %}
|
||||
class="w-5 h-5 rounded border-gray-300 dark:border-gray-600 text-orange-600 focus:ring-orange-500 dark:bg-gray-700">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Activer les notifications Humble Bundle</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="twitch_refresh_token" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Refresh Token</label>
|
||||
<input type="text" name="twitch_refresh_token" id="twitch_refresh_token" value="{{ configuration.getValue('twitch_refresh_token') }}" readonly class="w-full px-3 py-2 bg-slate-100 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-500 dark:text-slate-400 font-mono">
|
||||
<label for="humble_bundle_channel" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Canal de notification</label>
|
||||
<select name="humble_bundle_channel" id="humble_bundle_channel"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-orange-500 focus:border-transparent transition-all">
|
||||
{% for channel in channels %}
|
||||
<option value="{{channel.id}}" {% if configuration.getIntValue('humble_bundle_channel')==channel.id %}selected{% endif %}>{{channel.name}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-amber-600 dark:text-amber-400">Nécessite un redémarrage après l'obtention des tokens</p>
|
||||
{% endif %}
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<button type="submit" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600 text-white text-sm font-medium rounded-lg transition-colors">
|
||||
Enregistrer
|
||||
<button type="submit"
|
||||
class="px-6 py-2.5 bg-orange-600 hover:bg-orange-700 text-white font-medium rounded-lg transition-colors focus:ring-2 focus:ring-orange-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
|
||||
Enregistrer la configuration Humble Bundle
|
||||
</button>
|
||||
<a href="{{ url_for('twitchConfigurationHelp') }}" class="text-sm text-slate-600 dark:text-slate-400 hover:underline">Besoin d'aide ?</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||
<div class="px-5 py-4 border-b border-slate-200 dark:border-slate-700">
|
||||
<h2 class="text-lg font-medium text-slate-800 dark:text-white">Humble Bundle</h2>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<form action="{{ url_for('updateConfiguration') }}" method="POST" class="p-5 space-y-6">
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400">
|
||||
Activez les notifications pour recevoir automatiquement les nouveaux packs sur Discord.
|
||||
</p>
|
||||
|
||||
<label class="flex items-center gap-3 cursor-pointer">
|
||||
<input type="checkbox" name="humble_bundle_enable" {% if configuration.getValue('humble_bundle_enable') %}checked{% endif %} class="w-4 h-4 text-slate-600 bg-slate-100 dark:bg-slate-700 border-slate-300 dark:border-slate-600 rounded focus:ring-slate-500">
|
||||
<span class="text-sm text-slate-700 dark:text-slate-300">Activer les notifications Humble Bundle</span>
|
||||
</label>
|
||||
|
||||
<div>
|
||||
<label for="humble_bundle_channel" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Canal de notification</label>
|
||||
<select name="humble_bundle_channel" id="humble_bundle_channel" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
|
||||
{% for channel in channels %}
|
||||
<option value="{{ channel.id }}" {% if configuration.getIntValue('humble_bundle_channel') == channel.id %}selected{% endif %}>{{ channel.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600 text-white text-sm font-medium rounded-lg transition-colors">
|
||||
Enregistrer
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openTab(evt, tabName) {
|
||||
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
|
||||
document.querySelectorAll('.tab-button').forEach(el => {
|
||||
el.classList.remove('active', 'bg-slate-200', 'dark:bg-slate-600');
|
||||
el.classList.add('bg-slate-100', 'dark:bg-slate-700');
|
||||
el.classList.remove('bg-gray-200', 'dark:bg-gray-600', 'text-gray-900', 'dark:text-white');
|
||||
el.classList.add('bg-gray-100', 'dark:bg-gray-700', 'text-gray-600', 'dark:text-gray-400');
|
||||
});
|
||||
document.getElementById(tabName).classList.remove('hidden');
|
||||
evt.currentTarget.classList.add('active', 'bg-slate-200', 'dark:bg-slate-600');
|
||||
evt.currentTarget.classList.remove('bg-slate-100', 'dark:bg-slate-700');
|
||||
evt.currentTarget.classList.remove('bg-gray-100', 'dark:bg-gray-700', 'text-gray-600', 'dark:text-gray-400');
|
||||
evt.currentTarget.classList.add('bg-gray-200', 'dark:bg-gray-600', 'text-gray-900', 'dark:text-white');
|
||||
}
|
||||
document.getElementById("defaultOpen")?.click();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
{% extends "template.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-4">FreeLoot — Jeux gratuits</h1>
|
||||
{% if request.args.get('msg') %}
|
||||
{% set msg_type = request.args.get('type') %}
|
||||
<div class="mb-4 p-4 rounded-lg {% if msg_type == 'success' %}bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 text-green-700 dark:text-green-300{% elif msg_type == 'error' %}bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-300{% else %}bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 text-blue-700 dark:text-blue-300{% endif %}">
|
||||
{{ request.args.get('msg') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-4">
|
||||
<p class="text-gray-700 dark:text-gray-300">
|
||||
Notifications des jeux gratuits (Epic Games, Amazon Prime, GOG, Google Play, Apple App Store) via le flux
|
||||
<a href="https://feed.eikowagenknecht.com/lootscraper.xml" target="_blank" rel="noopener" class="text-amber-700 dark:text-amber-300 hover:underline">LootScraper</a>.
|
||||
Choisissez le canal Discord et les types de loot à notifier (PC, Android, iOS selon la source). Le bot vérifie le flux environ toutes les 30 minutes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<span class="text-3xl">🎁</span>
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Configuration FreeLoot</h2>
|
||||
</div>
|
||||
|
||||
<form action="{{ url_for('updateFreeLoot') }}" method="POST" class="space-y-6">
|
||||
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4 space-y-4">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" name="freeloot_enable" {% if configuration.getValue('freeloot_enable') %}checked{% endif %}
|
||||
class="w-5 h-5 rounded border-gray-300 dark:border-gray-600 text-amber-600 focus:ring-amber-500 dark:bg-gray-700">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Activer les notifications FreeLoot</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="freeloot_channel_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Canal Discord pour les notifications</label>
|
||||
<select name="freeloot_channel_id" id="freeloot_channel_id"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-amber-500 focus:border-transparent transition-all">
|
||||
<option value="">— Choisir un canal —</option>
|
||||
{% for channel in channels %}
|
||||
<option value="{{ channel.id }}" {% if configuration.getIntValue('freeloot_channel_id') == channel.id %}selected{% endif %}>{{ channel.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4 space-y-4">
|
||||
<h3 class="font-medium text-gray-800 dark:text-gray-200">Mentions (optionnel)</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Choisissez qui mentionner au début du message (avant l’embed).</p>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" name="freeloot_mention_everyone" {% if mention_everyone %}checked{% endif %}
|
||||
class="w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-amber-600 focus:ring-amber-500 dark:bg-gray-700">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">@everyone</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" name="freeloot_mention_here" {% if mention_here %}checked{% endif %}
|
||||
class="w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-amber-600 focus:ring-amber-500 dark:bg-gray-700">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">@here</span>
|
||||
</label>
|
||||
</div>
|
||||
{% if roles %}
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Rôles à mentionner</p>
|
||||
{% if roles|length > 1 %}
|
||||
<div class="flex flex-wrap gap-1 border-b border-gray-200 dark:border-gray-600 mb-3">
|
||||
{% for guild_data in roles %}
|
||||
<button type="button" class="freeloot-role-tab px-4 py-2 text-sm font-medium rounded-t-lg transition-colors {% if loop.first %}bg-gray-200 dark:bg-gray-600 text-gray-900 dark:text-white{% else %}bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600{% endif %}"
|
||||
data-tab="freeloot-roles-{{ guild_data.guild_id }}" {% if loop.first %}data-default{% endif %}>
|
||||
{{ guild_data.guild_name }}
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for guild_data in roles %}
|
||||
<div id="freeloot-roles-{{ guild_data.guild_id }}" class="freeloot-role-panel {% if not loop.first %}hidden{% endif %} max-h-48 overflow-y-auto border border-gray-200 dark:border-gray-600 rounded-lg p-3 space-y-2">
|
||||
{% for role in guild_data.roles %}
|
||||
<label class="flex items-center gap-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600/50 p-1 rounded">
|
||||
<input type="checkbox" name="freeloot_mention_roles" value="{{ role.id }}"
|
||||
{% if role.id|string in mention_role_ids %}checked{% endif %}
|
||||
class="w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-amber-600 focus:ring-amber-500 dark:bg-gray-700">
|
||||
{% if role.color is defined and role.color is not none and role.color.value != 0 %}
|
||||
<span class="w-3 h-3 rounded-full flex-shrink-0" style="background-color:#{{ '%06x'|format(role.color.value) }}"></span>
|
||||
{% else %}
|
||||
<span class="w-3 h-3 rounded-full flex-shrink-0 bg-gray-400"></span>
|
||||
{% endif %}
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{ role.name }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4 space-y-4">
|
||||
<h3 class="font-medium text-gray-800 dark:text-gray-200">Types de loot à notifier</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Cochez les sources et plateformes pour lesquelles vous voulez recevoir une notification.</p>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{% for key, label, emoji in sources %}
|
||||
<label class="flex items-center gap-2 cursor-pointer p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-600/50 transition-colors">
|
||||
<input type="checkbox" name="freeloot_sources" value="{{ key }}"
|
||||
{% if not enabled_sources or key in enabled_sources %}checked{% endif %}
|
||||
class="w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-amber-600 focus:ring-amber-500 dark:bg-gray-700">
|
||||
<span class="text-lg" title="{{ label }}">{{ emoji }}</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{ label }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit"
|
||||
class="px-6 py-2.5 bg-amber-600 hover:bg-amber-700 text-white font-medium rounded-lg transition-colors focus:ring-2 focus:ring-amber-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
|
||||
Enregistrer
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="mt-8 pt-8 border-t border-gray-200 dark:border-gray-700">
|
||||
<h3 class="font-medium text-gray-800 dark:text-gray-200 mb-3">Aperçu de l’embed Discord (style DraftBot)</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">Exemple du message envoyé dans le canal.</p>
|
||||
<div class="inline-block rounded-r-lg overflow-hidden border border-gray-300 dark:border-gray-600 bg-[#2f3136] max-w-lg shadow-lg" style="border-left: 4px solid #E67E22;">
|
||||
<div class="p-4">
|
||||
<div class="flex items-start gap-2 mb-2">
|
||||
<a href="#" class="text-[#00a8fc] hover:underline font-semibold text-base flex-1">Definitely Not Fried Chicken gratuit sur l'Epic Games Store !</a>
|
||||
<img src="https://store.epicgames.com/favicon.ico" alt="" class="w-10 h-10 rounded flex-shrink-0" title="Logo boutique">
|
||||
</div>
|
||||
<p class="text-[#dcddde] text-sm leading-relaxed mb-3">Definitely Not Fried Chicken is a business management sim with a twist! Grow your drugs trade through legitimate fronts, managing both sides of the business. Acquire new "businesses", meet new clientele, develop more potent narcotics…</p>
|
||||
<div class="text-sm mb-2">
|
||||
<span class="text-[#b9bbbe]">Prix</span>
|
||||
<p class="text-[#dcddde]"><strong>Gratuit</strong> • jusqu'au 05/02/2026</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-x-4 gap-y-1 text-sm mb-2">
|
||||
<div><span class="text-[#b9bbbe]">Prix recommandé</span><p class="text-[#dcddde]">39.99 EUR</p></div>
|
||||
<div><span class="text-[#b9bbbe]">Genres</span><p class="text-[#dcddde]">Simulation, Indie</p></div>
|
||||
<div><span class="text-[#b9bbbe]">Ratings</span><p class="text-[#dcddde]">PEGI 18, USK 18</p></div>
|
||||
</div>
|
||||
<p class="mb-3"><a href="#" class="text-[#00a8fc] hover:underline text-sm">Ouvrir dans la boutique !</a></p>
|
||||
<div class="rounded overflow-hidden bg-[#202225] aspect-video flex items-center justify-center my-2">
|
||||
<span class="text-4xl text-gray-500">🎁</span>
|
||||
</div>
|
||||
<p class="text-xs text-[#72767d] pt-1">MamieHenriette • FreeLoot</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('.freeloot-role-tab').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
var tabId = this.getAttribute('data-tab');
|
||||
document.querySelectorAll('.freeloot-role-panel').forEach(p => p.classList.add('hidden'));
|
||||
document.querySelectorAll('.freeloot-role-tab').forEach(b => {
|
||||
b.classList.remove('bg-gray-200', 'dark:bg-gray-600', 'text-gray-900', 'dark:text-white');
|
||||
b.classList.add('bg-gray-100', 'dark:bg-gray-700', 'text-gray-600', 'dark:text-gray-400');
|
||||
});
|
||||
document.getElementById(tabId).classList.remove('hidden');
|
||||
this.classList.remove('bg-gray-100', 'dark:bg-gray-700', 'text-gray-600', 'dark:text-gray-400');
|
||||
this.classList.add('bg-gray-200', 'dark:bg-gray-600', 'text-gray-900', 'dark:text-white');
|
||||
});
|
||||
});
|
||||
document.querySelector('.freeloot-role-tab[data-default]')?.click();
|
||||
</script>
|
||||
|
||||
{% if entries %}
|
||||
<div class="mt-8">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Jeux gratuits actuellement disponibles</h2>
|
||||
</div>
|
||||
<div class="p-6 overflow-x-auto">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{% for e in entries %}
|
||||
<article class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden hover:shadow-md transition-shadow flex flex-col">
|
||||
<div class="aspect-video bg-gray-100 dark:bg-gray-700 flex items-center justify-center overflow-hidden">
|
||||
{% if e.image_url %}
|
||||
<img src="{{ e.image_url }}" alt="" class="w-full h-full object-cover" loading="lazy" />
|
||||
{% else %}
|
||||
<span class="text-4xl text-gray-400 dark:text-gray-500">🎁</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="p-4 flex flex-col flex-1 border-t border-gray-200 dark:border-gray-700">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white mb-1 line-clamp-2" title="{{ e.game_name }}">{{ e.game_name }}</h3>
|
||||
<p class="flex items-center gap-1.5 text-sm text-gray-600 dark:text-gray-400 mb-2">
|
||||
<span>{{ e.emoji }}</span>
|
||||
<span>{{ e.source_label }}</span>
|
||||
</p>
|
||||
{% if e.recommended_price or e.genres or e.rating %}
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 space-y-0.5 mb-2">
|
||||
{% if e.recommended_price %}<p><span class="text-gray-500 dark:text-gray-500">Prix recommandé:</span> {{ e.recommended_price }}</p>{% endif %}
|
||||
{% if e.genres %}<p><span class="text-gray-500 dark:text-gray-500">Genres:</span> {{ e.genres }}</p>{% endif %}
|
||||
{% if e.rating %}<p><span class="text-gray-500 dark:text-gray-500">Ratings:</span> {{ e.rating }}</p>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if e.updated_formatted %}
|
||||
<p class="text-xs text-gray-500 dark:text-gray-500 mb-3">{{ e.updated_formatted }}</p>
|
||||
{% endif %}
|
||||
<div class="mt-auto flex flex-col gap-2">
|
||||
<form action="{{ url_for('send_free_loot_to_discord') }}" method="POST" class="w-full">
|
||||
<input type="hidden" name="entry_id" value="{{ e.id }}">
|
||||
<button type="submit" class="w-full inline-flex items-center justify-center gap-2 px-3 py-2 text-sm font-medium rounded-lg bg-indigo-600 hover:bg-indigo-700 text-white transition-colors focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03z"/></svg>
|
||||
Envoyer sur Discord
|
||||
</button>
|
||||
</form>
|
||||
{% if e.link %}
|
||||
<a href="{{ e.link }}" target="_blank" rel="noopener noreferrer" class="inline-flex items-center justify-center gap-2 w-full px-3 py-2 text-sm font-medium rounded-lg bg-amber-600 hover:bg-amber-700 text-white transition-colors focus:ring-2 focus:ring-amber-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
|
||||
Ouvrir dans la boutique
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path></svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="mt-8 p-4 rounded-lg bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<p class="text-gray-600 dark:text-gray-400 text-sm">Le flux LootScraper n’a pas pu être chargé. Réessayez plus tard.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -1,42 +1,67 @@
|
||||
{% extends "template.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-semibold text-slate-800 dark:text-white mb-1">Humeurs</h1>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400">
|
||||
Statuts Discord qui changeront automatiquement toutes les 10 minutes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden mb-6">
|
||||
{% if humeurs %}
|
||||
<ul class="divide-y divide-slate-200 dark:divide-slate-700">
|
||||
{% for humeur in humeurs %}
|
||||
<li class="flex items-center justify-between px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors group">
|
||||
<span class="text-sm text-slate-700 dark:text-slate-300">{{ humeur.text }}</span>
|
||||
<a href="{{ url_for('delHumeur', id = humeur.id) }}" onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette humeur ?')" class="p-1.5 text-slate-400 hover:text-red-500 dark:hover:text-red-400 rounded opacity-0 group-hover:opacity-100 transition-all" title="Supprimer">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<div class="px-4 py-8 text-center">
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">Aucune humeur configurée</p>
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-4">Humeurs de Mamie</h1>
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<p class="text-gray-700 dark:text-gray-300">
|
||||
Définissez les statuts Discord qui changeront automatiquement toutes les 10 minutes pour donner de la personnalité à votre bot.
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 p-5">
|
||||
<h2 class="text-lg font-medium text-slate-800 dark:text-white mb-4">Ajouter une humeur</h2>
|
||||
<div class="mb-8">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Liste des humeurs</h2>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Texte</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-24">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{% for humeur in humeurs %}
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
|
||||
<td class="px-6 py-4 text-gray-700 dark:text-gray-300">{{ humeur.text }}</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
<a href="{{ url_for('delHumeur', id = humeur.id) }}"
|
||||
onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette humeur ?')"
|
||||
class="p-2 rounded-lg hover:bg-red-100 dark:hover:bg-red-900/30 transition-colors text-red-600 dark:text-red-400 inline-block"
|
||||
title="Supprimer">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="2" class="px-6 py-8 text-center text-gray-500 dark:text-gray-400">
|
||||
Aucune humeur configurée. Ajoutez-en une ci-dessous.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-6">Ajouter une humeur</h2>
|
||||
|
||||
<form action="{{ url_for('addHumeur') }}" method="POST">
|
||||
<div class="flex flex-col sm:flex-row gap-3">
|
||||
<div class="flex-1">
|
||||
<input type="text" name="text" id="text" placeholder="Joue à un super jeu..." class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
|
||||
</div>
|
||||
<button type="submit" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600 text-white text-sm font-medium rounded-lg transition-colors whitespace-nowrap">
|
||||
Ajouter
|
||||
<form action="{{ url_for('addHumeur') }}" method="POST" class="space-y-6">
|
||||
<div>
|
||||
<label for="text" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Texte du statut</label>
|
||||
<input name="text" id="text" type="text" required
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all"
|
||||
placeholder="Joue à un jeu vidéo..."/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="submit"
|
||||
class="px-6 py-2.5 bg-purple-600 hover:bg-purple-700 text-white font-medium rounded-lg transition-colors focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
|
||||
Ajouter l'humeur
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
+56
-71
@@ -10,84 +10,69 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-10">
|
||||
<a href="/live-alert" class="group bg-white dark:bg-slate-800 rounded-lg p-5 border border-slate-200 dark:border-slate-700 hover:border-slate-300 dark:hover:border-slate-600 hover:shadow-md transition-all">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-slate-100 dark:bg-slate-700 flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-slate-600 dark:text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-medium text-slate-800 dark:text-white">Alertes Live</h3>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">Notifications Twitch</p>
|
||||
</div>
|
||||
<svg class="w-5 h-5 text-slate-400 ml-auto opacity-0 group-hover:opacity-100 transition-opacity" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden mb-8">
|
||||
<div class="p-4 sm:p-6 border-b border-slate-200 dark:border-slate-700 flex flex-wrap items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="inline-flex items-center justify-center w-3 h-3 rounded-full {% if discord_connected %}bg-emerald-500 ring-4 ring-emerald-500/30{% else %}bg-slate-400 ring-4 ring-slate-400/30{% endif %}" title="{% if discord_connected %}Bot Discord connecté{% else %}Bot Discord déconnecté{% endif %}"></span>
|
||||
<h2 class="text-xl font-semibold text-slate-800 dark:text-white">Discord</h2>
|
||||
</div>
|
||||
</a>
|
||||
<span class="text-sm text-slate-500 dark:text-slate-400">
|
||||
{% if discord_connected %}Connecté{% else %}Déconnecté{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="p-4 sm:p-6 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div class="rounded-lg bg-slate-50 dark:bg-slate-700/50 p-4 border border-slate-200 dark:border-slate-600">
|
||||
<p class="text-sm font-medium text-slate-500 dark:text-slate-400">Serveurs connectés</p>
|
||||
<p class="text-2xl font-bold text-slate-800 dark:text-white mt-1">{{ discord_guild_count }}</p>
|
||||
</div>
|
||||
<div class="rounded-lg bg-slate-50 dark:bg-slate-700/50 p-4 border border-slate-200 dark:border-slate-600">
|
||||
<p class="text-sm font-medium text-slate-500 dark:text-slate-400">Sanctions enregistrées</p>
|
||||
<p class="text-2xl font-bold text-slate-800 dark:text-white mt-1">{{ sanctions_count }}</p>
|
||||
</div>
|
||||
<div class="rounded-lg bg-slate-50 dark:bg-slate-700/50 p-4 border border-slate-200 dark:border-slate-600 sm:col-span-2 lg:col-span-2 flex items-center justify-center">
|
||||
<div class="flex flex-wrap gap-3 justify-center">
|
||||
<a href="/live-alert" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white text-sm font-medium hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors">Alertes Live</a>
|
||||
<a href="/youtube" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white text-sm font-medium hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors">Notification YouTube</a>
|
||||
<a href="/humeurs" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white text-sm font-medium hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors">Humeurs</a>
|
||||
<a href="/protondb" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white text-sm font-medium hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors">ProtonDB</a>
|
||||
<a href="/commandes" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white text-sm font-medium hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors">Commandes</a>
|
||||
<a href="/moderation" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white text-sm font-medium hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors">Modération</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="/commandes" class="group bg-white dark:bg-slate-800 rounded-lg p-5 border border-slate-200 dark:border-slate-700 hover:border-slate-300 dark:hover:border-slate-600 hover:shadow-md transition-all">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-slate-100 dark:bg-slate-700 flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-slate-600 dark:text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-medium text-slate-800 dark:text-white">Commandes</h3>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">Commandes personnalisées</p>
|
||||
</div>
|
||||
<svg class="w-5 h-5 text-slate-400 ml-auto opacity-0 group-hover:opacity-100 transition-opacity" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden mb-8">
|
||||
<div class="p-4 sm:p-6 border-b border-slate-200 dark:border-slate-700 flex flex-wrap items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="inline-flex items-center justify-center w-3 h-3 rounded-full {% if twitch_connected %}bg-emerald-500 ring-4 ring-emerald-500/30{% else %}bg-slate-400 ring-4 ring-slate-400/30{% endif %}" title="{% if twitch_connected %}Bot Twitch connecté{% else %}Bot Twitch déconnecté{% endif %}"></span>
|
||||
<h2 class="text-xl font-semibold text-slate-800 dark:text-white">Twitch</h2>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="/humeurs" class="group bg-white dark:bg-slate-800 rounded-lg p-5 border border-slate-200 dark:border-slate-700 hover:border-slate-300 dark:hover:border-slate-600 hover:shadow-md transition-all">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-slate-100 dark:bg-slate-700 flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-slate-600 dark:text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-medium text-slate-800 dark:text-white">Humeurs</h3>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">Statuts Discord rotatifs</p>
|
||||
</div>
|
||||
<svg class="w-5 h-5 text-slate-400 ml-auto opacity-0 group-hover:opacity-100 transition-opacity" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
|
||||
<span class="text-sm text-slate-500 dark:text-slate-400">
|
||||
{% if twitch_connected %}Connecté{% else %}Déconnecté{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="p-4 sm:p-6 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div class="rounded-lg bg-slate-50 dark:bg-slate-700/50 p-4 border border-slate-200 dark:border-slate-600">
|
||||
<p class="text-sm font-medium text-slate-500 dark:text-slate-400">Canal connecté</p>
|
||||
<p class="text-2xl font-bold text-slate-800 dark:text-white mt-1">{% if twitch_channel_name %}{{ twitch_channel_name }}{% else %}—{% endif %}</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="/moderation" class="group bg-white dark:bg-slate-800 rounded-lg p-5 border border-slate-200 dark:border-slate-700 hover:border-slate-300 dark:hover:border-slate-600 hover:shadow-md transition-all">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-slate-100 dark:bg-slate-700 flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-slate-600 dark:text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-medium text-slate-800 dark:text-white">Modération</h3>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">Historique et actions</p>
|
||||
</div>
|
||||
<svg class="w-5 h-5 text-slate-400 ml-auto opacity-0 group-hover:opacity-100 transition-opacity" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
|
||||
<div class="rounded-lg bg-slate-50 dark:bg-slate-700/50 p-4 border border-slate-200 dark:border-slate-600">
|
||||
<p class="text-sm font-medium text-slate-500 dark:text-slate-400">Annonces configurées</p>
|
||||
<p class="text-2xl font-bold text-slate-800 dark:text-white mt-1">{{ twitch_announcements_count }}</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="/protondb" class="group bg-white dark:bg-slate-800 rounded-lg p-5 border border-slate-200 dark:border-slate-700 hover:border-slate-300 dark:hover:border-slate-600 hover:shadow-md transition-all">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-slate-100 dark:bg-slate-700 flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-slate-600 dark:text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 4a2 2 0 114 0v1a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-1a2 2 0 100 4h1a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-1a2 2 0 10-4 0v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-3a1 1 0 00-1-1H4a2 2 0 110-4h1a1 1 0 001-1V7a1 1 0 011-1h3a1 1 0 001-1V4z"></path></svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-medium text-slate-800 dark:text-white">ProtonDB</h3>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">Compatibilité Linux</p>
|
||||
</div>
|
||||
<svg class="w-5 h-5 text-slate-400 ml-auto opacity-0 group-hover:opacity-100 transition-opacity" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
|
||||
<div class="rounded-lg bg-slate-50 dark:bg-slate-700/50 p-4 border border-slate-200 dark:border-slate-600">
|
||||
<p class="text-sm font-medium text-slate-500 dark:text-slate-400">Actions de modération</p>
|
||||
<p class="text-2xl font-bold text-slate-800 dark:text-white mt-1">{{ twitch_moderation_count }}</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a href="/configurations" class="group bg-white dark:bg-slate-800 rounded-lg p-5 border border-slate-200 dark:border-slate-700 hover:border-slate-300 dark:hover:border-slate-600 hover:shadow-md transition-all">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-slate-100 dark:bg-slate-700 flex items-center justify-center">
|
||||
<svg class="w-5 h-5 text-slate-600 dark:text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
||||
<div class="rounded-lg bg-slate-50 dark:bg-slate-700/50 p-4 border border-slate-200 dark:border-slate-600 flex items-center justify-center">
|
||||
<div class="flex flex-wrap gap-3 justify-center">
|
||||
<a href="/announcements" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-purple-600 text-white text-sm font-medium hover:bg-purple-700 transition-colors">Annonces</a>
|
||||
<a href="/twitch-moderation" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white text-sm font-medium hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors">Modération</a>
|
||||
<a href="/link-filter" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-slate-200 dark:bg-slate-600 text-slate-800 dark:text-white text-sm font-medium hover:bg-slate-300 dark:hover:bg-slate-500 transition-colors">Filtre de liens</a>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-medium text-slate-800 dark:text-white">Configurations</h3>
|
||||
<p class="text-sm text-slate-500 dark:text-slate-400">Paramètres du bot</p>
|
||||
</div>
|
||||
<svg class="w-5 h-5 text-slate-400 ml-auto opacity-0 group-hover:opacity-100 transition-opacity" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg p-6 border border-slate-200 dark:border-slate-700">
|
||||
@@ -98,7 +83,7 @@
|
||||
<div>
|
||||
<h3 class="font-medium text-slate-800 dark:text-white mb-2">À propos</h3>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400 mb-4">
|
||||
Mamie Henriette est un bot open source pour Discord et Twitch, développé par la communauté.
|
||||
Mamie Henriette est un bot open source pour Discord et Twitch, développé par la communauté.
|
||||
Cette interface vous permet de configurer et gérer toutes les fonctionnalités.
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
{% extends "template.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">Filtre de liens</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400">Bloquez les liens non autorises sur votre chat Twitch.</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-3 rounded-lg {{ 'bg-green-100 dark:bg-green-900/30' if config.enabled else 'bg-gray-100 dark:bg-gray-700' }}">
|
||||
<svg class="w-6 h-6 {{ 'text-green-600 dark:text-green-400' if config.enabled else 'text-gray-500' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Protection des liens</h2>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">{{ 'Active' if config.enabled else 'Desactive' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ url_for('toggle_link_filter') }}" class="px-4 py-2 rounded-lg font-medium transition-colors {{ 'bg-red-100 text-red-700 hover:bg-red-200 dark:bg-red-900/30 dark:text-red-400' if config.enabled else 'bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400' }}">
|
||||
{{ 'Desactiver' if config.enabled else 'Activer' }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form action="{{ url_for('update_link_filter') }}" method="POST" class="space-y-6">
|
||||
<div class="grid md:grid-cols-2 gap-6">
|
||||
<div class="space-y-4">
|
||||
<h3 class="font-medium text-gray-900 dark:text-white">Autoriser les liens pour</h3>
|
||||
<label class="flex items-center gap-3 p-3 rounded-lg border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
||||
<input type="checkbox" name="allow_moderators" {{ 'checked' if config.allow_moderators }} class="rounded border-gray-300 text-purple-600 focus:ring-purple-500">
|
||||
<div>
|
||||
<span class="text-gray-900 dark:text-white font-medium">Moderateurs</span>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">Les modos peuvent toujours poster des liens</p>
|
||||
</div>
|
||||
</label>
|
||||
<label class="flex items-center gap-3 p-3 rounded-lg border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
||||
<input type="checkbox" name="allow_vips" {{ 'checked' if config.allow_vips }} class="rounded border-gray-300 text-purple-600 focus:ring-purple-500">
|
||||
<div>
|
||||
<span class="text-gray-900 dark:text-white font-medium">VIP</span>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">Les VIP peuvent poster des liens</p>
|
||||
</div>
|
||||
</label>
|
||||
<label class="flex items-center gap-3 p-3 rounded-lg border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer">
|
||||
<input type="checkbox" name="allow_subscribers" {{ 'checked' if config.allow_subscribers }} class="rounded border-gray-300 text-purple-600 focus:ring-purple-500">
|
||||
<div>
|
||||
<span class="text-gray-900 dark:text-white font-medium">Abonnes</span>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">Les abonnes peuvent poster des liens</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Duree du timeout (secondes)</label>
|
||||
<input type="number" name="timeout_duration" value="{{ config.timeout_duration }}" min="0" max="1209600"
|
||||
class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 focus:border-transparent">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">0 = pas de timeout, juste suppression du message</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Message d'avertissement</label>
|
||||
<textarea name="warning_message" rows="2"
|
||||
class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 focus:border-transparent resize-none">{{ config.warning_message }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition-colors font-medium">
|
||||
Enregistrer les parametres
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-6">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"></path>
|
||||
</svg>
|
||||
Domaines autorises ({{ domains|length }})
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<form action="{{ url_for('add_allowed_domain') }}" method="POST" class="p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex gap-2">
|
||||
<input type="text" name="domain" placeholder="exemple.com" required
|
||||
class="flex-1 px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 focus:border-transparent text-sm">
|
||||
<button type="submit" class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors text-sm font-medium">
|
||||
Ajouter
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if domains %}
|
||||
<div class="max-h-48 overflow-y-auto">
|
||||
<div class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{% for domain in domains %}
|
||||
<div class="px-4 py-2 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
||||
<code class="text-sm text-gray-700 dark:text-gray-300 font-mono">{{ domain.domain }}</code>
|
||||
<a href="{{ url_for('delete_allowed_domain', domain_id=domain.id) }}" class="text-red-600 hover:text-red-700 text-sm">Supprimer</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="px-4 py-6 text-center text-gray-500 dark:text-gray-400 text-sm">
|
||||
Aucun domaine autorise
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
|
||||
</svg>
|
||||
Viewers autorises ({{ users|length }})
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<form action="{{ url_for('add_allowed_user') }}" method="POST" class="p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex gap-2">
|
||||
<input type="text" name="username" placeholder="@pseudo" required
|
||||
class="flex-1 px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 focus:border-transparent text-sm">
|
||||
<button type="submit" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors text-sm font-medium">
|
||||
Ajouter
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% if users %}
|
||||
<div class="max-h-48 overflow-y-auto">
|
||||
<div class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{% for user in users %}
|
||||
<div class="px-4 py-2 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">@{{ user.username }}</span>
|
||||
<a href="{{ url_for('delete_allowed_user', user_id=user.id) }}" class="text-red-600 hover:text-red-700 text-sm">Supprimer</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="px-4 py-6 text-center text-gray-500 dark:text-gray-400 text-sm">
|
||||
Aucun viewer en liste blanche
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gradient-to-r from-blue-50 to-purple-50 dark:from-blue-900/20 dark:to-purple-900/20 rounded-xl p-6 border border-blue-200 dark:border-blue-800">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
Commande Permit
|
||||
</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400 text-sm mb-4">
|
||||
Utilisez la commande <code class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 rounded text-xs">!permit</code> pour autoriser temporairement un viewer a poster un lien.
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-4 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<code class="px-2 py-1 bg-white dark:bg-gray-800 rounded text-xs font-mono text-purple-600 dark:text-purple-400">!permit @viewer</code>
|
||||
<span class="text-gray-600 dark:text-gray-400">Autorise 1 min</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<code class="px-2 py-1 bg-white dark:bg-gray-800 rounded text-xs font-mono text-purple-600 dark:text-purple-400">!permit @viewer 5</code>
|
||||
<span class="text-gray-600 dark:text-gray-400">Autorise 5 min</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
+314
-114
@@ -1,135 +1,335 @@
|
||||
{% extends "template.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-semibold text-slate-800 dark:text-white mb-1">Alertes Live</h1>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400">
|
||||
Chaînes Twitch surveillées. Vérification toutes les 5 minutes, maximum 100 chaînes.
|
||||
</p>
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-4">Alerte Live</h1>
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<p class="text-gray-700 dark:text-gray-300">
|
||||
Liste des chaînes surveillées pour les alertes de live Twitch.
|
||||
Le bot vérifie toutes les 5 minutes qui est en live dans la liste en dessous.
|
||||
Le bot enregistre le status de stream toutes les 5 minutes, quand le status passe de "hors-ligne" à "en ligne" alors
|
||||
le bot enverra une notification (embed Discord) sur le canal choisi.
|
||||
<span class="font-medium text-blue-600 dark:text-blue-400">Ne peut surveiller qu'au maximum 100 chaînes.</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if not alert %}
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden mb-6">
|
||||
<div class="px-5 py-4 border-b border-slate-200 dark:border-slate-700">
|
||||
<h2 class="text-lg font-medium text-slate-800 dark:text-white">Alertes configurées</h2>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead>
|
||||
<tr class="bg-slate-50 dark:bg-slate-700/50 border-b border-slate-200 dark:border-slate-700">
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Chaîne</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Canal Discord</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Message</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-200 dark:divide-slate-700">
|
||||
{% for alert in alerts %}
|
||||
<tr class="hover:bg-slate-50 dark:hover:bg-slate-700/30 transition-colors">
|
||||
<td class="px-4 py-3">
|
||||
<a href="https://www.twitch.tv/{{ alert.login }}" target="_blank" class="text-sm font-medium text-slate-700 dark:text-slate-300 hover:underline">{{ alert.login }}</a>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<span class="text-sm text-slate-600 dark:text-slate-400">#{{ alert.notify_channel_name }}</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-600 dark:text-slate-400 max-w-xs">
|
||||
<div class="line-clamp-2">{{ alert.message }}</div>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<a href="{{ url_for('toggleLiveAlert', id = alert.id) }}" class="text-sm text-slate-500 hover:text-slate-700 dark:hover:text-slate-300 transition-colors" title="{{ 'Désactiver' if alert.enable else 'Activer' }}">
|
||||
{% if alert.enable %}
|
||||
<span class="text-green-600 dark:text-green-500">Actif</span>
|
||||
{% else %}
|
||||
<span class="text-slate-400">Inactif</span>
|
||||
{% endif %}
|
||||
<div class="mb-8">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Alertes configurées</h2>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Chaîne</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Canal</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Message / Embed</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider" title="Le bot affichera 'Regarde [streamer]' comme activité">Activité</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{% for alert in alerts %}
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<a href="https://www.twitch.tv/{{alert.login}}" target="_blank" class="text-purple-600 dark:text-purple-400 hover:underline font-medium">{{alert.login}}</a>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-gray-700 dark:text-gray-300">{{alert.notify_channel_name}}</td>
|
||||
<td class="px-6 py-4 text-gray-600 dark:text-gray-400 max-w-md truncate">{{alert.message or '(embed)'}}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center">
|
||||
<a href="{{ url_for('toggleWatchActivity', id = alert.id) }}"
|
||||
class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors"
|
||||
title="{{ 'Désactiver l\'activité' if alert.watch_activity else 'Activer l\'activité' }}">
|
||||
{{ '👁️' if alert.watch_activity else '👁️🗨️' }}
|
||||
</a>
|
||||
<a href="{{ url_for('openEditLiveAlert', id = alert.id) }}" class="text-sm text-slate-500 hover:text-slate-700 dark:hover:text-slate-300 transition-colors">
|
||||
Modifier
|
||||
</a>
|
||||
<a href="{{ url_for('delLiveAlert', id = alert.id) }}" onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette alerte ?')" class="text-sm text-slate-500 hover:text-red-600 dark:hover:text-red-400 transition-colors">
|
||||
Supprimer
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4" class="px-4 py-8 text-center text-sm text-slate-500 dark:text-slate-400">
|
||||
Aucune alerte configurée
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center">
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<a href="{{ url_for('toggleLiveAlert', id = alert.id) }}"
|
||||
class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors"
|
||||
title="{{ 'Désactiver' if alert.enable else 'Activer' }}">
|
||||
{{ '✅' if alert.enable else '❌' }}
|
||||
</a>
|
||||
<a href="{{ url_for('openEditLiveAlert', id = alert.id) }}"
|
||||
class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors text-blue-600 dark:text-blue-400"
|
||||
title="Modifier">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
|
||||
</a>
|
||||
<a href="{{ url_for('delLiveAlert', id = alert.id) }}"
|
||||
onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette alerte ?')"
|
||||
class="p-2 rounded-lg hover:bg-red-100 dark:hover:bg-red-900/30 transition-colors text-red-600 dark:text-red-400"
|
||||
title="Supprimer">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="5" class="px-6 py-8 text-center text-gray-500 dark:text-gray-400">
|
||||
Aucune alerte configurée. Ajoutez-en une ci-dessous.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 p-5">
|
||||
<h2 class="text-lg font-medium text-slate-800 dark:text-white mb-5">
|
||||
{% if alert %}Modifier l'alerte{% else %}Ajouter une alerte{% endif %}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-6">
|
||||
{{ 'Modifier l\'alerte' if alert else 'Ajouter une alerte de Live' }}
|
||||
</h2>
|
||||
|
||||
<form action="{{ url_for('submitEditLiveAlert', id = alert.id) if alert else url_for('addLiveAlert') }}" method="POST" class="space-y-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="login" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Chaîne Twitch</label>
|
||||
<input type="text" name="login" id="login" maxlength="32" required value="{{ alert.login if alert else '' }}" placeholder="chainesteve" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
|
||||
<p class="mt-1 text-xs text-slate-500 dark:text-slate-400">Le login de la chaîne, ex: chainesteve</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="notify_channel" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Canal de Notification</label>
|
||||
<select name="notify_channel" id="notify_channel" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
|
||||
{% for channel in channels %}
|
||||
<option value="{{ channel.id }}" {% if alert and alert.notify_channel == channel.id %}selected{% endif %}>{{ channel.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<div>
|
||||
<label for="message" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Message de notification</label>
|
||||
<textarea name="message" id="message" rows="4" required placeholder="🔴 **{0.user_name}** est en live !" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all resize-none">{{ alert.message if alert else '' }}</textarea>
|
||||
<form id="live-alert-form" action="{{ url_for('submitEditLiveAlert', id = alert.id) if alert else url_for('addLiveAlert') }}" method="POST" class="space-y-6">
|
||||
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4 space-y-4">
|
||||
<h3 class="font-medium text-gray-800 dark:text-gray-200">Configuration de base</h3>
|
||||
|
||||
<div>
|
||||
<label for="login" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Chaîne Twitch</label>
|
||||
<input name="login" id="login" type="text" maxlength="32" required
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all"
|
||||
placeholder="chainesteve"
|
||||
value="{{alert.login if alert}}"/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="notify_channel" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Canal de notification Discord</label>
|
||||
<select name="notify_channel" id="notify_channel"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all">
|
||||
{% for channel in channels %}
|
||||
<option value="{{channel.id}}" {% if alert and alert.notify_channel == channel.id %}selected{% endif %}>{{channel.name}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="message" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Message (optionnel, avant l'embed)</label>
|
||||
<textarea name="message" id="message" rows="2"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all resize-y"
|
||||
placeholder="Message envoyé avant l'embed">{{alert.message if alert}}</textarea>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Variables: {user_name}, {title}, [lien](https://www.twitch.tv/{user_login})</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<input type="checkbox" name="watch_activity" id="watch_activity" value="1"
|
||||
class="w-5 h-5 rounded border-gray-300 dark:border-gray-600 text-purple-600 focus:ring-purple-500 dark:bg-gray-700"
|
||||
{% if alert and alert.watch_activity %}checked{% endif %}>
|
||||
<label for="watch_activity" class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Afficher "Regarde ce stream" comme activité du bot Discord
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4 space-y-4">
|
||||
<h3 class="font-medium text-gray-800 dark:text-gray-200">Personnalisation de l'embed Discord</h3>
|
||||
|
||||
<div>
|
||||
<label for="embed_title" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Titre de l'embed</label>
|
||||
<input name="embed_title" id="embed_title" type="text" maxlength="256"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all"
|
||||
placeholder="{title}"
|
||||
value="{{alert.embed_title if alert else '{title}'}}"/>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Variables: {title}, {user_name}, {game_name}, {stream_url}, {user_login}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="embed_description" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Description de l'embed</label>
|
||||
<textarea name="embed_description" id="embed_description" rows="2"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all resize-y"
|
||||
placeholder="Description optionnelle">{{alert.embed_description if alert}}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="embed_color" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Couleur</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input name="embed_color" id="embed_color" type="color"
|
||||
class="w-12 h-10 rounded border border-gray-300 dark:border-gray-600 cursor-pointer"
|
||||
value="#{{alert.embed_color if alert else '9146FF'}}"/>
|
||||
<input type="text" id="embed_color_text" maxlength="6"
|
||||
class="flex-1 px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white font-mono text-sm"
|
||||
value="{{alert.embed_color if alert else '9146FF'}}" placeholder="9146FF"/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="embed_author_name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Nom de l'auteur</label>
|
||||
<input name="embed_author_name" id="embed_author_name" type="text" maxlength="256"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all"
|
||||
placeholder="{user_name}"
|
||||
value="{{alert.embed_author_name if alert}}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="embed_author_icon" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Icône de l'auteur (URL)</label>
|
||||
<input name="embed_author_icon" id="embed_author_icon" type="text" maxlength="512"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all"
|
||||
placeholder="Laissez vide pour l'avatar Twitch"
|
||||
value="{{alert.embed_author_icon if alert}}"/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="embed_footer" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Pied de page</label>
|
||||
<input name="embed_footer" id="embed_footer" type="text" maxlength="2048"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all"
|
||||
placeholder="Texte optionnel en bas"
|
||||
value="{{alert.embed_footer if alert}}"/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" name="embed_thumbnail" id="embed_thumbnail"
|
||||
{% if not alert or alert.embed_thumbnail %}checked{% endif %}
|
||||
class="w-5 h-5 rounded border-gray-300 dark:border-gray-600 text-purple-600 focus:ring-purple-500 dark:bg-gray-700">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Miniature (preview)</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" name="embed_image" id="embed_image"
|
||||
{% if not alert or alert.embed_image %}checked{% endif %}
|
||||
class="w-5 h-5 rounded border-gray-300 dark:border-gray-600 text-purple-600 focus:ring-purple-500 dark:bg-gray-700">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Image principale</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<button type="submit"
|
||||
class="px-6 py-2.5 bg-purple-600 hover:bg-purple-700 text-white font-medium rounded-lg transition-colors focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
|
||||
{{ 'Enregistrer' if alert else 'Ajouter l\'alerte' }}
|
||||
</button>
|
||||
{% if alert %}
|
||||
<a href="{{ url_for('openLiveAlert') }}"
|
||||
class="px-6 py-2.5 bg-gray-200 dark:bg-gray-600 hover:bg-gray-300 dark:hover:bg-gray-500 text-gray-700 dark:text-gray-200 font-medium rounded-lg transition-colors">
|
||||
Annuler
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="bg-slate-50 dark:bg-slate-700/50 rounded-lg p-4">
|
||||
<p class="text-sm font-medium text-slate-700 dark:text-slate-300 mb-3">Variables disponibles :</p>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<code class="px-2 py-0.5 bg-slate-200 dark:bg-slate-600 rounded text-xs font-mono">{0.user_login}</code>
|
||||
<span class="text-slate-600 dark:text-slate-400">Lien vers la chaîne</span>
|
||||
|
||||
<div>
|
||||
<h3 class="font-medium text-gray-800 dark:text-gray-200 mb-4">Prévisualisation de l'embed Discord</h3>
|
||||
<div id="embed-preview" class="bg-[#2f3136] rounded p-4 font-sans text-[#dcddde] max-w-xl border-l-4" style="border-left-color: #9146FF;">
|
||||
<div id="embed-author" class="flex items-center mb-2 text-sm">
|
||||
<img id="embed-author-icon" src="https://static-cdn.jtvnw.net/ttv-favicon/favicon-32x32.png" class="w-5 h-5 rounded-full mr-2" onerror="this.style.display='none'"/>
|
||||
<span id="embed-author-name" class="font-semibold">Nom du streamer</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<code class="px-2 py-0.5 bg-slate-200 dark:bg-slate-600 rounded text-xs font-mono">{0.user_name}</code>
|
||||
<span class="text-slate-600 dark:text-slate-400">Nom du streamer</span>
|
||||
<a id="embed-title" href="#" class="text-[#00aff4] no-underline text-base font-semibold block mb-2">Titre du stream</a>
|
||||
<div id="embed-description" class="text-sm leading-relaxed mb-2 text-[#dcddde]"></div>
|
||||
<div id="embed-thumbnail-container" class="my-2">
|
||||
<img id="embed-thumbnail" src="" class="max-w-[80px] max-h-[80px] rounded float-right ml-4 hidden"/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<code class="px-2 py-0.5 bg-slate-200 dark:bg-slate-600 rounded text-xs font-mono">{0.game_name}</code>
|
||||
<span class="text-slate-600 dark:text-slate-400">Jeu en cours</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<code class="px-2 py-0.5 bg-slate-200 dark:bg-slate-600 rounded text-xs font-mono">{0.title}</code>
|
||||
<span class="text-slate-600 dark:text-slate-400">Titre du stream</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<code class="px-2 py-0.5 bg-slate-200 dark:bg-slate-600 rounded text-xs font-mono">{0.language}</code>
|
||||
<span class="text-slate-600 dark:text-slate-400">Langue du stream</span>
|
||||
<div id="embed-image-container" class="mt-4">
|
||||
<img id="embed-image" src="https://static-cdn.jtvnw.net/previews-ttv/live_user_chaine-320x180.jpg" class="max-w-full rounded hidden"/>
|
||||
</div>
|
||||
<div id="embed-footer" class="mt-2 text-xs text-[#72767d]"></div>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">Cette prévisualisation est approximative.</p>
|
||||
|
||||
<div class="mt-6 bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4">
|
||||
<h4 class="font-medium text-gray-800 dark:text-gray-200 mb-2">Variables disponibles (embed)</h4>
|
||||
<ul class="text-sm text-gray-600 dark:text-gray-400 space-y-1">
|
||||
<li><code class="px-1.5 py-0.5 bg-gray-200 dark:bg-gray-600 rounded">{user_login}</code> — Login Twitch</li>
|
||||
<li><code class="px-1.5 py-0.5 bg-gray-200 dark:bg-gray-600 rounded">{user_name}</code> — Nom d'affichage</li>
|
||||
<li><code class="px-1.5 py-0.5 bg-gray-200 dark:bg-gray-600 rounded">{game_name}</code> — Jeu en cours</li>
|
||||
<li><code class="px-1.5 py-0.5 bg-gray-200 dark:bg-gray-600 rounded">{title}</code> — Titre du stream</li>
|
||||
<li><code class="px-1.5 py-0.5 bg-gray-200 dark:bg-gray-600 rounded">{language}</code> — Langue</li>
|
||||
<li><code class="px-1.5 py-0.5 bg-gray-200 dark:bg-gray-600 rounded">{stream_url}</code> — Lien Twitch</li>
|
||||
<li><code class="px-1.5 py-0.5 bg-gray-200 dark:bg-gray-600 rounded">{thumbnail}</code> — URL preview</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end gap-3">
|
||||
{% if alert %}
|
||||
<a href="{{ url_for('openLiveAlert') }}" class="px-4 py-2 text-slate-700 dark:text-slate-300 text-sm font-medium rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors">
|
||||
Annuler
|
||||
</a>
|
||||
{% endif %}
|
||||
<button type="submit" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600 text-white text-sm font-medium rounded-lg transition-colors">
|
||||
{% if alert %}Enregistrer{% else %}Ajouter{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function formatText(text, vars) {
|
||||
if (!text) return '';
|
||||
return text.replace(/\{(\w+)\}/g, function(match, key) {
|
||||
return vars[key] !== undefined && vars[key] !== null ? vars[key] : match;
|
||||
});
|
||||
}
|
||||
|
||||
function updatePreview() {
|
||||
const embedTitle = document.getElementById('embed_title').value || '{title}';
|
||||
const embedDescription = document.getElementById('embed_description').value || '';
|
||||
const embedColor = document.getElementById('embed_color_text').value || '9146FF';
|
||||
const embedAuthorName = document.getElementById('embed_author_name').value || '{user_name}';
|
||||
const embedAuthorIcon = document.getElementById('embed_author_icon').value || '';
|
||||
const embedFooter = document.getElementById('embed_footer').value || '';
|
||||
const embedThumbnail = document.getElementById('embed_thumbnail').checked;
|
||||
const embedImage = document.getElementById('embed_image').checked;
|
||||
|
||||
const vars = {
|
||||
user_login: 'chainesteve',
|
||||
user_name: 'ChaîneSteve',
|
||||
game_name: 'Minecraft',
|
||||
title: '🔴 Live chill avec les viewers',
|
||||
language: 'fr',
|
||||
stream_url: 'https://www.twitch.tv/chainesteve',
|
||||
thumbnail: 'https://static-cdn.jtvnw.net/previews-ttv/live_user_chainesteve-320x180.jpg'
|
||||
};
|
||||
|
||||
document.getElementById('embed-title').textContent = formatText(embedTitle, vars);
|
||||
document.getElementById('embed-title').href = vars.stream_url;
|
||||
document.getElementById('embed-description').textContent = formatText(embedDescription, vars);
|
||||
document.getElementById('embed-author-name').textContent = formatText(embedAuthorName, vars);
|
||||
document.getElementById('embed-author-icon').src = embedAuthorIcon || 'https://static-cdn.jtvnw.net/ttv-favicon/favicon-32x32.png';
|
||||
document.getElementById('embed-author-icon').style.display = embedAuthorIcon ? '' : 'none';
|
||||
document.getElementById('embed-footer').textContent = formatText(embedFooter, vars);
|
||||
|
||||
document.getElementById('embed-preview').style.borderLeftColor = '#' + embedColor;
|
||||
|
||||
if (embedThumbnail) {
|
||||
document.getElementById('embed-thumbnail').src = vars.thumbnail;
|
||||
document.getElementById('embed-thumbnail').style.display = 'block';
|
||||
} else {
|
||||
document.getElementById('embed-thumbnail').style.display = 'none';
|
||||
}
|
||||
|
||||
if (embedImage) {
|
||||
document.getElementById('embed-image').src = vars.thumbnail;
|
||||
document.getElementById('embed-image').style.display = 'block';
|
||||
} else {
|
||||
document.getElementById('embed-image').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
const colorInput = document.getElementById('embed_color');
|
||||
if (colorInput) {
|
||||
colorInput.addEventListener('input', function(e) {
|
||||
document.getElementById('embed_color_text').value = e.target.value.substring(1).toUpperCase();
|
||||
updatePreview();
|
||||
});
|
||||
}
|
||||
|
||||
const colorText = document.getElementById('embed_color_text');
|
||||
if (colorText) {
|
||||
colorText.addEventListener('input', function(e) {
|
||||
const val = e.target.value.replace(/[^0-9A-Fa-f]/g, '').substring(0, 6);
|
||||
e.target.value = val;
|
||||
if (val.length === 6) {
|
||||
document.getElementById('embed_color').value = '#' + val;
|
||||
updatePreview();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const formFields = ['embed_title', 'embed_description', 'embed_author_name', 'embed_author_icon', 'embed_footer', 'embed_thumbnail', 'embed_image'];
|
||||
formFields.forEach(function(field) {
|
||||
const el = document.getElementById(field);
|
||||
if (el) {
|
||||
el.addEventListener('input', updatePreview);
|
||||
el.addEventListener('change', updatePreview);
|
||||
}
|
||||
});
|
||||
|
||||
updatePreview();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
{% extends "template.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-md mx-auto py-12">
|
||||
<h1 class="text-2xl font-bold text-slate-800 dark:text-white mb-6 text-center">Connexion</h1>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="mb-4 space-y-2">
|
||||
{% for category, msg in messages %}
|
||||
<p class="p-3 rounded-lg text-sm {% if category == 'error' %}bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-200{% else %}bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200{% endif %}">{{ msg }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="post" action="{{ url_for('login') }}" class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-6 space-y-4">
|
||||
<div>
|
||||
<label for="identifier" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Identifiant (nom d'utilisateur ou e-mail)</label>
|
||||
<input type="text" id="identifier" name="identifier" required autocomplete="username" class="w-full px-4 py-2 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent" placeholder="nom ou email">
|
||||
</div>
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Mot de passe</label>
|
||||
<input type="password" id="password" name="password" required autocomplete="current-password" class="w-full px-4 py-2 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent">
|
||||
</div>
|
||||
<button type="submit" class="w-full py-2.5 px-4 rounded-lg bg-primary-600 dark:bg-primary-500 text-white font-medium hover:bg-primary-700 dark:hover:bg-primary-600 transition-colors">Se connecter</button>
|
||||
</form>
|
||||
|
||||
{% if registration_enabled %}
|
||||
<p class="mt-4 text-center text-sm text-slate-600 dark:text-slate-400">
|
||||
Pas encore de compte ? <a href="{{ url_for('register') }}" class="text-primary-600 dark:text-primary-400 hover:underline">Créer un compte</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -8,6 +8,53 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||
<div class="px-5 py-4 border-b border-slate-200 dark:border-slate-700">
|
||||
<h2 class="text-lg font-medium text-slate-800 dark:text-white">Top 3 sanctions</h2>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Utilisateurs les plus sanctionnés</p>
|
||||
</div>
|
||||
<div class="divide-y divide-slate-200 dark:divide-slate-700">
|
||||
{% for row in top_sanctioned %}
|
||||
<div class="px-5 py-3 flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<span class="flex-shrink-0 w-7 h-7 rounded-full bg-slate-200 dark:bg-slate-600 flex items-center justify-center text-sm font-bold text-slate-700 dark:text-slate-300">{{ loop.index }}</span>
|
||||
<div class="min-w-0">
|
||||
<span class="block text-sm font-medium text-slate-800 dark:text-white truncate">{{ row.username or '—' }}</span>
|
||||
<span class="block text-xs text-slate-500 dark:text-slate-400 font-mono truncate">{{ row.discord_id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="flex-shrink-0 text-sm font-semibold text-slate-600 dark:text-slate-300">{{ row.count }} sanction{{ 's' if row.count > 1 else '' }}</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="px-5 py-6 text-center text-sm text-slate-500 dark:text-slate-400">Aucune sanction enregistrée</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||
<div class="px-5 py-4 border-b border-slate-200 dark:border-slate-700">
|
||||
<h2 class="text-lg font-medium text-slate-800 dark:text-white">Top 3 modérateurs</h2>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">Staff ayant effectué le plus d'actions</p>
|
||||
</div>
|
||||
<div class="divide-y divide-slate-200 dark:divide-slate-700">
|
||||
{% for row in top_moderators %}
|
||||
<div class="px-5 py-3 flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<span class="flex-shrink-0 w-7 h-7 rounded-full bg-slate-200 dark:bg-slate-600 flex items-center justify-center text-sm font-bold text-slate-700 dark:text-slate-300">{{ loop.index }}</span>
|
||||
<div class="min-w-0">
|
||||
<span class="block text-sm font-medium text-slate-800 dark:text-white truncate">{{ row.staff_name or '—' }}</span>
|
||||
<span class="block text-xs text-slate-500 dark:text-slate-400 font-mono truncate">{{ row.staff_id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="flex-shrink-0 text-sm font-semibold text-slate-600 dark:text-slate-300">{{ row.count }} action{{ 's' if row.count > 1 else '' }}</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="px-5 py-6 text-center text-sm text-slate-500 dark:text-slate-400">Aucune action enregistrée</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden mb-6">
|
||||
<details class="group">
|
||||
<summary class="flex items-center justify-between px-5 py-4 cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-700/30 transition-colors">
|
||||
|
||||
+124
-113
@@ -1,124 +1,135 @@
|
||||
{% extends "template.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-semibold text-slate-800 dark:text-white mb-1">ProtonDB</h1>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400">
|
||||
Compatibilité des jeux Windows sur Linux via Steam Play.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{% if configuration.getValue('proton_db_enable_enable') %}
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden mb-6">
|
||||
<div class="px-5 py-4 border-b border-slate-200 dark:border-slate-700">
|
||||
<h2 class="text-lg font-medium text-slate-800 dark:text-white">Alias de jeux</h2>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead>
|
||||
<tr class="bg-slate-50 dark:bg-slate-700/50 border-b border-slate-200 dark:border-slate-700">
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Alias</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Nom du jeu</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-200 dark:divide-slate-700">
|
||||
{% for a in aliases %}
|
||||
<tr class="hover:bg-slate-50 dark:hover:bg-slate-700/30 transition-colors">
|
||||
<td class="px-4 py-3">
|
||||
<code class="px-1.5 py-0.5 bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded text-xs font-mono">{{ a.alias }}</code>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-600 dark:text-slate-400">
|
||||
{{ a.name }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<a href="{{ url_for('delGameAlias', id = a.id) }}" onclick="return confirm('Êtes-vous sûr de vouloir supprimer cet alias ?')" class="text-sm text-slate-500 hover:text-red-600 dark:hover:text-red-400 transition-colors">
|
||||
Supprimer
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="3" class="px-4 py-8 text-center text-sm text-slate-500 dark:text-slate-400">
|
||||
Aucun alias configuré
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 p-5 mb-6">
|
||||
<h2 class="text-lg font-medium text-slate-800 dark:text-white mb-5">Ajouter un alias</h2>
|
||||
|
||||
<form action="{{ url_for('addGameAlias') }}" method="POST" class="space-y-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="alias" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Alias</label>
|
||||
<input type="text" name="alias" id="alias" maxlength="32" required placeholder="GTA" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
|
||||
</div>
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Nom complet</label>
|
||||
<input type="text" name="name" id="name" maxlength="256" required placeholder="Grand Theft Auto" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<button type="submit" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600 text-white text-sm font-medium rounded-lg transition-colors">
|
||||
Ajouter
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-5 bg-slate-50 dark:bg-slate-700/50 rounded-lg p-4">
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400">
|
||||
<strong class="text-slate-800 dark:text-white">Exemple :</strong> Si vous créez un alias GTA → Grand Theft Auto, alors !protondb GTA 5 fera une recherche sur Grand Theft Auto 5.
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-4">ProtonDB</h1>
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<p class="text-gray-700 dark:text-gray-300">
|
||||
ProtonDB évalue la compatibilité des jeux Windows sur Linux via Steam Play.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
|
||||
<div class="px-5 py-4 border-b border-slate-200 dark:border-slate-700">
|
||||
<h2 class="text-lg font-medium text-slate-800 dark:text-white">Configuration</h2>
|
||||
</div>
|
||||
|
||||
<form action="{{ url_for('updateConfiguration') }}" method="POST" class="p-5 space-y-6">
|
||||
<label class="flex items-center gap-3 cursor-pointer">
|
||||
<input type="checkbox" name="proton_db_enable_enable" {% if configuration.getValue('proton_db_enable_enable') %}checked{% endif %} class="w-4 h-4 text-slate-600 bg-slate-100 dark:bg-slate-700 border-slate-300 dark:border-slate-600 rounded focus:ring-slate-500">
|
||||
<span class="text-sm text-slate-700 dark:text-slate-300">Activer la commande ProtonDB</span>
|
||||
</label>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="proton_db_api_id" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">API ID</label>
|
||||
<input type="text" name="proton_db_api_id" id="proton_db_api_id" value="{{ configuration.getValue('proton_db_api_id') }}" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
|
||||
</div>
|
||||
<div>
|
||||
<label for="proton_db_api_key" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">Clé API</label>
|
||||
<input type="text" name="proton_db_api_key" id="proton_db_api_key" value="{{ configuration.getValue('proton_db_api_key') }}" class="w-full px-3 py-2 bg-slate-50 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 rounded-lg text-sm text-slate-900 dark:text-white focus:ring-2 focus:ring-slate-500 focus:border-transparent transition-all">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 dark:bg-slate-700 dark:hover:bg-slate-600 text-white text-sm font-medium rounded-lg transition-colors">
|
||||
Enregistrer
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="px-5 pb-5">
|
||||
<div class="bg-slate-50 dark:bg-slate-700/50 rounded-lg p-4">
|
||||
<p class="text-sm font-medium text-slate-800 dark:text-white mb-2">Comment trouver les clés API ?</p>
|
||||
<ol class="list-decimal list-inside space-y-1 text-sm text-slate-600 dark:text-slate-400">
|
||||
<li>Ouvrez l'outil d'inspection de votre navigateur (F12)</li>
|
||||
<li>Allez dans l'onglet Réseau/Network</li>
|
||||
<li>Faites une recherche de jeu sur ProtonDB</li>
|
||||
<li>Cherchez les clés dans les requêtes réseau</li>
|
||||
</ol>
|
||||
<a href="/static/img/algolia-key.jpg" target="_blank" class="inline-block mt-3 text-sm text-slate-600 dark:text-slate-400 hover:underline">
|
||||
Voir l'exemple en image
|
||||
</a>
|
||||
{% if configuration.getValue('proton_db_enable_enable') %}
|
||||
<div class="mb-8">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Alias de jeux</h2>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Alias</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Jeu</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-24">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{% for a in aliases %}
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<code class="px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded text-purple-600 dark:text-purple-400 font-mono">{{ a.alias }}</code>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-gray-700 dark:text-gray-300">{{ a.name }}</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
<a href="{{ url_for('delGameAlias', id = a.id) }}"
|
||||
onclick="return confirm('Êtes-vous sûr de vouloir supprimer cet alias ?')"
|
||||
class="p-2 rounded-lg hover:bg-red-100 dark:hover:bg-red-900/30 transition-colors text-red-600 dark:text-red-400 inline-block"
|
||||
title="Supprimer">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="3" class="px-6 py-8 text-center text-gray-500 dark:text-gray-400">
|
||||
Aucun alias configuré. Ajoutez-en un ci-dessous.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6 mb-8">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-6">Ajouter un alias</h2>
|
||||
|
||||
<form action="{{ url_for('addGameAlias') }}" method="POST" class="space-y-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="alias" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Alias</label>
|
||||
<input name="alias" id="alias" type="text" maxlength="32" required
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all"
|
||||
placeholder="GTA"/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Nom du jeu</label>
|
||||
<input name="name" id="name" type="text" maxlength="256" required
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all"
|
||||
placeholder="Grand Theft Auto"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Si vous créez un alias <code class="px-1.5 py-0.5 bg-gray-200 dark:bg-gray-600 rounded">GTA</code> → <code class="px-1.5 py-0.5 bg-gray-200 dark:bg-gray-600 rounded">Grand Theft Auto</code>,
|
||||
alors la commande <code class="px-1.5 py-0.5 bg-gray-200 dark:bg-gray-600 rounded">!protondb GTA 5</code> fera une recherche sur <strong class="text-gray-800 dark:text-gray-200">Grand Theft Auto 5</strong>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="submit"
|
||||
class="px-6 py-2.5 bg-purple-600 hover:bg-purple-700 text-white font-medium rounded-lg transition-colors focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
|
||||
Ajouter l'alias
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-6">Configuration</h2>
|
||||
|
||||
<form action="{{ url_for('updateConfiguration') }}" method="POST" class="space-y-6">
|
||||
<div class="flex items-center gap-3 p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg">
|
||||
<input type="checkbox" name="proton_db_enable_enable" id="proton_db_enable_enable"
|
||||
{% if configuration.getValue('proton_db_enable_enable') %}checked{% endif %}
|
||||
class="w-5 h-5 rounded border-gray-300 dark:border-gray-600 text-purple-600 focus:ring-purple-500 dark:bg-gray-700">
|
||||
<label for="proton_db_enable_enable" class="text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer">
|
||||
Activer la commande ProtonDB
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="proton_db_api_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">API ID</label>
|
||||
<input name="proton_db_api_id" id="proton_db_api_id" type="text"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all"
|
||||
value="{{ configuration.getValue('proton_db_api_id') }}"/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="proton_db_api_key" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Clé API</label>
|
||||
<input name="proton_db_api_key" id="proton_db_api_key" type="text"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all"
|
||||
value="{{ configuration.getValue('proton_db_api_key') }}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-4">
|
||||
<p class="text-sm text-amber-800 dark:text-amber-200">
|
||||
Pour trouver les clés : ouvrez l'outil d'inspection (F12) dans votre navigateur, faites une recherche de jeux sur ProtonDB,
|
||||
puis cherchez les clés dans les requêtes (onglet Réseau/Network).
|
||||
<a href="/static/img/algolia-key.jpg" target="_blank" class="underline hover:no-underline">Voir l'exemple</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="submit"
|
||||
class="px-6 py-2.5 bg-purple-600 hover:bg-purple-700 text-white font-medium rounded-lg transition-colors focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
|
||||
Enregistrer la configuration
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
{% extends "template.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-md mx-auto py-12">
|
||||
<h1 class="text-2xl font-bold text-slate-800 dark:text-white mb-6 text-center">Créer un compte</h1>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="mb-4 space-y-2">
|
||||
{% for category, msg in messages %}
|
||||
<p class="p-3 rounded-lg text-sm {% if category == 'error' %}bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-200{% else %}bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200{% endif %}">{{ msg }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="post" action="{{ url_for('register') }}" class="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-6 space-y-4">
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Nom d'utilisateur</label>
|
||||
<input type="text" id="username" name="username" required minlength="3" autocomplete="username" class="w-full px-4 py-2 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent" placeholder="min. 3 caractères">
|
||||
</div>
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Adresse e-mail</label>
|
||||
<input type="email" id="email" name="email" required autocomplete="email" class="w-full px-4 py-2 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent" placeholder="vous@exemple.com">
|
||||
</div>
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Mot de passe</label>
|
||||
<input type="password" id="password" name="password" required minlength="8" autocomplete="new-password" class="w-full px-4 py-2 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent" placeholder="min. 8 caractères">
|
||||
</div>
|
||||
<div>
|
||||
<label for="password_confirm" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Confirmer le mot de passe</label>
|
||||
<input type="password" id="password_confirm" name="password_confirm" required minlength="8" autocomplete="new-password" class="w-full px-4 py-2 rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-700 text-slate-900 dark:text-white focus:ring-2 focus:ring-primary-500 focus:border-transparent">
|
||||
</div>
|
||||
<button type="submit" class="w-full py-2.5 px-4 rounded-lg bg-primary-600 dark:bg-primary-500 text-white font-medium hover:bg-primary-700 dark:hover:bg-primary-600 transition-colors">S'inscrire</button>
|
||||
</form>
|
||||
|
||||
<p class="mt-4 text-center text-sm text-slate-600 dark:text-slate-400">
|
||||
Déjà un compte ? <a href="{{ url_for('login') }}" class="text-primary-600 dark:text-primary-400 hover:underline">Se connecter</a>
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,386 @@
|
||||
{% extends "template.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">Paramètres et Permissions</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400">Configuration des rôles et des accès aux pages (super administrateur uniquement)</p>
|
||||
</div>
|
||||
<button type="button" onclick="document.getElementById('help-modal').classList.remove('hidden')"
|
||||
class="px-4 py-2 rounded-lg bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 hover:bg-primary-200 dark:hover:bg-primary-900/50 transition-colors flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>Aide</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="mb-6 space-y-2">
|
||||
{% for category, msg in messages %}
|
||||
<div class="p-4 rounded-lg {% if category == 'error' %}bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-200 border border-red-200 dark:border-red-800{% else %}bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200 border border-green-200 dark:border-green-800{% endif %}">
|
||||
{{ msg }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="space-y-8">
|
||||
<!-- Inscriptions -->
|
||||
<section class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6 shadow-sm">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-amber-100 dark:bg-amber-900/30 flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-amber-600 dark:text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-2">Inscriptions</h2>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">Autoriser ou bloquer la création de nouveaux comptes par les visiteurs</p>
|
||||
<form action="{{ url_for('settings_toggle_registration') }}" method="post">
|
||||
<label class="inline-flex items-center gap-3 cursor-pointer group">
|
||||
<div class="relative">
|
||||
<input type="checkbox" name="enabled" value="1" {% if registration_enabled %}checked{% endif %}
|
||||
onchange="this.form.submit()"
|
||||
class="sr-only peer">
|
||||
<div class="w-14 h-7 bg-gray-300 dark:bg-gray-600 rounded-full peer peer-checked:bg-green-500 dark:peer-checked:bg-green-600 transition-colors"></div>
|
||||
<div class="absolute left-1 top-1 w-5 h-5 bg-white rounded-full transition-transform peer-checked:translate-x-7 shadow-md"></div>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300 group-hover:text-gray-900 dark:group-hover:text-white">
|
||||
{% if registration_enabled %}✓ Inscriptions activées{% else %}✗ Inscriptions désactivées{% endif %}
|
||||
</span>
|
||||
</label>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Rôles -->
|
||||
<section class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm overflow-hidden">
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-2">Hiérarchie des rôles</h2>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Les rôles définissent le niveau d'accès des utilisateurs. Plus le niveau est élevé, plus les permissions sont étendues.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="space-y-3 mb-6">
|
||||
{% for r in roles %}
|
||||
<div class="bg-gray-50 dark:bg-gray-700/30 rounded-lg p-4 border border-gray-200 dark:border-gray-600 hover:border-gray-300 dark:hover:border-gray-500 transition-colors">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg flex items-center justify-center" style="background-color: {{ r.color or '#6B7280' }}20;">
|
||||
<span class="text-2xl" style="color: {{ r.color or '#6B7280' }};">●</span>
|
||||
</div>
|
||||
<div class="flex-grow min-w-0">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">{{ r.name }}</h3>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
|
||||
style="background-color: {{ r.color or '#6B7280' }}20; color: {{ r.color or '#6B7280' }};">
|
||||
Niveau {{ r.level }}
|
||||
</span>
|
||||
</div>
|
||||
{% if r.description %}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3">{{ r.description }}</p>
|
||||
{% else %}
|
||||
{% set default_desc = default_roles_meta.get(r.name, {}).get('description') %}
|
||||
{% if default_desc %}
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3">{{ default_desc }}</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<details class="group">
|
||||
<summary class="text-sm text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 cursor-pointer list-none flex items-center gap-1">
|
||||
<svg class="w-4 h-4 transition-transform group-open:rotate-90" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<span>Modifier ce rôle</span>
|
||||
</summary>
|
||||
<form action="{{ url_for('settings_role_edit', role_id=r.id) }}" method="post" class="mt-3 pt-3 border-t border-gray-200 dark:border-gray-600 grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Niveau (0-99)</label>
|
||||
<input type="number" name="level" value="{{ r.level }}" min="0" max="99"
|
||||
class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-primary-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Couleur</label>
|
||||
<input type="color" name="color" value="{{ r.color or '#6B7280' }}"
|
||||
class="w-full h-10 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 cursor-pointer">
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Description</label>
|
||||
<input type="text" name="description" value="{{ r.description or '' }}" placeholder="Description du rôle..."
|
||||
class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-primary-500">
|
||||
</div>
|
||||
<div class="sm:col-span-2 flex items-center gap-2">
|
||||
<button type="submit" class="px-4 py-2 rounded-lg bg-primary-600 dark:bg-primary-500 text-white text-sm font-medium hover:bg-primary-700 dark:hover:bg-primary-600 transition-colors">
|
||||
Enregistrer
|
||||
</button>
|
||||
{% if r.name not in ['viewer_twitch','utilisateur_discord','moderateur_discord','expert_discord','moderateur_twitch','super_administrateur'] %}
|
||||
<button type="button" onclick="if(confirm('Supprimer ce rôle ?')) { this.closest('details').querySelector('form').setAttribute('action', '{{ url_for('settings_role_delete', role_id=r.id) }}'); this.closest('form').submit(); }"
|
||||
class="px-4 py-2 rounded-lg bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 text-sm font-medium hover:bg-red-200 dark:hover:bg-red-900/50 transition-colors">
|
||||
Supprimer
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<details class="bg-primary-50 dark:bg-primary-900/20 rounded-lg border border-primary-200 dark:border-primary-800">
|
||||
<summary class="px-4 py-3 cursor-pointer list-none flex items-center gap-2 text-primary-700 dark:text-primary-300 font-medium hover:text-primary-800 dark:hover:text-primary-200">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
<span>Créer un nouveau rôle</span>
|
||||
</summary>
|
||||
<form action="{{ url_for('settings_role_add') }}" method="post" class="p-4 pt-0 grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Nom du rôle *</label>
|
||||
<input type="text" name="name" placeholder="ex: editeur_contenu" required minlength="2"
|
||||
class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-primary-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Niveau (0-99) *</label>
|
||||
<input type="number" name="level" value="0" min="0" max="99"
|
||||
class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-primary-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Couleur</label>
|
||||
<input type="color" name="color" value="#6B7280"
|
||||
class="w-full h-10 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 cursor-pointer">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Icône (optionnel)</label>
|
||||
<input type="text" name="icon" placeholder="ex: star, shield, user"
|
||||
class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-primary-500">
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<label class="block text-xs font-medium text-gray-600 dark:text-gray-400 mb-1">Description</label>
|
||||
<input type="text" name="description" placeholder="Description du rôle..."
|
||||
class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-primary-500">
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<button type="submit" class="px-4 py-2 rounded-lg bg-primary-600 dark:bg-primary-500 text-white font-medium hover:bg-primary-700 dark:hover:bg-primary-600 transition-colors">
|
||||
Créer le rôle
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Permissions par page -->
|
||||
<section class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm overflow-hidden">
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-lg bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-2">Accès aux pages</h2>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Définissez le rôle minimum requis pour accéder à chaque section de l'interface
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Appliquer en masse -->
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/30">
|
||||
<details class="group">
|
||||
<summary class="cursor-pointer list-none flex items-center gap-2 text-gray-700 dark:text-gray-300 font-medium hover:text-gray-900 dark:hover:text-white">
|
||||
<svg class="w-5 h-5 transition-transform group-open:rotate-90" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
<span>⚡ Modification en masse</span>
|
||||
</summary>
|
||||
<form action="{{ url_for('settings_permissions_bulk') }}" method="post" class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-600">
|
||||
<div class="flex flex-wrap items-center gap-4 mb-4">
|
||||
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
<span>Appliquer le rôle :</span>
|
||||
<select name="role" required class="px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-primary-500">
|
||||
{% for r in roles %}
|
||||
<option value="{{ r.name }}" style="color: {{ r.color or '#6B7280' }};">{{ r.name }} (niveau {{ r.level }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">aux pages cochées :</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
<button type="button" onclick="document.querySelectorAll('.bulk-page-cb').forEach(c => c.checked = true)"
|
||||
class="text-xs px-3 py-1.5 rounded-lg bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-500 transition-colors">
|
||||
Tout cocher
|
||||
</button>
|
||||
<button type="button" onclick="document.querySelectorAll('.bulk-page-cb').forEach(c => c.checked = false)"
|
||||
class="text-xs px-3 py-1.5 rounded-lg bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-500 transition-colors">
|
||||
Tout décocher
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2 mb-4">
|
||||
{% for page_key, meta in page_metadata.items() %}
|
||||
<label class="flex items-center gap-2 px-3 py-2 rounded-lg bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 hover:border-primary-300 dark:hover:border-primary-600 cursor-pointer transition-colors">
|
||||
<input type="checkbox" name="page_keys" value="{{ page_key }}" class="bulk-page-cb rounded border-gray-300 dark:border-gray-600 text-primary-600 focus:ring-primary-500">
|
||||
<span class="text-xs text-gray-700 dark:text-gray-300">{{ meta.label }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button type="submit" class="px-4 py-2 rounded-lg bg-primary-600 dark:bg-primary-500 text-white font-medium hover:bg-primary-700 dark:hover:bg-primary-600 transition-colors">
|
||||
Appliquer à la sélection
|
||||
</button>
|
||||
</form>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- Pages par catégorie -->
|
||||
<div class="p-6">
|
||||
{% for category_key in ['general', 'content', 'moderation', 'config', 'admin'] %}
|
||||
{% if category_key in pages_by_category %}
|
||||
{% set category_info = category_labels[category_key] %}
|
||||
<div class="mb-8 last:mb-0">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="w-8 h-8 rounded-lg flex items-center justify-center" style="background-color: {{ category_info.color }}20;">
|
||||
<span class="text-lg" style="color: {{ category_info.color }};">●</span>
|
||||
</div>
|
||||
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">{{ category_info.label }}</h3>
|
||||
<div class="flex-grow h-px bg-gray-200 dark:bg-gray-700"></div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{% for page_data in pages_by_category[category_key] %}
|
||||
{% set page_key = page_data.key %}
|
||||
{% set meta = page_data.meta %}
|
||||
{% set perm = page_data.permission %}
|
||||
{% set min_lvl = perm.min_level if perm else 0 %}
|
||||
|
||||
<div class="bg-gray-50 dark:bg-gray-700/30 rounded-lg p-4 border border-gray-200 dark:border-gray-600 hover:border-gray-300 dark:hover:border-gray-500 transition-colors">
|
||||
<div class="flex items-start justify-between gap-3 mb-2">
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-900 dark:text-white mb-1">{{ meta.label }}</h4>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">{{ meta.description }}</p>
|
||||
</div>
|
||||
{% for r in roles %}
|
||||
{% if r.level == min_lvl %}
|
||||
<span class="flex-shrink-0 inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium whitespace-nowrap"
|
||||
style="background-color: {{ r.color or '#6B7280' }}20; color: {{ r.color or '#6B7280' }};">
|
||||
{{ r.name }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<form action="{{ url_for('settings_permissions_update') }}" method="post" class="flex items-center gap-2">
|
||||
<input type="hidden" name="page_key" value="{{ page_key }}">
|
||||
<select name="role" class="flex-grow px-3 py-1.5 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-xs focus:ring-2 focus:ring-primary-500">
|
||||
{% for r in roles %}
|
||||
<option value="{{ r.name }}" {% if r.level == min_lvl %}selected{% endif %}>{{ r.name }} (niv. {{ r.level }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit" class="px-3 py-1.5 rounded-lg bg-primary-600 dark:bg-primary-500 text-white text-xs font-medium hover:bg-primary-700 dark:hover:bg-primary-600 transition-colors">
|
||||
OK
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Modal d'aide -->
|
||||
<div id="help-modal" class="hidden fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
||||
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div class="fixed inset-0 bg-gray-500 dark:bg-gray-900 bg-opacity-75 dark:bg-opacity-80 transition-opacity" onclick="document.getElementById('help-modal').classList.add('hidden')"></div>
|
||||
<div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-2xl sm:w-full">
|
||||
<div class="bg-white dark:bg-gray-800 px-6 pt-6 pb-4">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">Guide des permissions</h3>
|
||||
<button type="button" onclick="document.getElementById('help-modal').classList.add('hidden')"
|
||||
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-y-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-2">🎭 Rôles</h4>
|
||||
<p>Les rôles définissent le niveau d'autorité d'un utilisateur. Plus le niveau est élevé, plus l'utilisateur a accès à des fonctionnalités.</p>
|
||||
<ul class="list-disc list-inside mt-2 space-y-1 ml-4">
|
||||
<li><strong>Niveau 0-1</strong> : Accès basique en lecture seule</li>
|
||||
<li><strong>Niveau 2-3</strong> : Modification de contenu et gestion basique</li>
|
||||
<li><strong>Niveau 4</strong> : Modération et configuration avancée</li>
|
||||
<li><strong>Niveau 5+</strong> : Administration complète du système</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-2">🔐 Permissions par page</h4>
|
||||
<p>Chaque page peut être restreinte à un rôle minimum. Un utilisateur doit avoir au moins le niveau requis pour y accéder.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-2">📋 Catégories</h4>
|
||||
<ul class="list-disc list-inside space-y-1 ml-4">
|
||||
<li><strong style="color: #6B7280;">●</strong> <strong>Général</strong> : Pages d'accueil et navigation</li>
|
||||
<li><strong style="color: #3B82F6;">●</strong> <strong>Contenu</strong> : Gestion des commandes, alertes, humeurs, etc.</li>
|
||||
<li><strong style="color: #EF4444;">●</strong> <strong>Modération</strong> : Outils de modération Discord/Twitch</li>
|
||||
<li><strong style="color: #8B5CF6;">●</strong> <strong>Configuration</strong> : Paramètres techniques des bots</li>
|
||||
<li><strong style="color: #F59E0B;">●</strong> <strong>Administration</strong> : Gestion des utilisateurs et permissions</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold text-gray-900 dark:text-white mb-2">💡 Bonnes pratiques</h4>
|
||||
<ul class="list-disc list-inside space-y-1 ml-4">
|
||||
<li>Attribuez le rôle le plus bas possible selon les besoins</li>
|
||||
<li>Testez les permissions avec un compte utilisateur avant de les déployer</li>
|
||||
<li>Documentez les rôles personnalisés avec des descriptions claires</li>
|
||||
<li>Vérifiez régulièrement les accès des utilisateurs</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 dark:bg-gray-700/50 px-6 py-4">
|
||||
<button type="button" onclick="document.getElementById('help-modal').classList.add('hidden')"
|
||||
class="w-full px-4 py-2 rounded-lg bg-primary-600 dark:bg-primary-500 text-white font-medium hover:bg-primary-700 dark:hover:bg-primary-600 transition-colors">
|
||||
Compris !
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Améliorer le style du toggle switch */
|
||||
input[type="checkbox"].sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
+151
-33
@@ -77,46 +77,117 @@
|
||||
|
||||
<!-- Navigation Desktop -->
|
||||
<div class="hidden md:flex items-center gap-1">
|
||||
<a href="/live-alert" class="px-4 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 transition-all">
|
||||
<span class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
|
||||
Alerte live
|
||||
</span>
|
||||
<!-- Discord (sous-menus) -->
|
||||
<div class="relative group">
|
||||
<button type="button" class="px-4 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 transition-all flex items-center gap-1.5">
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>
|
||||
Discord
|
||||
<svg class="w-4 h-4 transition-transform group-hover:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
|
||||
</button>
|
||||
<div class="absolute left-0 top-full pt-1 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1 min-w-[200px]">
|
||||
<a href="/humeurs" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
Humeur
|
||||
</a>
|
||||
<a href="/live-alert" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
|
||||
Notification Twitch
|
||||
</a>
|
||||
<a href="{{ url_for('open_twitch_events') }}" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"></path></svg>
|
||||
Événements Twitch (sub, raid, clip)
|
||||
</a>
|
||||
<a href="/youtube" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>
|
||||
Notification YouTube
|
||||
</a>
|
||||
<a href="/protondb" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 4a2 2 0 114 0v1a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-1a2 2 0 100 4h1a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-1a2 2 0 10-4 0v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-3a1 1 0 00-1-1H4a2 2 0 110-4h1a1 1 0 001-1V7a1 1 0 011-1h3a1 1 0 001-1V4z"></path></svg>
|
||||
ProtonDB
|
||||
</a>
|
||||
<a href="{{ url_for('openFreeLoot') }}" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<span class="text-lg">🎁</span>
|
||||
FreeLoot
|
||||
</a>
|
||||
<div class="border-t border-gray-200 dark:border-gray-700 my-1"></div>
|
||||
<a href="/commandes" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
|
||||
Commandes
|
||||
</a>
|
||||
<a href="/moderation" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>
|
||||
Modération
|
||||
</a>
|
||||
<a href="/configurations#auto-rooms" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v3m0 0v-3a7 7 0 017-7"></path></svg>
|
||||
Auto Rooms
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Twitch (futur bot) -->
|
||||
<div class="relative group">
|
||||
<button type="button" class="px-4 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 transition-all flex items-center gap-1.5">
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M11.571 4.714h1.715v5.143H11.57l-.002-5.143zm3.43 0H16.714v5.143H15V4.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0H6zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714v9.429z"/></svg>
|
||||
Twitch
|
||||
<svg class="w-4 h-4 transition-transform group-hover:rotate-180" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path></svg>
|
||||
</button>
|
||||
<div class="absolute left-0 top-full pt-1 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 py-1 min-w-[200px]">
|
||||
<a href="/live-alert" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
|
||||
Alerte live
|
||||
</a>
|
||||
<a href="{{ url_for('open_twitch_events') }}" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"></path></svg>
|
||||
Événements (sub, raid, clip)
|
||||
</a>
|
||||
<a href="/announcements" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5.882V19.24a1.76 1.76 0 01-3.417.592l-2.147-6.15M18 13a3 3 0 100-6M5.436 13.683A4.001 4.001 0 017 6h1.832c4.1 0 7.625-1.234 9.168-3v14c-1.543-1.766-5.067-3-9.168-3H7a3.988 3.988 0 01-1.564-.317z"></path></svg>
|
||||
Annonces
|
||||
</a>
|
||||
<a href="/twitch-moderation" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>
|
||||
Moderation
|
||||
</a>
|
||||
<a href="/link-filter" class="flex items-center gap-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path></svg>
|
||||
Filtre de liens
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Configuration locale -->
|
||||
<a href="/configurations" class="px-4 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 transition-all flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
||||
Configuration
|
||||
</a>
|
||||
<a href="/commandes" class="px-4 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 transition-all">
|
||||
<span class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
|
||||
Commandes
|
||||
</span>
|
||||
{% if current_user.is_authenticated and current_user_level >= 5 %}
|
||||
<a href="{{ url_for('users_list') }}" class="px-4 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 transition-all flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path></svg>
|
||||
Utilisateurs
|
||||
</a>
|
||||
<a href="/humeurs" class="px-4 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 transition-all">
|
||||
<span class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
Humeurs
|
||||
</span>
|
||||
</a>
|
||||
<a href="/moderation" class="px-4 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 transition-all">
|
||||
<span class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>
|
||||
Modération
|
||||
</span>
|
||||
</a>
|
||||
<a href="/protondb" class="px-4 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 transition-all">
|
||||
<span class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 4a2 2 0 114 0v1a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-1a2 2 0 100 4h1a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-1a2 2 0 10-4 0v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-3a1 1 0 00-1-1H4a2 2 0 110-4h1a1 1 0 001-1V7a1 1 0 011-1h3a1 1 0 001-1V4z"></path></svg>
|
||||
ProtonDB
|
||||
</span>
|
||||
</a>
|
||||
<a href="/configurations" class="px-4 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 transition-all">
|
||||
<span class="flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
||||
Configurations
|
||||
</span>
|
||||
<a href="{{ url_for('settings') }}" class="px-4 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 transition-all flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
||||
Paramètres
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center gap-2">
|
||||
{% if current_user.is_authenticated %}
|
||||
<span class="hidden sm:inline text-sm text-gray-600 dark:text-gray-400" title="Rôle : {{ current_user.role }}">{{ current_user.username }}</span>
|
||||
<a href="{{ url_for('logout') }}" class="px-3 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">Déconnexion</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('login') }}" class="px-3 py-2 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">Connexion</a>
|
||||
{% if registration_enabled %}
|
||||
<a href="{{ url_for('register') }}" class="px-3 py-2 rounded-lg text-sm font-medium bg-primary-600 dark:bg-primary-500 text-white hover:bg-primary-700 dark:hover:bg-primary-600 transition-colors">Créer un compte</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<!-- Dark Mode Toggle -->
|
||||
<button onclick="toggleDarkMode()" class="p-2 rounded-lg text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-primary-600 dark:hover:text-primary-400 transition-all" title="Mode sombre">
|
||||
<svg class="w-5 h-5 hidden dark:block" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"></path></svg>
|
||||
@@ -138,6 +209,26 @@
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path></svg>
|
||||
Alerte live
|
||||
</a>
|
||||
<a href="/announcements" class="flex items-center gap-3 px-4 py-3 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5.882V19.24a1.76 1.76 0 01-3.417.592l-2.147-6.15M18 13a3 3 0 100-6M5.436 13.683A4.001 4.001 0 017 6h1.832c4.1 0 7.625-1.234 9.168-3v14c-1.543-1.766-5.067-3-9.168-3H7a3.988 3.988 0 01-1.564-.317z"></path></svg>
|
||||
Annonces Twitch
|
||||
</a>
|
||||
<a href="/twitch-moderation" class="flex items-center gap-3 px-4 py-3 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>
|
||||
Moderation Twitch
|
||||
</a>
|
||||
<a href="/link-filter" class="flex items-center gap-3 px-4 py-3 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path></svg>
|
||||
Filtre de liens
|
||||
</a>
|
||||
<a href="{{ url_for('open_twitch_events') }}" class="flex items-center gap-3 px-4 py-3 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"></path></svg>
|
||||
Événements Twitch (sub, raid, clip)
|
||||
</a>
|
||||
<a href="/youtube" class="flex items-center gap-3 px-4 py-3 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>
|
||||
YouTube
|
||||
</a>
|
||||
<a href="/commandes" class="flex items-center gap-3 px-4 py-3 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
|
||||
Commandes
|
||||
@@ -150,14 +241,41 @@
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>
|
||||
Modération
|
||||
</a>
|
||||
<a href="/configurations#auto-rooms" class="flex items-center gap-3 px-4 py-3 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v3m0 0v-3a7 7 0 017-7"></path></svg>
|
||||
Auto Rooms
|
||||
</a>
|
||||
<a href="/protondb" class="flex items-center gap-3 px-4 py-3 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 4a2 2 0 114 0v1a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-1a2 2 0 100 4h1a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-1a2 2 0 10-4 0v1a1 1 0 01-1 1H7a1 1 0 01-1-1v-3a1 1 0 00-1-1H4a2 2 0 110-4h1a1 1 0 001-1V7a1 1 0 011-1h3a1 1 0 001-1V4z"></path></svg>
|
||||
ProtonDB
|
||||
</a>
|
||||
<a href="{{ url_for('openFreeLoot') }}" class="flex items-center gap-3 px-4 py-3 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
|
||||
<span class="text-lg">🎁</span>
|
||||
FreeLoot
|
||||
</a>
|
||||
<a href="/configurations" class="flex items-center gap-3 px-4 py-3 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
||||
Configurations
|
||||
</a>
|
||||
{% if current_user.is_authenticated and current_user_level >= 5 %}
|
||||
<a href="{{ url_for('users_list') }}" class="flex items-center gap-3 px-4 py-3 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path></svg>
|
||||
Utilisateurs
|
||||
</a>
|
||||
<a href="{{ url_for('settings') }}" class="flex items-center gap-3 px-4 py-3 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">
|
||||
Paramètres
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if current_user.is_authenticated %}
|
||||
<a href="{{ url_for('logout') }}" class="flex items-center gap-3 px-4 py-3 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all border-t border-gray-200 dark:border-gray-700 mt-2 pt-4">
|
||||
Déconnexion ({{ current_user.username }})
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ url_for('login') }}" class="flex items-center gap-3 px-4 py-3 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all border-t border-gray-200 dark:border-gray-700 mt-2 pt-4">Connexion</a>
|
||||
{% if registration_enabled %}
|
||||
<a href="{{ url_for('register') }}" class="flex items-center gap-3 px-4 py-3 rounded-lg text-primary-600 dark:text-primary-400 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all">Créer un compte</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
{% extends "template.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-4">Notifications d'événements Twitch</h1>
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<p class="text-gray-700 dark:text-gray-300">
|
||||
Configurez les notifications pour les abonnements, follows, raids et nouveaux clips.
|
||||
Pour chaque type d'événement vous pouvez activer l'envoi dans le <strong>chat Twitch</strong> et/ou dans un <strong>canal Discord</strong>.
|
||||
Les événements sub, follow et raid utilisent Twitch EventSub ; les clips sont détectés par vérification périodique.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="{{ url_for('save_twitch_events') }}" method="POST" class="space-y-8">
|
||||
{% for cfg in configs %}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div class="px-6 py-4 bg-gray-50 dark:bg-gray-700/50 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between flex-wrap gap-4">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">{{ labels[cfg.event_type] }}</h2>
|
||||
<div class="flex items-center gap-4">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="hidden" name="ev_{{ cfg.event_type }}_enable" value="0">
|
||||
<input type="checkbox" name="ev_{{ cfg.event_type }}_enable" value="1" {{ 'checked' if cfg.enable }}
|
||||
class="rounded border-gray-300 dark:border-gray-600 text-purple-600 focus:ring-purple-500">
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">Activer</span>
|
||||
</label>
|
||||
<a href="{{ url_for('toggle_twitch_event', event_type=cfg.event_type) }}"
|
||||
class="text-sm {{ 'text-green-600 dark:text-green-400' if cfg.enable else 'text-gray-500' }}">
|
||||
{{ 'Activé' if cfg.enable else 'Désactivé' }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6 space-y-6">
|
||||
<div class="grid md:grid-cols-2 gap-6">
|
||||
<div class="space-y-4">
|
||||
<h3 class="font-medium text-gray-800 dark:text-gray-200">Où notifier</h3>
|
||||
<label class="flex items-center gap-3 cursor-pointer">
|
||||
<input type="hidden" name="ev_{{ cfg.event_type }}_notify_twitch_chat" value="0">
|
||||
<input type="checkbox" name="ev_{{ cfg.event_type }}_notify_twitch_chat" value="1" {{ 'checked' if cfg.notify_twitch_chat }}
|
||||
class="rounded border-gray-300 dark:border-gray-600 text-purple-600 focus:ring-purple-500">
|
||||
<span class="text-gray-700 dark:text-gray-300">Chat Twitch</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-3 cursor-pointer">
|
||||
<input type="hidden" name="ev_{{ cfg.event_type }}_notify_discord" value="0">
|
||||
<input type="checkbox" name="ev_{{ cfg.event_type }}_notify_discord" value="1" {{ 'checked' if cfg.notify_discord }}
|
||||
class="rounded border-gray-300 dark:border-gray-600 text-purple-600 focus:ring-purple-500">
|
||||
<span class="text-gray-700 dark:text-gray-300">Discord (canal de notifs)</span>
|
||||
</label>
|
||||
<div>
|
||||
<label for="ev_{{ cfg.event_type }}_discord_channel" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Canal Discord</label>
|
||||
<select name="ev_{{ cfg.event_type }}_discord_channel_id" id="ev_{{ cfg.event_type }}_discord_channel"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white">
|
||||
<option value="">— Choisir un canal —</option>
|
||||
{% for ch in channels %}
|
||||
<option value="{{ ch.id }}" {{ 'selected' if cfg.discord_channel_id == ch.id }}>{{ ch.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="ev_{{ cfg.event_type }}_message_twitch" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Message (chat Twitch)</label>
|
||||
<input type="text" name="ev_{{ cfg.event_type }}_message_twitch" id="ev_{{ cfg.event_type }}_message_twitch" maxlength="500"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
value="{{ cfg.message_twitch }}" placeholder="Merci {user} !">
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Sub/Follow: <code>{user}</code> <code>{user_name}</code> —
|
||||
Raid: <code>{from_broadcaster_name}</code> <code>{viewers}</code> —
|
||||
Clip: <code>{user}</code> <code>{title}</code> <code>{url}</code>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="ev_{{ cfg.event_type }}_message_discord" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Message Discord (optionnel, avant l'embed)</label>
|
||||
<textarea name="ev_{{ cfg.event_type }}_message_discord" id="ev_{{ cfg.event_type }}_message_discord" rows="2" maxlength="2000"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white resize-y">{{ cfg.message_discord or '' }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t border-gray-200 dark:border-gray-700 pt-4">
|
||||
<h4 class="font-medium text-gray-800 dark:text-gray-200 mb-3">Embed Discord (optionnel)</h4>
|
||||
<div class="grid sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="ev_{{ cfg.event_type }}_embed_title" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Titre</label>
|
||||
<input type="text" name="ev_{{ cfg.event_type }}_embed_title" id="ev_{{ cfg.event_type }}_embed_title" maxlength="256"
|
||||
class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
||||
value="{{ cfg.embed_title or '' }}" placeholder="Ex: Nouveau clip">
|
||||
</div>
|
||||
<div>
|
||||
<label for="ev_{{ cfg.event_type }}_embed_color" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Couleur (hex)</label>
|
||||
<input type="text" name="ev_{{ cfg.event_type }}_embed_color" id="ev_{{ cfg.event_type }}_embed_color" maxlength="6"
|
||||
class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white font-mono"
|
||||
value="{{ cfg.embed_color or '9146FF' }}" placeholder="9146FF">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<label for="ev_{{ cfg.event_type }}_embed_description" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Description</label>
|
||||
<textarea name="ev_{{ cfg.event_type }}_embed_description" id="ev_{{ cfg.event_type }}_embed_description" rows="2" maxlength="2000"
|
||||
class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white resize-y">{{ cfg.embed_description or '' }}</textarea>
|
||||
</div>
|
||||
<label class="flex items-center gap-2 mt-3 cursor-pointer">
|
||||
<input type="hidden" name="ev_{{ cfg.event_type }}_embed_thumbnail" value="0">
|
||||
<input type="checkbox" name="ev_{{ cfg.event_type }}_embed_thumbnail" value="1" {{ 'checked' if cfg.embed_thumbnail }}
|
||||
class="rounded border-gray-300 dark:border-gray-600 text-purple-600 focus:ring-purple-500">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Miniature dans l'embed (clips)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="flex justify-end">
|
||||
<button type="submit" class="px-6 py-2.5 bg-purple-600 hover:bg-purple-700 text-white font-medium rounded-lg transition-colors focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
|
||||
Enregistrer tout
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,964 @@
|
||||
{% extends "template.html" %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
.panel-moderation {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(320px, 56%) 1fr;
|
||||
gap: 1rem;
|
||||
min-height: calc(100vh - 8rem);
|
||||
}
|
||||
@media (max-width: 1024px) {
|
||||
.panel-moderation { grid-template-columns: 1fr; }
|
||||
}
|
||||
.player-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-bottom: 56.25%;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
border-radius: 12px;
|
||||
background: #0e0e10;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,.25);
|
||||
}
|
||||
.player-wrap iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
.chat-wrap {
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: #18181b;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,.2);
|
||||
min-height: 320px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.chat-wrap iframe {
|
||||
flex: 1;
|
||||
min-height: 280px;
|
||||
border: none;
|
||||
}
|
||||
.mod-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
background: #1f1f23;
|
||||
border-bottom: 1px solid #2d2d35;
|
||||
}
|
||||
.mod-toolbar input[type="text"] {
|
||||
width: 140px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #3d3d48;
|
||||
background: #18181b;
|
||||
color: #efeff1;
|
||||
font-size: 13px;
|
||||
}
|
||||
.mod-toolbar input::placeholder { color: #6b6b7b; }
|
||||
.mod-toolbar button {
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background .15s, transform .05s;
|
||||
}
|
||||
.mod-toolbar button:active { transform: scale(0.98); }
|
||||
.btn-timeout { background: #e0a82e; color: #1f1f23; }
|
||||
.btn-timeout:hover { background: #e8b84a; }
|
||||
.btn-ban { background: #eb0400; color: #fff; }
|
||||
.btn-ban:hover { background: #ff1a15; }
|
||||
.btn-unban { background: #338847; color: #fff; }
|
||||
.btn-unban:hover { background: #3da352; }
|
||||
.btn-clean { background: #53535f; color: #efeff1; }
|
||||
.btn-clean:hover { background: #63636f; }
|
||||
.btn-copy { background: #9147ff; color: #fff; }
|
||||
.btn-copy:hover { background: #a970ff; }
|
||||
.right-col { display: flex; flex-direction: column; gap: 1rem; min-width: 0; }
|
||||
.commandes-logs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.section-card {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e5e7eb;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,.06);
|
||||
}
|
||||
.dark .section-card {
|
||||
background: #1f2937;
|
||||
border-color: #374151;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,.2);
|
||||
}
|
||||
.section-card-header {
|
||||
padding: 10px 14px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: #111827;
|
||||
background: #f9fafb;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
.dark .section-card-header {
|
||||
background: #111827;
|
||||
color: #f3f4f6;
|
||||
border-color: #374151;
|
||||
}
|
||||
.section-card-body {
|
||||
max-height: 220px;
|
||||
overflow: auto;
|
||||
}
|
||||
.section-card-body-logs {
|
||||
max-height: 280px;
|
||||
}
|
||||
.cmd-table, .logs-table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
||||
.cmd-table th, .logs-table th {
|
||||
text-align: left;
|
||||
padding: 8px 12px;
|
||||
background: #f3f4f6;
|
||||
color: #4b5563;
|
||||
font-weight: 600;
|
||||
}
|
||||
.dark .cmd-table th, .dark .logs-table th { background: #374151; color: #9ca3af; }
|
||||
.cmd-table td, .logs-table td { padding: 6px 12px; border-bottom: 1px solid #e5e7eb; color: #374151; }
|
||||
.dark .cmd-table td, .dark .logs-table td { border-color: #374151; color: #e5e7eb; }
|
||||
.cmd-table tr:hover, .logs-table tr:hover { background: #f9fafb; }
|
||||
.dark .cmd-table tr:hover, .dark .logs-table tr:hover { background: #374151; }
|
||||
.page-title-mod { font-size: 1.25rem; margin-bottom: 0.5rem; }
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h1 class="page-title-mod font-bold text-gray-900 dark:text-white">Modération Twitch</h1>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">Commandes et logs de modération pour {{ twitch_channel }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Info bricks: Live status, Link filter, Banned words -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<!-- Live Status -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 rounded-lg {{ 'bg-red-100 dark:bg-red-900/30' if is_live else 'bg-gray-100 dark:bg-gray-700' }}">
|
||||
<svg class="w-5 h-5 {{ 'text-red-600 dark:text-red-400' if is_live else 'text-gray-500' }}" fill="currentColor" viewBox="0 0 20 20">
|
||||
<circle cx="10" cy="10" r="8"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white text-sm">Live</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ 'En ligne' if is_live else 'Hors ligne' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% if is_live %}
|
||||
<div class="text-right">
|
||||
<div class="text-lg font-bold text-purple-600 dark:text-purple-400">{{ viewer_count }}</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">viewers</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Link Filter -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 rounded-lg {{ 'bg-green-100 dark:bg-green-900/30' if link_filter_enabled else 'bg-gray-100 dark:bg-gray-700' }}">
|
||||
<svg class="w-5 h-5 {{ 'text-green-600 dark:text-green-400' if link_filter_enabled else 'text-gray-500' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white text-sm">Filtre de liens</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ 'Actif' if link_filter_enabled else 'Inactif' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ url_for('link_filter') }}" class="text-xs text-purple-600 dark:text-purple-400 hover:underline">Configurer</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Banned Words -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 rounded-lg {{ 'bg-orange-100 dark:bg-orange-900/30' if banned_words else 'bg-gray-100 dark:bg-gray-700' }}">
|
||||
<svg class="w-5 h-5 {{ 'text-orange-600 dark:text-orange-400' if banned_words else 'text-gray-500' }}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white text-sm">Mots interdits</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ banned_words|length }} mot{{ 's' if banned_words|length > 1 else '' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Commandes & logs Twitch (sans lecteur/iframe) -->
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<!-- Commandes de modération -->
|
||||
<div class="section-card">
|
||||
<div class="section-card-header flex items-center justify-between">
|
||||
<span>Commandes de modération</span>
|
||||
<input type="text" id="searchInput" placeholder="Rechercher..." class="w-36 px-2 py-1 text-xs rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white">
|
||||
</div>
|
||||
<div class="section-card-body">
|
||||
<table class="cmd-table" id="commandsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Commande(s)</th>
|
||||
<th>Usage</th>
|
||||
<th>Permission</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="commandsBody">
|
||||
{% for cmd in commands %}
|
||||
<tr class="command-row" data-search="{{ cmd.commands|join(' ') }} {{ cmd.usage }} {{ cmd.description }}">
|
||||
<td>
|
||||
{% for c in cmd.commands %}
|
||||
<code class="px-1.5 py-0.5 bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 rounded text-xs font-mono">{{ c }}</code>
|
||||
{% if not loop.last %} {% endif %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td><code class="text-xs font-mono text-gray-600 dark:text-gray-400">{{ cmd.usage }}</code></td>
|
||||
<td><span class="text-xs text-gray-500 dark:text-gray-400">{{ cmd.permission }}</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logs de modération -->
|
||||
<div class="section-card">
|
||||
<div class="section-card-header flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2">
|
||||
<span>Logs de modération (<span id="logsCount">{{ logs|length }}</span>)</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="text" id="logsSearchInput" placeholder="Rechercher..." class="w-32 px-2 py-1 text-xs rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white">
|
||||
{% if logs %}
|
||||
<a href="{{ url_for('clear_twitch_logs') }}" onclick="return confirm('Effacer tous les logs ?')" class="text-xs text-red-600 hover:text-red-700">Effacer</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-card-body">
|
||||
{% if logs %}
|
||||
<table class="logs-table" id="logsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Action</th>
|
||||
<th>Modo</th>
|
||||
<th>Cible</th>
|
||||
<th>Détails</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="logsBody">
|
||||
{% for log in logs %}
|
||||
<tr class="log-row" data-search="{{ log.action }} {{ log.moderator }} {{ log.target or '' }} {{ log.details or '' }}">
|
||||
<td class="whitespace-nowrap text-gray-500 dark:text-gray-400">{{ log.created_at.strftime('%d/%m %H:%M') }}</td>
|
||||
<td><code class="px-1.5 py-0.5 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded text-xs">{{ log.action }}</code></td>
|
||||
<td>{{ log.moderator }}</td>
|
||||
<td>{{ log.target or '-' }}</td>
|
||||
<td class="max-w-[120px] truncate" title="{{ log.details or '' }}">{{ log.details or '-' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="px-4 py-6 text-center text-sm text-gray-500 dark:text-gray-400">Aucun log</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lecteur Live + Chat Twitch (si live en cours) OU Formulaire + Chat (hors live) -->
|
||||
<div class="mt-6">
|
||||
{% if is_live %}
|
||||
<!-- Layout avec lecteur vidéo (live en cours) -->
|
||||
<div class="grid lg:grid-cols-3 gap-6">
|
||||
<!-- Lecteur Twitch (2/3) -->
|
||||
<div class="lg:col-span-2 bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div class="aspect-video bg-gray-900">
|
||||
<iframe
|
||||
src="https://player.twitch.tv/?channel={{ twitch_channel }}&enableExtensions=true&muted=false&parent={{ embed_parent }}&player=popout&quality=auto&volume=0.5"
|
||||
style="width: 100%; height: 100%; border: none;"
|
||||
allowfullscreen>
|
||||
</iframe>
|
||||
</div>
|
||||
<div class="p-3 bg-gray-50 dark:bg-gray-700 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="flex h-2 w-2 relative">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-red-500"></span>
|
||||
</span>
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white">EN DIRECT</span>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">• {{ viewer_count }} viewers</span>
|
||||
</div>
|
||||
<a href="https://www.twitch.tv/{{ twitch_channel }}" target="_blank" class="text-xs text-purple-600 dark:text-purple-400 hover:underline flex items-center gap-1">
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
|
||||
</svg>
|
||||
Ouvrir sur Twitch
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Formulaire rapide (1/3) -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-4">
|
||||
<h2 class="text-sm font-semibold text-gray-900 dark:text-white mb-3 flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||
</svg>
|
||||
Nouvelle commande
|
||||
</h2>
|
||||
<form action="{{ url_for('add_twitch_commande') }}" method="POST" class="space-y-3">
|
||||
<input type="text" name="trigger" placeholder="!commande" required
|
||||
class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 text-sm">
|
||||
<select name="twitch_permission"
|
||||
class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 text-sm">
|
||||
{% for value, label in twitch_permissions.items() %}
|
||||
<option value="{{ value }}">{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<textarea name="response" rows="3" placeholder="Réponse... ({user} = mention)" required
|
||||
class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 text-sm resize-none"></textarea>
|
||||
<button type="submit" class="w-full px-3 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition-colors font-medium text-sm">Ajouter</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chat en dessous du lecteur -->
|
||||
<div class="mt-6 bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col" style="height: 500px;">
|
||||
{% else %}
|
||||
<!-- Layout sans lecteur (hors live) -->
|
||||
<div class="grid md:grid-cols-2 gap-6">
|
||||
<!-- Formulaire d'ajout de commande -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-4">
|
||||
<h2 class="text-base font-semibold text-gray-900 dark:text-white mb-3 flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||
</svg>
|
||||
Ajouter une commande
|
||||
</h2>
|
||||
<form action="{{ url_for('add_twitch_commande') }}" method="POST" class="space-y-3">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<input type="text" name="trigger" placeholder="!commande" required
|
||||
class="px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 text-sm">
|
||||
<select name="twitch_permission"
|
||||
class="px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 text-sm">
|
||||
{% for value, label in twitch_permissions.items() %}
|
||||
<option value="{{ value }}">{{ label }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<textarea name="response" rows="2" placeholder="Réponse du bot... ({user} = mention)" required
|
||||
class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 text-sm resize-none"></textarea>
|
||||
<button type="submit" class="w-full px-3 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition-colors font-medium text-sm">Ajouter</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Chat -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col" style="height: 500px;">
|
||||
{% endif %}
|
||||
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white">Chat en direct</h3>
|
||||
<span class="flex h-2 w-2 relative">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
|
||||
</span>
|
||||
</div>
|
||||
<a href="https://www.twitch.tv/{{ twitch_channel }}/chat" target="_blank" class="text-xs text-purple-600 dark:text-purple-400 hover:underline flex items-center gap-1">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path>
|
||||
</svg>
|
||||
Ouvrir sur Twitch
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Zone d'affichage des messages et actions -->
|
||||
<div class="flex-1 overflow-y-auto bg-gray-50 dark:bg-gray-900/50 p-4" id="chatDisplay">
|
||||
<div class="space-y-2">
|
||||
<div class="text-center text-sm text-gray-500 dark:text-gray-400 py-8">
|
||||
<div class="animate-pulse mb-4">
|
||||
<svg class="w-12 h-12 mx-auto mb-3 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="font-medium text-gray-700 dark:text-gray-300">Récupération du chat...</p>
|
||||
<p class="text-xs mt-1">Les messages du chat Twitch apparaîtront ici en temps réel</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Zone de saisie et boutons -->
|
||||
<div class="p-3 bg-gray-50 dark:bg-gray-700/50 border-t border-gray-200 dark:border-gray-600">
|
||||
<!-- Boutons d'action rapide -->
|
||||
<div class="grid grid-cols-4 gap-1.5 mb-3">
|
||||
<!-- Ligne 1 : Actions sur les users -->
|
||||
<button onclick="executeQuickAction('ban')" class="px-2 py-1.5 bg-red-600 hover:bg-red-700 text-white rounded transition-colors text-xs font-medium flex items-center justify-center gap-1" title="Bannir un utilisateur">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 75.636 5.636m12.728 12.728L5.636 5.636"></path>
|
||||
</svg>
|
||||
Ban
|
||||
</button>
|
||||
<button onclick="executeQuickAction('timeout')" class="px-2 py-1.5 bg-orange-600 hover:bg-orange-700 text-white rounded transition-colors text-xs font-medium flex items-center justify-center gap-1" title="Timeout un utilisateur">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
Timeout
|
||||
</button>
|
||||
<button onclick="executeQuickAction('clean')" class="px-2 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded transition-colors text-xs font-medium flex items-center justify-center gap-1" title="Nettoyer le chat">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||
</svg>
|
||||
Clean
|
||||
</button>
|
||||
<button onclick="executeQuickAction('permit')" class="px-2 py-1.5 bg-teal-600 hover:bg-teal-700 text-white rounded transition-colors text-xs font-medium flex items-center justify-center gap-1" title="Permettre un lien">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"></path>
|
||||
</svg>
|
||||
Permit
|
||||
</button>
|
||||
|
||||
<!-- Ligne 2 : Modes du chat -->
|
||||
<button onclick="executeQuickAction('subon')" class="px-2 py-1.5 bg-purple-600 hover:bg-purple-700 text-white rounded transition-colors text-xs font-medium flex items-center justify-center gap-1" title="Activer mode abonnés">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
|
||||
</svg>
|
||||
Sub ON
|
||||
</button>
|
||||
<button onclick="executeQuickAction('suboff')" class="px-2 py-1.5 bg-gray-500 hover:bg-gray-600 text-white rounded transition-colors text-xs font-medium flex items-center justify-center gap-1" title="Désactiver mode abonnés">
|
||||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 11V7a4 4 0 118 0m-4 8v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
Sub OFF
|
||||
</button>
|
||||
<button onclick="executeQuickAction('emoteon')" class="px-2 py-1.5 bg-yellow-600 hover:bg-yellow-700 text-white rounded transition-colors text-xs font-medium flex items-center justify-center gap-1" title="Activer mode emotes">
|
||||
😀 ON
|
||||
</button>
|
||||
<button onclick="executeQuickAction('emoteoff')" class="px-2 py-1.5 bg-gray-500 hover:bg-gray-600 text-white rounded transition-colors text-xs font-medium flex items-center justify-center gap-1" title="Désactiver mode emotes">
|
||||
😀 OFF
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Input de message -->
|
||||
<form onsubmit="sendMessage(event)" class="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
id="chatMessage"
|
||||
placeholder="Message ou commande..."
|
||||
class="flex-1 px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 text-sm"
|
||||
autocomplete="off">
|
||||
<button type="submit" class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors font-medium text-sm">
|
||||
Envoyer
|
||||
</button>
|
||||
</form>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-2">Envoyé via le bot • <a href="https://www.twitch.tv/popout/{{ twitch_channel }}/chat" target="_blank" class="text-purple-600 dark:text-purple-400 hover:underline">Ouvrir le chat</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- Fermeture du conteneur principal -->
|
||||
|
||||
{% if custom_commands %}
|
||||
<div class="mt-6 bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white">Commandes personnalisées Twitch ({{ custom_commands|length }})</h3>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700/50">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold text-gray-600 dark:text-gray-300">Commande</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold text-gray-600 dark:text-gray-300">Réponse</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold text-gray-600 dark:text-gray-300">Permission</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold text-gray-600 dark:text-gray-300">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{% for cmd in custom_commands %}
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
||||
<td class="px-4 py-2"><code class="px-2 py-0.5 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 rounded text-xs">{{ cmd.trigger }}</code></td>
|
||||
<td class="px-4 py-2 text-gray-700 dark:text-gray-300 max-w-xs truncate">{{ cmd.response }}</td>
|
||||
<td class="px-4 py-2">{{ twitch_permissions.get(cmd.twitch_permission or 'viewer', 'Tous') }}</td>
|
||||
<td class="px-4 py-2"><a href="{{ url_for('delete_commande', commande_id=cmd.id) }}" onclick="return confirm('Supprimer ?')" class="text-red-600 hover:text-red-700 text-xs">Supprimer</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Section mots interdits -->
|
||||
<div class="mt-6 bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white">Mots interdits ({{ banned_words|length }})</h3>
|
||||
</div>
|
||||
|
||||
<!-- Formulaire d'ajout -->
|
||||
<div class="p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<form action="{{ url_for('add_banned_word') }}" method="POST" class="flex flex-wrap gap-2 items-end">
|
||||
<div class="flex-1 min-w-[200px]">
|
||||
<label for="banned_word" class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Mot interdit</label>
|
||||
<input type="text" name="word" id="banned_word" placeholder="mot" required
|
||||
class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 text-sm">
|
||||
</div>
|
||||
<div class="w-32">
|
||||
<label for="banned_word_timeout" class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Timeout (s)</label>
|
||||
<input type="number" name="timeout_duration" id="banned_word_timeout" value="60" min="0" max="1209600"
|
||||
class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:ring-2 focus:ring-purple-500 text-sm">
|
||||
</div>
|
||||
<button type="submit" class="px-4 py-2 bg-orange-600 hover:bg-orange-700 text-white rounded-lg transition-colors font-medium text-sm">Ajouter</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Liste des mots interdits -->
|
||||
{% if banned_words %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700/50">
|
||||
<tr>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold text-gray-600 dark:text-gray-300">Mot</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold text-gray-600 dark:text-gray-300">Timeout</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold text-gray-600 dark:text-gray-300">Ajouté le</th>
|
||||
<th class="px-4 py-2 text-left text-xs font-semibold text-gray-600 dark:text-gray-300">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{% for word in banned_words %}
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
||||
<td class="px-4 py-2"><code class="px-2 py-1 bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 rounded text-xs font-mono">{{ word.word }}</code></td>
|
||||
<td class="px-4 py-2 text-gray-700 dark:text-gray-300">{{ word.timeout_duration }}s</td>
|
||||
<td class="px-4 py-2 text-gray-500 dark:text-gray-400 text-xs">{{ word.created_at.strftime('%d/%m/%Y %H:%M') if word.created_at else '-' }}</td>
|
||||
<td class="px-4 py-2"><a href="{{ url_for('delete_banned_word', word_id=word.id) }}" onclick="return confirm('Supprimer ce mot interdit ?')" class="text-red-600 hover:text-red-700 text-xs">Supprimer</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="px-4 py-6 text-center text-sm text-gray-500 dark:text-gray-400">Aucun mot interdit configuré</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var modTarget = document.getElementById('modTarget');
|
||||
var copyCmdBtn = document.getElementById('copyCmdBtn');
|
||||
var lastCmd = '';
|
||||
|
||||
document.querySelectorAll('.mod-toolbar button[data-cmd]').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var user = (modTarget && modTarget.value.trim()) || '';
|
||||
lastCmd = this.getAttribute('data-cmd').replace(/\{\}/g, user).trim();
|
||||
if (user && (this.classList.contains('btn-clean') && lastCmd === '!clean')) {
|
||||
lastCmd = '!clean';
|
||||
}
|
||||
navigator.clipboard && navigator.clipboard.writeText(lastCmd).then(function() {
|
||||
var t = copyCmdBtn.textContent;
|
||||
copyCmdBtn.textContent = 'Copié !';
|
||||
setTimeout(function() { copyCmdBtn.textContent = t; }, 1200);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (copyCmdBtn) {
|
||||
copyCmdBtn.addEventListener('click', function() {
|
||||
if (lastCmd) {
|
||||
navigator.clipboard && navigator.clipboard.writeText(lastCmd).then(function() {
|
||||
var t = copyCmdBtn.textContent;
|
||||
copyCmdBtn.textContent = 'Copié !';
|
||||
setTimeout(function() { copyCmdBtn.textContent = t; }, 1200);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var rows = document.querySelectorAll('.command-row');
|
||||
var searchInput = document.getElementById('searchInput');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', function() {
|
||||
var q = this.value.toLowerCase();
|
||||
rows.forEach(function(row) {
|
||||
row.style.display = row.dataset.search.toLowerCase().indexOf(q) >= 0 ? '' : 'none';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var logRows = document.querySelectorAll('.log-row');
|
||||
var logsSearchInput = document.getElementById('logsSearchInput');
|
||||
var logsCount = document.getElementById('logsCount');
|
||||
if (logsSearchInput) {
|
||||
logsSearchInput.addEventListener('input', function() {
|
||||
var q = this.value.toLowerCase();
|
||||
var n = 0;
|
||||
logRows.forEach(function(row) {
|
||||
var ok = row.dataset.search.toLowerCase().indexOf(q) >= 0;
|
||||
row.style.display = ok ? '' : 'none';
|
||||
if (ok) n++;
|
||||
});
|
||||
if (logsCount) logsCount.textContent = n;
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
// Fonctions pour le chat Twitch
|
||||
var displayedMessages = new Set();
|
||||
var isAutoScroll = true;
|
||||
|
||||
function addMessageToDisplay(username, message, timestamp, badges, userColor) {
|
||||
var chatDisplay = document.getElementById('chatDisplay');
|
||||
var messagesContainer = chatDisplay.querySelector('.space-y-2');
|
||||
|
||||
// Supprimer le message d'aide si c'est le premier message
|
||||
var helpText = messagesContainer.querySelector('.text-center');
|
||||
if (helpText) {
|
||||
helpText.remove();
|
||||
}
|
||||
|
||||
// Créer un ID unique pour ce message
|
||||
var messageId = timestamp + username + message;
|
||||
if (displayedMessages.has(messageId)) {
|
||||
return; // Message déjà affiché
|
||||
}
|
||||
displayedMessages.add(messageId);
|
||||
|
||||
// Créer l'élément de message
|
||||
var messageDiv = document.createElement('div');
|
||||
var msgDate = new Date(timestamp);
|
||||
var timeStr = msgDate.getHours().toString().padStart(2, '0') + ':' + msgDate.getMinutes().toString().padStart(2, '0');
|
||||
|
||||
// Déterminer les badges
|
||||
var badgeIcons = '';
|
||||
if (badges && badges.is_mod) {
|
||||
badgeIcons += '<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400" title="Modérateur">MOD</span>';
|
||||
}
|
||||
if (badges && badges.is_vip) {
|
||||
badgeIcons += '<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-pink-100 text-pink-800 dark:bg-pink-900/30 dark:text-pink-400" title="VIP">VIP</span>';
|
||||
}
|
||||
if (badges && badges.is_subscriber) {
|
||||
badgeIcons += '<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400" title="Abonné">SUB</span>';
|
||||
}
|
||||
|
||||
messageDiv.className = 'bg-white dark:bg-gray-800 rounded-lg p-3 text-sm animate-fade-in border-l-2 border-gray-300 dark:border-gray-600';
|
||||
messageDiv.style.borderLeftColor = userColor || '#9146FF';
|
||||
messageDiv.innerHTML = `
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-1 flex-wrap">
|
||||
<span class="font-bold" style="color: ${userColor || '#9146FF'}">${escapeHtml(username)}</span>
|
||||
${badgeIcons}
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">${timeStr}</span>
|
||||
</div>
|
||||
<div class="text-gray-900 dark:text-gray-100 break-words">${escapeHtml(message)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
messagesContainer.appendChild(messageDiv);
|
||||
|
||||
// Limiter à 100 messages affichés
|
||||
while (messagesContainer.children.length > 100) {
|
||||
messagesContainer.removeChild(messagesContainer.firstChild);
|
||||
}
|
||||
|
||||
// Scroll vers le bas si auto-scroll activé
|
||||
if (isAutoScroll) {
|
||||
chatDisplay.scrollTop = chatDisplay.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
function addBotMessageToDisplay(message, type) {
|
||||
var chatDisplay = document.getElementById('chatDisplay');
|
||||
var messagesContainer = chatDisplay.querySelector('.space-y-2');
|
||||
|
||||
// Supprimer le message d'aide si c'est le premier message
|
||||
var helpText = messagesContainer.querySelector('.text-center');
|
||||
if (helpText) {
|
||||
helpText.remove();
|
||||
}
|
||||
|
||||
var now = new Date();
|
||||
var timeStr = now.getHours().toString().padStart(2, '0') + ':' + now.getMinutes().toString().padStart(2, '0');
|
||||
|
||||
var bgColor = 'bg-blue-50 dark:bg-blue-900/20';
|
||||
var borderColor = '#3b82f6';
|
||||
|
||||
if (type === 'command') {
|
||||
bgColor = 'bg-purple-50 dark:bg-purple-900/20';
|
||||
borderColor = '#9146FF';
|
||||
}
|
||||
|
||||
var messageDiv = document.createElement('div');
|
||||
messageDiv.className = `${bgColor} rounded-lg p-3 text-sm animate-fade-in border-l-2`;
|
||||
messageDiv.style.borderLeftColor = borderColor;
|
||||
messageDiv.innerHTML = `
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="font-bold text-purple-600 dark:text-purple-400">Bot</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">${timeStr}</span>
|
||||
</div>
|
||||
<div class="text-gray-900 dark:text-gray-100 break-words">${escapeHtml(message)}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
messagesContainer.appendChild(messageDiv);
|
||||
|
||||
// Limiter à 100 messages
|
||||
while (messagesContainer.children.length > 100) {
|
||||
messagesContainer.removeChild(messagesContainer.firstChild);
|
||||
}
|
||||
|
||||
// Scroll vers le bas
|
||||
if (isAutoScroll) {
|
||||
chatDisplay.scrollTop = chatDisplay.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// Polling pour récupérer les messages
|
||||
function fetchChatMessages() {
|
||||
fetch('{{ url_for("get_twitch_messages") }}')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.messages && data.messages.length > 0) {
|
||||
data.messages.forEach(function(msg) {
|
||||
addMessageToDisplay(
|
||||
msg.username,
|
||||
msg.text,
|
||||
msg.timestamp,
|
||||
{
|
||||
is_mod: msg.is_mod,
|
||||
is_vip: msg.is_vip,
|
||||
is_subscriber: msg.is_subscriber
|
||||
},
|
||||
msg.color
|
||||
);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Erreur lors de la récupération des messages:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Démarrer le polling
|
||||
setInterval(fetchChatMessages, 2000); // Toutes les 2 secondes
|
||||
fetchChatMessages(); // Premier chargement immédiat
|
||||
|
||||
// Détecter le scroll manuel pour désactiver l'auto-scroll
|
||||
document.getElementById('chatDisplay').addEventListener('scroll', function() {
|
||||
var element = this;
|
||||
var isAtBottom = element.scrollHeight - element.scrollTop <= element.clientHeight + 50;
|
||||
isAutoScroll = isAtBottom;
|
||||
});
|
||||
|
||||
function escapeHtml(text) {
|
||||
var div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function sendMessage(event) {
|
||||
event.preventDefault();
|
||||
var input = document.getElementById('chatMessage');
|
||||
var message = input.value.trim();
|
||||
|
||||
if (!message) return;
|
||||
|
||||
// Désactiver le bouton pendant l'envoi
|
||||
var submitBtn = event.target.querySelector('button[type="submit"]');
|
||||
var originalText = submitBtn.textContent;
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = 'Envoi...';
|
||||
|
||||
// Utiliser la fonction sendTextMessage pour les messages normaux
|
||||
fetch('{{ url_for("send_twitch_message") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ message: message })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Ajouter le message à l'affichage
|
||||
var type = message.startsWith('!') ? 'command' : 'message';
|
||||
addBotMessageToDisplay(message, type);
|
||||
input.value = '';
|
||||
showNotification('Message envoyé', 'success');
|
||||
} else {
|
||||
showNotification(data.error || 'Erreur lors de l\'envoi', 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showNotification('Erreur réseau', 'error');
|
||||
})
|
||||
.finally(function() {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = originalText;
|
||||
});
|
||||
}
|
||||
|
||||
function executeQuickAction(action) {
|
||||
// Commandes simples sans paramètres
|
||||
if (['subon', 'suboff', 'emoteon', 'emoteoff'].includes(action)) {
|
||||
executeModerationAction(action, {});
|
||||
return;
|
||||
}
|
||||
|
||||
// Ban : demander utilisateur + raison
|
||||
if (action === 'ban') {
|
||||
var username = prompt('Nom d\'utilisateur à bannir :', '@');
|
||||
if (username && username.trim() && username !== '@') {
|
||||
username = username.trim().replace('@', '');
|
||||
var reason = prompt('Raison (optionnelle) :', 'Violation des règles');
|
||||
executeModerationAction('ban', {
|
||||
username: username,
|
||||
reason: reason || 'Ban'
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Timeout : demander utilisateur + durée + raison
|
||||
if (action === 'timeout') {
|
||||
var username = prompt('Nom d\'utilisateur à timeout :', '@');
|
||||
if (username && username.trim() && username !== '@') {
|
||||
username = username.trim().replace('@', '');
|
||||
var duration = prompt('Durée en minutes (défaut: 10) :', '10');
|
||||
duration = parseInt(duration) || 10;
|
||||
var reason = prompt('Raison (optionnelle) :', 'Timeout');
|
||||
executeModerationAction('timeout', {
|
||||
username: username,
|
||||
duration: duration * 60, // Convertir en secondes
|
||||
reason: reason || 'Timeout'
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Clean : demander si utilisateur spécifique ou tout le chat
|
||||
if (action === 'clean') {
|
||||
var username = prompt('Nom d\'utilisateur (ou laissez vide pour tout le chat) :', '');
|
||||
if (username === null) return; // Annulé
|
||||
executeModerationAction('clean', {
|
||||
username: username ? username.trim().replace('@', '') : ''
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Permit : demander utilisateur + durée
|
||||
if (action === 'permit') {
|
||||
var username = prompt('Nom d\'utilisateur à autoriser :', '@');
|
||||
if (username && username.trim() && username !== '@') {
|
||||
username = username.trim().replace('@', '');
|
||||
var duration = prompt('Durée en minutes (défaut: 1) :', '1');
|
||||
duration = parseInt(duration) || 1;
|
||||
executeModerationAction('permit', {
|
||||
username: username,
|
||||
duration: duration * 60 // Convertir en secondes
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function executeModerationAction(action, params) {
|
||||
fetch('{{ url_for("execute_moderation_action") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ action: action, params: params })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showNotification(data.message || 'Action exécutée', 'success');
|
||||
// Ajouter un message visuel dans le chat
|
||||
var actionText = action.toUpperCase();
|
||||
if (params.username) {
|
||||
actionText += ' @' + params.username;
|
||||
}
|
||||
if (params.duration) {
|
||||
actionText += ' (' + (params.duration / 60) + 'min)';
|
||||
}
|
||||
if (params.reason) {
|
||||
actionText += ' - ' + params.reason;
|
||||
}
|
||||
addBotMessageToDisplay(actionText, 'command');
|
||||
} else {
|
||||
showNotification(data.error || 'Erreur lors de l\'exécution', 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showNotification('Erreur réseau', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Cette fonction est conservée pour les messages texte normaux (non-modération)
|
||||
function sendTextMessage(message) {
|
||||
fetch('{{ url_for("send_twitch_message") }}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ message: message })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
var type = message.startsWith('!') ? 'command' : 'message';
|
||||
addBotMessageToDisplay(message, type);
|
||||
showNotification('Message envoyé', 'success');
|
||||
} else {
|
||||
showNotification(data.error || 'Erreur lors de l\'envoi', 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showNotification('Erreur réseau', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function showNotification(message, type) {
|
||||
var color = type === 'success' ? 'bg-green-500' : 'bg-red-500';
|
||||
var notification = document.createElement('div');
|
||||
notification.className = `fixed bottom-4 right-4 ${color} text-white px-4 py-2 rounded-lg shadow-lg z-50`;
|
||||
notification.textContent = message;
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(function() {
|
||||
notification.remove();
|
||||
}, 3000);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,292 @@
|
||||
{% extends "template.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mb-8">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex-shrink-0 w-12 h-12 rounded-xl bg-amber-100 dark:bg-amber-900/30 flex items-center justify-center">
|
||||
<svg class="w-7 h-7 text-amber-600 dark:text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-2">Gestion des utilisateurs</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400">Liste des comptes webapp et attribution des rôles</p>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" onclick="document.getElementById('create-user-modal').classList.remove('hidden')"
|
||||
class="flex-shrink-0 px-4 py-2 rounded-lg bg-primary-600 dark:bg-primary-500 text-white font-medium hover:bg-primary-700 dark:hover:bg-primary-600 transition-colors flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
<span>Créer un utilisateur</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="mb-6 space-y-2">
|
||||
{% for category, msg in messages %}
|
||||
<div class="p-4 rounded-lg {% if category == 'error' %}bg-red-100 dark:bg-red-900/30 text-red-800 dark:text-red-200 border border-red-200 dark:border-red-800{% else %}bg-green-100 dark:bg-green-900/30 text-green-800 dark:text-green-200 border border-green-200 dark:border-green-800{% endif %}">
|
||||
{{ msg }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm overflow-hidden">
|
||||
{% if users %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700/50 border-b border-gray-200 dark:border-gray-700">
|
||||
<tr>
|
||||
<th class="px-6 py-4 text-left text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
|
||||
Utilisateur
|
||||
</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
|
||||
E-mail
|
||||
</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
|
||||
Rôle actuel
|
||||
</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
|
||||
Inscrit le
|
||||
</th>
|
||||
<th class="px-6 py-4 text-left text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider">
|
||||
Modifier le rôle
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{% for u in users %}
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/30 transition-colors">
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-full bg-gradient-to-br from-primary-400 to-primary-600 flex items-center justify-center text-white font-semibold">
|
||||
{{ u.username[0].upper() }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium text-gray-900 dark:text-white">{{ u.username }}</div>
|
||||
{% if u.id == current_user.id %}
|
||||
<span class="text-xs text-primary-600 dark:text-primary-400 font-medium">C'est vous</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">{{ u.email }}</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
{% set user_role = None %}
|
||||
{% for r in roles %}
|
||||
{% if r == u.role %}
|
||||
{% set user_role = r %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if user_role %}
|
||||
{% set role_obj = namespace(found=None) %}
|
||||
{% for role_name in roles %}
|
||||
{% if role_name == user_role %}
|
||||
{% for r_obj in roles %}
|
||||
{# Obtenir l'objet WebappRole complet #}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<span class="inline-flex items-center gap-2 px-3 py-1.5 rounded-full text-sm font-medium whitespace-nowrap"
|
||||
style="background-color: #6B728020; color: #6B7280;">
|
||||
<span class="w-2 h-2 rounded-full" style="background-color: #6B7280;"></span>
|
||||
{{ role_labels.get(u.role, u.role) }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="inline-flex items-center px-3 py-1.5 rounded-full text-sm font-medium bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300">
|
||||
{{ role_labels.get(u.role, u.role) }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{% if u.created_at %}{{ u.created_at.strftime('%d/%m/%Y à %H:%M') }}{% else %}—{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<form action="{{ url_for('users_set_role', user_id=u.id) }}" method="post" class="flex items-center gap-2">
|
||||
<select name="role" class="px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white text-sm focus:ring-2 focus:ring-primary-500 transition-colors"
|
||||
{% if u.id == current_user.id %}disabled title="Vous ne pouvez pas modifier votre propre rôle"{% endif %}>
|
||||
{% for r in roles %}
|
||||
<option value="{{ r }}" {% if u.role == r %}selected{% endif %}>{{ role_labels.get(r, r) }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit"
|
||||
{% if u.id == current_user.id %}disabled{% endif %}
|
||||
class="px-4 py-2 rounded-lg bg-primary-600 dark:bg-primary-500 text-white text-sm font-medium hover:bg-primary-700 dark:hover:bg-primary-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
Appliquer
|
||||
</button>
|
||||
</form>
|
||||
{% if u.id != current_user.id %}
|
||||
<form action="{{ url_for('users_delete', user_id=u.id) }}" method="post" class="inline" onsubmit="return confirm('Êtes-vous sûr de vouloir supprimer cet utilisateur ?');">
|
||||
<button type="submit" class="px-3 py-2 rounded-lg bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 text-sm font-medium hover:bg-red-200 dark:hover:bg-red-900/50 transition-colors" title="Supprimer cet utilisateur">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if u.id == current_user.id %}
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">Protection: modification/suppression impossible</p>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-12 text-center">
|
||||
<svg class="w-16 h-16 mx-auto text-gray-400 dark:text-gray-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
<p class="text-gray-500 dark:text-gray-400 text-lg">Aucun utilisateur enregistré</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Légende des rôles -->
|
||||
{% if users %}
|
||||
<div class="mt-6 bg-blue-50 dark:bg-blue-900/20 rounded-xl border border-blue-200 dark:border-blue-800 p-6">
|
||||
<h3 class="text-lg font-semibold text-blue-900 dark:text-blue-200 mb-4 flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>Hiérarchie des rôles</span>
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{% for role_name in roles %}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg p-3 border border-blue-100 dark:border-blue-900">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="w-3 h-3 rounded-full" style="background-color: #6B7280;"></span>
|
||||
<span class="font-medium text-gray-900 dark:text-white text-sm">{{ role_labels.get(role_name, role_name) }}</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400 ml-5">
|
||||
{% if role_name == 'viewer_twitch' %}
|
||||
Accès minimal, consultation uniquement
|
||||
{% elif role_name == 'utilisateur_discord' %}
|
||||
Modification de contenu basique
|
||||
{% elif role_name == 'moderateur_discord' %}
|
||||
Outils de modération Discord
|
||||
{% elif role_name == 'expert_discord' %}
|
||||
Gestion avancée du contenu
|
||||
{% elif role_name == 'moderateur_twitch' %}
|
||||
Outils de modération Twitch
|
||||
{% elif role_name == 'super_administrateur' %}
|
||||
Accès complet au système
|
||||
{% else %}
|
||||
Rôle personnalisé
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="mt-4 p-3 bg-blue-100 dark:bg-blue-900/30 rounded-lg">
|
||||
<p class="text-sm text-blue-800 dark:text-blue-200">
|
||||
<strong>💡 Conseil :</strong> Vous pouvez personnaliser les rôles et leurs permissions dans la page
|
||||
<a href="{{ url_for('settings') }}" class="underline hover:text-blue-900 dark:hover:text-blue-100">Paramètres</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Modal de création d'utilisateur -->
|
||||
<div id="create-user-modal" class="hidden fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
|
||||
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div class="fixed inset-0 bg-gray-500 dark:bg-gray-900 bg-opacity-75 dark:bg-opacity-80 transition-opacity" onclick="document.getElementById('create-user-modal').classList.add('hidden')"></div>
|
||||
<div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
|
||||
<form action="{{ url_for('users_create') }}" method="post">
|
||||
<div class="bg-white dark:bg-gray-800 px-6 pt-6 pb-4">
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-lg bg-primary-100 dark:bg-primary-900/30 flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-primary-600 dark:text-primary-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">Créer un utilisateur</h3>
|
||||
</div>
|
||||
<button type="button" onclick="document.getElementById('create-user-modal').classList.add('hidden')"
|
||||
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Nom d'utilisateur <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="text" id="username" name="username" required minlength="3"
|
||||
class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-primary-500"
|
||||
placeholder="ex: john_doe">
|
||||
</div>
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Adresse e-mail <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="email" id="email" name="email" required
|
||||
class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-primary-500"
|
||||
placeholder="ex: john@example.com">
|
||||
</div>
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Mot de passe <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="password" id="password" name="password" required minlength="8"
|
||||
class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-primary-500"
|
||||
placeholder="Au moins 8 caractères">
|
||||
</div>
|
||||
<div>
|
||||
<label for="password_confirm" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Confirmer le mot de passe <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="password" id="password_confirm" name="password_confirm" required minlength="8"
|
||||
class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-primary-500"
|
||||
placeholder="Retapez le mot de passe">
|
||||
</div>
|
||||
<div>
|
||||
<label for="role" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Rôle <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<select id="role" name="role" required
|
||||
class="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-primary-500">
|
||||
{% for r in roles %}
|
||||
<option value="{{ r }}">{{ role_labels.get(r, r) }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 dark:bg-gray-700/50 px-6 py-4 flex gap-3 justify-end">
|
||||
<button type="button" onclick="document.getElementById('create-user-modal').classList.add('hidden')"
|
||||
class="px-4 py-2 rounded-lg bg-gray-200 dark:bg-gray-600 text-gray-700 dark:text-gray-300 font-medium hover:bg-gray-300 dark:hover:bg-gray-500 transition-colors">
|
||||
Annuler
|
||||
</button>
|
||||
<button type="submit"
|
||||
class="px-4 py-2 rounded-lg bg-primary-600 dark:bg-primary-500 text-white font-medium hover:bg-primary-700 dark:hover:bg-primary-600 transition-colors">
|
||||
Créer l'utilisateur
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Animation au survol des lignes */
|
||||
tbody tr {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,342 @@
|
||||
{% extends "template.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white mb-4">Notifications YouTube</h1>
|
||||
|
||||
{% if msg %}
|
||||
<div id="alert-msg" class="mb-4 p-4 rounded-lg {{ 'bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-300' if msg_type == 'error' else 'bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 text-green-700 dark:text-green-300' }}">
|
||||
{{ msg }}
|
||||
</div>
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
var el = document.getElementById('alert-msg');
|
||||
if (el) el.style.display = 'none';
|
||||
}, 5000);
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
<div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||
<p class="text-gray-700 dark:text-gray-300">
|
||||
Liste des chaînes YouTube surveillées pour les notifications de nouvelles vidéos.
|
||||
Le bot vérifie toutes les 5 minutes les nouvelles vidéos des chaînes en dessous.
|
||||
Quand une nouvelle vidéo est détectée, le bot enverra une notification sur Discord.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if not notification %}
|
||||
<div class="mb-8">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Notifications configurées</h2>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Chaîne YouTube</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Canal Discord</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Type</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Message</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{% for notification in notifications %}
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<code class="px-2 py-1 bg-gray-100 dark:bg-gray-700 rounded text-red-600 dark:text-red-400 font-mono text-sm">{{ notification.channel_id }}</code>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-gray-700 dark:text-gray-300">{{ notification.notify_channel_name }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-2 py-1 text-xs font-medium rounded-full
|
||||
{% if notification.video_type == 'all' %}bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300
|
||||
{% elif notification.video_type == 'video' %}bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300
|
||||
{% else %}bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300{% endif %}">
|
||||
{% if notification.video_type == 'all' %}Toutes
|
||||
{% elif notification.video_type == 'video' %}Vidéos
|
||||
{% else %}Shorts{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-gray-600 dark:text-gray-400 max-w-xs truncate">{{ notification.message }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center">
|
||||
<div class="flex items-center justify-center gap-2">
|
||||
<a href="{{ url_for('toggleYouTube', id = notification.id) }}"
|
||||
class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors"
|
||||
title="{{ 'Désactiver' if notification.enable else 'Activer' }}">
|
||||
{{ '✅' if notification.enable else '❌' }}
|
||||
</a>
|
||||
<a href="{{ url_for('openEditYouTube', id = notification.id) }}"
|
||||
class="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors text-blue-600 dark:text-blue-400"
|
||||
title="Modifier">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
|
||||
</a>
|
||||
<a href="{{ url_for('delYouTube', id = notification.id) }}"
|
||||
onclick="return confirm('Êtes-vous sûr de vouloir supprimer cette notification ?')"
|
||||
class="p-2 rounded-lg hover:bg-red-100 dark:hover:bg-red-900/30 transition-colors text-red-600 dark:text-red-400"
|
||||
title="Supprimer">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="5" class="px-6 py-8 text-center text-gray-500 dark:text-gray-400">
|
||||
Aucune notification configurée. Ajoutez-en une ci-dessous.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-6">
|
||||
{{ 'Modifier la notification' if notification else 'Ajouter une notification YouTube' }}
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<div>
|
||||
<form id="youtube-form" action="{{ url_for('submitEditYouTube', id = notification.id) if notification else url_for('addYouTube') }}" method="POST" class="space-y-6">
|
||||
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4 space-y-4">
|
||||
<h3 class="font-medium text-gray-800 dark:text-gray-200">Configuration de base</h3>
|
||||
|
||||
<div>
|
||||
<label for="channel_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Lien ou ID de la chaîne YouTube</label>
|
||||
<input name="channel_id" id="channel_id" type="text" maxlength="256" required
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-red-500 focus:border-transparent transition-all"
|
||||
placeholder="https://www.youtube.com/@513v3 ou UC..."
|
||||
value="{{notification.channel_id if notification}}"/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="notify_channel" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Canal de notification Discord</label>
|
||||
<select name="notify_channel" id="notify_channel"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-red-500 focus:border-transparent transition-all">
|
||||
{% for channel in channels %}
|
||||
<option value="{{channel.id}}" {% if notification and notification.notify_channel == channel.id %}selected{% endif %}>{{channel.name}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="video_type" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Type de vidéo à notifier</label>
|
||||
<select name="video_type" id="video_type"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-red-500 focus:border-transparent transition-all">
|
||||
<option value="all" {% if notification and notification.video_type == 'all' %}selected{% endif %}>Toutes (vidéos + shorts)</option>
|
||||
<option value="video" {% if notification and notification.video_type == 'video' %}selected{% endif %}>Vidéos uniquement</option>
|
||||
<option value="short" {% if notification and notification.video_type == 'short' %}selected{% endif %}>Shorts uniquement</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="message" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Message (optionnel)</label>
|
||||
<textarea name="message" id="message" rows="2"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-red-500 focus:border-transparent transition-all resize-y"
|
||||
placeholder="Message envoyé avant l'embed">{{notification.message if notification}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4 space-y-4">
|
||||
<h3 class="font-medium text-gray-800 dark:text-gray-200">Personnalisation de l'embed Discord</h3>
|
||||
|
||||
<div>
|
||||
<label for="embed_title" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Titre de l'embed</label>
|
||||
<input name="embed_title" id="embed_title" type="text" maxlength="256"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-red-500 focus:border-transparent transition-all"
|
||||
placeholder="{video_title}"
|
||||
value="{{notification.embed_title if notification}}"/>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Variables: {video_title}, {channel_name}, {video_url}, {video_id}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="embed_description" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Description de l'embed</label>
|
||||
<textarea name="embed_description" id="embed_description" rows="2"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-red-500 focus:border-transparent transition-all resize-y"
|
||||
placeholder="Description optionnelle">{{notification.embed_description if notification}}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="embed_color" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Couleur</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input name="embed_color" id="embed_color" type="color"
|
||||
class="w-12 h-10 rounded border border-gray-300 dark:border-gray-600 cursor-pointer"
|
||||
value="#{{notification.embed_color if notification else 'FF0000'}}"/>
|
||||
<input type="text" id="embed_color_text" maxlength="6"
|
||||
class="flex-1 px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white font-mono text-sm"
|
||||
value="{{notification.embed_color if notification else 'FF0000'}}" placeholder="FF0000"/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="embed_author_name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Nom de l'auteur</label>
|
||||
<input name="embed_author_name" id="embed_author_name" type="text" maxlength="256"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-red-500 focus:border-transparent transition-all"
|
||||
placeholder="{channel_name}"
|
||||
value="{{notification.embed_author_name if notification}}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="embed_author_icon" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Icône de l'auteur (URL)</label>
|
||||
<input name="embed_author_icon" id="embed_author_icon" type="text" maxlength="512"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-red-500 focus:border-transparent transition-all"
|
||||
placeholder="https://www.youtube.com/img/desktop/yt_1200.png"
|
||||
value="{{notification.embed_author_icon if notification}}"/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="embed_footer" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Pied de page</label>
|
||||
<input name="embed_footer" id="embed_footer" type="text" maxlength="2048"
|
||||
class="w-full px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-red-500 focus:border-transparent transition-all"
|
||||
placeholder="Texte optionnel en bas"
|
||||
value="{{notification.embed_footer if notification}}"/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" name="embed_thumbnail" id="embed_thumbnail"
|
||||
{% if not notification or notification.embed_thumbnail %}checked{% endif %}
|
||||
class="w-5 h-5 rounded border-gray-300 dark:border-gray-600 text-red-600 focus:ring-red-500 dark:bg-gray-700">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Miniature</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" name="embed_image" id="embed_image"
|
||||
{% if not notification or notification.embed_image %}checked{% endif %}
|
||||
class="w-5 h-5 rounded border-gray-300 dark:border-gray-600 text-red-600 focus:ring-red-500 dark:bg-gray-700">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Image principale</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<button type="submit"
|
||||
class="px-6 py-2.5 bg-red-600 hover:bg-red-700 text-white font-medium rounded-lg transition-colors focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
|
||||
{{ 'Enregistrer' if notification else 'Ajouter la notification' }}
|
||||
</button>
|
||||
{% if notification %}
|
||||
<a href="{{ url_for('youtube') }}"
|
||||
class="px-6 py-2.5 bg-gray-200 dark:bg-gray-600 hover:bg-gray-300 dark:hover:bg-gray-500 text-gray-700 dark:text-gray-200 font-medium rounded-lg transition-colors">
|
||||
Annuler
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-medium text-gray-800 dark:text-gray-200 mb-4">Prévisualisation de l'embed Discord</h3>
|
||||
<div id="embed-preview" class="bg-[#2f3136] rounded p-4 font-sans text-[#dcddde] max-w-xl border-l-4" style="border-left-color: #FF0000;">
|
||||
<div id="embed-author" class="flex items-center mb-2 text-sm">
|
||||
<img id="embed-author-icon" src="https://www.youtube.com/img/desktop/yt_1200.png" class="w-5 h-5 rounded-full mr-2" onerror="this.style.display='none'"/>
|
||||
<span id="embed-author-name" class="font-semibold">Nom de la chaîne</span>
|
||||
</div>
|
||||
<a id="embed-title" href="#" class="text-[#00aff4] no-underline text-base font-semibold block mb-2">Titre de la vidéo</a>
|
||||
<div id="embed-description" class="text-sm leading-relaxed mb-2 text-[#dcddde]"></div>
|
||||
<div id="embed-thumbnail-container" class="my-2">
|
||||
<img id="embed-thumbnail" src="" class="max-w-[80px] max-h-[80px] rounded float-right ml-4 hidden"/>
|
||||
</div>
|
||||
<div id="embed-image-container" class="mt-4">
|
||||
<img id="embed-image" src="https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg" class="max-w-full rounded hidden"/>
|
||||
</div>
|
||||
<div id="embed-footer" class="mt-2 text-xs text-[#72767d]"></div>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">Cette prévisualisation est approximative.</p>
|
||||
|
||||
<div class="mt-6 bg-gray-50 dark:bg-gray-700/50 rounded-lg p-4">
|
||||
<h4 class="font-medium text-gray-800 dark:text-gray-200 mb-2">Variables disponibles</h4>
|
||||
<ul class="text-sm text-gray-600 dark:text-gray-400 space-y-1">
|
||||
<li><code class="px-1.5 py-0.5 bg-gray-200 dark:bg-gray-600 rounded">{channel_name}</code> — Nom de la chaîne</li>
|
||||
<li><code class="px-1.5 py-0.5 bg-gray-200 dark:bg-gray-600 rounded">{video_title}</code> — Titre de la vidéo</li>
|
||||
<li><code class="px-1.5 py-0.5 bg-gray-200 dark:bg-gray-600 rounded">{video_url}</code> — Lien vers la vidéo</li>
|
||||
<li><code class="px-1.5 py-0.5 bg-gray-200 dark:bg-gray-600 rounded">{video_id}</code> — ID de la vidéo</li>
|
||||
<li><code class="px-1.5 py-0.5 bg-gray-200 dark:bg-gray-600 rounded">{thumbnail}</code> — URL de la miniature</li>
|
||||
<li><code class="px-1.5 py-0.5 bg-gray-200 dark:bg-gray-600 rounded">{published_at}</code> — Date de publication</li>
|
||||
<li><code class="px-1.5 py-0.5 bg-gray-200 dark:bg-gray-600 rounded">{is_short}</code> — True si c'est un short</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function formatText(text, vars) {
|
||||
if (!text) return '';
|
||||
return text.replace(/\{(\w+)\}/g, function(match, key) {
|
||||
return vars[key] || match;
|
||||
});
|
||||
}
|
||||
|
||||
function updatePreview() {
|
||||
const embedTitle = document.getElementById('embed_title').value || '{video_title}';
|
||||
const embedDescription = document.getElementById('embed_description').value || '';
|
||||
const embedColor = document.getElementById('embed_color_text').value || 'FF0000';
|
||||
const embedAuthorName = document.getElementById('embed_author_name').value || '{channel_name}';
|
||||
const embedAuthorIcon = document.getElementById('embed_author_icon').value || 'https://www.youtube.com/img/desktop/yt_1200.png';
|
||||
const embedFooter = document.getElementById('embed_footer').value || '';
|
||||
const embedThumbnail = document.getElementById('embed_thumbnail').checked;
|
||||
const embedImage = document.getElementById('embed_image').checked;
|
||||
|
||||
const vars = {
|
||||
video_title: 'Nouvelle vidéo de test',
|
||||
channel_name: 'Ma Chaîne YouTube',
|
||||
video_url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
||||
video_id: 'dQw4w9WgXcQ',
|
||||
thumbnail: 'https://i.ytimg.com/vi/dQw4w9WgXcQ/maxresdefault.jpg',
|
||||
published_at: '2026-01-25T12:00:00Z',
|
||||
is_short: false
|
||||
};
|
||||
|
||||
document.getElementById('embed-title').textContent = formatText(embedTitle, vars);
|
||||
document.getElementById('embed-title').href = vars.video_url;
|
||||
document.getElementById('embed-description').textContent = formatText(embedDescription, vars);
|
||||
document.getElementById('embed-author-name').textContent = formatText(embedAuthorName, vars);
|
||||
document.getElementById('embed-author-icon').src = embedAuthorIcon;
|
||||
document.getElementById('embed-footer').textContent = formatText(embedFooter, vars);
|
||||
|
||||
document.getElementById('embed-preview').style.borderLeftColor = '#' + embedColor;
|
||||
|
||||
if (embedThumbnail) {
|
||||
document.getElementById('embed-thumbnail').src = vars.thumbnail;
|
||||
document.getElementById('embed-thumbnail').style.display = 'block';
|
||||
} else {
|
||||
document.getElementById('embed-thumbnail').style.display = 'none';
|
||||
}
|
||||
|
||||
if (embedImage) {
|
||||
document.getElementById('embed-image').src = vars.thumbnail;
|
||||
document.getElementById('embed-image').style.display = 'block';
|
||||
} else {
|
||||
document.getElementById('embed-image').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('embed_color').addEventListener('input', function(e) {
|
||||
document.getElementById('embed_color_text').value = e.target.value.substring(1).toUpperCase();
|
||||
updatePreview();
|
||||
});
|
||||
|
||||
document.getElementById('embed_color_text').addEventListener('input', function(e) {
|
||||
const val = e.target.value.replace(/[^0-9A-Fa-f]/g, '').substring(0, 6);
|
||||
e.target.value = val;
|
||||
if (val.length === 6) {
|
||||
document.getElementById('embed_color').value = '#' + val;
|
||||
updatePreview();
|
||||
}
|
||||
});
|
||||
|
||||
const formFields = ['embed_title', 'embed_description', 'embed_author_name', 'embed_author_icon', 'embed_footer', 'embed_thumbnail', 'embed_image'];
|
||||
formFields.forEach(field => {
|
||||
const el = document.getElementById(field);
|
||||
if (el) {
|
||||
el.addEventListener('input', updatePreview);
|
||||
el.addEventListener('change', updatePreview);
|
||||
}
|
||||
});
|
||||
|
||||
updatePreview();
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user