initialisation de la v2 du projet

migration sur une solution php
This commit is contained in:
Esenjin 2025-03-09 18:45:32 +01:00
parent ffee932dd9
commit 6241e093b1
57 changed files with 3033 additions and 2181 deletions

66
.htaccess Normal file
View File

@ -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
<FilesMatch "\.(db|sqlite)$">
Order Allow,Deny
Deny from all
</FilesMatch>
# 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
<IfModule mod_headers.c>
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"
</IfModule>
# Protection contre les attaques par clickjacking
<IfModule mod_headers.c>
Header always append X-Frame-Options SAMEORIGIN
</IfModule>
# Améliorer la sécurité des cookies (à utiliser avec HTTPS)
<IfModule mod_php7.c>
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
</IfModule>
# Bloquer l'accès aux fichiers sensibles
<FilesMatch "^(\.htaccess|\.htpasswd|\.git|\.svn|\.DS_Store)">
Order Allow,Deny
Deny from all
</FilesMatch>
# Performance - Mise en cache des ressources statiques
<IfModule mod_expires.c>
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"
</IfModule>
# Activer la compression des fichiers
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/css application/javascript text/plain text/xml application/json
</IfModule>

View File

@ -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.

View File

@ -1,3 +0,0 @@
# Chasse_aux_couronnes
Site pour partager ses quêtes d'investigations afin de faire profiter aux copains de nos couronnes !

View File

@ -1,244 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Administration - MH Wilds Couronnes</title>
<!-- Métadonnées pour le partage -->
<meta property="og:title" content="Administration - MH Wilds Couronnes">
<meta property="og:description" content="Interface d'administration pour le site de partage de quêtes à couronnes.">
<meta property="og:image" content="img/logo.png">
<meta property="og:url" content="https://votresite.com/admin.html">
<meta name="twitter:card" content="summary_large_image">
<!-- Favicon -->
<link rel="icon" href="img/logo.png" type="image/png">
<!-- Bootstrap CSS with dark theme -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Notre CSS personnalisé -->
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<header class="bg-dark text-white py-3">
<div class="container">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<img src="img/logo.png" alt="Logo" height="40" class="me-2">
<h1 class="h2 mb-0">Administration</h1>
</div>
<div>
<a href="index.html" class="btn btn-outline-light me-2">Retour au site</a>
<button id="logoutBtn" class="btn btn-danger">Déconnexion</button>
</div>
</div>
</div>
</header>
<main class="container my-4">
<div class="row">
<div class="col-md-3">
<div class="list-group">
<a href="#announcementsSection" class="list-group-item list-group-item-action active" id="announcementsTab">Annonces</a>
<a href="#monstersSection" class="list-group-item list-group-item-action" id="monstersTab">Monstres</a>
<a href="#maintenanceSection" class="list-group-item list-group-item-action" id="maintenanceTab">Maintenance</a>
</div>
</div>
<div class="col-md-9">
<!-- Section des annonces -->
<div id="announcementsSection" class="admin-section">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h2 class="h4 mb-0">Gestion des annonces</h2>
<button class="btn btn-primary btn-sm" id="addAnnouncementBtn">Ajouter une annonce</button>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Texte</th>
<th>Statut</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="announcementsList">
<!-- Liste des annonces générée dynamiquement -->
</tbody>
</table>
</div>
<div id="emptyAnnouncementsMessage" class="alert alert-info d-none">
Aucune annonce pour le moment.
</div>
</div>
</div>
</div>
<!-- Section des monstres -->
<div id="monstersSection" class="admin-section d-none">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h2 class="h4 mb-0">Gestion des monstres</h2>
<button class="btn btn-primary btn-sm" id="addMonsterBtn">Ajouter un monstre</button>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Image</th>
<th>Nom</th>
<th>Quêtes</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="monstersList">
<!-- Liste des monstres générée dynamiquement -->
</tbody>
</table>
</div>
<div id="emptyMonstersMessage" class="alert alert-info d-none">
Aucun monstre pour le moment.
</div>
</div>
</div>
</div>
<!-- Section de maintenance -->
<div id="maintenanceSection" class="admin-section d-none">
<div class="card">
<div class="card-header">
<h2 class="h4 mb-0">Maintenance du site</h2>
</div>
<div class="card-body">
<div class="mb-4">
<h3 class="h5">Nettoyer les quêtes anciennes</h3>
<p>Cette action supprimera toutes les quêtes datant de plus de 7 jours.</p>
<button class="btn btn-warning" id="cleanOldQuestsBtn">Nettoyer les quêtes anciennes</button>
<div id="cleanResult" class="alert alert-success mt-3 d-none"></div>
</div>
<hr>
<div>
<h3 class="h5">Statistiques du site</h3>
<ul class="list-group">
<li class="list-group-item d-flex justify-content-between align-items-center">
Nombre total de monstres
<span class="badge bg-primary rounded-pill" id="totalMonstersCount">0</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Nombre total de quêtes
<span class="badge bg-primary rounded-pill" id="totalQuestsCount">0</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Quêtes avec petite couronne
<span class="badge bg-warning rounded-pill" id="smallCrownQuestsCount">0</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Quêtes avec grande couronne
<span class="badge bg-secondary rounded-pill" id="largeCrownQuestsCount">0</span>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- Modale d'ajout/édition d'annonce -->
<div class="modal fade" id="announcementModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="announcementModalTitle">Ajouter une annonce</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
</div>
<div class="modal-body">
<form id="announcementForm">
<input type="hidden" id="announcementId">
<div class="mb-3">
<label for="announcementText" class="form-label">Texte de l'annonce</label>
<textarea class="form-control" id="announcementText" rows="3" required></textarea>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="announcementActive" checked>
<label class="form-check-label" for="announcementActive">
Activer l'annonce
</label>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary" id="saveAnnouncementBtn">Enregistrer</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Modale d'ajout/édition de monstre -->
<div class="modal fade" id="monsterModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="monsterModalTitle">Ajouter un monstre</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
</div>
<div class="modal-body">
<form id="monsterForm">
<input type="hidden" id="monsterId">
<div class="mb-3">
<label for="monsterName" class="form-label">Nom du monstre</label>
<input type="text" class="form-control" id="monsterName" required>
</div>
<div class="mb-3">
<label for="monsterImage" class="form-label">Chemin de l'image</label>
<div class="input-group">
<span class="input-group-text">img/</span>
<input type="text" class="form-control" id="monsterImage" required placeholder="Nom_du_monstre.jpg">
</div>
<div class="form-text">Format recommandé: jpg, 300x200px. Exemple: Rathalos.jpg</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary" id="saveMonsterBtn">Enregistrer</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Modale de confirmation de suppression -->
<div class="modal fade" id="confirmDeleteModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="confirmDeleteTitle">Confirmer la suppression</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
</div>
<div class="modal-body">
<p id="confirmDeleteMessage">Êtes-vous sûr de vouloir supprimer cet élément ?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">Supprimer</button>
</div>
</div>
</div>
</div>
<footer class="bg-dark text-white py-3 mt-auto">
<div class="container text-center">
<p class="mb-0">Ce site est réalisé dans le cadre de la branche <a href="https://camelia-studio.org/branches/alt+tab/" target="_blank">Alt Tab</a> de l'association <a href="https://camelia-studio.org/" target="_blank">Camélia Studio</a>.</p>
<p class="mb-0 mt-1"><small>Images des monstres par Sui Yun - Site sous licence MIT, code source sur <a href="https://git.crystalyx.net/camelia-studio/Chasse_aux_couronnes" target="_blank">Gitea</a></small></p>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- Charger data.js avant admin.js -->
<script src="js/data.js"></script>
<script src="js/main.js"></script>
<script src="js/admin.js"></script>
</body>
</html>

