initialisation de la v2 du projet
migration sur une solution php
66
.htaccess
Normal 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>
|
9
LICENSE
@ -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.
|
@ -1,3 +0,0 @@
|
||||
# Chasse_aux_couronnes
|
||||
|
||||
Site pour partager ses quêtes d'investigations afin de faire profiter aux copains de nos couronnes !
|
244
admin.html
@ -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
@ -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
@ -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
@ -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
@ -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;
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 109 KiB |
Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 107 KiB |
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 102 KiB |
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 109 KiB |
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 109 KiB |
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 95 KiB |
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 116 KiB |
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 105 KiB |
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB |
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 110 KiB |
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 111 KiB |
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 104 KiB |
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 112 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
502
assets/js/admin.js
Normal 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.');
|
||||
});
|
||||
}
|
395
css/styles.css
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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(); ?>
|
236
index.html
@ -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
@ -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
@ -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>';
|
||||
}
|
487
js/admin.js
@ -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;
|
||||
}
|
52
js/data.js
@ -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 }
|
||||
]
|
||||
};
|
38
js/login.js
@ -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 = '';
|
||||
}
|
||||
});
|
||||
});
|
542
js/main.js
@ -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;
|
||||
}
|
76
login.html
@ -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
@ -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';
|
@ -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
@ -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>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.php" class="btn btn-primary">Retourner à la liste des monstres</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// Inclure le pied de page
|
||||
include 'includes/footer.php';
|