From 6241e093b1932025391a5f1dcb36b85cf85796e1 Mon Sep 17 00:00:00 2001 From: Esenjin Date: Sun, 9 Mar 2025 18:45:32 +0100 Subject: [PATCH] initialisation de la v2 du projet migration sur une solution php --- .htaccess | 66 +++ LICENSE | 9 - README.md | 3 - admin.html | 244 -------- admin/index.php | 290 ++++++++++ admin/logout.php | 25 + api.php | 316 ++++++++++ assets/css/styles.css | 324 +++++++++++ {img => assets/img}/Ajarakan.jpg | Bin .../img/Anjanath_Tonnerre_Gardien.jpg | Bin {img => assets/img}/Arkveld.jpg | Bin {img => assets/img}/Arkveld_Gardien.jpg | Bin {img => assets/img}/Balahara.jpg | Bin {img => assets/img}/Blangonga.jpg | Bin {img => assets/img}/Chatacabra.jpg | Bin {img => assets/img}/Congalala.jpg | Bin {img => assets/img}/Doshaguma.jpg | Bin {img => assets/img}/Doshaguma_Gardien.jpg | Bin {img => assets/img}/Gore_Malaga.jpg | Bin {img => assets/img}/Gravios.jpg | Bin {img => assets/img}/Gypceros.jpg | Bin {img => assets/img}/Hirabami.jpg | Bin {img => assets/img}/Jin_Dahaad.jpg | Bin {img => assets/img}/Lala_Barina.jpg | Bin {img => assets/img}/Nerscylla.jpg | Bin {img => assets/img}/Nu_Udra.jpg | Bin .../img}/Odogaron_Desastre_Gardien.jpg | Bin {img => assets/img}/Quematrice.jpg | Bin {img => assets/img}/Rathalos.jpg | Bin {img => assets/img}/Rathalos_Gardien.jpg | Bin {img => assets/img}/Rathian.jpg | Bin {img => assets/img}/Rey_Dau.jpg | Bin {img => assets/img}/Rompopolo.jpg | Bin {img => assets/img}/Uth_Duna.jpg | Bin {img => assets/img}/Xu_Wu.jpg | Bin {img => assets/img}/Yian_Kut-Ku.jpg | Bin {img => assets/img}/Zoh_Shia.jpg | Bin {img => assets/img}/logo.png | Bin {img => assets/img}/readme.md | 0 assets/js/admin.js | 502 ++++++++++++++++ css/styles.css | 395 ------------- includes/config.php | 47 ++ includes/database.php | 370 ++++++++++++ includes/footer.php | 13 + includes/functions.php | 196 +++++++ includes/header.php | 59 ++ index.html | 236 -------- index.php | 261 +++++++++ initdb.php | 398 +++++++++++++ js/admin.js | 487 ---------------- js/data.js | 52 -- js/login.js | 38 -- js/main.js | 542 ------------------ login.html | 76 --- login.php | 87 +++ tutorial.html | 99 ---- tutorial.php | 79 +++ 57 files changed, 3033 insertions(+), 2181 deletions(-) create mode 100644 .htaccess delete mode 100644 LICENSE delete mode 100644 README.md delete mode 100644 admin.html create mode 100644 admin/index.php create mode 100644 admin/logout.php create mode 100644 api.php create mode 100644 assets/css/styles.css rename {img => assets/img}/Ajarakan.jpg (100%) rename img/Anjanath_tonnerre_Gardien.jpg => assets/img/Anjanath_Tonnerre_Gardien.jpg (100%) rename {img => assets/img}/Arkveld.jpg (100%) rename {img => assets/img}/Arkveld_Gardien.jpg (100%) rename {img => assets/img}/Balahara.jpg (100%) rename {img => assets/img}/Blangonga.jpg (100%) rename {img => assets/img}/Chatacabra.jpg (100%) rename {img => assets/img}/Congalala.jpg (100%) rename {img => assets/img}/Doshaguma.jpg (100%) rename {img => assets/img}/Doshaguma_Gardien.jpg (100%) rename {img => assets/img}/Gore_Malaga.jpg (100%) rename {img => assets/img}/Gravios.jpg (100%) rename {img => assets/img}/Gypceros.jpg (100%) rename {img => assets/img}/Hirabami.jpg (100%) rename {img => assets/img}/Jin_Dahaad.jpg (100%) rename {img => assets/img}/Lala_Barina.jpg (100%) rename {img => assets/img}/Nerscylla.jpg (100%) rename {img => assets/img}/Nu_Udra.jpg (100%) rename {img => assets/img}/Odogaron_Desastre_Gardien.jpg (100%) rename {img => assets/img}/Quematrice.jpg (100%) rename {img => assets/img}/Rathalos.jpg (100%) rename {img => assets/img}/Rathalos_Gardien.jpg (100%) rename {img => assets/img}/Rathian.jpg (100%) rename {img => assets/img}/Rey_Dau.jpg (100%) rename {img => assets/img}/Rompopolo.jpg (100%) rename {img => assets/img}/Uth_Duna.jpg (100%) rename {img => assets/img}/Xu_Wu.jpg (100%) rename {img => assets/img}/Yian_Kut-Ku.jpg (100%) rename {img => assets/img}/Zoh_Shia.jpg (100%) rename {img => assets/img}/logo.png (100%) rename {img => assets/img}/readme.md (100%) create mode 100644 assets/js/admin.js delete mode 100644 css/styles.css create mode 100644 includes/config.php create mode 100644 includes/database.php create mode 100644 includes/footer.php create mode 100644 includes/functions.php create mode 100644 includes/header.php delete mode 100644 index.html create mode 100644 index.php create mode 100644 initdb.php delete mode 100644 js/admin.js delete mode 100644 js/data.js delete mode 100644 js/login.js delete mode 100644 js/main.js delete mode 100644 login.html create mode 100644 login.php delete mode 100644 tutorial.html create mode 100644 tutorial.php 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 - - - - - - - - - - - - - - - - - -
-
-
-
- Logo -

Administration

-
-
- Retour au site - -
-
-
-
- -
-
- -
- -
-
-
-

Gestion des annonces

- -
-
-
- - - - - - - - - - - -
TexteStatutActions
-
-
- Aucune annonce pour le moment. -
-
-
-
- - -
-
-
-

Gestion des monstres

- -
-
-
- - - - - - - - - - - - -
ImageNomQuêtesActions
-
-
- Aucun monstre pour le moment. -
-
-
-
- - -
-
-
-

Maintenance du site

-
-
-
-

Nettoyer les quêtes anciennes

-

Cette action supprimera toutes les quêtes datant de plus de 7 jours.

- -
-
-
-
-

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 -
  • -
-
-
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - \ 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'; +?> + +
+ +
+ +
+
+
+

Gestion des annonces

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
TexteStatutDateActions
Aucune annonce pour le moment.
+ + Active + + Inactive + + +
+ + +
+
+
+
+
+
+ + +
+
+
+

Gestion des monstres

+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
ImageNomQuêtesActions
Aucun monstre pour le moment.
+ <?php echo secure_output($monster['name']); ?> + +
+ + +
+
+
+
+
+
+ + +
+
+
+

Maintenance du site

+
+
+
+

Nettoyer les quêtes anciennes

+

Cette action supprimera toutes les quêtes datant de plus de 7 jours.

+
+ + +
+
+
+
+
+

Statistiques du site

+
    +
  • + Nombre total de monstres + +
  • +
  • + Nombre total de quêtes + +
  • +
  • + Quêtes avec petite couronne + +
  • +
  • + Quêtes avec grande couronne + +
  • +