290
admin/index.php Normal file
View File

@ -0,0 +1,290 @@
<?php
/**
* Page principale d'administration - MH Wilds - Partage de Quêtes à Couronnes
*/
// Démarrer la session
session_start();
// Définir une constante pour empêcher l'accès direct aux includes
define('SECURE_ACCESS', true);
// Inclure les fichiers nécessaires
require_once '../includes/config.php';
require_once '../includes/database.php';
require_once '../includes/functions.php';
// Vérifier l'authentification
require_login();
// Titre de la page
$page_title = 'Administration - MH Wilds Couronnes';
$page_description = 'Interface d\'administration pour le site de partage de quêtes à couronnes.';
$header_title = 'Administration';
$is_admin_page = true;
// Scripts JS supplémentaires
$extra_js = <<<EOT
<script src="../assets/js/admin.js"></script>
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';
?>
<div class="row">
<div class="col-md-3">
<div class="list-group">
<a href="#announcementsSection" class="list-group-item list-group-item-action active" id="announcementsTab">Annonces</a>
<a href="#monstersSection" class="list-group-item list-group-item-action" id="monstersTab">Monstres</a>
<a href="#maintenanceSection" class="list-group-item list-group-item-action" id="maintenanceTab">Maintenance</a>
</div>
</div>
<div class="col-md-9">
<!-- Section des annonces -->
<div id="announcementsSection" class="admin-section">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h2 class="h4 mb-0">Gestion des annonces</h2>
<button class="btn btn-primary btn-sm" id="addAnnouncementBtn">Ajouter une annonce</button>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Texte</th>
<th>Statut</th>
<th>Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="announcementsList">
<?php if (empty($announcements)): ?>
<tr>
<td colspan="4" class="text-center">Aucune annonce pour le moment.</td>
</tr>
<?php else: ?>
<?php foreach ($announcements as $announcement): ?>
<tr>
<td><?php echo secure_output($announcement['text']); ?></td>
<td>
<?php if ($announcement['active']): ?>
<span class="badge bg-success">Active</span>
<?php else: ?>
<span class="badge bg-secondary">Inactive</span>
<?php endif; ?>
</td>
<td><?php echo date('d/m/Y', strtotime($announcement['created_at'])); ?></td>
<td>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary edit-announcement-btn" data-id="<?php echo $announcement['id']; ?>">
Éditer
</button>
<button class="btn btn-outline-danger delete-announcement-btn" data-id="<?php echo $announcement['id']; ?>">
Supprimer
</button>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Section des monstres -->
<div id="monstersSection" class="admin-section d-none">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h2 class="h4 mb-0">Gestion des monstres</h2>
<button class="btn btn-primary btn-sm" id="addMonsterBtn">Ajouter un monstre</button>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Image</th>
<th>Nom</th>
<th>Quêtes</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="monstersList">
<?php if (empty($monsters)): ?>
<tr>
<td colspan="4" class="text-center">Aucun monstre pour le moment.</td>
</tr>
<?php else: ?>
<?php foreach ($monsters as $monster): ?>
<?php $quest_count = count_quests_for_monster($monster['id']); ?>
<tr>
<td>
<img src="../<?php echo secure_output($monster['image']); ?>" alt="<?php echo secure_output($monster['name']); ?>" class="img-thumbnail" width="80">
</td>
<td><?php echo secure_output($monster['name']); ?></td>
<td><?php echo $quest_count; ?></td>
<td>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary edit-monster-btn" data-id="<?php echo $monster['id']; ?>">
Éditer
</button>
<button class="btn btn-outline-danger delete-monster-btn" data-id="<?php echo $monster['id']; ?>">
Supprimer
</button>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Section de maintenance -->
<div id="maintenanceSection" class="admin-section d-none">
<div class="card">
<div class="card-header">
<h2 class="h4 mb-0">Maintenance du site</h2>
</div>
<div class="card-body">
<div class="mb-4">
<h3 class="h5">Nettoyer les quêtes anciennes</h3>
<p>Cette action supprimera toutes les quêtes datant de plus de 7 jours.</p>
<form id="cleanQuestsForm">
<input type="hidden" name="csrf_token" value="<?php echo generate_csrf_token(); ?>">
<button type="button" class="btn btn-warning" id="cleanOldQuestsBtn">Nettoyer les quêtes anciennes</button>
</form>
<div id="cleanResult" class="alert alert-success mt-3 d-none"></div>
</div>
<hr>
<div>
<h3 class="h5">Statistiques du site</h3>
<ul class="list-group">
<li class="list-group-item d-flex justify-content-between align-items-center">
Nombre total de monstres
<span class="badge bg-primary rounded-pill" id="totalMonstersCount"><?php echo $statistics['total_monsters']; ?></span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Nombre total de quêtes
<span class="badge bg-primary rounded-pill" id="totalQuestsCount"><?php echo $statistics['total_quests']; ?></span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Quêtes avec petite couronne
<span class="badge bg-warning rounded-pill" id="smallCrownQuestsCount"><?php echo $statistics['small_crown_quests']; ?></span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
Quêtes avec grande couronne
<span class="badge bg-secondary rounded-pill" id="largeCrownQuestsCount"><?php echo $statistics['large_crown_quests']; ?></span>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modale d'ajout/édition d'annonce -->
<div class="modal fade" id="announcementModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="announcementModalTitle">Ajouter une annonce</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
</div>
<div class="modal-body">
<form id="announcementForm">
<input type="hidden" name="csrf_token" value="<?php echo generate_csrf_token(); ?>">
<input type="hidden" id="announcementId" name="announcementId">
<div class="mb-3">
<label for="announcementText" class="form-label">Texte de l'annonce</label>
<textarea class="form-control" id="announcementText" name="announcementText" rows="3" required></textarea>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="announcementActive" name="announcementActive" checked>
<label class="form-check-label" for="announcementActive">
Activer l'annonce
</label>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary" id="saveAnnouncementBtn">Enregistrer</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Modale d'ajout/édition de monstre -->
<div class="modal fade" id="monsterModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="monsterModalTitle">Ajouter un monstre</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
</div>
<div class="modal-body">
<form id="monsterForm">
<input type="hidden" name="csrf_token" value="<?php echo generate_csrf_token(); ?>">
<input type="hidden" id="monsterId" name="monsterId">
<div class="mb-3">
<label for="monsterName" class="form-label">Nom du monstre</label>
<input type="text" class="form-control" id="monsterName" name="monsterName" required>
</div>
<div class="mb-3">
<label for="monsterImage" class="form-label">Chemin de l'image</label>
<div class="input-group">
<span class="input-group-text">assets/img/</span>
<input type="text" class="form-control" id="monsterImage" name="monsterImage" required placeholder="Nom_du_monstre.jpg">
</div>
<div class="form-text">Format recommandé: jpg, 300x200px. Exemple: Rathalos.jpg</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary" id="saveMonsterBtn">Enregistrer</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Modale de confirmation de suppression -->
<div class="modal fade" id="confirmDeleteModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="confirmDeleteTitle">Confirmer la suppression</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
</div>
<div class="modal-body">
<p id="confirmDeleteMessage">Êtes-vous sûr de vouloir supprimer cet élément ?</p>
<form id="confirmDeleteForm">
<input type="hidden" name="csrf_token" value="<?php echo generate_csrf_token(); ?>">
<input type="hidden" name="deletionType">
<input type="hidden" name="deletionId">
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">Supprimer</button>
</div>
</div>
</div>
</div>
<?php
// Inclure le pied de page
include '../includes/footer.php';

