diff --git a/.htaccess b/.htaccess
new file mode 100644
index 0000000..2f1dbed
--- /dev/null
+++ b/.htaccess
@@ -0,0 +1,66 @@
+# Activer le moteur de réécriture d'URL
+RewriteEngine On
+
+# Bloquer l'accès direct au dossier includes
+RewriteRule ^includes/ - [F,L]
+
+# Bloquer l'accès direct au dossier data
+RewriteRule ^data/ - [F,L]
+
+# Bloquer l'accès direct aux fichiers de base de données
+
+ Order Allow,Deny
+ Deny from all
+
+
+# Empêcher la navigation dans les répertoires
+Options -Indexes
+
+# Définir l'encodage par défaut
+AddDefaultCharset UTF-8
+
+# Sécurité - Protection contre les XSS
+
+ Header set X-XSS-Protection "1; mode=block"
+ Header set X-Content-Type-Options "nosniff"
+ Header set X-Frame-Options "SAMEORIGIN"
+ Header set Referrer-Policy "strict-origin-when-cross-origin"
+
+
+# Protection contre les attaques par clickjacking
+
+ Header always append X-Frame-Options SAMEORIGIN
+
+
+# Améliorer la sécurité des cookies (à utiliser avec HTTPS)
+
+ php_value session.cookie_httponly 1
+ php_value session.use_only_cookies 1
+ # Décommenter la ligne suivante si le site est en HTTPS
+ # php_value session.cookie_secure 1
+
+
+# Bloquer l'accès aux fichiers sensibles
+
+ Order Allow,Deny
+ Deny from all
+
+
+# Performance - Mise en cache des ressources statiques
+
+ ExpiresActive On
+ ExpiresDefault "access plus 1 month"
+ ExpiresByType text/css "access plus 1 year"
+ ExpiresByType application/javascript "access plus 1 year"
+ ExpiresByType image/gif "access plus 1 year"
+ ExpiresByType image/jpeg "access plus 1 year"
+ ExpiresByType image/png "access plus 1 year"
+ ExpiresByType image/webp "access plus 1 year"
+ ExpiresByType image/svg+xml "access plus 1 year"
+ ExpiresByType image/x-icon "access plus 1 year"
+
+
+# Activer la compression des fichiers
+
+ AddOutputFilterByType DEFLATE text/html text/css application/javascript text/plain text/xml application/json
+
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index 817d2e9..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,9 +0,0 @@
-MIT License
-
-Copyright (c) 2025 camelia-studio
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
deleted file mode 100644
index 6a52311..0000000
--- a/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Chasse_aux_couronnes
-
-Site pour partager ses quêtes d'investigations afin de faire profiter aux copains de nos couronnes !
\ No newline at end of file
diff --git a/admin.html b/admin.html
deleted file mode 100644
index c676186..0000000
--- a/admin.html
+++ /dev/null
@@ -1,244 +0,0 @@
-
-
-
-
-
- Administration - MH Wilds Couronnes
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Administration
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Texte
- Statut
- Actions
-
-
-
-
-
-
-
-
- Aucune annonce pour le moment.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Nettoyer les quêtes anciennes
-
Cette action supprimera toutes les quêtes datant de plus de 7 jours.
-
Nettoyer les quêtes anciennes
-
-
-
-
-
Statistiques du site
-
-
- Nombre total de monstres
- 0
-
-
- Nombre total de quêtes
- 0
-
-
- Quêtes avec petite couronne
- 0
-
-
- Quêtes avec grande couronne
- 0
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Nom du monstre
-
-
-
-
Chemin de l'image
-
- img/
-
-
-
Format recommandé: jpg, 300x200px. Exemple: Rathalos.jpg
-
-
- Enregistrer
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Êtes-vous sûr de vouloir supprimer cet élément ?
-
-
-
-
-
-
-
-
-
Ce site est réalisé dans le cadre de la branche Alt Tab de l'association Camélia Studio .
-
Images des monstres par Sui Yun - Site sous licence MIT, code source sur Gitea
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/admin/index.php b/admin/index.php
new file mode 100644
index 0000000..9955f67
--- /dev/null
+++ b/admin/index.php
@@ -0,0 +1,290 @@
+
+EOT;
+
+// Récupérer les données
+$monsters = get_all_monsters();
+$announcements = get_all_announcements(false); // Récupérer toutes les annonces, y compris inactives
+$statistics = get_site_statistics();
+
+// Inclure l'en-tête
+include '../includes/header.php';
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Texte
+ Statut
+ Date
+ Actions
+
+
+
+
+
+ Aucune annonce pour le moment.
+
+
+
+
+
+
+
+ Active
+
+ Inactive
+
+
+
+
+
+
+ Éditer
+
+
+ Supprimer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Nettoyer les quêtes anciennes
+
Cette action supprimera toutes les quêtes datant de plus de 7 jours.
+
+
+ Nettoyer les quêtes anciennes
+
+
+
+
+
+
Statistiques du site
+
+
+ Nombre total de monstres
+
+
+
+ Nombre total de quêtes
+
+
+
+ Quêtes avec petite couronne
+
+
+
+ Quêtes avec grande couronne
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Texte de l'annonce
+
+
+
+
+
+ Activer l'annonce
+
+
+
+ Enregistrer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Nom du monstre
+
+
+
+
Chemin de l'image
+
+ assets/img/
+
+
+
Format recommandé: jpg, 300x200px. Exemple: Rathalos.jpg
+
+
+ Enregistrer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Êtes-vous sûr de vouloir supprimer cet élément ?
+
+
+
+
+
+
+
+
+
+
+
+ true, 'monsters' => $monsters]);
+ break;
+
+ case 'getQuests':
+ // Vérifier que l'ID du monstre est fourni
+ if (!isset($_GET['monsterId'])) {
+ send_json_response(['success' => false, 'message' => 'ID de monstre manquant'], 400);
+ }
+
+ $monster_id = intval($_GET['monsterId']);
+ $crown_type = isset($_GET['crownType']) ? $_GET['crownType'] : 'all';
+
+ // Récupérer les quêtes
+ if ($crown_type === 'all') {
+ $quests = get_quests_by_monster($monster_id);
+ } else {
+ $quests = get_quests_by_monster_and_crown($monster_id, $crown_type);
+ }
+
+ // Récupérer le monstre pour inclure son nom
+ $monster = get_monster_by_id($monster_id);
+
+ // Ajouter les infos de fraîcheur aux quêtes
+ foreach ($quests as &$quest) {
+ $quest['freshness'] = format_relative_date($quest['date']);
+ }
+
+ send_json_response([
+ 'success' => true,
+ 'quests' => $quests,
+ 'monster' => $monster ? $monster['name'] : 'Monstre inconnu'
+ ]);
+ break;
+
+ case 'getAnnouncements':
+ // Récupérer les annonces actives
+ $announcements = get_all_announcements(true);
+ send_json_response(['success' => true, 'announcements' => $announcements]);
+ break;
+
+ case 'getStatistics':
+ // Vérifier l'authentification pour les statistiques
+ if (!is_logged_in()) {
+ send_json_response(['success' => false, 'message' => 'Authentification requise'], 401);
+ }
+
+ // Récupérer les statistiques
+ $stats = get_site_statistics();
+ send_json_response(['success' => true, 'stats' => $stats]);
+ break;
+
+ default:
+ send_json_response(['success' => false, 'message' => 'Action non reconnue'], 400);
+ }
+} elseif ($method === 'POST') {
+ // Actions de modification de données
+
+ // Traiter les données POST
+ $post_data = json_decode(file_get_contents('php://input'), true);
+ if (!$post_data) {
+ $post_data = $_POST;
+ }
+
+ // Vérifier le jeton CSRF pour les actions non administratives
+ if (in_array($action, ['addQuest', 'deleteQuest'])) {
+ if (!isset($post_data['csrf_token']) || !verify_csrf_token($post_data['csrf_token'])) {
+ send_json_response(['success' => false, 'message' => 'Jeton de sécurité invalide'], 403);
+ }
+ }
+
+ // Vérifier l'authentification pour les actions administratives
+ if (in_array($action, ['addMonster', 'updateMonster', 'deleteMonster', 'addAnnouncement', 'updateAnnouncement', 'deleteAnnouncement', 'cleanOldQuests'])) {
+ if (!is_logged_in()) {
+ send_json_response(['success' => false, 'message' => 'Authentification requise'], 401);
+ }
+ }
+
+ switch ($action) {
+ case 'addQuest':
+ // Vérifier les données requises
+ if (!isset($post_data['selectedMonsterId']) || !isset($post_data['crownType']) ||
+ !isset($post_data['playerName']) || !isset($post_data['playerId'])) {
+ send_json_response(['success' => false, 'message' => 'Données manquantes'], 400);
+ }
+
+ $monster_id = intval($post_data['selectedMonsterId']);
+ $crown_type = $post_data['crownType'];
+ $player_name = trim($post_data['playerName']);
+ $player_id = trim($post_data['playerId']);
+
+ // Validation
+ if (!in_array($crown_type, ['small', 'large'])) {
+ send_json_response(['success' => false, 'message' => 'Type de couronne invalide'], 400);
+ }
+
+ if (empty($player_name) || empty($player_id)) {
+ send_json_response(['success' => false, 'message' => 'Nom ou ID du joueur manquant'], 400);
+ }
+
+ // Vérifier que le monstre existe
+ if (!get_monster_by_id($monster_id)) {
+ send_json_response(['success' => false, 'message' => 'Monstre invalide'], 400);
+ }
+
+ // Ajouter la quête
+ $quest_id = add_quest($monster_id, $crown_type, $player_name, $player_id);
+
+ if ($quest_id) {
+ send_json_response(['success' => true, 'questId' => $quest_id]);
+ } else {
+ send_json_response(['success' => false, 'message' => 'Erreur lors de l\'ajout de la quête'], 500);
+ }
+ break;
+
+ case 'deleteQuest':
+ // Vérifier les données requises
+ if (!isset($post_data['deleteQuestId'])) {
+ send_json_response(['success' => false, 'message' => 'ID de quête manquant'], 400);
+ }
+
+ $quest_id = intval($post_data['deleteQuestId']);
+
+ // Supprimer la quête
+ if (delete_quest($quest_id)) {
+ send_json_response(['success' => true]);
+ } else {
+ send_json_response(['success' => false, 'message' => 'Erreur lors de la suppression de la quête'], 500);
+ }
+ break;
+
+ case 'addMonster':
+ // Vérifier les données requises
+ if (!isset($post_data['name']) || !isset($post_data['image'])) {
+ send_json_response(['success' => false, 'message' => 'Données manquantes'], 400);
+ }
+
+ $name = trim($post_data['name']);
+ $image = trim($post_data['image']);
+
+ // Validation
+ if (empty($name)) {
+ send_json_response(['success' => false, 'message' => 'Nom du monstre manquant'], 400);
+ }
+
+ if (!is_valid_image_url($image)) {
+ send_json_response(['success' => false, 'message' => 'URL d\'image invalide'], 400);
+ }
+
+ // Ajouter le monstre
+ $monster_id = add_monster($name, $image);
+
+ if ($monster_id) {
+ send_json_response(['success' => true, 'monsterId' => $monster_id]);
+ } else {
+ send_json_response(['success' => false, 'message' => 'Erreur lors de l\'ajout du monstre'], 500);
+ }
+ break;
+
+ case 'updateMonster':
+ // Vérifier les données requises
+ if (!isset($post_data['id']) || !isset($post_data['name']) || !isset($post_data['image'])) {
+ send_json_response(['success' => false, 'message' => 'Données manquantes'], 400);
+ }
+
+ $id = intval($post_data['id']);
+ $name = trim($post_data['name']);
+ $image = trim($post_data['image']);
+
+ // Validation
+ if (empty($name)) {
+ send_json_response(['success' => false, 'message' => 'Nom du monstre manquant'], 400);
+ }
+
+ if (!is_valid_image_url($image)) {
+ send_json_response(['success' => false, 'message' => 'URL d\'image invalide'], 400);
+ }
+
+ // Mettre à jour le monstre
+ if (update_monster($id, $name, $image)) {
+ send_json_response(['success' => true]);
+ } else {
+ send_json_response(['success' => false, 'message' => 'Erreur lors de la mise à jour du monstre'], 500);
+ }
+ break;
+
+ case 'deleteMonster':
+ // Vérifier les données requises
+ if (!isset($post_data['id'])) {
+ send_json_response(['success' => false, 'message' => 'ID de monstre manquant'], 400);
+ }
+
+ $id = intval($post_data['id']);
+
+ // Supprimer le monstre
+ if (delete_monster($id)) {
+ send_json_response(['success' => true]);
+ } else {
+ send_json_response(['success' => false, 'message' => 'Erreur lors de la suppression du monstre'], 500);
+ }
+ break;
+
+ case 'addAnnouncement':
+ // Vérifier les données requises
+ if (!isset($post_data['text'])) {
+ send_json_response(['success' => false, 'message' => 'Texte d\'annonce manquant'], 400);
+ }
+
+ $text = trim($post_data['text']);
+ $active = isset($post_data['active']) ? (bool)$post_data['active'] : true;
+
+ // Validation
+ if (empty($text)) {
+ send_json_response(['success' => false, 'message' => 'Texte d\'annonce vide'], 400);
+ }
+
+ // Ajouter l'annonce
+ $announcement_id = add_announcement($text, $active);
+
+ if ($announcement_id) {
+ send_json_response(['success' => true, 'announcementId' => $announcement_id]);
+ } else {
+ send_json_response(['success' => false, 'message' => 'Erreur lors de l\'ajout de l\'annonce'], 500);
+ }
+ break;
+
+ case 'updateAnnouncement':
+ // Vérifier les données requises
+ if (!isset($post_data['id']) || !isset($post_data['text'])) {
+ send_json_response(['success' => false, 'message' => 'Données manquantes'], 400);
+ }
+
+ $id = intval($post_data['id']);
+ $text = trim($post_data['text']);
+ $active = isset($post_data['active']) ? (bool)$post_data['active'] : true;
+
+ // Validation
+ if (empty($text)) {
+ send_json_response(['success' => false, 'message' => 'Texte d\'annonce vide'], 400);
+ }
+
+ // Mettre à jour l'annonce
+ if (update_announcement($id, $text, $active)) {
+ send_json_response(['success' => true]);
+ } else {
+ send_json_response(['success' => false, 'message' => 'Erreur lors de la mise à jour de l\'annonce'], 500);
+ }
+ break;
+
+ case 'deleteAnnouncement':
+ // Vérifier les données requises
+ if (!isset($post_data['id'])) {
+ send_json_response(['success' => false, 'message' => 'ID d\'annonce manquant'], 400);
+ }
+
+ $id = intval($post_data['id']);
+
+ // Supprimer l'annonce
+ if (delete_announcement($id)) {
+ send_json_response(['success' => true]);
+ } else {
+ send_json_response(['success' => false, 'message' => 'Erreur lors de la suppression de l\'annonce'], 500);
+ }
+ break;
+
+ case 'cleanOldQuests':
+ // Nettoyer les quêtes expirées
+ $cleaned_count = clean_old_quests();
+ send_json_response(['success' => true, 'cleanedCount' => $cleaned_count]);
+ break;
+
+ default:
+ send_json_response(['success' => false, 'message' => 'Action non reconnue'], 400);
+ }
+} else {
+ // Méthode HTTP non autorisée
+ send_json_response(['success' => false, 'message' => 'Méthode non autorisée'], 405);
+}
\ No newline at end of file
diff --git a/assets/css/styles.css b/assets/css/styles.css
new file mode 100644
index 0000000..348e287
--- /dev/null
+++ b/assets/css/styles.css
@@ -0,0 +1,324 @@
+/**
+ * Styles pour MH Wilds - Partage de Quêtes à Couronnes
+ */
+
+ :root {
+ /* Palette de couleurs inspirée par MH Wilds */
+ --mh-dark: #272420;
+ --mh-dark-bg: #3a362f;
+ --mh-light: #f5f0e6;
+ --mh-light-bg: #4a463f;
+ --mh-accent: #e0b968; /* Or des couronnes */
+ --mh-accent-hover: #c9a65a;
+ --mh-primary: #5a90b1; /* Bleu similaire au thème MH */
+ --mh-danger: #e05e4e; /* Rouge pour les alertes */
+ --mh-success: #6bc46f; /* Vert pour les succès */
+ --mh-alert: #f3d384; /* Alerte douce */
+}
+
+/* Styles de base */
+body {
+ background-color: var(--mh-dark);
+ color: var(--mh-light);
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+}
+
+.container {
+ flex: 1;
+}
+
+/* En-tête et pied de page */
+header, footer {
+ background-color: var(--mh-dark-bg);
+}
+
+header .btn-outline-light:hover {
+ background-color: var(--mh-accent);
+ border-color: var(--mh-accent);
+ color: var(--mh-dark);
+}
+
+footer a {
+ color: var(--mh-accent);
+ text-decoration: none;
+}
+
+footer a:hover {
+ color: var(--mh-accent-hover);
+ text-decoration: underline;
+}
+
+/* Cartes des monstres */
+.card {
+ background-color: var(--mh-dark-bg);
+ border: none;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
+ transition: transform 0.2s ease-in-out;
+ height: 100%;
+}
+
+.monster-card {
+ cursor: pointer;
+}
+
+.monster-card:hover {
+ transform: translateY(-5px);
+}
+
+.card-img-container {
+ position: relative;
+ overflow: hidden;
+ height: 150px;
+ background-color: var(--mh-dark);
+}
+
+.card-img-top {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ transition: transform 0.3s ease;
+}
+
+.monster-card:hover .card-img-top {
+ transform: scale(1.05);
+}
+
+.card-title {
+ color: var(--mh-accent);
+ font-weight: 600;
+}
+
+.card-body {
+ background-color: var(--mh-dark-bg);
+}
+
+.card-header {
+ background-color: var(--mh-accent);
+ color: var(--mh-dark);
+ font-weight: bold;
+}
+
+/* Badges de couronnes */
+.crown-badge {
+ display: inline-block;
+ padding: 0.25rem 0.5rem;
+ margin-right: 0.5rem;
+ border-radius: 0.25rem;
+ font-weight: bold;
+}
+
+.small-crown {
+ background-color: #ffe066;
+ color: #5c4d00;
+}
+
+.large-crown {
+ background-color: #ffd700;
+ color: #5c4d00;
+}
+
+.crown-icon {
+ margin-right: 0.25rem;
+}
+
+/* Boutons et éléments interactifs */
+.btn-primary {
+ background-color: var(--mh-accent);
+ border-color: var(--mh-accent);
+ color: var(--mh-dark);
+}
+
+.btn-primary:hover {
+ background-color: var(--mh-accent-hover);
+ border-color: var(--mh-accent-hover);
+ color: var(--mh-dark);
+}
+
+.btn-outline-primary {
+ border-color: var(--mh-accent);
+ color: var(--mh-accent);
+}
+
+.btn-outline-primary:hover {
+ background-color: var(--mh-accent);
+ color: var(--mh-dark);
+}
+
+.btn-outline-accent {
+ border-color: var(--mh-accent);
+ color: var(--mh-accent);
+}
+
+.btn-outline-accent:hover,
+.btn-check:checked + .btn-outline-accent {
+ background-color: var(--mh-accent);
+ color: var(--mh-dark);
+}
+
+.btn-danger {
+ background-color: var(--mh-danger);
+ border-color: var(--mh-danger);
+}
+
+/* Modales */
+.modal-content {
+ background-color: var(--mh-dark-bg);
+ color: var(--mh-light);
+ border: none;
+}
+
+.modal-header {
+ border-bottom-color: var(--mh-dark);
+}
+
+.modal-footer {
+ border-top-color: var(--mh-dark);
+}
+
+.modal-title {
+ color: var(--mh-accent);
+}
+
+/* Formulaires */
+.form-control, .form-select, .input-group-text {
+ background-color: var(--mh-light-bg);
+ border-color: var(--mh-dark);
+ color: var(--mh-light);
+}
+
+.form-control:focus, .form-select:focus {
+ background-color: var(--mh-light-bg);
+ color: var(--mh-light);
+ border-color: var(--mh-accent);
+ box-shadow: 0 0 0 0.25rem rgba(224, 185, 104, 0.25);
+}
+
+.form-check-input:checked {
+ background-color: var(--mh-accent);
+ border-color: var(--mh-accent);
+}
+
+.form-check-input:focus {
+ border-color: var(--mh-accent);
+ box-shadow: 0 0 0 0.25rem rgba(224, 185, 104, 0.25);
+}
+
+/* Messages et alertes */
+.alert-info {
+ background-color: var(--mh-light-bg);
+ color: var(--mh-light);
+ border-color: var(--mh-alert);
+}
+
+.alert-success {
+ background-color: var(--mh-success);
+ color: var(--mh-dark);
+ border-color: var(--mh-success);
+}
+
+.alert-danger {
+ background-color: var(--mh-danger);
+ color: var(--mh-dark);
+ border-color: var(--mh-danger);
+}
+
+.empty-message {
+ text-align: center;
+ padding: 2rem;
+ background-color: var(--mh-dark-bg);
+ border-radius: 0.375rem;
+ margin: 1rem 0;
+}
+
+/* Animation de fondu */
+.fade-in {
+ animation: fadeIn 0.5s ease-in-out;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(10px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+/* Styles pour les quêtes */
+.quest-card {
+ position: relative;
+ transition: transform 0.2s ease;
+}
+
+.quest-card:hover {
+ transform: translateY(-3px);
+}
+
+.quest-date {
+ font-size: 0.85rem;
+ color: #aaa;
+ margin-bottom: 0.5rem;
+}
+
+/* Styles d'administration */
+.list-group-item {
+ background-color: var(--mh-dark-bg);
+ color: var(--mh-light);
+ border-color: var(--mh-dark);
+}
+
+.list-group-item:hover {
+ background-color: var(--mh-light-bg);
+}
+
+.list-group-item.active {
+ background-color: var(--mh-accent);
+ border-color: var(--mh-accent);
+ color: var(--mh-dark);
+}
+
+.table {
+ color: var(--mh-light);
+}
+
+.table-striped > tbody > tr:nth-of-type(odd) {
+ background-color: rgba(255, 255, 255, 0.05);
+}
+
+.admin-section {
+ margin-bottom: 2rem;
+}
+
+.badge {
+ font-weight: 500;
+}
+
+/* Page tutoriel */
+.tutorial-content h3 {
+ color: var(--mh-accent);
+ margin-top: 1.5rem;
+}
+
+.tutorial-content h4 {
+ color: var(--mh-primary);
+ margin-top: 1.25rem;
+}
+
+.tutorial-content ul {
+ margin-bottom: 1.5rem;
+}
+
+/* Styles de responsive */
+@media (max-width: 767.98px) {
+ .card-img-container {
+ height: 120px;
+ }
+
+ h2 {
+ font-size: 1.5rem;
+ }
+
+ .crown-badge {
+ padding: 0.15rem 0.3rem;
+ font-size: 0.85rem;
+ }
+}
\ No newline at end of file
diff --git a/img/Ajarakan.jpg b/assets/img/Ajarakan.jpg
similarity index 100%
rename from img/Ajarakan.jpg
rename to assets/img/Ajarakan.jpg
diff --git a/img/Anjanath_tonnerre_Gardien.jpg b/assets/img/Anjanath_Tonnerre_Gardien.jpg
similarity index 100%
rename from img/Anjanath_tonnerre_Gardien.jpg
rename to assets/img/Anjanath_Tonnerre_Gardien.jpg
diff --git a/img/Arkveld.jpg b/assets/img/Arkveld.jpg
similarity index 100%
rename from img/Arkveld.jpg
rename to assets/img/Arkveld.jpg
diff --git a/img/Arkveld_Gardien.jpg b/assets/img/Arkveld_Gardien.jpg
similarity index 100%
rename from img/Arkveld_Gardien.jpg
rename to assets/img/Arkveld_Gardien.jpg
diff --git a/img/Balahara.jpg b/assets/img/Balahara.jpg
similarity index 100%
rename from img/Balahara.jpg
rename to assets/img/Balahara.jpg
diff --git a/img/Blangonga.jpg b/assets/img/Blangonga.jpg
similarity index 100%
rename from img/Blangonga.jpg
rename to assets/img/Blangonga.jpg
diff --git a/img/Chatacabra.jpg b/assets/img/Chatacabra.jpg
similarity index 100%
rename from img/Chatacabra.jpg
rename to assets/img/Chatacabra.jpg
diff --git a/img/Congalala.jpg b/assets/img/Congalala.jpg
similarity index 100%
rename from img/Congalala.jpg
rename to assets/img/Congalala.jpg
diff --git a/img/Doshaguma.jpg b/assets/img/Doshaguma.jpg
similarity index 100%
rename from img/Doshaguma.jpg
rename to assets/img/Doshaguma.jpg
diff --git a/img/Doshaguma_Gardien.jpg b/assets/img/Doshaguma_Gardien.jpg
similarity index 100%
rename from img/Doshaguma_Gardien.jpg
rename to assets/img/Doshaguma_Gardien.jpg
diff --git a/img/Gore_Malaga.jpg b/assets/img/Gore_Malaga.jpg
similarity index 100%
rename from img/Gore_Malaga.jpg
rename to assets/img/Gore_Malaga.jpg
diff --git a/img/Gravios.jpg b/assets/img/Gravios.jpg
similarity index 100%
rename from img/Gravios.jpg
rename to assets/img/Gravios.jpg
diff --git a/img/Gypceros.jpg b/assets/img/Gypceros.jpg
similarity index 100%
rename from img/Gypceros.jpg
rename to assets/img/Gypceros.jpg
diff --git a/img/Hirabami.jpg b/assets/img/Hirabami.jpg
similarity index 100%
rename from img/Hirabami.jpg
rename to assets/img/Hirabami.jpg
diff --git a/img/Jin_Dahaad.jpg b/assets/img/Jin_Dahaad.jpg
similarity index 100%
rename from img/Jin_Dahaad.jpg
rename to assets/img/Jin_Dahaad.jpg
diff --git a/img/Lala_Barina.jpg b/assets/img/Lala_Barina.jpg
similarity index 100%
rename from img/Lala_Barina.jpg
rename to assets/img/Lala_Barina.jpg
diff --git a/img/Nerscylla.jpg b/assets/img/Nerscylla.jpg
similarity index 100%
rename from img/Nerscylla.jpg
rename to assets/img/Nerscylla.jpg
diff --git a/img/Nu_Udra.jpg b/assets/img/Nu_Udra.jpg
similarity index 100%
rename from img/Nu_Udra.jpg
rename to assets/img/Nu_Udra.jpg
diff --git a/img/Odogaron_Desastre_Gardien.jpg b/assets/img/Odogaron_Desastre_Gardien.jpg
similarity index 100%
rename from img/Odogaron_Desastre_Gardien.jpg
rename to assets/img/Odogaron_Desastre_Gardien.jpg
diff --git a/img/Quematrice.jpg b/assets/img/Quematrice.jpg
similarity index 100%
rename from img/Quematrice.jpg
rename to assets/img/Quematrice.jpg
diff --git a/img/Rathalos.jpg b/assets/img/Rathalos.jpg
similarity index 100%
rename from img/Rathalos.jpg
rename to assets/img/Rathalos.jpg
diff --git a/img/Rathalos_Gardien.jpg b/assets/img/Rathalos_Gardien.jpg
similarity index 100%
rename from img/Rathalos_Gardien.jpg
rename to assets/img/Rathalos_Gardien.jpg
diff --git a/img/Rathian.jpg b/assets/img/Rathian.jpg
similarity index 100%
rename from img/Rathian.jpg
rename to assets/img/Rathian.jpg
diff --git a/img/Rey_Dau.jpg b/assets/img/Rey_Dau.jpg
similarity index 100%
rename from img/Rey_Dau.jpg
rename to assets/img/Rey_Dau.jpg
diff --git a/img/Rompopolo.jpg b/assets/img/Rompopolo.jpg
similarity index 100%
rename from img/Rompopolo.jpg
rename to assets/img/Rompopolo.jpg
diff --git a/img/Uth_Duna.jpg b/assets/img/Uth_Duna.jpg
similarity index 100%
rename from img/Uth_Duna.jpg
rename to assets/img/Uth_Duna.jpg
diff --git a/img/Xu_Wu.jpg b/assets/img/Xu_Wu.jpg
similarity index 100%
rename from img/Xu_Wu.jpg
rename to assets/img/Xu_Wu.jpg
diff --git a/img/Yian_Kut-Ku.jpg b/assets/img/Yian_Kut-Ku.jpg
similarity index 100%
rename from img/Yian_Kut-Ku.jpg
rename to assets/img/Yian_Kut-Ku.jpg
diff --git a/img/Zoh_Shia.jpg b/assets/img/Zoh_Shia.jpg
similarity index 100%
rename from img/Zoh_Shia.jpg
rename to assets/img/Zoh_Shia.jpg
diff --git a/img/logo.png b/assets/img/logo.png
similarity index 100%
rename from img/logo.png
rename to assets/img/logo.png
diff --git a/img/readme.md b/assets/img/readme.md
similarity index 100%
rename from img/readme.md
rename to assets/img/readme.md
diff --git a/assets/js/admin.js b/assets/js/admin.js
new file mode 100644
index 0000000..377d62a
--- /dev/null
+++ b/assets/js/admin.js
@@ -0,0 +1,502 @@
+/**
+ * Script d'administration pour MH Wilds - Partage de Quêtes à Couronnes
+ */
+
+// Variables globales
+let currentDeletionType = null;
+let currentDeletionId = null;
+
+// Initialisation au chargement du DOM
+document.addEventListener('DOMContentLoaded', () => {
+ // Éléments DOM pour les onglets
+ const announcementsTab = document.getElementById('announcementsTab');
+ const monstersTab = document.getElementById('monstersTab');
+ const maintenanceTab = document.getElementById('maintenanceTab');
+
+ // Éléments DOM pour les annonces
+ const announcementsListEl = document.getElementById('announcementsList');
+ const addAnnouncementBtn = document.getElementById('addAnnouncementBtn');
+ const announcementForm = document.getElementById('announcementForm');
+
+ // Éléments DOM pour les monstres
+ const monstersListEl = document.getElementById('monstersList');
+ const addMonsterBtn = document.getElementById('addMonsterBtn');
+ const monsterForm = document.getElementById('monsterForm');
+
+ // Éléments DOM pour la maintenance
+ const cleanOldQuestsBtn = document.getElementById('cleanOldQuestsBtn');
+
+ // Éléments DOM pour la confirmation de suppression
+ const confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
+
+ // Événements pour les onglets
+ if (announcementsTab) {
+ announcementsTab.addEventListener('click', (e) => {
+ e.preventDefault();
+ showSection('announcementsSection');
+ });
+ }
+
+ if (monstersTab) {
+ monstersTab.addEventListener('click', (e) => {
+ e.preventDefault();
+ showSection('monstersSection');
+ });
+ }
+
+ if (maintenanceTab) {
+ maintenanceTab.addEventListener('click', (e) => {
+ e.preventDefault();
+ showSection('maintenanceSection');
+ updateStatistics();
+ });
+ }
+
+ // Événements pour les annonces
+ if (addAnnouncementBtn) {
+ addAnnouncementBtn.addEventListener('click', () => {
+ resetAnnouncementForm();
+ document.getElementById('announcementModalTitle').textContent = 'Ajouter une annonce';
+ new bootstrap.Modal(document.getElementById('announcementModal')).show();
+ });
+ }
+
+ if (announcementForm) {
+ announcementForm.addEventListener('submit', handleSaveAnnouncement);
+ }
+
+ // Ajouter les événements pour les boutons d'édition et de suppression des annonces
+ if (announcementsListEl) {
+ document.querySelectorAll('.edit-announcement-btn').forEach(btn => {
+ btn.addEventListener('click', () => {
+ const announcementId = parseInt(btn.dataset.id);
+ editAnnouncement(announcementId);
+ });
+ });
+
+ document.querySelectorAll('.delete-announcement-btn').forEach(btn => {
+ btn.addEventListener('click', () => {
+ const announcementId = parseInt(btn.dataset.id);
+ showDeleteConfirmation('announcement', announcementId);
+ });
+ });
+ }
+
+ // Événements pour les monstres
+ if (addMonsterBtn) {
+ addMonsterBtn.addEventListener('click', () => {
+ resetMonsterForm();
+ document.getElementById('monsterModalTitle').textContent = 'Ajouter un monstre';
+ new bootstrap.Modal(document.getElementById('monsterModal')).show();
+ });
+ }
+
+ if (monsterForm) {
+ monsterForm.addEventListener('submit', handleSaveMonster);
+ }
+
+ // Ajouter les événements pour les boutons d'édition et de suppression des monstres
+ if (monstersListEl) {
+ document.querySelectorAll('.edit-monster-btn').forEach(btn => {
+ btn.addEventListener('click', () => {
+ const monsterId = parseInt(btn.dataset.id);
+ editMonster(monsterId);
+ });
+ });
+
+ document.querySelectorAll('.delete-monster-btn').forEach(btn => {
+ btn.addEventListener('click', () => {
+ const monsterId = parseInt(btn.dataset.id);
+ showDeleteConfirmation('monster', monsterId);
+ });
+ });
+ }
+
+ // Événements pour la maintenance
+ if (cleanOldQuestsBtn) {
+ cleanOldQuestsBtn.addEventListener('click', handleCleanOldQuests);
+ }
+
+ // Événement pour la confirmation de suppression
+ if (confirmDeleteBtn) {
+ confirmDeleteBtn.addEventListener('click', handleConfirmDelete);
+ }
+
+ // Événement pour la déconnexion
+ const logoutBtn = document.getElementById('logoutBtn');
+ if (logoutBtn) {
+ logoutBtn.addEventListener('click', () => {
+ window.location.href = 'logout.php';
+ });
+ }
+});
+
+/**
+ * Fonctions pour la navigation entre les sections
+ *
+ * @param {string} sectionId ID de la section à afficher
+ */
+function showSection(sectionId) {
+ // Désactiver tous les onglets et masquer toutes les sections
+ document.querySelectorAll('.list-group-item').forEach(tab => {
+ tab.classList.remove('active');
+ });
+
+ document.querySelectorAll('.admin-section').forEach(section => {
+ section.classList.add('d-none');
+ });
+
+ // Activer l'onglet sélectionné et afficher sa section
+ document.getElementById(sectionId).classList.remove('d-none');
+
+ if (sectionId === 'announcementsSection') {
+ document.getElementById('announcementsTab').classList.add('active');
+ } else if (sectionId === 'monstersSection') {
+ document.getElementById('monstersTab').classList.add('active');
+ } else if (sectionId === 'maintenanceSection') {
+ document.getElementById('maintenanceTab').classList.add('active');
+ }
+}
+
+/**
+ * ============ ANNONCES ============
+ */
+
+/**
+ * Réinitialiser le formulaire d'annonce
+ */
+function resetAnnouncementForm() {
+ const form = document.getElementById('announcementForm');
+ form.reset();
+ document.getElementById('announcementId').value = '';
+}
+
+/**
+ * Éditer une annonce
+ *
+ * @param {number} announcementId ID de l'annonce à éditer
+ */
+function editAnnouncement(announcementId) {
+ // Récupérer les données de l'annonce via AJAX
+ fetch(`../api.php?action=getAnnouncements`)
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ const announcement = data.announcements.find(a => a.id == announcementId);
+
+ if (announcement) {
+ document.getElementById('announcementId').value = announcement.id;
+ document.getElementById('announcementText').value = announcement.text;
+ document.getElementById('announcementActive').checked = announcement.active == 1;
+
+ document.getElementById('announcementModalTitle').textContent = 'Modifier l\'annonce';
+ new bootstrap.Modal(document.getElementById('announcementModal')).show();
+ }
+ } else {
+ alert('Erreur lors de la récupération des données de l\'annonce.');
+ }
+ })
+ .catch(error => {
+ console.error('Erreur:', error);
+ alert('Erreur de connexion. Veuillez réessayer.');
+ });
+}
+
+/**
+ * Gérer la sauvegarde d'une annonce
+ *
+ * @param {Event} e Événement de soumission du formulaire
+ */
+function handleSaveAnnouncement(e) {
+ e.preventDefault();
+
+ const announcementId = document.getElementById('announcementId').value.trim();
+ const announcementText = document.getElementById('announcementText').value.trim();
+ const announcementActive = document.getElementById('announcementActive').checked;
+
+ if (!announcementText) {
+ alert('Veuillez saisir le texte de l\'annonce');
+ return;
+ }
+
+ const formData = new FormData();
+ formData.append('text', announcementText);
+ formData.append('active', announcementActive ? 1 : 0);
+ formData.append('csrf_token', document.querySelector('input[name="csrf_token"]').value);
+
+ let url = '../api.php?action=';
+
+ if (announcementId) {
+ // Mode édition
+ url += 'updateAnnouncement';
+ formData.append('id', announcementId);
+ } else {
+ // Mode ajout
+ url += 'addAnnouncement';
+ }
+
+ // Envoyer la requête
+ fetch(url, {
+ method: 'POST',
+ body: formData
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ // Fermer la modale
+ bootstrap.Modal.getInstance(document.getElementById('announcementModal')).hide();
+
+ // Recharger la page pour mettre à jour la liste
+ window.location.reload();
+ } else {
+ alert('Erreur lors de la sauvegarde de l\'annonce: ' + (data.message || 'Erreur inconnue'));
+ }
+ })
+ .catch(error => {
+ console.error('Erreur:', error);
+ alert('Erreur de connexion. Veuillez réessayer.');
+ });
+}
+
+/**
+ * ============ MONSTRES ============
+ */
+
+/**
+ * Réinitialiser le formulaire de monstre
+ */
+function resetMonsterForm() {
+ const form = document.getElementById('monsterForm');
+ form.reset();
+ document.getElementById('monsterId').value = '';
+}
+
+/**
+ * Éditer un monstre
+ *
+ * @param {number} monsterId ID du monstre à éditer
+ */
+function editMonster(monsterId) {
+ // Récupérer les données du monstre via AJAX
+ fetch(`../api.php?action=getMonsters`)
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ const monster = data.monsters.find(m => m.id == monsterId);
+
+ if (monster) {
+ document.getElementById('monsterId').value = monster.id;
+ document.getElementById('monsterName').value = monster.name;
+
+ // Retirer le préfixe pour l'affichage dans le formulaire
+ let imagePath = monster.image;
+ if (imagePath.startsWith('assets/img/')) {
+ imagePath = imagePath.substring(11);
+ }
+ document.getElementById('monsterImage').value = imagePath;
+
+ document.getElementById('monsterModalTitle').textContent = 'Modifier le monstre';
+ new bootstrap.Modal(document.getElementById('monsterModal')).show();
+ }
+ } else {
+ alert('Erreur lors de la récupération des données du monstre.');
+ }
+ })
+ .catch(error => {
+ console.error('Erreur:', error);
+ alert('Erreur de connexion. Veuillez réessayer.');
+ });
+}
+
+/**
+ * Gérer la sauvegarde d'un monstre
+ *
+ * @param {Event} e Événement de soumission du formulaire
+ */
+function handleSaveMonster(e) {
+ e.preventDefault();
+
+ const monsterId = document.getElementById('monsterId').value.trim();
+ const monsterName = document.getElementById('monsterName').value.trim();
+ let monsterImage = document.getElementById('monsterImage').value.trim();
+
+ // Ajouter le préfixe 'assets/img/' si ce n'est pas déjà fait
+ if (!monsterImage.startsWith('assets/img/')) {
+ monsterImage = 'assets/img/' + monsterImage;
+ }
+
+ if (!monsterName || !monsterImage) {
+ alert('Veuillez remplir tous les champs');
+ return;
+ }
+
+ const formData = new FormData();
+ formData.append('name', monsterName);
+ formData.append('image', monsterImage);
+ formData.append('csrf_token', document.querySelector('input[name="csrf_token"]').value);
+
+ let url = '../api.php?action=';
+
+ if (monsterId) {
+ // Mode édition
+ url += 'updateMonster';
+ formData.append('id', monsterId);
+ } else {
+ // Mode ajout
+ url += 'addMonster';
+ }
+
+ // Envoyer la requête
+ fetch(url, {
+ method: 'POST',
+ body: formData
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ // Fermer la modale
+ bootstrap.Modal.getInstance(document.getElementById('monsterModal')).hide();
+
+ // Recharger la page pour mettre à jour la liste
+ window.location.reload();
+ } else {
+ alert('Erreur lors de la sauvegarde du monstre: ' + (data.message || 'Erreur inconnue'));
+ }
+ })
+ .catch(error => {
+ console.error('Erreur:', error);
+ alert('Erreur de connexion. Veuillez réessayer.');
+ });
+}
+
+/**
+ * ============ MAINTENANCE ============
+ */
+
+/**
+ * Mettre à jour les statistiques
+ */
+function updateStatistics() {
+ // Récupérer les statistiques via AJAX
+ fetch('../api.php?action=getStatistics')
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ document.getElementById('totalMonstersCount').textContent = data.stats.total_monsters;
+ document.getElementById('totalQuestsCount').textContent = data.stats.total_quests;
+ document.getElementById('smallCrownQuestsCount').textContent = data.stats.small_crown_quests;
+ document.getElementById('largeCrownQuestsCount').textContent = data.stats.large_crown_quests;
+ }
+ })
+ .catch(error => {
+ console.error('Erreur lors de la récupération des statistiques:', error);
+ });
+}
+
+/**
+ * Nettoyer les quêtes anciennes
+ */
+function handleCleanOldQuests() {
+ if (!confirm('Êtes-vous sûr de vouloir supprimer toutes les quêtes datant de plus de 7 jours ?')) {
+ return;
+ }
+
+ const formData = new FormData();
+ formData.append('csrf_token', document.querySelector('input[name="csrf_token"]').value);
+
+ // Envoyer la requête
+ fetch('../api.php?action=cleanOldQuests', {
+ method: 'POST',
+ body: formData
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ const cleanResult = document.getElementById('cleanResult');
+ cleanResult.innerHTML = `${data.cleanedCount} quête(s) supprimée(s) avec succès.`;
+ cleanResult.classList.remove('d-none');
+
+ // Mettre à jour les statistiques
+ updateStatistics();
+
+ // Masquer le message après 3 secondes
+ setTimeout(() => {
+ cleanResult.classList.add('d-none');
+ }, 3000);
+ } else {
+ alert('Erreur lors du nettoyage des quêtes: ' + (data.message || 'Erreur inconnue'));
+ }
+ })
+ .catch(error => {
+ console.error('Erreur:', error);
+ alert('Erreur de connexion. Veuillez réessayer.');
+ });
+}
+
+/**
+ * ============ SUPPRESSION ============
+ */
+
+/**
+ * Afficher la confirmation de suppression
+ *
+ * @param {string} type Type d'élément à supprimer ('announcement' ou 'monster')
+ * @param {number} id ID de l'élément à supprimer
+ */
+function showDeleteConfirmation(type, id) {
+ currentDeletionType = type;
+ currentDeletionId = id;
+
+ const confirmDeleteForm = document.getElementById('confirmDeleteForm');
+ confirmDeleteForm.querySelector('input[name="deletionType"]').value = type;
+ confirmDeleteForm.querySelector('input[name="deletionId"]').value = id;
+
+ if (type === 'announcement') {
+ document.getElementById('confirmDeleteTitle').textContent = 'Supprimer l\'annonce';
+ document.getElementById('confirmDeleteMessage').textContent = 'Êtes-vous sûr de vouloir supprimer cette annonce ?';
+ } else if (type === 'monster') {
+ document.getElementById('confirmDeleteTitle').textContent = 'Supprimer le monstre';
+ document.getElementById('confirmDeleteMessage').textContent = 'Êtes-vous sûr de vouloir supprimer ce monstre ? Cette action supprimera également toutes les quêtes associées.';
+ }
+
+ new bootstrap.Modal(document.getElementById('confirmDeleteModal')).show();
+}
+
+/**
+ * Gérer la confirmation de suppression
+ */
+function handleConfirmDelete() {
+ const formData = new FormData(document.getElementById('confirmDeleteForm'));
+
+ let url = '../api.php?action=';
+
+ if (currentDeletionType === 'announcement') {
+ url += 'deleteAnnouncement';
+ } else if (currentDeletionType === 'monster') {
+ url += 'deleteMonster';
+ } else {
+ return;
+ }
+
+ // Envoyer la requête
+ fetch(url, {
+ method: 'POST',
+ body: formData
+ })
+ .then(response => response.json())
+ .then(data => {
+ if (data.success) {
+ // Fermer la modale
+ bootstrap.Modal.getInstance(document.getElementById('confirmDeleteModal')).hide();
+
+ // Recharger la page pour mettre à jour la liste
+ window.location.reload();
+ } else {
+ alert('Erreur lors de la suppression: ' + (data.message || 'Erreur inconnue'));
+ }
+ })
+ .catch(error => {
+ console.error('Erreur:', error);
+ alert('Erreur de connexion. Veuillez réessayer.');
+ });
+}
\ No newline at end of file
diff --git a/css/styles.css b/css/styles.css
deleted file mode 100644
index 7b9439a..0000000
--- a/css/styles.css
+++ /dev/null
@@ -1,395 +0,0 @@
-.btn-outline-accent {
- border-color: var(--mh-accent);
- color: var(--mh-accent);
- font-weight: 500;
-}
-
-.btn-outline-accent:hover,
-.btn-outline-accent:focus,
-.btn-check:checked + .btn-outline-accent {
- background-color: var(--mh-accent);
- border-color: var(--mh-accent);
- color: var(--mh-dark);
-}
-
-/* Styles spécifiques pour la page tutoriel */
-.tutorial-content h3,
-.tutorial-content h4 {
- color: var(--mh-accent);
- margin-top: 1.5rem;
- text-shadow: var(--mh-text-shadow);
-}
-
-.tutorial-content p,
-.tutorial-content ul,
-.tutorial-content ol,
-.tutorial-content li {
- color: var(--mh-light);
- line-height: 1.6;
- margin-bottom: 1rem;
- letter-spacing: 0.01rem;
-}
-
-.tutorial-content ul,
-.tutorial-content ol {
- padding-left: 1.5rem;
-}
-
-.tutorial-content li {
- margin-bottom: 0.5rem;
-}
-
-.tutorial-content strong {
- font-weight: 700;
- color: var(--mh-light);
-}.bg-info {
- background-color: var(--mh-moss) !important;
-}
-
-.text-info {
- color: var(--mh-moss) !important;
-}
-
-.btn-info {
- background-color: var(--mh-moss);
- border-color: var(--mh-moss);
- color: white;
-}
-
-.btn-outline-info {
- border-color: var(--mh-moss);
- color: var(--mh-moss);
-}
-
-.btn-outline-info:hover, .btn-outline-info:focus {
- background-color: var(--mh-moss);
- border-color: var(--mh-moss);
- color: white;
-}.card-text {
- color: var(--mh-light);
- font-size: 1rem;
- margin-bottom: 0.75rem;
- line-height: 1.5;
- letter-spacing: 0.01rem;
-}
-
-strong {
- color: var(--mh-light);
- font-weight: 700;
-}/* Styles pour les badges */
-.badge {
- font-weight: bold;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
-}
-
-.badge.bg-primary {
- background-color: var(--mh-accent) !important;
- color: var(--mh-dark);
-}
-
-.badge.bg-warning {
- color: var(--mh-dark);
-}
-
-.badge.bg-success {
- background-color: var(--mh-green) !important;
-}
-
-.badge.bg-secondary {
- background-color: #c0c0c0 !important;
- color: var(--mh-dark);
-}/* Style pour les textes dans les formulaires */
-.form-label, .form-check-label {
- color: var(--mh-light);
- font-weight: 500;
-}
-
-.form-text {
- color: #bdb7ad;
-}
-
-/* Style pour garantir la lisibilité dans les modales */
-.modal-title, .modal-body {
- color: var(--mh-light);
-}
-
-.modal-header {
- border-bottom: 1px solid rgba(255, 255, 255, 0.1);
- background-color: var(--mh-dark);
-}
-
-.modal-footer {
- border-top: 1px solid rgba(255, 255, 255, 0.1);
-}
-
-/* Style pour les tableaux */
-.table {
- color: var(--mh-light);
-}
-
-.table-striped>tbody>tr:nth-of-type(odd)>* {
- background-color: rgba(0, 0, 0, 0.15);
- color: var(--mh-light);
-}
-
-/* Style pour les sélecteurs d'option */
-option {
- background-color: var(--mh-bg);
- color: var(--mh-light);
-}
-
-/* Style pour les placehoders */
-::placeholder {
- color: rgba(236, 230, 217, 0.6) !important;
-}.monster-card:hover {
- transform: translateY(-5px);
- box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
-}/* Variables de couleurs du thème Monster Hunter */
-:root {
- --mh-dark: #1a1914;
- --mh-bg: #272420;
- --mh-light-bg: #3a362f;
- --mh-light: #f5f0e6;
- --mh-accent: #e0b968;
- --mh-green: #6bc46f;
- --mh-red: #e05e4e;
- --mh-blue: #5a90b1;
- --mh-moss: #7ab16a;
- --mh-shadow: rgba(0, 0, 0, 0.5);
- --mh-text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);
-}
-
-/* Styles généraux */
-body {
- min-height: 100vh;
- display: flex;
- flex-direction: column;
- background-color: var(--mh-bg);
- color: var(--mh-light);
-}
-
-/* Personnalisation de Bootstrap */
-.bg-dark {
- background-color: var(--mh-dark) !important;
-}
-
-.bg-dark a {
- color: var(--mh-green) !important;
- text-decoration: none;
-}
-
-.bg-dark a:hover {
- color: var(--mh-moss) !important;
-}
-
-.bg-primary {
- background-color: var(--mh-accent) !important;
-}
-
-.btn-primary {
- background-color: var(--mh-accent);
- border-color: var(--mh-accent);
- color: var(--mh-dark);
- font-weight: bold;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
-}
-
-.btn-primary:hover, .btn-primary:focus {
- background-color: #d0a845;
- border-color: #d0a845;
- color: var(--mh-dark);
-}
-
-.btn-outline-primary {
- border-color: var(--mh-accent);
- color: var(--mh-accent);
- font-weight: 500;
-}
-
-.btn-outline-primary:hover, .btn-outline-primary:focus {
- background-color: var(--mh-accent);
- border-color: var(--mh-accent);
- color: var(--mh-dark);
-}
-
-.btn-outline-light {
- border-color: var(--mh-light);
- color: var(--mh-light);
- font-weight: 500;
-}
-
-.btn-outline-light:hover, .btn-outline-light:focus {
- background-color: var(--mh-light);
- color: var(--mh-dark);
-}
-
-.card {
- background-color: var(--mh-light-bg);
- border: none;
- box-shadow: 0 4px 8px var(--mh-shadow);
-}
-
-.card-header {
- background-color: var(--mh-dark);
- border-bottom: 2px solid var(--mh-accent);
-}
-
-.card-title {
- color: var(--mh-light);
- text-shadow: var(--mh-text-shadow);
-}
-
-.form-control, .form-select {
- background-color: #3e3a33;
- border: 1px solid #6c6557;
- color: var(--mh-light);
- font-weight: 500;
-}
-
-.form-control:focus, .form-select:focus {
- background-color: #3e3a33;
- color: var(--mh-light);
- border-color: var(--mh-accent);
- box-shadow: 0 0 0 0.25rem rgba(208, 168, 92, 0.25);
-}
-
-.input-group-text {
- background-color: var(--mh-dark);
- border: 1px solid #5c574c;
- color: var(--mh-light);
-}
-
-.table {
- color: var(--mh-light);
-}
-
-.alert-info {
- background-color: var(--mh-moss);
- color: white;
- border-color: #5a9950;
- text-shadow: var(--mh-text-shadow);
-}
-
-.modal-content {
- background-color: var(--mh-bg);
- border: 1px solid var(--mh-dark);
-}
-
-.modal-header {
- border-bottom: 1px solid var(--mh-dark);
-}
-
-.modal-footer {
- border-top: 1px solid var(--mh-dark);
-}
-
-.list-group-item {
- background-color: var(--mh-light-bg);
- color: var(--mh-light);
- border-color: rgba(255, 255, 255, 0.1);
-}
-
-.list-group-item.active {
- background-color: var(--mh-accent);
- border-color: var(--mh-accent);
- color: var(--mh-dark);
- font-weight: bold;
-}
-
-.empty-message {
- color: var(--mh-light);
- padding: 2rem;
- border: 1px solid rgba(255, 255, 255, 0.1);
- border-radius: 0.375rem;
- text-align: center;
-}
-
-/* Styles pour les cartes de monstres */
-
-/* Styles pour les cartes de monstres */
-.monster-card {
- border-radius: 8px;
- transition: transform 0.2s, box-shadow 0.2s;
- cursor: pointer;
- height: 100%;
- display: flex;
- flex-direction: column;
-}
-
-.monster-card .card-img-container {
- position: relative;
- width: 100%;
- padding-bottom: 100%; /* Rapport 1:1 pour créer un carré */
- overflow: hidden;
-}
-
-.monster-card img {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- object-fit: cover;
- border-top-left-radius: 8px;
- border-top-right-radius: 8px;
-}
-
-.crown-badge {
- position: relative;
- display: inline-block;
- margin-right: 10px;
-}
-
-.small-crown {
- color: var(--mh-accent);
- font-weight: bold;
- text-shadow: var(--mh-text-shadow);
-}
-
-.large-crown {
- color: #f0f0f0;
- font-weight: bold;
- text-shadow: var(--mh-text-shadow);
-}
-
-.quest-card {
- margin-bottom: 15px;
- position: relative;
- background-color: var(--mh-light-bg);
- border: 1px solid var(--mh-dark);
-}
-
-.quest-delete-btn {
- position: absolute;
- top: 10px;
- right: 10px;
-}
-
-/* Style pour l'icône de couronne */
-.crown-icon {
- font-size: 1.5rem;
-}
-
-/* Animation de chargement */
-@keyframes fadeIn {
- from { opacity: 0; }
- to { opacity: 1; }
-}
-
-.fade-in {
- animation: fadeIn 0.3s;
-}
-
-/* Style pour le message vide */
-.empty-message {
- text-align: center;
- padding: 30px;
- border-radius: 8px;
- margin: 20px 0;
-}
-
-/* Style pour le délai de fraîcheur des quêtes */
-.quest-date {
- font-size: 0.8rem;
- color: #6c757d;
-}
\ No newline at end of file
diff --git a/includes/config.php b/includes/config.php
new file mode 100644
index 0000000..13ebbf3
--- /dev/null
+++ b/includes/config.php
@@ -0,0 +1,47 @@
+setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+
+ // Activer les clés étrangères
+ $db->exec('PRAGMA foreign_keys = ON;');
+
+ // Si la base de données est nouvellement créée, initialiser le schéma
+ if (!$db_exists) {
+ init_database($db);
+ }
+
+ } catch (PDOException $e) {
+ // En mode débogage, afficher l'erreur
+ if (DEBUG_MODE) {
+ echo "Erreur de connexion à la base de données: " . $e->getMessage();
+ }
+ // Journaliser l'erreur
+ error_log("Erreur de connexion à la base de données: " . $e->getMessage());
+ die("Une erreur s'est produite lors de la connexion à la base de données.");
+ }
+ }
+
+ return $db;
+}
+
+/**
+ * Initialiser la base de données avec le schéma et les données initiales
+ *
+ * @param PDO $db Instance de connexion à la base de données
+ */
+function init_database($db) {
+ try {
+ // Charger le fichier de schéma SQL
+ $schema = file_get_contents(__DIR__ . '/../data/schema.sql');
+
+ // Exécuter toutes les requêtes SQL du schéma
+ $db->exec($schema);
+
+ return true;
+ } catch (PDOException $e) {
+ // En mode débogage, afficher l'erreur
+ if (DEBUG_MODE) {
+ echo "Erreur d'initialisation de la base de données: " . $e->getMessage();
+ }
+ // Journaliser l'erreur
+ error_log("Erreur d'initialisation de la base de données: " . $e->getMessage());
+ die("Une erreur s'est produite lors de l'initialisation de la base de données.");
+ }
+}
+
+/**
+ * ======== FONCTIONS D'ACCÈS AUX MONSTRES ========
+ */
+
+/**
+ * Récupérer tous les monstres
+ *
+ * @return array Liste des monstres
+ */
+function get_all_monsters() {
+ $db = get_db_connection();
+ $stmt = $db->query('SELECT * FROM monsters ORDER BY name ASC');
+ return $stmt->fetchAll(PDO::FETCH_ASSOC);
+}
+
+/**
+ * Récupérer un monstre par son ID
+ *
+ * @param int $id ID du monstre
+ * @return array|false Données du monstre ou false si non trouvé
+ */
+function get_monster_by_id($id) {
+ $db = get_db_connection();
+ $stmt = $db->prepare('SELECT * FROM monsters WHERE id = :id');
+ $stmt->bindParam(':id', $id, PDO::PARAM_INT);
+ $stmt->execute();
+ return $stmt->fetch(PDO::FETCH_ASSOC);
+}
+
+/**
+ * Ajouter un nouveau monstre
+ *
+ * @param string $name Nom du monstre
+ * @param string $image Chemin de l'image
+ * @return int|false ID du nouveau monstre ou false en cas d'échec
+ */
+function add_monster($name, $image) {
+ $db = get_db_connection();
+ $stmt = $db->prepare('INSERT INTO monsters (name, image) VALUES (:name, :image)');
+ $stmt->bindParam(':name', $name);
+ $stmt->bindParam(':image', $image);
+
+ if ($stmt->execute()) {
+ return $db->lastInsertId();
+ }
+
+ return false;
+}
+
+/**
+ * Mettre à jour un monstre existant
+ *
+ * @param int $id ID du monstre
+ * @param string $name Nouveau nom
+ * @param string $image Nouveau chemin d'image
+ * @return bool Succès de la mise à jour
+ */
+function update_monster($id, $name, $image) {
+ $db = get_db_connection();
+ $stmt = $db->prepare('UPDATE monsters SET name = :name, image = :image WHERE id = :id');
+ $stmt->bindParam(':id', $id, PDO::PARAM_INT);
+ $stmt->bindParam(':name', $name);
+ $stmt->bindParam(':image', $image);
+ return $stmt->execute();
+}
+
+/**
+ * Supprimer un monstre
+ *
+ * @param int $id ID du monstre à supprimer
+ * @return bool Succès de la suppression
+ */
+function delete_monster($id) {
+ $db = get_db_connection();
+ $stmt = $db->prepare('DELETE FROM monsters WHERE id = :id');
+ $stmt->bindParam(':id', $id, PDO::PARAM_INT);
+ return $stmt->execute();
+}
+
+/**
+ * ======== FONCTIONS D'ACCÈS AUX QUÊTES ========
+ */
+
+/**
+ * Récupérer toutes les quêtes pour un monstre spécifique
+ *
+ * @param int $monster_id ID du monstre
+ * @return array Liste des quêtes
+ */
+function get_quests_by_monster($monster_id) {
+ $db = get_db_connection();
+ $stmt = $db->prepare('SELECT * FROM quests WHERE monster_id = :monster_id ORDER BY date DESC');
+ $stmt->bindParam(':monster_id', $monster_id, PDO::PARAM_INT);
+ $stmt->execute();
+ return $stmt->fetchAll(PDO::FETCH_ASSOC);
+}
+
+/**
+ * Récupérer toutes les quêtes d'un certain type (small/large) pour un monstre
+ *
+ * @param int $monster_id ID du monstre
+ * @param string $crown_type Type de couronne ('small' ou 'large')
+ * @return array Liste des quêtes
+ */
+function get_quests_by_monster_and_crown($monster_id, $crown_type) {
+ $db = get_db_connection();
+ $stmt = $db->prepare('SELECT * FROM quests WHERE monster_id = :monster_id AND crown_type = :crown_type ORDER BY date DESC');
+ $stmt->bindParam(':monster_id', $monster_id, PDO::PARAM_INT);
+ $stmt->bindParam(':crown_type', $crown_type);
+ $stmt->execute();
+ return $stmt->fetchAll(PDO::FETCH_ASSOC);
+}
+
+/**
+ * Ajouter une nouvelle quête
+ *
+ * @param int $monster_id ID du monstre
+ * @param string $crown_type Type de couronne ('small' ou 'large')
+ * @param string $player_name Nom du joueur
+ * @param string $player_id ID du joueur en jeu
+ * @return int|false ID de la nouvelle quête ou false en cas d'échec
+ */
+function add_quest($monster_id, $crown_type, $player_name, $player_id) {
+ $db = get_db_connection();
+ $stmt = $db->prepare('INSERT INTO quests (monster_id, crown_type, player_name, player_id) VALUES (:monster_id, :crown_type, :player_name, :player_id)');
+ $stmt->bindParam(':monster_id', $monster_id, PDO::PARAM_INT);
+ $stmt->bindParam(':crown_type', $crown_type);
+ $stmt->bindParam(':player_name', $player_name);
+ $stmt->bindParam(':player_id', $player_id);
+
+ if ($stmt->execute()) {
+ return $db->lastInsertId();
+ }
+
+ return false;
+}
+
+/**
+ * Supprimer une quête
+ *
+ * @param int $id ID de la quête à supprimer
+ * @return bool Succès de la suppression
+ */
+function delete_quest($id) {
+ $db = get_db_connection();
+ $stmt = $db->prepare('DELETE FROM quests WHERE id = :id');
+ $stmt->bindParam(':id', $id, PDO::PARAM_INT);
+ return $stmt->execute();
+}
+
+/**
+ * Nettoyer les quêtes expirées (plus vieilles que QUESTS_EXPIRATION_DAYS)
+ *
+ * @return int Nombre de quêtes supprimées
+ */
+function clean_old_quests() {
+ $db = get_db_connection();
+ $days = QUESTS_EXPIRATION_DAYS;
+ $stmt = $db->prepare("DELETE FROM quests WHERE date < datetime('now', '-{$days} days')");
+ $stmt->execute();
+ return $stmt->rowCount();
+}
+
+/**
+ * Compter les quêtes par type de couronne
+ *
+ * @return array Statistiques des quêtes
+ */
+function count_quests_by_crown_type() {
+ $db = get_db_connection();
+ $stmt = $db->query("
+ SELECT
+ COUNT(*) as total_quests,
+ SUM(CASE WHEN crown_type = 'small' THEN 1 ELSE 0 END) as small_crown_quests,
+ SUM(CASE WHEN crown_type = 'large' THEN 1 ELSE 0 END) as large_crown_quests
+ FROM quests
+ ");
+ return $stmt->fetch(PDO::FETCH_ASSOC);
+}
+
+/**
+ * ======== FONCTIONS D'ACCÈS AUX ANNONCES ========
+ */
+
+/**
+ * Récupérer toutes les annonces
+ *
+ * @param bool $active_only Ne récupérer que les annonces actives
+ * @return array Liste des annonces
+ */
+function get_all_announcements($active_only = false) {
+ $db = get_db_connection();
+ $query = 'SELECT * FROM announcements';
+
+ if ($active_only) {
+ $query .= ' WHERE active = 1';
+ }
+
+ $query .= ' ORDER BY created_at DESC';
+ $stmt = $db->query($query);
+ return $stmt->fetchAll(PDO::FETCH_ASSOC);
+}
+
+/**
+ * Récupérer une annonce par son ID
+ *
+ * @param int $id ID de l'annonce
+ * @return array|false Données de l'annonce ou false si non trouvée
+ */
+function get_announcement_by_id($id) {
+ $db = get_db_connection();
+ $stmt = $db->prepare('SELECT * FROM announcements WHERE id = :id');
+ $stmt->bindParam(':id', $id, PDO::PARAM_INT);
+ $stmt->execute();
+ return $stmt->fetch(PDO::FETCH_ASSOC);
+}
+
+/**
+ * Ajouter une nouvelle annonce
+ *
+ * @param string $text Texte de l'annonce
+ * @param bool $active Statut actif de l'annonce
+ * @return int|false ID de la nouvelle annonce ou false en cas d'échec
+ */
+function add_announcement($text, $active = true) {
+ $db = get_db_connection();
+ $active_int = $active ? 1 : 0;
+ $stmt = $db->prepare('INSERT INTO announcements (text, active) VALUES (:text, :active)');
+ $stmt->bindParam(':text', $text);
+ $stmt->bindParam(':active', $active_int, PDO::PARAM_INT);
+
+ if ($stmt->execute()) {
+ return $db->lastInsertId();
+ }
+
+ return false;
+}
+
+/**
+ * Mettre à jour une annonce existante
+ *
+ * @param int $id ID de l'annonce
+ * @param string $text Nouveau texte
+ * @param bool $active Nouveau statut
+ * @return bool Succès de la mise à jour
+ */
+function update_announcement($id, $text, $active = true) {
+ $db = get_db_connection();
+ $active_int = $active ? 1 : 0;
+ $stmt = $db->prepare('UPDATE announcements SET text = :text, active = :active WHERE id = :id');
+ $stmt->bindParam(':id', $id, PDO::PARAM_INT);
+ $stmt->bindParam(':text', $text);
+ $stmt->bindParam(':active', $active_int, PDO::PARAM_INT);
+ return $stmt->execute();
+}
+
+/**
+ * Supprimer une annonce
+ *
+ * @param int $id ID de l'annonce à supprimer
+ * @return bool Succès de la suppression
+ */
+function delete_announcement($id) {
+ $db = get_db_connection();
+ $stmt = $db->prepare('DELETE FROM announcements WHERE id = :id');
+ $stmt->bindParam(':id', $id, PDO::PARAM_INT);
+ return $stmt->execute();
+}
+
+/**
+ * ======== FONCTIONS D'AUTHENTIFICATION ========
+ */
+
+/**
+ * Vérifier les identifiants d'un utilisateur
+ *
+ * @param string $username Nom d'utilisateur
+ * @param string $password Mot de passe
+ * @return bool L'authentification est-elle valide
+ */
+function check_login($username, $password) {
+ $db = get_db_connection();
+ $stmt = $db->prepare('SELECT * FROM users WHERE username = :username');
+ $stmt->bindParam(':username', $username);
+ $stmt->execute();
+ $user = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if ($user && password_verify($password, $user['password'])) {
+ return true;
+ }
+
+ return false;
+}
\ No newline at end of file
diff --git a/includes/footer.php b/includes/footer.php
new file mode 100644
index 0000000..96bda9f
--- /dev/null
+++ b/includes/footer.php
@@ -0,0 +1,13 @@
+
+
+
+
+
Ce site est réalisé dans le cadre de la branche Alt Tab de l'association Camélia Studio .
+
Images des monstres par Sui Yun - Site sous licence MIT, code source sur Gitea
+
+
+
+
+
+