+
+
+
+
+
+
+ + + + + + + + + + + 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 @@ + + + + + + + + \ No newline at end of file diff --git a/includes/functions.php b/includes/functions.php new file mode 100644 index 0000000..2f373bf --- /dev/null +++ b/includes/functions.php @@ -0,0 +1,196 @@ + $type, + 'message' => $message + ]; +} + +/** + * Récupérer et effacer un message flash de la session + * + * @return array|null Message flash ou null + */ +function get_flash_message() { + if (isset($_SESSION['flash_message'])) { + $message = $_SESSION['flash_message']; + unset($_SESSION['flash_message']); + return $message; + } + return null; +} + +/** + * Afficher un message flash s'il existe + */ +function display_flash_message() { + $flash = get_flash_message(); + if ($flash) { + $type = $flash['type']; + $message = $flash['message']; + + // Convertir le type en classe Bootstrap + $class = 'alert-info'; + if ($type === 'error') { + $class = 'alert-danger'; + } elseif ($type === 'success') { + $class = 'alert-success'; + } elseif ($type === 'warning') { + $class = 'alert-warning'; + } + + echo ""; + } +} + +/** + * Formater une date relative (aujourd'hui, hier, il y a X jours) + * + * @param string $date Date au format ISO ou timestamp + * @return string Date relative formatée + */ +function format_relative_date($date) { + if (is_numeric($date)) { + $timestamp = $date; + } else { + $timestamp = strtotime($date); + } + + $now = time(); + $diff = $now - $timestamp; + $day_diff = floor($diff / 86400); + + if ($day_diff == 0) { + return "Aujourd'hui"; + } else if ($day_diff == 1) { + return "Hier"; + } else if ($day_diff < 7) { + return "Il y a $day_diff jours"; + } else { + return date('d/m/Y', $timestamp); + } +} + +/** + * Compter les quêtes pour un monstre + * + * @param int $monster_id ID du monstre + * @param string|null $crown_type Type de couronne (null pour toutes) + * @return int Nombre de quêtes + */ +function count_quests_for_monster($monster_id, $crown_type = null) { + $db = get_db_connection(); + + if ($crown_type === null) { + $stmt = $db->prepare('SELECT COUNT(*) as count FROM quests WHERE monster_id = :monster_id'); + $stmt->bindParam(':monster_id', $monster_id, PDO::PARAM_INT); + } else { + $stmt = $db->prepare('SELECT COUNT(*) as count FROM quests WHERE monster_id = :monster_id AND crown_type = :crown_type'); + $stmt->bindParam(':monster_id', $monster_id, PDO::PARAM_INT); + $stmt->bindParam(':crown_type', $crown_type); + } + + $stmt->execute(); + $result = $stmt->fetch(PDO::FETCH_ASSOC); + return $result['count']; +} + +/** + * Générer les statistiques globales + * + * @return array Tableau de statistiques + */ +function get_site_statistics() { + $db = get_db_connection(); + + // Compter les monstres + $stmt = $db->query('SELECT COUNT(*) as count FROM monsters'); + $monsters_count = $stmt->fetch(PDO::FETCH_ASSOC)['count']; + + // Récupérer les statistiques de quêtes + $quests_stats = count_quests_by_crown_type(); + + return [ + 'total_monsters' => $monsters_count, + 'total_quests' => $quests_stats['total_quests'], + 'small_crown_quests' => $quests_stats['small_crown_quests'], + 'large_crown_quests' => $quests_stats['large_crown_quests'] + ]; +} + +/** + * Vérifier si une URL d'image est valide + * + * @param string $url URL de l'image + * @return bool L'URL est-elle valide + */ +function is_valid_image_url($url) { + // Vérifier si l'URL commence par le dossier d'images + if (strpos($url, 'assets/img/') !== 0) { + return false; + } + + // Vérifier l'extension de l'image + $valid_extensions = ['jpg', 'jpeg', 'png', 'gif']; + $extension = strtolower(pathinfo($url, PATHINFO_EXTENSION)); + + return in_array($extension, $valid_extensions); +} \ No newline at end of file diff --git a/includes/header.php b/includes/header.php new file mode 100644 index 0000000..2f26db9 --- /dev/null +++ b/includes/header.php @@ -0,0 +1,59 @@ + + + + + + + <?php echo isset($page_title) ? secure_output($page_title) : 'MH Wilds - Partage de Quêtes à Couronnes'; ?> + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ Logo +

+
+
+ + Retour au site + Déconnexion + + Tutoriel + Admin + +
+
+
+
+ +
+ \ No newline at end of file diff --git a/index.html b/index.html deleted file mode 100644 index 1636df1..0000000 --- a/index.html +++ /dev/null @@ -1,236 +0,0 @@ - - - - - - MH Wilds - Partage de Quêtes à Couronnes - - - - - - - - - - - - - - - - - - - - -
-
-
-
- Logo -

MH Wilds - Quêtes à Couronnes

-
-
- Tutoriel - Admin -
-
-
-
- -
-
-

Liste des monstres

- -
- -
-
-
- 🔍 - - -
-
-
- -
- -
- -
- -
-
- Chargement... -
-

Chargement des monstres...

-
-
-
- -
-
-

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/index.php b/index.php new file mode 100644 index 0000000..f619bc7 --- /dev/null +++ b/index.php @@ -0,0 +1,261 @@ + + /* Styles pour le select searchable personnalisé */ + .monster-search-container { + position: relative; + } + + .monster-search-results { + position: absolute; + width: 100%; + max-height: 250px; + overflow-y: auto; + background-color: var(--mh-light-bg); + border: 1px solid var(--mh-accent); + border-radius: 0.375rem; + z-index: 1050; + margin-top: 2px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); + } + + .monster-search-item { + padding: 8px 12px; + cursor: pointer; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + color: var(--mh-light); + } + + .monster-search-item:hover, + .monster-search-item.active { + background-color: var(--mh-dark); + } + + .monster-search-item:last-child { + border-bottom: none; + } + + .selected-monster { + font-weight: bold; + color: var(--mh-accent); + } + + .monster-search-no-results { + padding: 10px; + text-align: center; + color: var(--mh-light); + font-style: italic; + } + +EOT; + +// Scripts JS supplémentaires +$extra_js = << +EOT; + +// Récupérer les données nécessaires +$monsters = get_all_monsters(); +$active_announcements = get_all_announcements(true); + +// Inclure l'en-tête +include 'includes/header.php'; +?> + +
+

Liste des monstres

+ +
+ +
+
+
+ 🔍 + + +
+
+
+ + +
+ +

+ +
+ + +
+ +
+
+

Aucun monstre disponible pour le moment.

+
+
+ + + +
+
+
+ <?php echo secure_output($monster['name']); ?> +
+
+
+
+
+ + 👑 + + + 👑 + +
+ +
+
+
+
+ + +
+ + + + + + + + + + +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + // Activer les clés étrangères + $db->exec('PRAGMA foreign_keys = ON;'); + + return $db; + } catch (PDOException $e) { + die("Erreur de connexion à la base de données: " . $e->getMessage()); + } +} + +// Vérifier si un paramètre de confirmation est présent +$confirmed = isset($_GET['confirm']) && $_GET['confirm'] === 'yes'; + +// Vérifier si un paramètre d'authentification est présent +$auth_key = isset($_GET['key']) ? $_GET['key'] : ''; +$valid_auth = $auth_key === 'mhwilds2025'; + +if (!$valid_auth) { + die('Accès non autorisé. Veuillez fournir une clé d\'authentification valide.'); +} + +// Liste des 29 monstres de Monster Hunter Wilds +$monsters = [ + ['id' => 1, 'name' => 'Chatacabra', 'image' => 'assets/img/Chatacabra.jpg'], + ['id' => 2, 'name' => 'Quematrice', 'image' => 'assets/img/Quematrice.jpg'], + ['id' => 3, 'name' => 'Lala Barina', 'image' => 'assets/img/Lala_Barina.jpg'], + ['id' => 4, 'name' => 'Congalala', 'image' => 'assets/img/Congalala.jpg'], + ['id' => 5, 'name' => 'Balahara', 'image' => 'assets/img/Balahara.jpg'], + ['id' => 6, 'name' => 'Doshaguma', 'image' => 'assets/img/Doshaguma.jpg'], + ['id' => 7, 'name' => 'Uth Duna', 'image' => 'assets/img/Uth_Duna.jpg'], + ['id' => 8, 'name' => 'Rompopolo', 'image' => 'assets/img/Rompopolo.jpg'], + ['id' => 9, 'name' => 'Rey Dau', 'image' => 'assets/img/Rey_Dau.jpg'], + ['id' => 10, 'name' => 'Nerscylla', 'image' => 'assets/img/Nerscylla.jpg'], + ['id' => 11, 'name' => 'Hirabami', 'image' => 'assets/img/Hirabami.jpg'], + ['id' => 12, 'name' => 'Ajarakan', 'image' => 'assets/img/Ajarakan.jpg'], + ['id' => 13, 'name' => 'Nu Udra', 'image' => 'assets/img/Nu_Udra.jpg'], + ['id' => 14, 'name' => 'Doshaguma Gardien', 'image' => 'assets/img/Doshaguma_Gardien.jpg'], + ['id' => 15, 'name' => 'Rathalos Gardien', 'image' => 'assets/img/Rathalos_Gardien.jpg'], + ['id' => 16, 'name' => 'Jin Dahaad', 'image' => 'assets/img/Jin_Dahaad.jpg'], + ['id' => 17, 'name' => 'Odogaron Désastre Gardien', 'image' => 'assets/img/Odogaron_Desastre_Gardien.jpg'], + ['id' => 18, 'name' => 'Xu Wu', 'image' => 'assets/img/Xu_Wu.jpg'], + ['id' => 19, 'name' => 'Arkveld Gardien', 'image' => 'assets/img/Arkveld_Gardien.jpg'], + ['id' => 20, 'name' => 'Zoh Shia', 'image' => 'assets/img/Zoh_Shia.jpg'], + ['id' => 21, 'name' => 'Yian Kut-Ku', 'image' => 'assets/img/Yian_Kut-Ku.jpg'], + ['id' => 22, 'name' => 'Gypceros', 'image' => 'assets/img/Gypceros.jpg'], + ['id' => 23, 'name' => 'Rathian', 'image' => 'assets/img/Rathian.jpg'], + ['id' => 24, 'name' => 'Anjanath Tonnerre Gardien', 'image' => 'assets/img/Anjanath_Tonnerre_Gardien.jpg'], + ['id' => 25, 'name' => 'Rathalos', 'image' => 'assets/img/Rathalos.jpg'], + ['id' => 26, 'name' => 'Gravios', 'image' => 'assets/img/Gravios.jpg'], + ['id' => 27, 'name' => 'Blangonga', 'image' => 'assets/img/Blangonga.jpg'], + ['id' => 28, 'name' => 'Gore Malaga', 'image' => 'assets/img/Gore_Malaga.jpg'], + ['id' => 29, 'name' => 'Arkveld', 'image' => 'assets/img/Arkveld.jpg'] +]; + +// Récupérer les données exemple pour les quêtes et annonces +$sample_quests = [ + ['monster_id' => 1, 'crown_type' => 'small', 'player_name' => 'Hunter123', 'player_id' => 'MHW-1234'], + ['monster_id' => 1, 'crown_type' => 'large', 'player_name' => 'DragonSlayer', 'player_id' => 'MHW-5678'], + ['monster_id' => 2, 'crown_type' => 'small', 'player_name' => 'ThunderLord', 'player_id' => 'MHW-9012'] +]; + +$sample_announcements = [ + ['text' => 'Bienvenue sur le site de partage de quêtes à couronnes pour Monster Hunter Wilds !', 'active' => 1] +]; + +// Si la confirmation n'est pas donnée, afficher une page d'information +if (!$confirmed) { + header('Content-Type: text/html; charset=utf-8'); + echo ' + + + + + Initialisation de la base de données - MH Wilds + + + + +
+
+
+

Initialisation de la base de données

+
+
+
+

Attention : Ce script va initialiser la base de données avec les 29 monstres de Monster Hunter Wilds.

+

Si une base de données existe déjà, les données existantes seront conservées mais les monstres manquants seront ajoutés.

+
+ +

Liste des monstres à initialiser :

+
    '; + + foreach ($monsters as $monster) { + echo '
  • ' . htmlspecialchars($monster['name']) . '
  • '; + } + + echo '
+ +

Ce script va également créer les tables nécessaires si elles n\'existent pas déjà.

+ + +
+
+
+ +'; + exit; +} + +// Si on arrive ici, c'est que la confirmation a été donnée +try { + // Obtenir une connexion à la base de données + $db = get_db_connection(); + + // Démarrer une transaction + $db->beginTransaction(); + + // Créer les tables si elles n'existent pas déjà + // Table des monstres + $db->exec("CREATE TABLE IF NOT EXISTS monsters ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + image TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + )"); + + // Table des quêtes + $db->exec("CREATE TABLE IF NOT EXISTS quests ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + monster_id INTEGER NOT NULL, + crown_type TEXT NOT NULL CHECK(crown_type IN ('small', 'large')), + player_name TEXT NOT NULL, + player_id TEXT NOT NULL, + date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (monster_id) REFERENCES monsters(id) ON DELETE CASCADE + )"); + + // Table des annonces + $db->exec("CREATE TABLE IF NOT EXISTS announcements ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + text TEXT NOT NULL, + active INTEGER DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + )"); + + // Table des utilisateurs admin + $db->exec("CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + )"); + + // Insérer l'utilisateur admin par défaut s'il n'existe pas + $stmt = $db->prepare("INSERT OR IGNORE INTO users (username, password) VALUES (?, ?)"); + $stmt->execute(['admin', '$2y$10$LQbUtHEEI8I2cHGStbqAq.MN.zktBVHr0slNyYPKYAKoTZ5WlKJr6']); + + // Insérer les monstres + $stmt = $db->prepare("INSERT OR IGNORE INTO monsters (id, name, image) VALUES (?, ?, ?)"); + $total_monsters = 0; + + foreach ($monsters as $monster) { + $stmt->execute([$monster['id'], $monster['name'], $monster['image']]); + if ($stmt->rowCount() > 0) { + $total_monsters++; + } + } + + // Insérer les quêtes et annonces d'exemple seulement s'il n'y a pas déjà des données + $quests_count = $db->query("SELECT COUNT(*) FROM quests")->fetchColumn(); + $announcements_count = $db->query("SELECT COUNT(*) FROM announcements")->fetchColumn(); + + $sample_data_added = false; + + if ($quests_count == 0) { + $stmt = $db->prepare("INSERT INTO quests (monster_id, crown_type, player_name, player_id) VALUES (?, ?, ?, ?)"); + foreach ($sample_quests as $quest) { + $stmt->execute([$quest['monster_id'], $quest['crown_type'], $quest['player_name'], $quest['player_id']]); + } + $sample_data_added = true; + } + + if ($announcements_count == 0) { + $stmt = $db->prepare("INSERT INTO announcements (text, active) VALUES (?, ?)"); + foreach ($sample_announcements as $announcement) { + $stmt->execute([$announcement['text'], $announcement['active']]); + } + $sample_data_added = true; + } + + // Valider la transaction + $db->commit(); + + // Afficher le résultat + header('Content-Type: text/html; charset=utf-8'); + echo ' + + + + + Initialisation réussie - MH Wilds + + + + +
+
+
+

Initialisation réussie

+
+
+
+

Succès : La base de données a été initialisée avec succès !

+
+ +

Résultats :

+
    +
  • ' . $total_monsters . ' monstres ont été ajoutés (les monstres existants ont été ignorés)
  • '; + + if ($sample_data_added) { + echo '
  • Les données d\'exemple (quêtes et annonces) ont été ajoutées
  • '; + } else { + echo '
  • Les données d\'exemple n\'ont pas été ajoutées car il existe déjà des données
  • '; + } + + echo '
+ + +
+
+
+ +'; + +} catch (Exception $e) { + // En cas d'erreur, annuler la transaction + if (isset($db) && $db->inTransaction()) { + $db->rollBack(); + } + + // Afficher l'erreur + header('Content-Type: text/html; charset=utf-8'); + echo ' + + + + + Erreur d\'initialisation - MH Wilds + + + + +
+
+
+

Erreur d\'initialisation

+
+
+
+

Erreur : Une erreur s\'est produite lors de l\'initialisation de la base de données.

+

Veuillez vérifier que le dossier "data" est accessible en écriture et que SQLite est activé sur votre serveur.

+
+ +

Détails de l\'erreur :

+
' . htmlspecialchars($e->getMessage()) . '
+ + +
+
+
+ +'; +} \ No newline at end of file diff --git a/js/admin.js b/js/admin.js deleted file mode 100644 index 24ef54a..0000000 --- a/js/admin.js +++ /dev/null @@ -1,487 +0,0 @@ -// Variables globales -let monsters = []; -let quests = []; -let announcements = []; - -// Éléments DOM pour les onglets -const announcementsTab = document.getElementById('announcementsTab'); -const monstersTab = document.getElementById('monstersTab'); -const maintenanceTab = document.getElementById('maintenanceTab'); -const sectionsEls = document.querySelectorAll('.admin-section'); - -// Éléments DOM pour les annonces -const announcementsListEl = document.getElementById('announcementsList'); -const emptyAnnouncementsMessageEl = document.getElementById('emptyAnnouncementsMessage'); -const addAnnouncementBtn = document.getElementById('addAnnouncementBtn'); -const announcementForm = document.getElementById('announcementForm'); -const announcementIdEl = document.getElementById('announcementId'); -const announcementTextEl = document.getElementById('announcementText'); -const announcementActiveEl = document.getElementById('announcementActive'); -const announcementModalTitleEl = document.getElementById('announcementModalTitle'); - -// Éléments DOM pour les monstres -const monstersListEl = document.getElementById('monstersList'); -const emptyMonstersMessageEl = document.getElementById('emptyMonstersMessage'); -const addMonsterBtn = document.getElementById('addMonsterBtn'); -const monsterForm = document.getElementById('monsterForm'); -const monsterIdEl = document.getElementById('monsterId'); -const monsterNameEl = document.getElementById('monsterName'); -const monsterImageEl = document.getElementById('monsterImage'); -const monsterModalTitleEl = document.getElementById('monsterModalTitle'); - -// Éléments DOM pour la maintenance -const cleanOldQuestsBtn = document.getElementById('cleanOldQuestsBtn'); -const cleanResultEl = document.getElementById('cleanResult'); -const totalMonstersCountEl = document.getElementById('totalMonstersCount'); -const totalQuestsCountEl = document.getElementById('totalQuestsCount'); -const smallCrownQuestsCountEl = document.getElementById('smallCrownQuestsCount'); -const largeCrownQuestsCountEl = document.getElementById('largeCrownQuestsCount'); - -// Éléments DOM pour la confirmation de suppression -const confirmDeleteBtn = document.getElementById('confirmDeleteBtn'); -const confirmDeleteMessageEl = document.getElementById('confirmDeleteMessage'); -const confirmDeleteTitleEl = document.getElementById('confirmDeleteTitle'); - -// Modales Bootstrap -const announcementModal = new bootstrap.Modal(document.getElementById('announcementModal')); -const monsterModal = new bootstrap.Modal(document.getElementById('monsterModal')); -const confirmDeleteModal = new bootstrap.Modal(document.getElementById('confirmDeleteModal')); - -// Variables pour la suppression -let currentDeletionType = null; -let currentDeletionId = null; - -// Vérifier l'authentification et rediriger si nécessaire -function checkAuthentication() { - if (localStorage.getItem('admin_authenticated') !== 'true') { - window.location.href = 'login.html'; - return false; - } - return true; -} - -// Déconnexion -function logout() { - localStorage.removeItem('admin_authenticated'); - window.location.href = 'login.html?logout=true'; -} - -// Initialisation -document.addEventListener('DOMContentLoaded', () => { - // Vérifier l'authentification - if (!checkAuthentication()) { - return; - } - - loadData(); - - // Événements pour les onglets - announcementsTab.addEventListener('click', (e) => { - e.preventDefault(); - showSection('announcementsSection'); - }); - - monstersTab.addEventListener('click', (e) => { - e.preventDefault(); - showSection('monstersSection'); - }); - - maintenanceTab.addEventListener('click', (e) => { - e.preventDefault(); - showSection('maintenanceSection'); - updateStatistics(); - }); - - // Événements pour les annonces - addAnnouncementBtn.addEventListener('click', () => { - resetAnnouncementForm(); - announcementModalTitleEl.textContent = 'Ajouter une annonce'; - announcementModal.show(); - }); - - announcementForm.addEventListener('submit', handleSaveAnnouncement); - - // Événements pour les monstres - addMonsterBtn.addEventListener('click', () => { - resetMonsterForm(); - monsterModalTitleEl.textContent = 'Ajouter un monstre'; - monsterModal.show(); - }); - - monsterForm.addEventListener('submit', handleSaveMonster); - - // Événements pour la maintenance - cleanOldQuestsBtn.addEventListener('click', handleCleanOldQuests); - - // Événement pour la confirmation de suppression - confirmDeleteBtn.addEventListener('click', handleConfirmDelete); - - // Événement pour la déconnexion - document.getElementById('logoutBtn').addEventListener('click', logout); -}); - -// Fonctions de chargement et sauvegarde des données -function loadData() { - // Vérifier si les données de jeu sont disponibles depuis data.js - if (window.gameData) { - // Charger les monstres depuis les données du jeu - monsters = [...window.gameData.monsters]; - - // Charger les quêtes depuis localStorage ou utiliser les données par défaut - const storedQuests = localStorage.getItem('mhw_quests'); - quests = storedQuests ? JSON.parse(storedQuests) : [...window.gameData.initialQuests]; - - // Charger les annonces depuis localStorage ou utiliser les données par défaut - const storedAnnouncements = localStorage.getItem('mhw_announcements'); - announcements = storedAnnouncements ? JSON.parse(storedAnnouncements) : [...window.gameData.initialAnnouncements]; - } else { - // Fallback au cas où data.js n'est pas chargé - console.warn("Données de jeu non disponibles. Utilisation des données locales de secours."); - monsters = []; - // Essayer de charger depuis localStorage - const storedMonsters = localStorage.getItem('mhw_monsters'); - const storedQuests = localStorage.getItem('mhw_quests'); - const storedAnnouncements = localStorage.getItem('mhw_announcements'); - - if (storedMonsters) monsters = JSON.parse(storedMonsters); - if (storedQuests) quests = JSON.parse(storedQuests); - if (storedAnnouncements) announcements = JSON.parse(storedAnnouncements); - } - - renderAnnouncementsList(); - renderMonstersList(); -} - -function saveData() { - // Sauvegarder dans le localStorage - localStorage.setItem('mhw_monsters', JSON.stringify(monsters)); - localStorage.setItem('mhw_quests', JSON.stringify(quests)); - localStorage.setItem('mhw_announcements', JSON.stringify(announcements)); - console.log('Données sauvegardées'); -} - -// Fonctions pour la navigation entre les sections -function showSection(sectionId) { - // Désactiver tous les onglets et masquer toutes les sections - document.querySelectorAll('.list-group-item').forEach(tab => { - tab.classList.remove('active'); - }); - - sectionsEls.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') { - announcementsTab.classList.add('active'); - } else if (sectionId === 'monstersSection') { - monstersTab.classList.add('active'); - } else if (sectionId === 'maintenanceSection') { - maintenanceTab.classList.add('active'); - } -} - -// ============ ANNONCES ============ - -// Rendu de la liste des annonces -function renderAnnouncementsList() { - if (announcements.length === 0) { - announcementsListEl.innerHTML = ''; - emptyAnnouncementsMessageEl.classList.remove('d-none'); - return; - } - - emptyAnnouncementsMessageEl.classList.add('d-none'); - - announcementsListEl.innerHTML = announcements.map(announcement => { - const statusBadge = announcement.active - ? 'Active' - : 'Inactive'; - - return ` - - ${announcement.text} - ${statusBadge} - -
- - -
- - - `; - }).join(''); - - // Ajouter les écouteurs d'événements pour les boutons - 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); - }); - }); -} - -// Réinitialiser le formulaire d'annonce -function resetAnnouncementForm() { - announcementIdEl.value = ''; - announcementTextEl.value = ''; - announcementActiveEl.checked = true; -} - -// Éditer une annonce -function editAnnouncement(announcementId) { - const announcement = announcements.find(a => a.id === announcementId); - - if (!announcement) return; - - announcementIdEl.value = announcement.id; - announcementTextEl.value = announcement.text; - announcementActiveEl.checked = announcement.active; - - announcementModalTitleEl.textContent = 'Modifier l\'annonce'; - announcementModal.show(); -} - -// Gérer la sauvegarde d'une annonce -function handleSaveAnnouncement(e) { - e.preventDefault(); - - const announcementId = announcementIdEl.value.trim(); - const announcementText = announcementTextEl.value.trim(); - const announcementActive = announcementActiveEl.checked; - - if (!announcementText) { - alert('Veuillez saisir le texte de l\'annonce'); - return; - } - - if (announcementId) { - // Mode édition - const index = announcements.findIndex(a => a.id === parseInt(announcementId)); - - if (index !== -1) { - announcements[index].text = announcementText; - announcements[index].active = announcementActive; - } - } else { - // Mode ajout - const newAnnouncementId = announcements.length > 0 - ? Math.max(...announcements.map(a => a.id)) + 1 - : 1; - - announcements.push({ - id: newAnnouncementId, - text: announcementText, - active: announcementActive - }); - } - - saveData(); - renderAnnouncementsList(); - announcementModal.hide(); -} - -// ============ MONSTRES ============ - -// Rendu de la liste des monstres -function renderMonstersList() { - if (monsters.length === 0) { - monstersListEl.innerHTML = ''; - emptyMonstersMessageEl.classList.remove('d-none'); - return; - } - - emptyMonstersMessageEl.classList.add('d-none'); - - monstersListEl.innerHTML = monsters.map(monster => { - const questCount = quests.filter(q => q.monsterId === monster.id).length; - - return ` - - - ${monster.name} - - ${monster.name} - ${questCount} - -
- - -
- - - `; - }).join(''); - - // Ajouter les écouteurs d'événements pour les boutons - 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); - }); - }); -} - -// Réinitialiser le formulaire de monstre -function resetMonsterForm() { - monsterIdEl.value = ''; - monsterNameEl.value = ''; - monsterImageEl.value = ''; -} - -// Éditer un monstre -function editMonster(monsterId) { - const monster = monsters.find(m => m.id === monsterId); - - if (!monster) return; - - monsterIdEl.value = monster.id; - monsterNameEl.value = monster.name; - - // Retirer le préfixe 'img/' pour l'affichage dans le formulaire - let imagePath = monster.image; - if (imagePath.startsWith('img/')) { - imagePath = imagePath.substring(4); - } - monsterImageEl.value = imagePath; - - monsterModalTitleEl.textContent = 'Modifier le monstre'; - monsterModal.show(); -} - -// Gérer la sauvegarde d'un monstre -function handleSaveMonster(e) { - e.preventDefault(); - - const monsterId = monsterIdEl.value.trim(); - const monsterName = monsterNameEl.value.trim(); - let monsterImage = monsterImageEl.value.trim(); - - // Ajouter le préfixe 'img/' si ce n'est pas déjà fait - if (!monsterImage.startsWith('img/')) { - monsterImage = 'img/' + monsterImage; - } - - if (!monsterName || !monsterImage) { - alert('Veuillez remplir tous les champs'); - return; - } - - if (monsterId) { - // Mode édition - const index = monsters.findIndex(m => m.id === parseInt(monsterId)); - - if (index !== -1) { - monsters[index].name = monsterName; - monsters[index].image = monsterImage; - } - } else { - // Mode ajout - const newMonsterId = monsters.length > 0 - ? Math.max(...monsters.map(m => m.id)) + 1 - : 1; - - monsters.push({ - id: newMonsterId, - name: monsterName, - image: monsterImage - }); - } - - saveData(); - renderMonstersList(); - monsterModal.hide(); -} - -// ============ MAINTENANCE ============ - -// Mettre à jour les statistiques -function updateStatistics() { - totalMonstersCountEl.textContent = monsters.length; - totalQuestsCountEl.textContent = quests.length; - smallCrownQuestsCountEl.textContent = quests.filter(q => q.crownType === 'small').length; - largeCrownQuestsCountEl.textContent = quests.filter(q => q.crownType === 'large').length; -} - -// Nettoyer les quêtes anciennes -function handleCleanOldQuests() { - const sevenDaysAgo = new Date(); - sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); - - const oldQuestsCount = quests.filter(q => new Date(q.date) < sevenDaysAgo).length; - - quests = quests.filter(q => new Date(q.date) >= sevenDaysAgo); - - saveData(); - updateStatistics(); - - cleanResultEl.innerHTML = `${oldQuestsCount} quête(s) supprimée(s) avec succès.`; - cleanResultEl.classList.remove('d-none'); - - // Masquer le message après 3 secondes - setTimeout(() => { - cleanResultEl.classList.add('d-none'); - }, 3000); -} - -// ============ SUPPRESSION ============ - -// Afficher la confirmation de suppression -function showDeleteConfirmation(type, id) { - currentDeletionType = type; - currentDeletionId = id; - - if (type === 'announcement') { - confirmDeleteTitleEl.textContent = 'Supprimer l\'annonce'; - confirmDeleteMessageEl.textContent = 'Êtes-vous sûr de vouloir supprimer cette annonce ?'; - } else if (type === 'monster') { - const questCount = quests.filter(q => q.monsterId === id).length; - - confirmDeleteTitleEl.textContent = 'Supprimer le monstre'; - confirmDeleteMessageEl.textContent = `Êtes-vous sûr de vouloir supprimer ce monstre ? Cette action supprimera également ${questCount} quête(s) associée(s).`; - } - - confirmDeleteModal.show(); -} - -// Gérer la confirmation de suppression -function handleConfirmDelete() { - if (currentDeletionType === 'announcement') { - announcements = announcements.filter(a => a.id !== currentDeletionId); - renderAnnouncementsList(); - } else if (currentDeletionType === 'monster') { - monsters = monsters.filter(m => m.id !== currentDeletionId); - quests = quests.filter(q => q.monsterId !== currentDeletionId); - renderMonstersList(); - } - - saveData(); - confirmDeleteModal.hide(); - - currentDeletionType = null; - currentDeletionId = null; -} \ No newline at end of file diff --git a/js/data.js b/js/data.js deleted file mode 100644 index e3f6128..0000000 --- a/js/data.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Fichier de données pour l'application MH Wilds - Quêtes à Couronnes - * Contient la liste des monstres du jeu - */ - -// Créer l'objet de données global -window.gameData = { - // Liste des monstres du jeu - monsters: [ - { id: 1, name: 'Chatacabra', image: 'img/Chatacabra.jpg' }, - { id: 2, name: 'Quematrice', image: 'img/Quematrice.jpg' }, - { id: 3, name: 'Lala Barina', image: 'img/Lala_Barina.jpg' }, - { id: 4, name: 'Congalala', image: 'img/Congalala.jpg' }, - { id: 5, name: 'Balahara', image: 'img/Balahara.jpg' }, - { id: 6, name: 'Doshaguma', image: 'img/Doshaguma.jpg' }, - { id: 7, name: 'Uth Duna', image: 'img/Uth_Duna.jpg' }, - { id: 8, name: 'Rompopolo', image: 'img/Rompopolo.jpg' }, - { id: 9, name: 'Rey Dau', image: 'img/Rey_Dau.jpg' }, - { id: 10, name: 'Nerscylla', image: 'img/Nerscylla.jpg' }, - { id: 11, name: 'Hirabami', image: 'img/Hirabami.jpg' }, - { id: 12, name: 'Ajarakan', image: 'img/Ajarakan.jpg' }, - { id: 13, name: 'Nu Udra', image: 'img/Nu_Udra.jpg' }, - { id: 14, name: 'Doshaguma Gardien', image: 'img/Doshaguma_Gardien.jpg' }, - { id: 15, name: 'Rathalos Gardien', image: 'img/Rathalos_Gardien.jpg' }, - { id: 16, name: 'Jin Dahaad', image: 'img/Jin_Dahaad.jpg' }, - { id: 17, name: 'Odogaron Désastre Gardien', image: 'img/Odogaron_Desastre_Gardien.jpg' }, - { id: 18, name: 'Xu Wu', image: 'img/Xu_Wu.jpg' }, - { id: 19, name: 'Arkveld Gardien', image: 'img/Arkveld_Gardien.jpg' }, - { id: 20, name: 'Zoh Shia', image: 'img/Zoh_Shia.jpg' }, - { id: 21, name: 'Yian Kut-Ku', image: 'img/Yian_Kut-Ku.jpg' }, - { id: 22, name: 'Gypceros', image: 'img/Gypceros.jpg' }, - { id: 23, name: 'Rathian', image: 'img/Rathian.jpg' }, - { id: 24, name: 'Anjanath Tonnerre Gardien', image: 'img/Anjanath_Tonnerre_Gardien.jpg' }, - { id: 25, name: 'Rathalos', image: 'img/Rathalos.jpg' }, - { id: 26, name: 'Gravios', image: 'img/Gravios.jpg' }, - { id: 27, name: 'Blangonga', image: 'img/Blangonga.jpg' }, - { id: 28, name: 'Gore Malaga', image: 'img/Gore_Malaga.jpg' }, - { id: 29, name: 'Arkveld', image: 'img/Arkveld.jpg' } - ], - - // Données initiales pour les quêtes (à titre d'exemple) - initialQuests: [ - { id: 1, monsterId: 1, crownType: 'small', playerName: 'Hunter123', playerId: 'MHW-1234', date: new Date().toISOString() }, - { id: 2, monsterId: 1, crownType: 'large', playerName: 'DragonSlayer', playerId: 'MHW-5678', date: new Date().toISOString() }, - { id: 3, monsterId: 2, crownType: 'small', playerName: 'ThunderLord', playerId: 'MHW-9012', date: new Date().toISOString() } - ], - - // Données initiales pour les annonces - initialAnnouncements: [ - { id: 1, text: "Bienvenue sur le site de partage de quêtes à couronnes pour Monster Hunter Wilds !", active: true } - ] -}; \ No newline at end of file diff --git a/js/login.js b/js/login.js deleted file mode 100644 index 28b8286..0000000 --- a/js/login.js +++ /dev/null @@ -1,38 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - const loginForm = document.getElementById('loginForm'); - const loginError = document.getElementById('loginError'); - - // Si le paramètre d'URL "logout" est présent, effacer les données d'authentification - const urlParams = new URLSearchParams(window.location.search); - if (urlParams.has('logout')) { - localStorage.removeItem('admin_authenticated'); - window.history.replaceState({}, document.title, 'login.html'); - } - - // Vérifier si l'utilisateur est déjà connecté - if (localStorage.getItem('admin_authenticated') === 'true') { - window.location.href = 'admin.html'; - return; - } - - // Gestionnaire d'événement pour le formulaire de connexion - loginForm.addEventListener('submit', (e) => { - e.preventDefault(); - - const username = document.getElementById('username').value.trim(); - const password = document.getElementById('password').value.trim(); - - // Vérifier les identifiants (à remplacer par un système plus sécurisé dans une application réelle) - if (username === 'admin' && password === 'mhwilds2025') { - // Identifiants corrects - localStorage.setItem('admin_authenticated', 'true'); - - // Rediriger vers l'administration - window.location.href = 'admin.html'; - } else { - // Identifiants incorrects - loginError.classList.remove('d-none'); - document.getElementById('password').value = ''; - } - }); -}); \ No newline at end of file diff --git a/js/main.js b/js/main.js deleted file mode 100644 index af05efb..0000000 --- a/js/main.js +++ /dev/null @@ -1,542 +0,0 @@ -// Variables globales pour les données -let monsters = []; -let quests = []; -let announcements = []; - -// Éléments DOM -const monsterListEl = document.getElementById('monsterList'); -const modalMonsterNameEl = document.getElementById('modalMonsterName'); -const questListEl = document.getElementById('questList'); -const addQuestBtn = document.getElementById('addQuestBtn'); -const addQuestForm = document.getElementById('addQuestForm'); -const crownFilterEls = document.querySelectorAll('input[name="crownFilter"]'); -const announcementAreaEl = document.getElementById('announcementArea'); -const confirmDeleteBtn = document.getElementById('confirmDeleteBtn'); -const monsterSearchEl = document.getElementById('monsterSearch'); -const clearSearchBtn = document.getElementById('clearSearchBtn'); - -// Variables globales -let currentMonsterId = null; -let currentQuestToDelete = null; - -// Modales Bootstrap -const questListModal = new bootstrap.Modal(document.getElementById('questListModal')); -const addQuestModal = new bootstrap.Modal(document.getElementById('addQuestModal')); -const deleteQuestModal = new bootstrap.Modal(document.getElementById('deleteQuestModal')); - -// Initialisation -document.addEventListener('DOMContentLoaded', () => { - loadData(); - renderMonsterList(); - initMonsterSearchSelect(); - displayAnnouncements(); - - // Événements - addQuestBtn.addEventListener('click', () => { - resetMonsterSearchSelect(); - addQuestModal.show(); - }); - addQuestForm.addEventListener('submit', handleAddQuest); - crownFilterEls.forEach(radio => { - radio.addEventListener('change', filterQuests); - }); - confirmDeleteBtn.addEventListener('click', handleDeleteQuest); - - // Événements pour la recherche - monsterSearchEl.addEventListener('input', searchMonsters); - clearSearchBtn.addEventListener('click', clearSearch); -}); - -// Fonctions de chargement et sauvegarde des données -function loadData() { - // Vérifier si les données de jeu sont disponibles depuis data.js - if (window.gameData) { - // Copier les monstres depuis gameData - monsters = [...window.gameData.monsters]; - - // Charger les quêtes depuis localStorage ou utiliser les données par défaut - const storedQuests = localStorage.getItem('mhw_quests'); - quests = storedQuests ? JSON.parse(storedQuests) : [...window.gameData.initialQuests]; - - // Charger les annonces depuis localStorage ou utiliser les données par défaut - const storedAnnouncements = localStorage.getItem('mhw_announcements'); - announcements = storedAnnouncements ? JSON.parse(storedAnnouncements) : [...window.gameData.initialAnnouncements]; - } else { - console.error("Erreur: données de jeu non disponibles. Assurez-vous que data.js est chargé avant main.js"); - monsterListEl.innerHTML = ` -
-
- Erreur de chargement des données. Veuillez rafraîchir la page. -
-
- `; - } -} - -function saveData() { - localStorage.setItem('mhw_quests', JSON.stringify(quests)); - localStorage.setItem('mhw_announcements', JSON.stringify(announcements)); - console.log('Données sauvegardées'); -} - -// Affichage des annonces -function displayAnnouncements() { - const activeAnnouncements = announcements.filter(a => a.active); - - if (activeAnnouncements.length > 0) { - announcementAreaEl.innerHTML = activeAnnouncements.map(a => `

${a.text}

`).join(''); - announcementAreaEl.classList.remove('d-none'); - } else { - announcementAreaEl.classList.add('d-none'); - } -} - -// Rendu de la liste des monstres -function renderMonsterList(filteredMonsters = null) { - const monstersToRender = filteredMonsters || monsters; - - if (monstersToRender.length === 0) { - monsterListEl.innerHTML = ` -
-
-

Aucun monstre disponible pour le moment.

-
-
- `; - return; - } - - monsterListEl.innerHTML = monstersToRender.map(monster => { - const smallCrownCount = quests.filter(q => q.monsterId === monster.id && q.crownType === 'small').length; - const largeCrownCount = quests.filter(q => q.monsterId === monster.id && q.crownType === 'large').length; - - return ` -
-
-
- ${monster.name} -
-
-
${monster.name}
-
-
- - 👑 ${smallCrownCount} - - - 👑 ${largeCrownCount} - -
- -
-
-
-
- `; - }).join(''); - - // Ajouter les écouteurs d'événements après avoir rendu la liste - document.querySelectorAll('.monster-card').forEach(card => { - card.addEventListener('click', () => { - const monsterId = parseInt(card.dataset.monsterId); - showQuestsForMonster(monsterId); - }); - }); -} - -// Recherche de monstres sur la page principale -function searchMonsters() { - const searchTerm = monsterSearchEl.value.trim().toLowerCase(); - - if (searchTerm === '') { - // Si la recherche est vide, afficher tous les monstres - renderMonsterList(); - return; - } - - // Filtrer les monstres qui correspondent à la recherche - const filteredMonsters = monsters.filter(monster => - monster.name.toLowerCase().includes(searchTerm) - ); - - // Afficher un message si aucun monstre ne correspond - if (filteredMonsters.length === 0) { - monsterListEl.innerHTML = ` -
-
-

Aucun monstre ne correspond à votre recherche "${monsterSearchEl.value}".

-
-
- `; - return; - } - - renderMonsterList(filteredMonsters); -} - -// Effacer la recherche -function clearSearch() { - monsterSearchEl.value = ''; - renderMonsterList(); -} - -// Afficher les quêtes pour un monstre spécifique -function showQuestsForMonster(monsterId) { - currentMonsterId = monsterId; - const monster = monsters.find(m => m.id === monsterId); - - if (!monster) return; - - modalMonsterNameEl.textContent = monster.name; - - // Réinitialiser le filtre - document.getElementById('filterAll').checked = true; - - // Afficher toutes les quêtes pour ce monstre - renderQuestList(monsterId); - - // Afficher la modale - questListModal.show(); -} - -// Rendu de la liste des quêtes -function renderQuestList(monsterId, filter = 'all') { - let monsterQuests = quests.filter(q => q.monsterId === monsterId); - - if (filter !== 'all') { - monsterQuests = monsterQuests.filter(q => q.crownType === filter); - } - - if (monsterQuests.length === 0) { - questListEl.innerHTML = ` -
-

Aucune quête disponible pour ce monstre avec ce filtre.

-
- `; - return; - } - - questListEl.innerHTML = monsterQuests.map(quest => { - const monster = monsters.find(m => m.id === quest.monsterId); - const crownClass = quest.crownType === 'small' ? 'small-crown' : 'large-crown'; - const crownText = quest.crownType === 'small' ? 'Petite couronne' : 'Grande couronne'; - const questDate = new Date(quest.date); - const daysDiff = Math.floor((new Date() - questDate) / (1000 * 60 * 60 * 24)); - const freshness = daysDiff <= 1 ? 'Aujourd\'hui' : - daysDiff <= 2 ? 'Hier' : - `Il y a ${daysDiff} jours`; - - return ` -
-
-
- 👑 - ${crownText} pour ${monster.name} -
-

- Proposée par: ${quest.playerName} (ID: ${quest.playerId}) -

-

Ajoutée: ${freshness}

- -
-
- `; - }).join(''); - - // Ajouter les écouteurs d'événements pour les boutons de suppression - document.querySelectorAll('.quest-delete-btn').forEach(btn => { - btn.addEventListener('click', (e) => { - e.stopPropagation(); - const questId = parseInt(btn.dataset.questId); - showDeleteConfirmation(questId); - }); - }); -} - -// Filtrer les quêtes -function filterQuests() { - const filterValue = document.querySelector('input[name="crownFilter"]:checked').value; - renderQuestList(currentMonsterId, filterValue); -} - -// Afficher la confirmation de suppression -function showDeleteConfirmation(questId) { - currentQuestToDelete = questId; - deleteQuestModal.show(); -} - -// Gérer la suppression d'une quête -function handleDeleteQuest() { - if (currentQuestToDelete === null) return; - - // Trouver l'index de la quête à supprimer - const questIndex = quests.findIndex(q => q.id === currentQuestToDelete); - - if (questIndex === -1) return; - - // Mémoriser le monsterId pour mettre à jour l'affichage - const monsterId = quests[questIndex].monsterId; - - // Supprimer la quête - quests.splice(questIndex, 1); - - // Sauvegarder les données - saveData(); - - // Mettre à jour l'affichage - renderMonsterList(); - - // Si la modale des quêtes est ouverte, mettre à jour son contenu - if (currentMonsterId === monsterId) { - renderQuestList(currentMonsterId); - } - - // Fermer la modale de confirmation - deleteQuestModal.hide(); - - // Réinitialiser - currentQuestToDelete = null; -} - -// Initialiser le sélecteur de monstre avec recherche -function initMonsterSearchSelect() { - const searchInput = document.getElementById('monsterSearchSelect'); - const searchResults = document.getElementById('monsterSearchResults'); - const hiddenInput = document.getElementById('selectedMonsterId'); - - if (!searchInput || !searchResults || !hiddenInput) return; - - // Événement pour l'input de recherche - searchInput.addEventListener('input', function() { - const searchTerm = this.value.trim().toLowerCase(); - - // Filtrer les monstres qui correspondent à la recherche - const filteredMonsters = monsters.filter(monster => - monster.name.toLowerCase().includes(searchTerm) - ); - - // Afficher les résultats - renderSearchResults(filteredMonsters, searchResults); - - // Montrer les résultats si l'input a du contenu - if (searchTerm.length > 0) { - searchResults.classList.remove('d-none'); - } else if (!hiddenInput.value) { - // Cacher seulement si aucun monstre n'est déjà sélectionné - searchResults.classList.add('d-none'); - } - }); - - // Événement pour le focus sur l'input - searchInput.addEventListener('focus', function() { - const searchTerm = this.value.trim().toLowerCase(); - - // Si l'input a du contenu ou si un monstre est déjà sélectionné, montrer les résultats - if (searchTerm.length > 0 || hiddenInput.value) { - // Filtrer les monstres qui correspondent à la recherche - const filteredMonsters = searchTerm.length > 0 - ? monsters.filter(monster => monster.name.toLowerCase().includes(searchTerm)) - : monsters; - - // Afficher les résultats - renderSearchResults(filteredMonsters, searchResults); - searchResults.classList.remove('d-none'); - } - }); - - // Fermer les résultats lors d'un clic à l'extérieur - document.addEventListener('click', function(e) { - if (!searchInput?.contains(e.target) && !searchResults?.contains(e.target)) { - searchResults?.classList.add('d-none'); - } - }); - - // Navigation avec les flèches du clavier - searchInput.addEventListener('keydown', function(e) { - if (searchResults.classList.contains('d-none')) return; - - const items = searchResults.querySelectorAll('.monster-search-item'); - const activeItem = searchResults.querySelector('.monster-search-item.active'); - - switch(e.key) { - case 'ArrowDown': - e.preventDefault(); - if (!activeItem) { - items[0]?.classList.add('active'); - ensureVisible(items[0], searchResults); - } else { - const nextItem = activeItem.nextElementSibling; - if (nextItem) { - activeItem.classList.remove('active'); - nextItem.classList.add('active'); - ensureVisible(nextItem, searchResults); - } - } - break; - - case 'ArrowUp': - e.preventDefault(); - if (activeItem) { - const prevItem = activeItem.previousElementSibling; - if (prevItem) { - activeItem.classList.remove('active'); - prevItem.classList.add('active'); - ensureVisible(prevItem, searchResults); - } - } - break; - - case 'Enter': - e.preventDefault(); - if (activeItem) { - const monsterId = activeItem.dataset.monsterId; - selectMonster(monsterId, searchInput, hiddenInput, searchResults); - } - break; - - case 'Escape': - e.preventDefault(); - searchResults.classList.add('d-none'); - break; - } - }); -} - -// S'assurer que l'élément actif est visible dans la liste déroulante -function ensureVisible(element, container) { - if (!element || !container) return; - - const containerRect = container.getBoundingClientRect(); - const elementRect = element.getBoundingClientRect(); - - if (elementRect.bottom > containerRect.bottom) { - container.scrollTop += elementRect.bottom - containerRect.bottom; - } else if (elementRect.top < containerRect.top) { - container.scrollTop -= containerRect.top - elementRect.top; - } -} - -// Afficher les résultats de recherche pour le sélecteur de monstre -function renderSearchResults(results, container) { - if (!container) return; - - if (results.length === 0) { - container.innerHTML = '
Aucun monstre trouvé
'; - return; - } - - container.innerHTML = results.map(monster => - `
- ${monster.name} -
` - ).join(''); - - // Ajouter les écouteurs d'événements pour les éléments de la liste - container.querySelectorAll('.monster-search-item').forEach(item => { - item.addEventListener('click', function() { - const monsterId = this.dataset.monsterId; - const searchInput = document.getElementById('monsterSearchSelect'); - const hiddenInput = document.getElementById('selectedMonsterId'); - const searchResults = document.getElementById('monsterSearchResults'); - - selectMonster(monsterId, searchInput, hiddenInput, searchResults); - }); - }); -} - -// Sélectionner un monstre dans le sélecteur -function selectMonster(monsterId, searchInput, hiddenInput, searchResults) { - const monster = monsters.find(m => m.id == monsterId); - - if (monster) { - searchInput.value = monster.name; - hiddenInput.value = monster.id; - searchInput.classList.add('selected-monster'); - searchResults.classList.add('d-none'); - } -} - -// Réinitialiser le sélecteur de monstre -function resetMonsterSearchSelect() { - const searchInput = document.getElementById('monsterSearchSelect'); - const hiddenInput = document.getElementById('selectedMonsterId'); - const searchResults = document.getElementById('monsterSearchResults'); - - if (searchInput) { - searchInput.value = ''; - searchInput.classList.remove('selected-monster'); - } - - if (hiddenInput) { - hiddenInput.value = ''; - } - - if (searchResults) { - searchResults.classList.add('d-none'); - } -} - -// Gérer l'ajout d'une quête -function handleAddQuest(e) { - e.preventDefault(); - - const monsterId = parseInt(document.getElementById('selectedMonsterId')?.value); - const crownType = document.querySelector('input[name="crownType"]:checked')?.value; - const playerName = document.getElementById('playerName')?.value.trim(); - const playerId = document.getElementById('playerId')?.value.trim(); - - if (!monsterId || !crownType || !playerName || !playerId) { - alert('Veuillez remplir tous les champs requis'); - return; - } - - // Générer un ID unique pour la nouvelle quête - const newQuestId = quests.length > 0 ? Math.max(...quests.map(q => q.id)) + 1 : 1; - - // Créer la nouvelle quête - const newQuest = { - id: newQuestId, - monsterId, - crownType, - playerName, - playerId, - date: new Date().toISOString() - }; - - // Ajouter la quête à la liste - quests.push(newQuest); - - // Sauvegarder les données - saveData(); - - // Mettre à jour l'affichage - renderMonsterList(); - - // Réinitialiser le formulaire et fermer la modale - addQuestForm.reset(); - resetMonsterSearchSelect(); - addQuestModal.hide(); - - // Si la modale des quêtes est ouverte, mettre à jour son contenu - if (currentMonsterId === monsterId) { - renderQuestList(currentMonsterId); - } -} - -// Nettoyer les quêtes de plus de 7 jours (à appeler depuis l'admin) -function cleanOldQuests() { - const sevenDaysAgo = new Date(); - sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7); - - const oldQuestsCount = quests.filter(q => new Date(q.date) < sevenDaysAgo).length; - - quests = quests.filter(q => new Date(q.date) >= sevenDaysAgo); - - saveData(); - renderMonsterList(); - - return oldQuestsCount; -} \ No newline at end of file diff --git a/login.html b/login.html deleted file mode 100644 index 41237cd..0000000 --- a/login.html +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - Connexion - Administration MH Wilds - - - - - - - - - - - - - - - - - -
-
-
-
- Logo -

MH Wilds - Administration

-
- Retour au site -
-
-
- -
-
-
-
-
-

Connexion à l'administration

-
-
- -
-
- - -
-
- - -
-
- -
-
-
-
-
-
-
- -
-
-

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/login.php b/login.php new file mode 100644 index 0000000..7dff06a --- /dev/null +++ b/login.php @@ -0,0 +1,87 @@ + + +
+
+
+
+

Connexion à l'administration

+
+
+ + + + +
+ +
+ + +
+
+ + +
+
+ +
+
+
+
+
+
+ + - - - - - Tutoriel - Système de Couronnes | MH Wilds - - - - - - - - - - - - - - - - - -
-
-
-
- Logo -

MH Wilds - Quêtes à Couronnes

-
- Retour à l'accueil -
-
-
- -
-
-
-
-
-

Tutoriel : Le système de couronnes dans Monster Hunter Wilds

-
-
-

Qu'est-ce que les couronnes ?

-

Dans Monster Hunter Wilds, les monstres que vous chassez peuvent apparaître avec différentes tailles. Si vous chassez un monstre qui a la taille minimum ou maximum de son espèce, vous obtiendrez une "couronne" associée dans votre bestiaire.

- -

Types de couronnes :

-
    -
  • Petite couronne (👑) : Attribuée lorsque vous chassez un monstre de taille minimale.
  • -
  • Grande couronne (👑) : Attribuée lorsque vous chassez un monstre de taille maximale.
  • -
- -

Collectionner ces couronnes est un objectif apprécié des chasseurs qui veulent compléter leur bestiaire à 100%.

- -
- -

Quêtes d'investigation avec couronnes

-

En utilisant les jumelles (dans votre barre d'inventaire par défaut) pour observer un monstre, une couronne d'or s'affichera si le monstre est excessivement grand ou excessivement petit par rapport à la moyenne de son espèce.

-

En utilisant la carte (carte de la région actuelle ou carte du monde, les deux fonctionnent), vous pouvez créer une investigation à partir des monstres présents pour pouvoir les chasser jusqu'à 3 fois dans le cadre de quêtes d'investigation auprès d'Alma.

- -

Comment fonctionnent les quêtes d'investigation ?

-

Là où c'est merveilleux, c'est que sauvegarder un monstre en investigation sauvegarde aussi sa taille, donc s'il s'agit d'une couronne d'or, vous aurez 3 chasses à couronne d'or garantie sur le monstre en question. Donc, on peut se partager nos couronnes d'or entre nous si on fait ces investigations ensemble !

- -
- -

Avertissement

-

Si vous utilisez des mods pour modifier le taux de pop de vos couronnes d'or, merci de ne pas utiliser ce site. L'objectif est de s'entraider pour essayer d'avoir toutes les couronnes de manière légitime, ne ruinez pas le plaisir du grind des autres !

- -

Fonctionnalités :

-
    -
  • Consulter les quêtes : Cliquez sur un monstre pour voir les quêtes partagées par d'autres joueurs.
  • -
  • Filtrer par type de couronne : Vous pouvez filtrer les quêtes par petite ou grande couronne selon ce que vous recherchez.
  • -
  • Partager votre quête : Si vous obtenez une quête d'investigation avec une bonne probabilité de couronne, partagez-la avec la communauté en cliquant sur le bouton "Ajouter ma quête".
  • -
  • Supprimer votre quête : Une fois que votre quête n'est plus disponible (expirée ou nombre de tentatives épuisé), n'oubliez pas de la supprimer.
  • -
- -
-

Note : Les quêtes partagées sur ce site sont automatiquement supprimées après 7 jours.

-
-
-
- - -
-
-
- -
-
-

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/tutorial.php b/tutorial.php new file mode 100644 index 0000000..1207e16 --- /dev/null +++ b/tutorial.php @@ -0,0 +1,79 @@ + + +
+
+
+
+

Tutoriel : Le système de couronnes dans Monster Hunter Wilds

+
+
+

Qu'est-ce que les couronnes ?

+

Dans Monster Hunter Wilds, les monstres que vous chassez peuvent apparaître avec différentes tailles. Si vous chassez un monstre qui a la taille minimum ou maximum de son espèce, vous obtiendrez une "couronne" associée dans votre bestiaire.

+ +

Types de couronnes :

+
    +
  • Petite couronne (👑) : Attribuée lorsque vous chassez un monstre de taille minimale.
  • +
  • Grande couronne (👑) : Attribuée lorsque vous chassez un monstre de taille maximale.
  • +
+ +

Collectionner ces couronnes est un objectif apprécié des chasseurs qui veulent compléter leur bestiaire à 100%.

+ +
+ +

Quêtes d'investigation avec couronnes

+

En utilisant les jumelles (dans votre barre d'inventaire par défaut) pour observer un monstre, une couronne d'or s'affichera si le monstre est excessivement grand ou excessivement petit par rapport à la moyenne de son espèce.

+

En utilisant la carte (carte de la région actuelle ou carte du monde, les deux fonctionnent), vous pouvez créer une investigation à partir des monstres présents pour pouvoir les chasser jusqu'à 3 fois dans le cadre de quêtes d'investigation auprès d'Alma.

+ +

Comment fonctionnent les quêtes d'investigation ?

+

Là où c'est merveilleux, c'est que sauvegarder un monstre en investigation sauvegarde aussi sa taille, donc s'il s'agit d'une couronne d'or, vous aurez 3 chasses à couronne d'or garantie sur le monstre en question. Donc, on peut se partager nos couronnes d'or entre nous si on fait ces investigations ensemble !

+ +
+ +

Avertissement

+

Si vous utilisez des mods pour modifier le taux de pop de vos couronnes d'or, merci de ne pas utiliser ce site. L'objectif est de s'entraider pour essayer d'avoir toutes les couronnes de manière légitime, ne ruinez pas le plaisir du grind des autres !

+ +

Fonctionnalités :

+
    +
  • Consulter les quêtes : Cliquez sur un monstre pour voir les quêtes partagées par d'autres joueurs.
  • +
  • Filtrer par type de couronne : Vous pouvez filtrer les quêtes par petite ou grande couronne selon ce que vous recherchez.
  • +
  • Partager votre quête : Si vous obtenez une quête d'investigation avec une bonne probabilité de couronne, partagez-la avec la communauté en cliquant sur le bouton "Ajouter ma quête".
  • +
  • Supprimer votre quête : Une fois que votre quête n'est plus disponible (expirée ou nombre de tentatives épuisé), n'oubliez pas de la supprimer.
  • +
+ +
+

Note : Les quêtes partagées sur ce site sont automatiquement supprimées après 7 jours.

+
+
+
+ + +
+
+ +