25
admin/logout.php Normal file
View File

@ -0,0 +1,25 @@
<?php
/**
* Déconnexion de l'administration - MH Wilds - Partage de Quêtes à Couronnes
*/
// Démarrer la session
session_start();
// Définir une constante pour empêcher l'accès direct aux includes
define('SECURE_ACCESS', true);
// Inclure les fichiers nécessaires
require_once '../includes/config.php';
require_once '../includes/functions.php';
// Détruire la session
session_destroy();
// Définir un message de confirmation
$_SESSION = array();
session_start();
set_flash_message('success', 'Vous avez été déconnecté avec succès.');
// Rediriger vers la page de connexion
redirect('../login.php');

316
api.php Normal file
View File

@ -0,0 +1,316 @@
<?php
/**
* API pour les requêtes AJAX - MH Wilds - Partage de Quêtes à Couronnes
*/
// Démarrer la session
session_start();
// Définir une constante pour empêcher l'accès direct aux includes
define('SECURE_ACCESS', true);
// Inclure les fichiers nécessaires
require_once 'includes/config.php';
require_once 'includes/database.php';
require_once 'includes/functions.php';
// Définir le type de contenu comme JSON
header('Content-Type: application/json');
// Fonction pour renvoyer une réponse JSON
function send_json_response($data, $http_code = 200) {
http_response_code($http_code);
echo json_encode($data);
exit;
}
// Vérifier la méthode HTTP
$method = $_SERVER['REQUEST_METHOD'];
// Récupérer l'action demandée
$action = isset($_GET['action']) ? $_GET['action'] : '';
// Traiter les requêtes selon la méthode HTTP et l'action
if ($method === 'GET') {
// Actions de récupération de données
switch ($action) {
case 'getMonsters':
// Récupérer tous les monstres
$monsters = get_all_monsters();
send_json_response(['success' => 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);
}

324
assets/css/styles.css Normal file
View File

@ -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;
}
}

View File

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

View File

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 109 KiB

View File

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 107 KiB

View File

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

View File

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

View File

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

View File

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

View File

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 109 KiB

View File

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

View File

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

View File

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

View File

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

View File

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 109 KiB

View File

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

View File

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 95 KiB

View File

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 116 KiB

View File

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

View File

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 117 KiB

View File

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 110 KiB

View File

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

View File

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 111 KiB

View File

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

View File

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

View File

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 104 KiB

View File

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

View File

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 KiB

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

502
assets/js/admin.js Normal file
View File

@ -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.');
});
}

View File

@ -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;
}

47
includes/config.php Normal file
View File

@ -0,0 +1,47 @@
<?php
/**
* Configuration de l'application MH Wilds - Partage de Quêtes à Couronnes
*/
// Empêcher l'accès direct à ce fichier
if (!defined('SECURE_ACCESS')) {
header('HTTP/1.0 403 Forbidden');
exit;
}
// Configuration de base
define('APP_NAME', 'MH Wilds - Partage de Quêtes à Couronnes');
define('APP_VERSION', '1.0.0');
define('BASE_URL', 'https://cila.camelia-studio.org/chasse-aux-couronnes'); // À modifier avec votre nom de domaine réel
define('ADMIN_EMAIL', 'contact.c.a@camelia-studio.org'); // À modifier avec votre email
// Configuration de la base de données
define('DB_PATH', __DIR__ . '/../data/mhwilds.db');
// Paramètres de session
ini_set('session.cookie_httponly', 1);
ini_set('session.use_only_cookies', 1);
ini_set('session.cookie_secure', 1); // Mettre à 1 si vous utilisez HTTPS
// Fuseau horaire
date_default_timezone_set('Europe/Paris'); // À modifier selon votre localisation
// Options diverses
define('QUESTS_EXPIRATION_DAYS', 7); // Nombre de jours avant qu'une quête soit considérée comme expirée
define('DEBUG_MODE', false); // Activer/désactiver le mode débogage
// Fonction pour générer un jeton CSRF
function generate_csrf_token() {
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
// Fonction pour vérifier un jeton CSRF
function verify_csrf_token($token) {
if (!isset($_SESSION['csrf_token']) || $token !== $_SESSION['csrf_token']) {
return false;
}
return true;
}

370
includes/database.php Normal file
View File

@ -0,0 +1,370 @@
<?php
/**
* Fonctions d'accès à la base de données pour MH Wilds - Partage de Quêtes à Couronnes
*/
// Empêcher l'accès direct à ce fichier
if (!defined('SECURE_ACCESS')) {
header('HTTP/1.0 403 Forbidden');
exit;
}
/**
* Obtenir une connexion à la base de données SQLite
*
* @return PDO Instance de connexion à la base de données
*/
function get_db_connection() {
static $db = null;
if ($db === null) {
try {
// Vérifier si le fichier de base de données existe
$db_exists = file_exists(DB_PATH);
// Créer une nouvelle connexion PDO à la base de données SQLite
$db = new PDO('sqlite:' . DB_PATH);
// Configurer PDO pour lever des exceptions en cas d'erreur
$db->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;
}

13
includes/footer.php Normal file
View File

@ -0,0 +1,13 @@
</main>
<footer class="bg-dark text-white py-3 mt-auto">
<div class="container text-center">
<p class="mb-0">Ce site est réalisé dans le cadre de la branche <a href="https://camelia-studio.org/branches/alt+tab/" target="_blank">Alt Tab</a> de l'association <a href="https://camelia-studio.org/" target="_blank">Camélia Studio</a>.</p>
<p class="mb-0 mt-1"><small>Images des monstres par Sui Yun - Site sous licence MIT, code source sur <a href="https://git.crystalyx.net/camelia-studio/Chasse_aux_couronnes" target="_blank">Gitea</a></small></p>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<?php if (isset($extra_js)) echo $extra_js; ?>
</body>
</html>

196
includes/functions.php Normal file
View File

@ -0,0 +1,196 @@
<?php
/**
* Fonctions utilitaires pour MH Wilds - Partage de Quêtes à Couronnes
*/
// Empêcher l'accès direct à ce fichier
if (!defined('SECURE_ACCESS')) {
header('HTTP/1.0 403 Forbidden');
exit;
}
/**
* Sécuriser une chaîne pour l'affichage
*
* @param string $string Chaîne à sécuriser
* @return string Chaîne sécurisée
*/
function secure_output($string) {
return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
}
/**
* Rediriger vers une URL
*
* @param string $url URL de destination
*/
function redirect($url) {
header("Location: $url");
exit;
}
/**
* Vérifier si l'utilisateur est connecté
*
* @return bool L'utilisateur est-il connecté
*/
function is_logged_in() {
return isset($_SESSION['admin_authenticated']) && $_SESSION['admin_authenticated'] === true;
}
/**
* Vérifier que l'utilisateur est connecté ou rediriger vers la page de connexion
*/
function require_login() {
if (!is_logged_in()) {
set_flash_message('error', 'Vous devez être connecté pour accéder à cette page.');
redirect('login.php');
}
}
/**
* Définir un message flash dans la session
*
* @param string $type Type de message ('success', 'error', 'info', 'warning')
* @param string $message Contenu du message
*/
function set_flash_message($type, $message) {
$_SESSION['flash_message'] = [
'type' => $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 "<div class='alert $class alert-dismissible fade show' role='alert'>";
echo secure_output($message);
echo "<button type='button' class='btn-close' data-bs-dismiss='alert' aria-label='Fermer'></button>";
echo "</div>";
}
}
/**
* 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);
}

59
includes/header.php Normal file
View File

@ -0,0 +1,59 @@
<?php
/**
* En-tête commun pour toutes les pages du site
*/
// Empêcher l'accès direct à ce fichier
if (!defined('SECURE_ACCESS')) {
header('HTTP/1.0 403 Forbidden');
exit;
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo isset($page_title) ? secure_output($page_title) : 'MH Wilds - Partage de Quêtes à Couronnes'; ?></title>
<!-- Métadonnées pour le partage -->
<meta property="og:title" content="<?php echo isset($page_title) ? secure_output($page_title) : 'MH Wilds - Partage de Quêtes à Couronnes'; ?>">
<meta property="og:description" content="<?php echo isset($page_description) ? secure_output($page_description) : 'Partagez vos quêtes d\'investigation avec couronnes pour Monster Hunter Wilds et complétez votre collection !'; ?>">
<meta property="og:image" content="<?php echo BASE_URL; ?>/assets/img/logo.png">
<meta property="og:url" content="<?php echo BASE_URL . $_SERVER['REQUEST_URI']; ?>">
<meta name="twitter:card" content="summary_large_image">
<!-- Favicon -->
<link rel="icon" href="<?php echo BASE_URL; ?>/assets/img/logo.png" type="image/png">
<!-- Bootstrap CSS with dark theme -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Notre CSS personnalisé -->
<link rel="stylesheet" href="<?php echo BASE_URL; ?>/assets/css/styles.css">
<?php if (isset($extra_css)) echo $extra_css; ?>
</head>
<body>
<header class="bg-dark text-white py-3">
<div class="container">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<img src="<?php echo BASE_URL; ?>/assets/img/logo.png" alt="Logo" height="40" class="me-2">
<h1 class="h2 mb-0"><?php echo isset($header_title) ? secure_output($header_title) : 'MH Wilds - Quêtes à Couronnes'; ?></h1>
</div>
<div>
<?php if (isset($is_admin_page) && $is_admin_page): ?>
<a href="<?php echo BASE_URL; ?>/index.php" class="btn btn-outline-light me-2">Retour au site</a>
<a href="<?php echo BASE_URL; ?>/admin/logout.php" class="btn btn-danger">Déconnexion</a>
<?php else: ?>
<a href="<?php echo BASE_URL; ?>/tutorial.php" class="btn btn-outline-light me-2">Tutoriel</a>
<a href="<?php echo BASE_URL; ?>/login.php" class="btn btn-outline-light">Admin</a>
<?php endif; ?>
</div>
</div>
</div>
</header>
<main class="container my-4">
<?php display_flash_message(); ?>

View File

@ -1,236 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MH Wilds - Partage de Quêtes à Couronnes</title>
<!-- Métadonnées pour le partage -->
<meta property="og:title" content="MH Wilds - Partage de Quêtes à Couronnes">
<meta property="og:description" content="Partagez vos quêtes d'investigation avec couronnes pour Monster Hunter Wilds et complétez votre collection !">
<meta property="og:image" content="img/logo.png">
<meta property="og:url" content="https://votresite.com">
<meta name="twitter:card" content="summary_large_image">
<!-- Favicon -->
<link rel="icon" href="img/logo.png" type="image/png">
<!-- Bootstrap CSS with dark theme -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Notre CSS personnalisé -->
<link rel="stylesheet" href="css/styles.css">
<!-- Styles spécifiques pour le sélecteur de monstre avec recherche -->
<style id="monster-search-styles">
/* 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;
}
</style>
</head>
<body>
<header class="bg-dark text-white py-3">
<div class="container">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<img src="img/logo.png" alt="Logo" height="40" class="me-2">
<h1 class="h2 mb-0">MH Wilds - Quêtes à Couronnes</h1>
</div>
<div>
<a href="tutorial.html" class="btn btn-outline-light me-2">Tutoriel</a>
<a href="login.html" class="btn btn-outline-light">Admin</a>
</div>
</div>
</div>
</header>
<main class="container my-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Liste des monstres</h2>
<button class="btn btn-primary" id="addQuestBtn">+ Ajouter ma quête</button>
</div>
<div class="row mb-4">
<div class="col-md-6 mx-auto">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-search">🔍</i></span>
<input type="text" class="form-control" id="monsterSearch" placeholder="Rechercher un monstre...">
<button class="btn btn-outline-light" type="button" id="clearSearchBtn">Effacer</button>
</div>
</div>
</div>
<div class="alert alert-info d-none" id="announcementArea">
<!-- Les annonces seront affichées ici -->
</div>
<div class="row g-4 mb-4" id="monsterList">
<!-- La liste des monstres sera générée dynamiquement ici -->
<div class="col-12 text-center py-5">
<div class="spinner-border" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
<p class="mt-2">Chargement des monstres...</p>
</div>
</div>
</main>
<footer class="bg-dark text-white py-3 mt-auto">
<div class="container text-center">
<p class="mb-0">Ce site est réalisé dans le cadre de la branche <a href="https://camelia-studio.org/branches/alt+tab/" target="_blank">Alt Tab</a> de l'association <a href="https://camelia-studio.org/" target="_blank">Camélia Studio</a>.</p>
<p class="mb-0 mt-1"><small>Images des monstres par Sui Yun - Site sous licence MIT, code source sur <a href="https://git.crystalyx.net/camelia-studio/Chasse_aux_couronnes" target="_blank">Gitea</a></small></p>
</div>
</footer>
<!-- Modale de consultation des quêtes -->
<div class="modal fade" id="questListModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Quêtes disponibles pour <span id="modalMonsterName"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<div class="btn-group" role="group">
<input type="radio" class="btn-check" name="crownFilter" id="filterAll" value="all" checked>
<label class="btn btn-outline-accent" for="filterAll">Toutes</label>
<input type="radio" class="btn-check" name="crownFilter" id="filterSmall" value="small">
<label class="btn btn-outline-accent" for="filterSmall">Petites couronnes</label>
<input type="radio" class="btn-check" name="crownFilter" id="filterLarge" value="large">
<label class="btn btn-outline-accent" for="filterLarge">Grandes couronnes</label>
</div>
</div>
<div id="questList">
<!-- La liste des quêtes sera générée dynamiquement ici -->
</div>
</div>
</div>
</div>
</div>
<!-- Modale d'ajout de quête -->
<div class="modal fade" id="addQuestModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Ajouter une quête</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
</div>
<div class="modal-body">
<form id="addQuestForm">
<div class="mb-3">
<label for="monsterSearchSelect" class="form-label">Monstre</label>
<div class="monster-search-container">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-search">🔍</i></span>
<input type="text" class="form-control" id="monsterSearchSelect"
placeholder="Rechercher et sélectionner un monstre..." autocomplete="off">
<input type="hidden" id="selectedMonsterId" name="selectedMonsterId" required>
</div>
<div class="monster-search-results d-none" id="monsterSearchResults">
<!-- Les résultats seront générés dynamiquement ici -->
</div>
</div>
<div class="invalid-feedback">Veuillez sélectionner un monstre</div>
</div>
<div class="mb-3">
<label class="form-label">Type de couronne</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="crownType" id="smallCrown" value="small" required>
<label class="form-check-label" for="smallCrown">
Petite couronne
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="crownType" id="largeCrown" value="large">
<label class="form-check-label" for="largeCrown">
Grande couronne
</label>
</div>
</div>
<div class="mb-3">
<label for="playerName" class="form-label">Votre pseudo</label>
<input type="text" class="form-control" id="playerName" required>
</div>
<div class="mb-3">
<label for="playerId" class="form-label">Votre ID en jeu</label>
<input type="text" class="form-control" id="playerId" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Ajouter la quête</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Modale de confirmation de suppression -->
<div class="modal fade" id="deleteQuestModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Supprimer la quête</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
</div>
<div class="modal-body">
<p>Êtes-vous sûr de vouloir supprimer cette quête ?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">Supprimer</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- Charger data.js avant main.js -->
<script src="js/data.js"></script>
<script src="js/main.js"></script>
</body>
</html>

261
index.php Normal file
View File

@ -0,0 +1,261 @@
<?php
/**
* Page d'accueil - MH Wilds - Partage de Quêtes à Couronnes
*/
// Démarrer la session
session_start();
// Définir une constante pour empêcher l'accès direct aux includes
define('SECURE_ACCESS', true);
// Inclure les fichiers nécessaires
require_once 'includes/config.php';
require_once 'includes/database.php';
require_once 'includes/functions.php';
// Titre de la page
$page_title = 'MH Wilds - Partage de Quêtes à Couronnes';
$page_description = 'Partagez vos quêtes d\'investigation avec couronnes pour Monster Hunter Wilds et complétez votre collection !';
// CSS supplémentaire pour la page d'accueil
$extra_css = <<<EOT
<style id="monster-search-styles">
/* 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;
}
</style>
EOT;
// Scripts JS supplémentaires
$extra_js = <<<EOT
<script src="assets/js/main.js"></script>
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';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>Liste des monstres</h2>
<button class="btn btn-primary" id="addQuestBtn">+ Ajouter ma quête</button>
</div>
<div class="row mb-4">
<div class="col-md-6 mx-auto">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-search">🔍</i></span>
<input type="text" class="form-control" id="monsterSearch" placeholder="Rechercher un monstre...">
<button class="btn btn-outline-light" type="button" id="clearSearchBtn">Effacer</button>
</div>
</div>
</div>
<?php if (!empty($active_announcements)): ?>
<div class="alert alert-info" id="announcementArea">
<?php foreach ($active_announcements as $announcement): ?>
<p><?php echo secure_output($announcement['text']); ?></p>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div class="row g-4 mb-4" id="monsterList">
<?php if (empty($monsters)): ?>
<div class="col-12">
<div class="empty-message">
<p>Aucun monstre disponible pour le moment.</p>
</div>
</div>
<?php else: ?>
<?php foreach ($monsters as $monster): ?>
<?php
$smallCrownCount = count_quests_for_monster($monster['id'], 'small');
$largeCrownCount = count_quests_for_monster($monster['id'], 'large');
?>
<div class="col-lg-3 col-md-4 col-sm-6">
<div class="card monster-card fade-in" data-monster-id="<?php echo $monster['id']; ?>">
<div class="card-img-container">
<img src="<?php echo secure_output($monster['image']); ?>" class="card-img-top" alt="<?php echo secure_output($monster['name']); ?>">
</div>
<div class="card-body">
<h5 class="card-title"><?php echo secure_output($monster['name']); ?></h5>
<div class="d-flex justify-content-between">
<div>
<span class="crown-badge small-crown" title="Petites couronnes">
<i class="bi bi-trophy-fill crown-icon">👑</i> <?php echo $smallCrownCount; ?>
</span>
<span class="crown-badge large-crown" title="Grandes couronnes">
<i class="bi bi-trophy-fill crown-icon">👑</i> <?php echo $largeCrownCount; ?>
</span>
</div>
<button class="btn btn-sm btn-outline-primary">Voir les quêtes</button>
</div>
</div>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<!-- Modale de consultation des quêtes -->
<div class="modal fade" id="questListModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Quêtes disponibles pour <span id="modalMonsterName"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<div class="btn-group" role="group">
<input type="radio" class="btn-check" name="crownFilter" id="filterAll" value="all" checked>
<label class="btn btn-outline-accent" for="filterAll">Toutes</label>
<input type="radio" class="btn-check" name="crownFilter" id="filterSmall" value="small">
<label class="btn btn-outline-accent" for="filterSmall">Petites couronnes</label>
<input type="radio" class="btn-check" name="crownFilter" id="filterLarge" value="large">
<label class="btn btn-outline-accent" for="filterLarge">Grandes couronnes</label>
</div>
</div>
<div id="questList">
<!-- La liste des quêtes sera générée dynamiquement en JS -->
<div class="spinner-border" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modale d'ajout de quête -->
<div class="modal fade" id="addQuestModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Ajouter une quête</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
</div>
<div class="modal-body">
<form id="addQuestForm">
<input type="hidden" name="csrf_token" value="<?php echo generate_csrf_token(); ?>">
<div class="mb-3">
<label for="monsterSearchSelect" class="form-label">Monstre</label>
<div class="monster-search-container">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-search">🔍</i></span>
<input type="text" class="form-control" id="monsterSearchSelect"
placeholder="Rechercher et sélectionner un monstre..." autocomplete="off">
<input type="hidden" id="selectedMonsterId" name="selectedMonsterId" required>
</div>
<div class="monster-search-results d-none" id="monsterSearchResults">
<!-- Les résultats seront générés dynamiquement en JS -->
</div>
</div>
<div class="invalid-feedback">Veuillez sélectionner un monstre</div>
</div>
<div class="mb-3">
<label class="form-label">Type de couronne</label>
<div class="form-check">
<input class="form-check-input" type="radio" name="crownType" id="smallCrown" value="small" required>
<label class="form-check-label" for="smallCrown">
Petite couronne
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="crownType" id="largeCrown" value="large">
<label class="form-check-label" for="largeCrown">
Grande couronne
</label>
</div>
</div>
<div class="mb-3">
<label for="playerName" class="form-label">Votre pseudo</label>
<input type="text" class="form-control" id="playerName" name="playerName" required>
</div>
<div class="mb-3">
<label for="playerId" class="form-label">Votre ID en jeu</label>
<input type="text" class="form-control" id="playerId" name="playerId" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Ajouter la quête</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Modale de confirmation de suppression -->
<div class="modal fade" id="deleteQuestModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Supprimer la quête</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
</div>
<div class="modal-body">
<p>Êtes-vous sûr de vouloir supprimer cette quête ?</p>
<form id="deleteQuestForm">
<input type="hidden" name="csrf_token" value="<?php echo generate_csrf_token(); ?>">
<input type="hidden" id="deleteQuestId" name="deleteQuestId">
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">Supprimer</button>
</div>
</div>
</div>
</div>
<?php
// Inclure le pied de page
include 'includes/footer.php';

398
initdb.php Normal file
View File

@ -0,0 +1,398 @@
<?php
/**
* Script d'initialisation de la base de données - MH Wilds - Partage de Quêtes à Couronnes
*/
// Activer l'affichage des erreurs pour le débogage
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
// Démarrer la session
session_start();
// Définir une constante pour empêcher l'accès direct aux includes
define('SECURE_ACCESS', true);
// Chemins absolus pour éviter les problèmes d'inclusion
$root_path = dirname(__FILE__);
// Inclure les fichiers nécessaires directement
require_once $root_path . '/includes/config.php';
require_once $root_path . '/includes/functions.php';
// Définir nos propres fonctions de base de données pour ce script
function get_db_connection() {
global $root_path;
$db_path = $root_path . '/data/mhwilds.db';
try {
// Créer le dossier data s'il n'existe pas
$data_dir = dirname($db_path);
if (!file_exists($data_dir)) {
mkdir($data_dir, 0755, true);
}
// Créer une nouvelle connexion PDO à la base de données SQLite
$db = new PDO('sqlite:' . $db_path);
// Configurer PDO pour lever des exceptions en cas d'erreur
$db->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 '<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Initialisation de la base de données - MH Wilds</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
background-color: #272420;
color: #f5f0e6;
}
.card {
background-color: #3a362f;
border: none;
}
.card-header {
background-color: #e0b968;
color: #1a1914;
}
.btn-primary {
background-color: #e0b968;
border-color: #e0b968;
color: #1a1914;
}
.btn-danger {
background-color: #e05e4e;
border-color: #e05e4e;
}
.btn-secondary {
background-color: #5a90b1;
border-color: #5a90b1;
}
</style>
</head>
<body>
<div class="container my-5">
<div class="card">
<div class="card-header">
<h1 class="h4">Initialisation de la base de données</h1>
</div>
<div class="card-body">
<div class="alert alert-warning">
<p><strong>Attention :</strong> Ce script va initialiser la base de données avec les 29 monstres de Monster Hunter Wilds.</p>
<p>Si une base de données existe déjà, les données existantes seront <strong>conservées</strong> mais les monstres manquants seront ajoutés.</p>
</div>
<h3>Liste des monstres à initialiser :</h3>
<ul>';
foreach ($monsters as $monster) {
echo '<li>' . htmlspecialchars($monster['name']) . '</li>';
}
echo '</ul>
<p>Ce script va également créer les tables nécessaires si elles n\'existent pas déjà.</p>
<div class="mt-4">
<a href="initdb.php?confirm=yes&key=' . $auth_key . '" class="btn btn-danger">Initialiser la base de données</a>
<a href="index.php" class="btn btn-secondary ms-2">Annuler</a>
</div>
</div>
</div>
</div>
</body>
</html>';
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 '<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Initialisation réussie - MH Wilds</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
background-color: #272420;
color: #f5f0e6;
}
.card {
background-color: #3a362f;
border: none;
}
.card-header {
background-color: #6bc46f;
color: #1a1914;
}
.btn-primary {
background-color: #e0b968;
border-color: #e0b968;
color: #1a1914;
}
.alert-success {
background-color: #6bc46f;
color: #1a1914;
border-color: #6bc46f;
}
</style>
</head>
<body>
<div class="container my-5">
<div class="card">
<div class="card-header">
<h1 class="h4">Initialisation réussie</h1>
</div>
<div class="card-body">
<div class="alert alert-success">
<p><strong>Succès :</strong> La base de données a été initialisée avec succès !</p>
</div>
<h3>Résultats :</h3>
<ul>
<li>' . $total_monsters . ' monstres ont été ajoutés (les monstres existants ont été ignorés)</li>';
if ($sample_data_added) {
echo '<li>Les données d\'exemple (quêtes et annonces) ont été ajoutées</li>';
} else {
echo '<li>Les données d\'exemple n\'ont pas été ajoutées car il existe déjà des données</li>';
}
echo '</ul>
<div class="mt-4">
<a href="index.php" class="btn btn-primary">Retour à l\'accueil</a>
</div>
</div>
</div>
</div>
</body>
</html>';
} 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 '<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Erreur d\'initialisation - MH Wilds</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
background-color: #272420;
color: #f5f0e6;
}
.card {
background-color: #3a362f;
border: none;
}
.card-header {
background-color: #e05e4e;
color: #1a1914;
}
.btn-secondary {
background-color: #5a90b1;
border-color: #5a90b1;
}
.alert-danger {
background-color: #e05e4e;
color: #1a1914;
border-color: #e05e4e;
}
pre {
background-color: #1a1914;
color: #f5f0e6;
padding: 10px;
border-radius: 5px;
overflow: auto;
}
</style>
</head>
<body>
<div class="container my-5">
<div class="card">
<div class="card-header">
<h1 class="h4">Erreur d\'initialisation</h1>
</div>
<div class="card-body">
<div class="alert alert-danger">
<p><strong>Erreur :</strong> Une erreur s\'est produite lors de l\'initialisation de la base de données.</p>
<p>Veuillez vérifier que le dossier "data" est accessible en écriture et que SQLite est activé sur votre serveur.</p>
</div>
<h3>Détails de l\'erreur :</h3>
<pre>' . htmlspecialchars($e->getMessage()) . '</pre>
<div class="mt-4">
<a href="index.php" class="btn btn-secondary">Retour à l\'accueil</a>
</div>
</div>
</div>
</div>
</body>
</html>';
}

View File

@ -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
? '<span class="badge bg-success">Active</span>'
: '<span class="badge bg-secondary">Inactive</span>';
return `
<tr>
<td>${announcement.text}</td>
<td>${statusBadge}</td>
<td>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary edit-announcement-btn" data-id="${announcement.id}">
Éditer
</button>
<button class="btn btn-outline-danger delete-announcement-btn" data-id="${announcement.id}">
Supprimer
</button>
</div>
</td>
</tr>
`;
}).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 `
<tr>
<td>
<img src="${monster.image}" alt="${monster.name}" class="img-thumbnail" width="80">
</td>
<td>${monster.name}</td>
<td>${questCount}</td>
<td>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary edit-monster-btn" data-id="${monster.id}">
Éditer
</button>
<button class="btn btn-outline-danger delete-monster-btn" data-id="${monster.id}">
Supprimer
</button>
</div>
</td>
</tr>
`;
}).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;
}

View File

@ -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 }
]
};

View File

@ -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 = '';
}
});
});

View File

@ -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 = `
<div class="col-12">
<div class="alert alert-danger">
Erreur de chargement des données. Veuillez rafraîchir la page.
</div>
</div>
`;
}
}
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 => `<p>${a.text}</p>`).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 = `
<div class="col-12">
<div class="empty-message">
<p>Aucun monstre disponible pour le moment.</p>
</div>
</div>
`;
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 `
<div class="col-lg-3 col-md-4 col-sm-6">
<div class="card monster-card fade-in" data-monster-id="${monster.id}">
<div class="card-img-container">
<img src="${monster.image}" class="card-img-top" alt="${monster.name}">
</div>
<div class="card-body">
<h5 class="card-title">${monster.name}</h5>
<div class="d-flex justify-content-between">
<div>
<span class="crown-badge small-crown" title="Petites couronnes">
<i class="bi bi-trophy-fill crown-icon">👑</i> ${smallCrownCount}
</span>
<span class="crown-badge large-crown" title="Grandes couronnes">
<i class="bi bi-trophy-fill crown-icon">👑</i> ${largeCrownCount}
</span>
</div>
<button class="btn btn-sm btn-outline-primary">Voir les quêtes</button>
</div>
</div>
</div>
</div>
`;
}).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 = `
<div class="col-12">
<div class="empty-message">
<p>Aucun monstre ne correspond à votre recherche "${monsterSearchEl.value}".</p>
</div>
</div>
`;
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 = `
<div class="empty-message">
<p>Aucune quête disponible pour ce monstre avec ce filtre.</p>
</div>
`;
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 `
<div class="card quest-card mb-3 fade-in">
<div class="card-body">
<h5 class="card-title">
<span class="${crownClass}"><i class="bi bi-trophy-fill">👑</i></span>
${crownText} pour ${monster.name}
</h5>
<p class="card-text">
Proposée par: <strong>${quest.playerName}</strong> (ID: ${quest.playerId})
</p>
<p class="quest-date">Ajoutée: ${freshness}</p>
<button class="btn btn-outline-danger btn-sm quest-delete-btn"
data-quest-id="${quest.id}">
Supprimer
</button>
</div>
</div>
`;
}).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 = '<div class="monster-search-no-results">Aucun monstre trouvé</div>';
return;
}
container.innerHTML = results.map(monster =>
`<div class="monster-search-item" data-monster-id="${monster.id}">
${monster.name}
</div>`
).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;
}

View File

@ -1,76 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Connexion - Administration MH Wilds</title>
<!-- Métadonnées pour le partage -->
<meta property="og:title" content="Connexion - Administration MH Wilds">
<meta property="og:description" content="Page de connexion pour l'administration du site de partage de quêtes à couronnes.">
<meta property="og:image" content="img/logo.png">
<meta property="og:url" content="https://votresite.com/login.html">
<meta name="twitter:card" content="summary_large_image">
<!-- Favicon -->
<link rel="icon" href="img/logo.png" type="image/png">
<!-- Bootstrap CSS with dark theme -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Notre CSS personnalisé -->
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<header class="bg-dark text-white py-3">
<div class="container">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<img src="img/logo.png" alt="Logo" height="40" class="me-2">
<h1 class="h2 mb-0">MH Wilds - Administration</h1>
</div>
<a href="index.html" class="btn btn-outline-light">Retour au site</a>
</div>
</div>
</header>
<main class="container my-5">
<div class="row justify-content-center">
<div class="col-md-6 col-lg-5">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h2 class="h4 mb-0">Connexion à l'administration</h2>
</div>
<div class="card-body">
<div id="loginError" class="alert alert-danger d-none" role="alert">
Identifiants incorrects. Veuillez réessayer.
</div>
<form id="loginForm">
<div class="mb-3">
<label for="username" class="form-label">Nom d'utilisateur</label>
<input type="text" class="form-control" id="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Mot de passe</label>
<input type="password" class="form-control" id="password" required>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">Se connecter</button>
</div>
</form>
</div>
</div>
</div>
</div>
</main>
<footer class="bg-dark text-white py-3 mt-auto">
<div class="container text-center">
<p class="mb-0">Ce site est réalisé dans le cadre de la branche <a href="https://camelia-studio.org/branches/alt+tab/" target="_blank">Alt Tab</a> de l'association <a href="https://camelia-studio.org/" target="_blank">Camélia Studio</a>.</p>
<p class="mb-0 mt-1"><small>Images des monstres par Sui Yun - Site sous licence MIT, code source sur <a href="https://git.crystalyx.net/camelia-studio/Chasse_aux_couronnes" target="_blank">Gitea</a></small></p>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="js/login.js"></script>
</body>
</html>

87
login.php Normal file
View File

@ -0,0 +1,87 @@
<?php
/**
* Page de connexion - MH Wilds - Partage de Quêtes à Couronnes
*/
// Démarrer la session
session_start();
// Définir une constante pour empêcher l'accès direct aux includes
define('SECURE_ACCESS', true);
// Inclure les fichiers nécessaires
require_once 'includes/config.php';
require_once 'includes/database.php';
require_once 'includes/functions.php';
// Vérifier si l'utilisateur est déjà connecté
if (is_logged_in()) {
redirect('admin/index.php');
}
// Titre de la page
$page_title = 'Connexion - Administration MH Wilds';
$page_description = 'Page de connexion pour l\'administration du site de partage de quêtes à couronnes.';
$header_title = 'MH Wilds - Administration';
// Traiter la soumission du formulaire
$error = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Vérifier le jeton CSRF
if (!isset($_POST['csrf_token']) || !verify_csrf_token($_POST['csrf_token'])) {
$error = 'Jeton de sécurité invalide. Veuillez réessayer.';
} else {
$username = isset($_POST['username']) ? trim($_POST['username']) : '';
$password = isset($_POST['password']) ? $_POST['password'] : '';
// Vérifier les identifiants
if (check_login($username, $password)) {
// Authentification réussie
$_SESSION['admin_authenticated'] = true;
redirect('admin/index.php');
} else {
// Identifiants incorrects
$error = 'Identifiants incorrects. Veuillez réessayer.';
}
}
}
// Inclure l'en-tête
include 'includes/header.php';
?>
<div class="row justify-content-center my-5">
<div class="col-md-6 col-lg-5">
<div class="card shadow">
<div class="card-header bg-primary text-white">
<h2 class="h4 mb-0">Connexion à l'administration</h2>
</div>
<div class="card-body">
<?php if ($error): ?>
<div class="alert alert-danger" role="alert">
<?php echo secure_output($error); ?>
</div>
<?php endif; ?>
<form method="post" action="login.php" id="loginForm">
<input type="hidden" name="csrf_token" value="<?php echo generate_csrf_token(); ?>">
<div class="mb-3">
<label for="username" class="form-label">Nom d'utilisateur</label>
<input type="text" class="form-control" id="username" name="username" required>
</div>
<div class="mb-3">
<label for="password" class="form-label">Mot de passe</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">Se connecter</button>
</div>
</form>
</div>
</div>
</div>
</div>
<?php
// Inclure le pied de page
include 'includes/footer.php';

View File

@ -1,99 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tutoriel - Système de Couronnes | MH Wilds</title>
<!-- Métadonnées pour le partage -->
<meta property="og:title" content="Tutoriel - Système de Couronnes | MH Wilds">
<meta property="og:description" content="Découvrez comment fonctionne le système de couronnes dans Monster Hunter Wilds et comment partager vos quêtes.">
<meta property="og:image" content="img/logo.png">
<meta property="og:url" content="https://votresite.com/tutorial.html">
<meta name="twitter:card" content="summary_large_image">
<!-- Favicon -->
<link rel="icon" href="img/logo.png" type="image/png">
<!-- Bootstrap CSS with dark theme -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Notre CSS personnalisé -->
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<header class="bg-dark text-white py-3">
<div class="container">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<img src="img/logo.png" alt="Logo" height="40" class="me-2">
<h1 class="h2 mb-0">MH Wilds - Quêtes à Couronnes</h1>
</div>
<a href="index.html" class="btn btn-outline-light">Retour à l'accueil</a>
</div>
</div>
</header>
<main class="container my-4">
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="card">
<div class="card-header bg-primary text-white">
<h2 class="h4 mb-0">Tutoriel : Le système de couronnes dans Monster Hunter Wilds</h2>
</div>
<div class="card-body tutorial-content">
<h3>Qu'est-ce que les couronnes ?</h3>
<p>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.</p>
<h4>Types de couronnes :</h4>
<ul>
<li><strong>Petite couronne</strong> (👑) : Attribuée lorsque vous chassez un monstre de taille minimale.</li>
<li><strong>Grande couronne</strong> (👑) : Attribuée lorsque vous chassez un monstre de taille maximale.</li>
</ul>
<p>Collectionner ces couronnes est un objectif apprécié des chasseurs qui veulent compléter leur bestiaire à 100%.</p>
<hr>
<h3>Quêtes d'investigation avec couronnes</h3>
<p>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.</p>
<p>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.</p>
<h4>Comment fonctionnent les quêtes d'investigation ?</h4>
<p>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 !</p>
<hr>
<h3>Avertissement</h3>
<p>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 !</p>
<h4>Fonctionnalités :</h4>
<ul>
<li><strong>Consulter les quêtes</strong> : Cliquez sur un monstre pour voir les quêtes partagées par d'autres joueurs.</li>
<li><strong>Filtrer par type de couronne</strong> : Vous pouvez filtrer les quêtes par petite ou grande couronne selon ce que vous recherchez.</li>
<li><strong>Partager votre quête</strong> : 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".</li>
<li><strong>Supprimer votre quête</strong> : Une fois que votre quête n'est plus disponible (expirée ou nombre de tentatives épuisé), n'oubliez pas de la supprimer.</li>
</ul>
<div class="alert alert-info mt-4">
<p class="mb-0"><strong>Note</strong> : Les quêtes partagées sur ce site sont automatiquement supprimées après 7 jours.</p>
</div>
</div>
</div>
<div class="text-center mt-4">
<a href="index.html" class="btn btn-primary">Retourner à la liste des monstres</a>
</div>
</div>
</div>
</main>
<footer class="bg-dark text-white py-3 mt-auto">
<div class="container text-center">
<p class="mb-0">Ce site est réalisé dans le cadre de la branche <a href="https://camelia-studio.org/branches/alt+tab/" target="_blank">Alt Tab</a> de l'association <a href="https://camelia-studio.org/" target="_blank">Camélia Studio</a>.</p>
<p class="mb-0 mt-1"><small>Images des monstres par Sui Yun - Site sous licence MIT, code source sur <a href="https://git.crystalyx.net/camelia-studio/Chasse_aux_couronnes" target="_blank">Gitea</a></small></p>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

79
tutorial.php Normal file
View File

@ -0,0 +1,79 @@
<?php
/**
* Page tutoriel - MH Wilds - Partage de Quêtes à Couronnes
*/
// Démarrer la session
session_start();
// Définir une constante pour empêcher l'accès direct aux includes
define('SECURE_ACCESS', true);
// Inclure les fichiers nécessaires
require_once 'includes/config.php';
require_once 'includes/database.php';
require_once 'includes/functions.php';
// Titre de la page
$page_title = 'Tutoriel - Système de Couronnes | MH Wilds';
$page_description = 'Découvrez comment fonctionne le système de couronnes dans Monster Hunter Wilds et comment partager vos quêtes.';
// Inclure l'en-tête
include 'includes/header.php';
?>
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="card">
<div class="card-header bg-primary text-white">
<h2 class="h4 mb-0">Tutoriel : Le système de couronnes dans Monster Hunter Wilds</h2>
</div>
<div class="card-body tutorial-content">
<h3>Qu'est-ce que les couronnes ?</h3>
<p>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.</p>
<h4>Types de couronnes :</h4>
<ul>
<li><strong>Petite couronne</strong> (👑) : Attribuée lorsque vous chassez un monstre de taille minimale.</li>
<li><strong>Grande couronne</strong> (👑) : Attribuée lorsque vous chassez un monstre de taille maximale.</li>
</ul>
<p>Collectionner ces couronnes est un objectif apprécié des chasseurs qui veulent compléter leur bestiaire à 100%.</p>
<hr>
<h3>Quêtes d'investigation avec couronnes</h3>
<p>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.</p>
<p>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.</p>
<h4>Comment fonctionnent les quêtes d'investigation ?</h4>
<p> 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 !</p>
<hr>
<h3>Avertissement</h3>
<p>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 !</p>
<h4>Fonctionnalités :</h4>
<ul>
<li><strong>Consulter les quêtes</strong> : Cliquez sur un monstre pour voir les quêtes partagées par d'autres joueurs.</li>
<li><strong>Filtrer par type de couronne</strong> : Vous pouvez filtrer les quêtes par petite ou grande couronne selon ce que vous recherchez.</li>
<li><strong>Partager votre quête</strong> : 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".</li>
<li><strong>Supprimer votre quête</strong> : Une fois que votre quête n'est plus disponible (expirée ou nombre de tentatives épuisé), n'oubliez pas de la supprimer.</li>
</ul>
<div class="alert alert-info mt-4">
<p class="mb-0"><strong>Note</strong> : Les quêtes partagées sur ce site sont automatiquement supprimées après 7 jours.</p>
</div>
</div>
</div>
<div class="text-center mt-4">
<a href="index.php" class="btn btn-primary">Retourner à la liste des monstres</a>
</div>
</div>
</div>
<?php
// Inclure le pied de page
include 'includes/footer.php';