Suppression easteregg + fix captcha
This commit is contained in:
+12
-9
@@ -180,16 +180,19 @@ $spamSubjects = [
|
||||
['text' => 'CONFIRMEZ VOTRE IDENTITÉ POUR RECEVOIR VOTRE ARGENT', 'reason' => 'Promesse d’argent contre des données personnelles'],
|
||||
];
|
||||
|
||||
// Liste des expéditeurs spam
|
||||
// Liste des expéditeurs spam : mêmes formats d'affichage (nom, pas d'adresse brute) que les
|
||||
// expéditeurs normaux ci-dessus, pour qu'il ne soit pas possible de repérer les mails
|
||||
// frauduleux d'un simple coup d'œil sur l'expéditeur. La seule façon de les distinguer doit
|
||||
// être de lire le contenu (sujet), comme un vrai email de phishing usurpant une identité connue.
|
||||
$spamSenders = [
|
||||
'service-urgent@compte-bloque-now.biz',
|
||||
'cadeau-gratuit@super-offre-win.click',
|
||||
'banque-alerte@verification-securite.top',
|
||||
'colis-stop@paiement-obligatoire.help',
|
||||
'admin-compte@action-immediate.world',
|
||||
'support-prime@argent-rapide-fast.support',
|
||||
'verification@motdepasse-danger.win',
|
||||
'alerte@compte-fermeture.click'
|
||||
'Sécurité du compte',
|
||||
'Service client',
|
||||
'Banque familiale',
|
||||
'Livraison Colis',
|
||||
'Support technique',
|
||||
'Service facturation',
|
||||
'Assurance Habitation',
|
||||
'Mutuelle Santé'
|
||||
];
|
||||
|
||||
// Liste des aperçus normaux
|
||||
|
||||
@@ -62,7 +62,7 @@ CREATE TABLE IF NOT EXISTS schools (
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
-- Réglages du site (clé / valeur), utilisés pour la notification de la page d'accueil
|
||||
-- Réglages du site (clé / valeur), utilisés pour la notification de la page d'accueil et le captcha
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
name VARCHAR(50) NOT NULL,
|
||||
value TEXT NULL,
|
||||
@@ -72,7 +72,8 @@ CREATE TABLE IF NOT EXISTS settings (
|
||||
INSERT INTO settings (name, value) VALUES
|
||||
('site_notice_title', 'Des bugs peuvent encore se produire.'),
|
||||
('site_notice_text', 'Si le CSS ou les images s''affichent mal, pensez à vider le cache de votre navigateur (Ctrl + F5 ou Cmd + Shift + R) pour voir les dernières modifications.'),
|
||||
('site_notice_enabled', '1')
|
||||
('site_notice_enabled', '1'),
|
||||
('captcha_enabled', '1')
|
||||
ON DUPLICATE KEY UPDATE value = VALUES(value);
|
||||
|
||||
-- items : un item par ligne, au format "classes-icone|Texte" (icône optionnelle)
|
||||
@@ -134,30 +135,3 @@ CREATE TABLE IF NOT EXISTS school_pdfs (
|
||||
PRIMARY KEY (school_id),
|
||||
CONSTRAINT fk_school_pdfs_school FOREIGN KEY (school_id) REFERENCES schools(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Jeu "Presse-Citron" (easter egg, code Konami) : comptes séparés des comptes admin ci-dessus.
|
||||
-- game_state (JSON) stocke bâtiments/améliorations possédés ; toute la logique du jeu vit côté JS.
|
||||
CREATE TABLE IF NOT EXISTS lemon_players (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
username VARCHAR(50) NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
score DOUBLE NOT NULL DEFAULT 0,
|
||||
lifetime_score DOUBLE NOT NULL DEFAULT 0,
|
||||
prestige_level INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
prestige_bonus DOUBLE NOT NULL DEFAULT 0,
|
||||
game_state LONGTEXT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
last_played DATETIME NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX idx_lemon_username (username)
|
||||
);
|
||||
|
||||
-- Photo de profil des joueurs "Presse-Citron", table à part pour la même raison que les PDF
|
||||
CREATE TABLE IF NOT EXISTS lemon_avatars (
|
||||
player_id INT UNSIGNED NOT NULL,
|
||||
avatar_data LONGBLOB NOT NULL,
|
||||
avatar_mime VARCHAR(50) NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (player_id),
|
||||
CONSTRAINT fk_lemon_avatars_player FOREIGN KEY (player_id) REFERENCES lemon_players(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
@@ -74,125 +74,5 @@ if (isset($_GET['success']) && $_GET['success'] === '1') {
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Easter egg : code Konami (↑↑↓↓←→←→BA) => ouvre le mini-jeu "Presse-Citron" -->
|
||||
<style>
|
||||
#konami-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.72);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3vh 3vw;
|
||||
animation: konami-fade-in 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes konami-fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.konami-modal {
|
||||
position: relative;
|
||||
width: min(1100px, 100%);
|
||||
height: min(760px, 94vh);
|
||||
border-radius: 18px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
.konami-modal iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.konami-close {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.konami-close:hover {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
// Séquence classique : Haut Haut Bas Bas Gauche Droite Gauche Droite B A
|
||||
var sequence = ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight', 'b', 'a'];
|
||||
var progres = 0;
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
// Ignore la saisie dans un champ de formulaire (évite de déclencher pendant qu'on tape)
|
||||
var cible = e.target;
|
||||
if (cible && (cible.tagName === 'INPUT' || cible.tagName === 'TEXTAREA' || cible.isContentEditable)) return;
|
||||
|
||||
var touche = e.key.length === 1 ? e.key.toLowerCase() : e.key;
|
||||
var attendue = sequence[progres];
|
||||
|
||||
if (touche === attendue) {
|
||||
progres++;
|
||||
if (progres === sequence.length) {
|
||||
progres = 0;
|
||||
ouvrirPresseCitron();
|
||||
}
|
||||
} else {
|
||||
// Permet de relancer la séquence même si elle recommence par la même touche
|
||||
progres = (touche === sequence[0]) ? 1 : 0;
|
||||
}
|
||||
});
|
||||
|
||||
function ouvrirPresseCitron() {
|
||||
if (document.getElementById('konami-overlay')) return;
|
||||
|
||||
var overlay = document.createElement('div');
|
||||
overlay.id = 'konami-overlay';
|
||||
overlay.innerHTML =
|
||||
'<div class="konami-modal">' +
|
||||
'<button class="konami-close" aria-label="Fermer"><i class="fa-solid fa-xmark"></i></button>' +
|
||||
'<iframe src="./../pages/citron.php" title="Presse-Citron"></iframe>' +
|
||||
'</div>';
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
overlay.querySelector('.konami-close').addEventListener('click', fermerPresseCitron);
|
||||
overlay.addEventListener('click', function (e) {
|
||||
if (e.target === overlay) fermerPresseCitron();
|
||||
});
|
||||
document.addEventListener('keydown', echapFerme);
|
||||
}
|
||||
|
||||
function echapFerme(e) {
|
||||
if (e.key === 'Escape') fermerPresseCitron();
|
||||
}
|
||||
|
||||
function fermerPresseCitron() {
|
||||
var overlay = document.getElementById('konami-overlay');
|
||||
if (overlay) overlay.remove();
|
||||
document.removeEventListener('keydown', echapFerme);
|
||||
}
|
||||
|
||||
// Le jeu (dans l'iframe) peut aussi demander la fermeture (touche Échap captée à
|
||||
// l'intérieur de l'iframe, qui ne reçoit pas les événements clavier de la page parente)
|
||||
window.addEventListener('message', function (e) {
|
||||
if (e.data === 'citron-close') fermerPresseCitron();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,66 +0,0 @@
|
||||
<?php
|
||||
|
||||
// Fonction utilitaire de validation/lecture d'une photo de profil envoyée par formulaire
|
||||
// (jeu "Presse-Citron"). Même esprit que functions/pdfUpload.php.
|
||||
|
||||
// Taille max acceptée côté serveur : généreuse, mais le client redimensionne déjà l'image
|
||||
// avant l'envoi (voir pages/citron.php), donc en pratique un envoi normal fait quelques
|
||||
// dizaines de Ko. Cette limite est un filet de sécurité si le redimensionnement côté
|
||||
// navigateur est contourné (requête forgée, JS désactivé, etc.)
|
||||
define('AVATAR_MAX_SIZE', 3 * 1024 * 1024); // 3 Mo
|
||||
|
||||
// Dimensions max acceptées (en pixels) : évite qu'une image immense (mais légère une fois
|
||||
// compressée) fasse exploser le temps de décodage côté navigateur pour les autres joueurs
|
||||
define('AVATAR_MAX_DIMENSION', 4000);
|
||||
|
||||
// Types d'image acceptés (liste blanche : pas de SVG, qui peut contenir du JavaScript)
|
||||
const AVATAR_MIME_AUTORISES = ['image/jpeg', 'image/png', 'image/webp'];
|
||||
|
||||
// Vérifie et lit une image de profil envoyée en upload ($_FILES)
|
||||
// Retourne :
|
||||
// - null si aucun fichier n'a été envoyé (ce n'est pas une erreur)
|
||||
// - une chaîne de texte si le fichier est invalide (message d'erreur à afficher)
|
||||
// - ['mime' => ..., 'data' => ...] si l'image est valide
|
||||
function lireUploadAvatar($champ = 'avatar') {
|
||||
if (!isset($_FILES[$champ]) || $_FILES[$champ]['error'] === UPLOAD_ERR_NO_FILE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($_FILES[$champ]['error'] !== UPLOAD_ERR_OK) {
|
||||
return "Erreur lors de l'envoi de l'image (code " . $_FILES[$champ]['error'] . ").";
|
||||
}
|
||||
|
||||
if ($_FILES[$champ]['size'] > AVATAR_MAX_SIZE) {
|
||||
return 'La photo dépasse la taille maximale autorisée (3 Mo).';
|
||||
}
|
||||
|
||||
// getimagesize() décode réellement l'en-tête de l'image (contrairement à une simple
|
||||
// vérification d'extension) : un fichier renommé en .jpg mais qui n'est pas une vraie
|
||||
// image est rejeté ici, sans avoir besoin de l'extension GD
|
||||
$infos = @getimagesize($_FILES[$champ]['tmp_name']);
|
||||
if ($infos === false) {
|
||||
return "Le fichier envoyé n'est pas une image valide.";
|
||||
}
|
||||
|
||||
$largeur = $infos[0];
|
||||
$hauteur = $infos[1];
|
||||
$mime = $infos['mime'];
|
||||
|
||||
if (!in_array($mime, AVATAR_MIME_AUTORISES, true)) {
|
||||
return 'Format non supporté : utilise une image JPEG, PNG ou WebP.';
|
||||
}
|
||||
|
||||
if ($largeur > AVATAR_MAX_DIMENSION || $hauteur > AVATAR_MAX_DIMENSION) {
|
||||
return 'Image trop grande (maximum ' . AVATAR_MAX_DIMENSION . ' pixels de côté).';
|
||||
}
|
||||
|
||||
$contenu = file_get_contents($_FILES[$champ]['tmp_name']);
|
||||
if ($contenu === false) {
|
||||
return "Impossible de lire le fichier envoyé.";
|
||||
}
|
||||
|
||||
return [
|
||||
'mime' => $mime,
|
||||
'data' => $contenu
|
||||
];
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
// Session du jeu "Presse-Citron" (easter egg code Konami) : totalement indépendante
|
||||
// des comptes admin du portfolio. Utilise des clés de session différentes
|
||||
// ($_SESSION['lemon_player_id'] / ['lemon_username']), jamais $_SESSION['role'] ou
|
||||
// ['user_id'] : se connecter au jeu ne donne et ne retire aucun droit sur le portfolio.
|
||||
|
||||
// Vérifie qu'un joueur est connecté au jeu, sinon répond en JSON et arrête le script
|
||||
function requireLemonAuth() {
|
||||
if (empty($_SESSION['lemon_player_id'])) {
|
||||
http_response_code(401);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['error' => 'Non connecté.']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,9 @@ $noticeTitle = recupererSetting('site_notice_title') ?? '';
|
||||
$noticeText = recupererSetting('site_notice_text') ?? '';
|
||||
$noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
||||
|
||||
// Valeur actuelle du captcha anti-robot (activé par défaut)
|
||||
$captchaEnabled = recupererSetting('captcha_enabled') ?? '1';
|
||||
|
||||
// Page du panneau d'administration
|
||||
?>
|
||||
|
||||
@@ -71,6 +74,7 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
||||
<a href="#admin-competences"><i class="fa-solid fa-code"></i> Compétences</a>
|
||||
<a href="#admin-logos"><i class="fa-solid fa-images"></i> Logos</a>
|
||||
<a href="#admin-notification"><i class="fa-solid fa-bell"></i> Notification</a>
|
||||
<a href="#admin-securite"><i class="fa-solid fa-shield-halved"></i> Sécurité</a>
|
||||
<a href="#admin-utilisateurs"><i class="fa-solid fa-users"></i> Utilisateurs</a>
|
||||
<a href="#admin-donnees"><i class="fa-solid fa-database"></i> Données</a>
|
||||
</nav>
|
||||
@@ -382,6 +386,7 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
||||
<!-- on envoie les données à updateSettings.php -->
|
||||
<form method="POST" action="../scripts/updateSettings.php">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||
<input type="hidden" name="settings_group" value="notice">
|
||||
|
||||
<label>Titre (affiché en gras)</label>
|
||||
<input type="text" name="notice_title" value="<?php echo htmlspecialchars($noticeTitle); ?>">
|
||||
@@ -402,6 +407,30 @@ $noticeEnabled = recupererSetting('site_notice_enabled') ?? '1';
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ========== SECTION SÉCURITÉ ========== -->
|
||||
<section id="admin-securite" class="admin-section">
|
||||
<h2 class="admin-section-title"><i class="fa-solid fa-shield-halved"></i> Sécurité</h2>
|
||||
|
||||
<div class="project-container">
|
||||
<div class="project-form-card">
|
||||
<!-- on envoie les données à updateSettings.php -->
|
||||
<form method="POST" action="../scripts/updateSettings.php">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars(csrfToken()); ?>">
|
||||
<input type="hidden" name="settings_group" value="captcha">
|
||||
|
||||
<!-- Checkbox pour activer / désactiver le test anti-robot à l'entrée du site -->
|
||||
<label class="checkbox-row">
|
||||
<input type="checkbox" name="captcha_enabled" value="1" <?php echo $captchaEnabled === '1' ? 'checked' : ''; ?>>
|
||||
Activer le test anti-robot (captcha) à l'entrée du site
|
||||
</label>
|
||||
|
||||
<input type="submit" name="submitsettings" value="Enregistrer" class="btn-submit">
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ========== SECTION UTILISATEURS ========== -->
|
||||
<section id="admin-utilisateurs" class="admin-section">
|
||||
<h2 class="admin-section-title"><i class="fa-solid fa-users"></i> Utilisateurs</h2>
|
||||
|
||||
-1092
File diff suppressed because it is too large
Load Diff
@@ -1,37 +0,0 @@
|
||||
<?php
|
||||
// Sert la photo de profil d'un joueur "Presse-Citron", stockée en base
|
||||
// (voir migration Juillet 2026 (8)). Public : pas besoin d'être connecté pour voir
|
||||
// les avatars affichés dans le classement.
|
||||
|
||||
require_once('../functions/databaseConnection.php');
|
||||
|
||||
$playerId = $_GET['player_id'] ?? '';
|
||||
|
||||
if (!is_numeric($playerId)) {
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $dbh->prepare('SELECT avatar_data, avatar_mime FROM lemon_avatars WHERE player_id = :id');
|
||||
$stmt->bindValue(':id', (int) $playerId, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!$row) {
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
header('Content-Type: ' . $row['avatar_mime']);
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('Content-Length: ' . strlen($row['avatar_data']));
|
||||
header('Content-Disposition: inline');
|
||||
// Les avatars changent rarement : cache navigateur d'une heure, comme les PDF (voir viewPdf.php)
|
||||
header('Cache-Control: public, max-age=3600');
|
||||
echo $row['avatar_data'];
|
||||
exit;
|
||||
@@ -1,29 +0,0 @@
|
||||
<?php
|
||||
// Suppression de la photo de profil du joueur "Presse-Citron" connecté (retour à
|
||||
// l'avatar par défaut : cercle avec l'initiale du pseudo).
|
||||
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
require_once('../functions/databaseConnection.php');
|
||||
require_once('../functions/lemonSession.php');
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'Méthode non autorisée.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
requireLemonAuth();
|
||||
requireCsrfToken();
|
||||
|
||||
try {
|
||||
$stmt = $dbh->prepare('DELETE FROM lemon_avatars WHERE player_id = :id');
|
||||
$stmt->bindValue(':id', (int) $_SESSION['lemon_player_id'], PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
|
||||
echo json_encode(['ok' => true]);
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Erreur serveur.']);
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
<?php
|
||||
// Upload / remplacement de la photo de profil du joueur "Presse-Citron" connecté.
|
||||
// Le fichier est déjà redimensionné et compressé côté navigateur (voir pages/citron.php),
|
||||
// mais la validation ici ne fait aucune confiance à ça : elle revérifie tout elle-même.
|
||||
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
require_once('../functions/databaseConnection.php');
|
||||
require_once('../functions/lemonSession.php');
|
||||
require_once('../functions/avatarUpload.php');
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'Méthode non autorisée.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
requireLemonAuth();
|
||||
requireCsrfToken();
|
||||
|
||||
$resultat = lireUploadAvatar('avatar');
|
||||
|
||||
if ($resultat === null) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Aucune image envoyée.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (is_string($resultat)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => $resultat]);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $dbh->prepare('
|
||||
INSERT INTO lemon_avatars (player_id, avatar_data, avatar_mime, updated_at)
|
||||
VALUES (:id, :data, :mime, NOW())
|
||||
ON DUPLICATE KEY UPDATE avatar_data = VALUES(avatar_data), avatar_mime = VALUES(avatar_mime), updated_at = NOW()
|
||||
');
|
||||
$stmt->bindValue(':id', (int) $_SESSION['lemon_player_id'], PDO::PARAM_INT);
|
||||
$stmt->bindValue(':data', $resultat['data'], PDO::PARAM_LOB);
|
||||
$stmt->bindValue(':mime', $resultat['mime'], PDO::PARAM_STR);
|
||||
$stmt->execute();
|
||||
|
||||
echo json_encode(['ok' => true]);
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Erreur serveur.']);
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
<?php
|
||||
// Classement public du jeu "Presse-Citron" (les 15 meilleurs scores cumulés), avec un
|
||||
// peu plus de détail que le simple score : nombre de bâtiments possédés (calculé depuis
|
||||
// game_state), niveau de prestige, date de dernière partie. Lecture seule, sans connexion.
|
||||
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
require_once('../functions/databaseConnection.php');
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
try {
|
||||
$stmt = $dbh->prepare('
|
||||
SELECT id, username, score, lifetime_score, prestige_level, prestige_bonus, game_state, last_played, created_at
|
||||
FROM lemon_players
|
||||
ORDER BY lifetime_score DESC
|
||||
LIMIT 15
|
||||
');
|
||||
$stmt->execute();
|
||||
$lignes = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
$lignes = [];
|
||||
}
|
||||
|
||||
// On calcule le nombre total de bâtiments possédés à partir du game_state (stocké en JSON) :
|
||||
// une info que le classement n'affichait pas jusqu'ici, et qui donne un vrai aperçu de
|
||||
// l'avancée d'un joueur au-delà du seul score
|
||||
$classement = array_map(function ($ligne) {
|
||||
$totalBatiments = 0;
|
||||
if (!empty($ligne['game_state'])) {
|
||||
$etat = json_decode($ligne['game_state'], true);
|
||||
if (is_array($etat) && !empty($etat['buildings']) && is_array($etat['buildings'])) {
|
||||
foreach ($etat['buildings'] as $possedes) {
|
||||
$totalBatiments += (int) $possedes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => (int) $ligne['id'],
|
||||
'username' => $ligne['username'],
|
||||
'score' => (float) $ligne['score'],
|
||||
'lifetime_score' => (float) $ligne['lifetime_score'],
|
||||
'prestige_level' => (int) $ligne['prestige_level'],
|
||||
'prestige_bonus' => (float) $ligne['prestige_bonus'],
|
||||
'total_buildings' => $totalBatiments,
|
||||
'last_played' => $ligne['last_played'],
|
||||
'created_at' => $ligne['created_at'],
|
||||
];
|
||||
}, $lignes);
|
||||
|
||||
echo json_encode($classement);
|
||||
@@ -1,61 +0,0 @@
|
||||
<?php
|
||||
// Connexion au jeu "Presse-Citron" (easter egg). Comptes totalement séparés
|
||||
// des comptes admin du portfolio (table lemon_players).
|
||||
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
require_once('../functions/databaseConnection.php');
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'Méthode non autorisée.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
requireCsrfToken();
|
||||
|
||||
$username = trim($_POST['username'] ?? '');
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
try {
|
||||
$stmt = $dbh->prepare('
|
||||
SELECT id, username, password, score, lifetime_score, prestige_level, prestige_bonus, game_state
|
||||
FROM lemon_players WHERE username = :username LIMIT 1
|
||||
');
|
||||
$stmt->execute([':username' => $username]);
|
||||
$player = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Erreur serveur.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!$player || !password_verify($password, $player['password'])) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => 'Nom de pressier ou mot de passe incorrect.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Régénère l'ID de session à la connexion : évite la fixation de session
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['lemon_player_id'] = (int) $player['id'];
|
||||
$_SESSION['lemon_username'] = $player['username'];
|
||||
|
||||
try {
|
||||
$stmt = $dbh->prepare('UPDATE lemon_players SET last_played = NOW() WHERE id = :id');
|
||||
$stmt->execute([':id' => $player['id']]);
|
||||
} catch (PDOException $e) {
|
||||
// Pas bloquant : la connexion reste valable même si cette mise à jour échoue
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'id' => (int) $player['id'],
|
||||
'username' => $player['username'],
|
||||
'score' => (float) $player['score'],
|
||||
'lifetime_score' => (float) $player['lifetime_score'],
|
||||
'prestige_level' => (int) $player['prestige_level'],
|
||||
'prestige_bonus' => (float) $player['prestige_bonus'],
|
||||
'game_state' => $player['game_state'],
|
||||
]);
|
||||
@@ -1,11 +0,0 @@
|
||||
<?php
|
||||
// Déconnexion du jeu "Presse-Citron" uniquement : ne touche jamais à une éventuelle
|
||||
// session admin du portfolio (clés de session complètement séparées)
|
||||
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
unset($_SESSION['lemon_player_id'], $_SESSION['lemon_username']);
|
||||
|
||||
echo json_encode(['ok' => true]);
|
||||
@@ -1,68 +0,0 @@
|
||||
<?php
|
||||
// Création d'un compte pour le jeu "Presse-Citron" (easter egg).
|
||||
// Comptes totalement séparés des comptes admin du portfolio (table lemon_players).
|
||||
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
require_once('../functions/databaseConnection.php');
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'Méthode non autorisée.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
requireCsrfToken();
|
||||
|
||||
$username = trim($_POST['username'] ?? '');
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
if ($username === '' || mb_strlen($username) > 50) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => "Nom de pressier invalide (1 à 50 caractères)."]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (mb_strlen($password) < 4) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Le mot de passe doit faire au moins 4 caractères.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
// On vérifie que le nom n'est pas déjà pris
|
||||
$stmt = $dbh->prepare('SELECT id FROM lemon_players WHERE username = :username LIMIT 1');
|
||||
$stmt->execute([':username' => $username]);
|
||||
if ($stmt->fetch()) {
|
||||
http_response_code(409);
|
||||
echo json_encode(['error' => 'Ce nom de pressier est déjà pris.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||
$stmt = $dbh->prepare('INSERT INTO lemon_players (username, password, created_at) VALUES (:username, :password, NOW())');
|
||||
$stmt->execute([':username' => $username, ':password' => $hash]);
|
||||
|
||||
$playerId = (int) $dbh->lastInsertId();
|
||||
|
||||
// Régénère l'ID de session à la création de compte (même logique que checkLogin.php
|
||||
// pour le portfolio : évite la fixation de session)
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['lemon_player_id'] = $playerId;
|
||||
$_SESSION['lemon_username'] = $username;
|
||||
|
||||
echo json_encode([
|
||||
'ok' => true,
|
||||
'id' => $playerId,
|
||||
'username' => $username,
|
||||
'score' => 0,
|
||||
'lifetime_score' => 0,
|
||||
'prestige_level' => 0,
|
||||
'prestige_bonus' => 0,
|
||||
'game_state' => null,
|
||||
]);
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Erreur serveur.']);
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
<?php
|
||||
// Sauvegarde de la partie en cours pour le joueur "Presse-Citron" connecté.
|
||||
// Toute la logique du jeu vit côté JS : ce script se contente de valider
|
||||
// grossièrement les données reçues (nombres positifs, JSON de taille raisonnable)
|
||||
// et de les enregistrer telles quelles pour le joueur de la session en cours.
|
||||
|
||||
require_once(__DIR__ . '/../functions/security.php');
|
||||
require_once('../functions/databaseConnection.php');
|
||||
require_once('../functions/lemonSession.php');
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'Méthode non autorisée.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
requireLemonAuth();
|
||||
requireCsrfToken();
|
||||
|
||||
$score = $_POST['score'] ?? null;
|
||||
$lifetimeScore = $_POST['lifetime_score'] ?? null;
|
||||
$prestigeLevel = $_POST['prestige_level'] ?? null;
|
||||
$prestigeBonus = $_POST['prestige_bonus'] ?? null;
|
||||
$gameState = $_POST['game_state'] ?? null;
|
||||
|
||||
// Validation basique : évite qu'une requête forgée n'enregistre n'importe quoi
|
||||
if (
|
||||
!is_numeric($score) || $score < 0 ||
|
||||
!is_numeric($lifetimeScore) || $lifetimeScore < 0 ||
|
||||
!is_numeric($prestigeLevel) || $prestigeLevel < 0 ||
|
||||
!is_numeric($prestigeBonus) || $prestigeBonus < 0
|
||||
) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Données de sauvegarde invalides.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// game_state : JSON de taille raisonnable (20 Ko largement suffisant pour la liste
|
||||
// des bâtiments/améliorations possédés), sinon on refuse plutôt que de tronquer
|
||||
if ($gameState !== null && (strlen($gameState) > 20000 || json_decode($gameState) === null)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'État de jeu invalide.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $dbh->prepare('
|
||||
UPDATE lemon_players
|
||||
SET score = :score, lifetime_score = :lifetime_score, prestige_level = :prestige_level,
|
||||
prestige_bonus = :prestige_bonus, game_state = :game_state, last_played = NOW()
|
||||
WHERE id = :id
|
||||
');
|
||||
$stmt->bindValue(':score', (float) $score);
|
||||
$stmt->bindValue(':lifetime_score', (float) $lifetimeScore);
|
||||
$stmt->bindValue(':prestige_level', (int) $prestigeLevel, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':prestige_bonus', (float) $prestigeBonus);
|
||||
$stmt->bindValue(':game_state', $gameState, $gameState === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
|
||||
$stmt->bindValue(':id', (int) $_SESSION['lemon_player_id'], PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
|
||||
echo json_encode(['ok' => true]);
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Erreur serveur.']);
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
<?php
|
||||
|
||||
if (!isset($_SESSION['captcha_valid']) || $_SESSION['captcha_valid'] !== true) {
|
||||
require_once(__DIR__ . '/../functions/listSettings.php');
|
||||
|
||||
// Le captcha peut être désactivé depuis le panel Admin (section Sécurité)
|
||||
$captchaEnabled = recupererSetting('captcha_enabled') ?? '1';
|
||||
|
||||
if ($captchaEnabled === '1' && (!isset($_SESSION['captcha_valid']) || $_SESSION['captcha_valid'] !== true)) {
|
||||
header('Location: ../captcha.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -23,11 +23,21 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
|
||||
// On récupère les données pour les mettres en variables locales
|
||||
// La checkbox n'est envoyée que si elle est cochée
|
||||
$reglages = [
|
||||
'site_notice_title' => trim($_POST['notice_title'] ?? ''),
|
||||
'site_notice_text' => trim($_POST['notice_text'] ?? ''),
|
||||
'site_notice_enabled' => isset($_POST['notice_enabled']) ? '1' : '0'
|
||||
];
|
||||
// settings_group : évite qu'un formulaire écrase les réglages d'un autre (ex : la case
|
||||
// "notice_enabled" non cochée ne doit pas désactiver le captcha, et inversement)
|
||||
$groupeReglages = $_POST['settings_group'] ?? 'notice';
|
||||
|
||||
if ($groupeReglages === 'captcha') {
|
||||
$reglages = [
|
||||
'captcha_enabled' => isset($_POST['captcha_enabled']) ? '1' : '0'
|
||||
];
|
||||
} else {
|
||||
$reglages = [
|
||||
'site_notice_title' => trim($_POST['notice_title'] ?? ''),
|
||||
'site_notice_text' => trim($_POST['notice_text'] ?? ''),
|
||||
'site_notice_enabled' => isset($_POST['notice_enabled']) ? '1' : '0'
|
||||
];
|
||||
}
|
||||
|
||||
try { // On essaie d'executer une requête qui est préparée pour chaque réglage
|
||||
// ON DUPLICATE KEY UPDATE : crée le réglage s'il n'existe pas encore, le met à jour sinon
|
||||
|
||||
@@ -1,897 +0,0 @@
|
||||
/* ---- Presse-Citron : easter egg Konami code (mini-jeu incrémental) ---- */
|
||||
|
||||
:root {
|
||||
--lemon-hue: 48; /* dérive lentement avec la progression (voir JS) */
|
||||
--lemon: hsl(var(--lemon-hue), 92%, 56%);
|
||||
--lemon-light: hsl(var(--lemon-hue), 100%, 74%);
|
||||
--lemon-pale: hsl(var(--lemon-hue), 100%, 88%);
|
||||
--lemon-dark: hsl(calc(var(--lemon-hue) - 6), 82%, 40%);
|
||||
--leaf: hsl(100, 45%, 42%);
|
||||
--bg: hsl(calc(var(--lemon-hue) + 8), 38%, 8%);
|
||||
--bg-soft: hsl(calc(var(--lemon-hue) + 8), 30%, 11%);
|
||||
--surface: hsl(calc(var(--lemon-hue) + 8), 24%, 14%);
|
||||
--surface-2: hsl(calc(var(--lemon-hue) + 8), 20%, 19%);
|
||||
--surface-3: hsl(calc(var(--lemon-hue) + 8), 18%, 24%);
|
||||
--border: hsl(calc(var(--lemon-hue) + 8), 18%, 26%);
|
||||
--accent-soft: hsla(var(--lemon-hue), 92%, 56%, 0.14);
|
||||
--text: #fbf8ef;
|
||||
--text-muted: #b9b3a2;
|
||||
--radius: 16px;
|
||||
--radius-sm: 11px;
|
||||
--shadow: 0 18px 44px rgba(0, 0, 0, 0.45);
|
||||
--font: 'Inter', system-ui, -apple-system, 'Segoe UI', Arial, sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font);
|
||||
background:
|
||||
radial-gradient(circle at 18% -10%, hsla(var(--lemon-hue), 80%, 45%, 0.25), transparent 45%),
|
||||
radial-gradient(circle at 85% 110%, hsla(calc(var(--lemon-hue) + 40), 70%, 40%, 0.18), transparent 50%),
|
||||
var(--bg);
|
||||
color: var(--text);
|
||||
overflow-x: hidden;
|
||||
transition: background 0.6s ease;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: var(--lemon);
|
||||
color: #2a1c00;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 9px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--surface-3);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.citron-shell {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ---- Le citron dessiné en CSS (aucun emoji) : logo, écran d'accueil, cible cliquable ---- */
|
||||
.lemon-mark {
|
||||
display: inline-block;
|
||||
width: 26px;
|
||||
height: 22px;
|
||||
border-radius: 52% 52% 46% 46% / 62% 62% 38% 38%;
|
||||
background: radial-gradient(circle at 34% 28%, var(--lemon-pale) 0%, var(--lemon) 48%, var(--lemon-dark) 100%);
|
||||
position: relative;
|
||||
box-shadow: inset -3px -3px 5px rgba(0, 0, 0, 0.2), inset 2px 2px 4px rgba(255, 255, 255, 0.35);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.lemon-mark-leaf {
|
||||
position: absolute;
|
||||
top: -7px;
|
||||
left: 50%;
|
||||
width: 10px;
|
||||
height: 15px;
|
||||
background: linear-gradient(135deg, #8bc34a, var(--leaf));
|
||||
border-radius: 0 100% 0 100%;
|
||||
transform: translateX(-50%) rotate(-18deg);
|
||||
box-shadow: 0 2px 3px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.lemon-mark-big {
|
||||
width: 68px;
|
||||
height: 58px;
|
||||
margin: 0 auto 10px;
|
||||
display: block;
|
||||
animation: citron-bob 2.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.lemon-mark-big .lemon-mark-leaf {
|
||||
width: 22px;
|
||||
height: 32px;
|
||||
top: -16px;
|
||||
}
|
||||
|
||||
@keyframes citron-bob {
|
||||
0%, 100% { transform: translateY(0) rotate(-4deg); }
|
||||
50% { transform: translateY(-8px) rotate(4deg); }
|
||||
}
|
||||
|
||||
/* ---- En-tête ---- */
|
||||
.citron-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 22px;
|
||||
background: rgba(0, 0, 0, 0.28);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.citron-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 19px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.citron-player-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
font-size: 13.5px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.citron-player-info i {
|
||||
color: var(--lemon-light);
|
||||
}
|
||||
|
||||
.citron-player-info strong {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ---- Photo de profil (en-tête du jeu) ---- */
|
||||
.citron-avatar-wrap {
|
||||
position: relative;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
flex: none;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.citron-avatar-fallback,
|
||||
.citron-avatar-img {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.citron-avatar-fallback {
|
||||
background: linear-gradient(135deg, var(--lemon-light), var(--lemon-dark));
|
||||
color: #2a1c00;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 800;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.citron-avatar-img {
|
||||
object-fit: cover;
|
||||
border: 2px solid var(--surface-2);
|
||||
}
|
||||
|
||||
.citron-avatar-edit {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.citron-avatar-wrap:hover .citron-avatar-edit {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.citron-btn {
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
padding: 9px 16px;
|
||||
font-family: var(--font);
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
background: var(--surface-2);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
transition: transform 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.citron-btn:hover {
|
||||
background: var(--surface-3);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.citron-btn.primary {
|
||||
background: linear-gradient(135deg, var(--lemon-light), var(--lemon) 60%, var(--lemon-dark));
|
||||
color: #2a1c00;
|
||||
box-shadow: 0 8px 20px var(--accent-soft);
|
||||
}
|
||||
|
||||
.citron-btn.primary:hover {
|
||||
filter: brightness(1.06);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.citron-btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* ---- Écran d'authentification ---- */
|
||||
.citron-auth {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 30px 16px;
|
||||
}
|
||||
|
||||
.citron-auth-card {
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
background: linear-gradient(180deg, var(--surface), var(--bg-soft));
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 30px 28px;
|
||||
text-align: center;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.citron-auth-card h1 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 21px;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.citron-auth-card p.subtitle {
|
||||
margin: 0 0 22px;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.citron-auth-tabs {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-bottom: 18px;
|
||||
background: var(--surface-2);
|
||||
border-radius: 999px;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.citron-auth-tabs button {
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font);
|
||||
font-weight: 700;
|
||||
font-size: 12.5px;
|
||||
padding: 9px;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
transition: background-color 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
.citron-auth-tabs button.active {
|
||||
background: linear-gradient(135deg, var(--lemon-light), var(--lemon));
|
||||
color: #2a1c00;
|
||||
}
|
||||
|
||||
.citron-field {
|
||||
text-align: left;
|
||||
margin-bottom: 13px;
|
||||
}
|
||||
|
||||
.citron-field label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.citron-field label i {
|
||||
color: var(--lemon-light);
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
.citron-field input {
|
||||
width: 100%;
|
||||
padding: 11px 13px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface-2);
|
||||
color: var(--text);
|
||||
font-family: var(--font);
|
||||
font-size: 14px;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.citron-field input:focus {
|
||||
outline: none;
|
||||
border-color: var(--lemon);
|
||||
box-shadow: 0 0 0 3px var(--accent-soft);
|
||||
}
|
||||
|
||||
.citron-auth-error {
|
||||
background: rgba(220, 60, 40, 0.16);
|
||||
border: 1px solid rgba(220, 60, 40, 0.4);
|
||||
color: #ffb3a8;
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 9px 12px;
|
||||
font-size: 12.5px;
|
||||
margin-bottom: 12px;
|
||||
display: none;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.citron-auth-error.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.citron-auth-submit {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
padding: 13px;
|
||||
font-size: 14px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* ---- Écran de jeu ---- */
|
||||
.citron-game {
|
||||
flex: 1;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.citron-game.visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.citron-score-bar {
|
||||
text-align: center;
|
||||
padding: 18px 12px 4px;
|
||||
}
|
||||
|
||||
.citron-score {
|
||||
font-size: clamp(32px, 6vw, 48px);
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--lemon-light);
|
||||
text-shadow: 0 4px 20px var(--accent-soft);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.citron-score-label {
|
||||
font-size: 12.5px;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.09em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.citron-score-label i {
|
||||
color: var(--lemon);
|
||||
}
|
||||
|
||||
.citron-cps {
|
||||
margin-top: 5px;
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.citron-main {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
/* minmax(0, ...) plutôt que 1fr/340px seuls : sans le "0", une piste de grille ne
|
||||
peut jamais devenir plus étroite que le plus large de ses enfants, ce qui fait
|
||||
déborder toute la page sur mobile dès qu'un item de liste est un peu large */
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 340px);
|
||||
gap: 18px;
|
||||
padding: 10px 18px 24px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.citron-main {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Le citron cliquable (dessiné en CSS) ---- */
|
||||
.citron-click-zone {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding-top: 18px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.citron-press {
|
||||
position: relative;
|
||||
width: min(44vw, 230px);
|
||||
height: min(38vw, 200px);
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.citron-press::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -14%;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, var(--accent-soft), transparent 70%);
|
||||
animation: lemon-pulse 3.2s ease-in-out infinite;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
@keyframes lemon-pulse {
|
||||
0%, 100% { transform: scale(0.92); opacity: 0.55; }
|
||||
50% { transform: scale(1.05); opacity: 1; }
|
||||
}
|
||||
|
||||
.lemon-visual {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50% 50% 47% 47% / 60% 60% 40% 40%;
|
||||
background: radial-gradient(circle at 32% 26%, var(--lemon-pale) 0%, var(--lemon) 46%, var(--lemon-dark) 100%);
|
||||
box-shadow:
|
||||
0 22px 46px rgba(0, 0, 0, 0.4),
|
||||
inset -14px -18px 30px rgba(0, 0, 0, 0.16),
|
||||
inset 10px 12px 22px rgba(255, 255, 255, 0.3);
|
||||
transition: transform 0.08s ease;
|
||||
}
|
||||
|
||||
.lemon-shine {
|
||||
position: absolute;
|
||||
top: 15%;
|
||||
left: 18%;
|
||||
width: 24%;
|
||||
height: 16%;
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
border-radius: 50%;
|
||||
filter: blur(3px);
|
||||
}
|
||||
|
||||
.lemon-leaf {
|
||||
position: absolute;
|
||||
top: -7%;
|
||||
left: 50%;
|
||||
width: 16%;
|
||||
height: 25%;
|
||||
background: linear-gradient(135deg, #8bc34a, var(--leaf));
|
||||
border-radius: 0 100% 0 100%;
|
||||
transform: translateX(-50%) rotate(-18deg);
|
||||
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.citron-press:active .lemon-visual,
|
||||
.citron-press.squish .lemon-visual {
|
||||
transform: scale(0.87) rotate(-4deg);
|
||||
}
|
||||
|
||||
.citron-press-hint {
|
||||
margin-top: 14px;
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.citron-float {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
pointer-events: none;
|
||||
font-weight: 800;
|
||||
color: var(--lemon-light);
|
||||
text-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);
|
||||
animation: citron-float-up 0.9s ease-out forwards;
|
||||
white-space: nowrap;
|
||||
font-size: 18px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@keyframes citron-float-up {
|
||||
0% { opacity: 1; transform: translate(-50%, -50%) scale(0.8); }
|
||||
100% { opacity: 0; transform: translate(-50%, -160%) scale(1.15); }
|
||||
}
|
||||
|
||||
.citron-drop {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
pointer-events: none;
|
||||
font-size: 15px;
|
||||
color: hsl(200, 90%, 65%);
|
||||
animation: citron-drop-fly 0.7s ease-out forwards;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@keyframes citron-drop-fly {
|
||||
0% { opacity: 1; transform: translate(-50%, -50%) translate(0, 0) scale(1); }
|
||||
100% { opacity: 0; transform: translate(-50%, -50%) translate(var(--dx), var(--dy)) scale(0.4); }
|
||||
}
|
||||
|
||||
/* ---- Prestige ---- */
|
||||
.citron-prestige {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.citron-prestige-btn {
|
||||
background: linear-gradient(135deg, #ffd93d, #ff9d3d, #ff5e5e);
|
||||
color: #2a1c00;
|
||||
font-size: 14px;
|
||||
padding: 13px 24px;
|
||||
border-radius: 999px;
|
||||
box-shadow: 0 12px 28px rgba(255, 150, 30, 0.4);
|
||||
}
|
||||
|
||||
.citron-prestige-note {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
max-width: 320px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ---- Colonnes latérales ---- */
|
||||
.citron-panel {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
max-height: 72vh;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.citron-panel-tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.citron-panel-tabs button {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 12px 4px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font);
|
||||
font-weight: 700;
|
||||
font-size: 11.5px;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: color 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.citron-panel-tabs button i {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.citron-panel-tabs button.active {
|
||||
color: var(--lemon-light);
|
||||
border-bottom-color: var(--lemon);
|
||||
}
|
||||
|
||||
.citron-panel-body {
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.citron-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
font-family: var(--font);
|
||||
color: var(--text);
|
||||
transition: background-color 0.15s ease, transform 0.1s ease, opacity 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.citron-item:hover:not(:disabled) {
|
||||
background: var(--surface-3);
|
||||
border-color: var(--lemon-dark);
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.citron-item:disabled {
|
||||
opacity: 0.42;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.citron-item .icon {
|
||||
flex: none;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
background: linear-gradient(145deg, var(--surface-3), var(--surface-2));
|
||||
color: var(--lemon-light);
|
||||
box-shadow: inset 0 0 0 1px var(--border);
|
||||
}
|
||||
|
||||
.citron-item .infos {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.citron-item .name {
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
font-size: 13.5px;
|
||||
}
|
||||
|
||||
.citron-item .desc {
|
||||
display: block;
|
||||
font-size: 11.5px;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.citron-item .cost {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--lemon-light);
|
||||
flex: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.citron-item .cost i {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.citron-item .owned {
|
||||
flex: none;
|
||||
min-width: 28px;
|
||||
text-align: center;
|
||||
font-weight: 800;
|
||||
background: var(--lemon-dark);
|
||||
color: #fff8e0;
|
||||
border-radius: 8px;
|
||||
padding: 4px 7px;
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
.citron-empty {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
padding: 24px 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.citron-empty i {
|
||||
font-size: 22px;
|
||||
color: var(--lemon-dark);
|
||||
}
|
||||
|
||||
/* ---- Classement ---- */
|
||||
.citron-leaderboard-row {
|
||||
display: grid;
|
||||
grid-template-columns: 26px 36px 1fr auto;
|
||||
align-items: center;
|
||||
gap: 11px;
|
||||
padding: 10px 11px;
|
||||
background: var(--surface-2);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.citron-leaderboard-row.lb-me {
|
||||
border-color: var(--lemon);
|
||||
background: var(--accent-soft);
|
||||
}
|
||||
|
||||
.lb-rank {
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.lb-rank-number {
|
||||
color: var(--text-muted);
|
||||
font-weight: 800;
|
||||
font-size: 12.5px;
|
||||
}
|
||||
|
||||
.lb-gold { color: #ffd700; }
|
||||
.lb-silver { color: #d6d6e0; }
|
||||
.lb-bronze { color: #cd7f32; }
|
||||
|
||||
.lb-avatar-wrap {
|
||||
position: relative;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.lb-avatar {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, var(--lemon-light), var(--lemon-dark));
|
||||
color: #2a1c00;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 800;
|
||||
font-size: 13.5px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.lb-avatar-img {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.lb-main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.lb-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.lb-prestige {
|
||||
flex: none;
|
||||
font-size: 10.5px;
|
||||
font-weight: 800;
|
||||
background: var(--lemon-dark);
|
||||
color: #fff8e0;
|
||||
padding: 2px 7px;
|
||||
border-radius: 999px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.lb-sub {
|
||||
margin-top: 2px;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.lb-sub i {
|
||||
color: var(--lemon-dark);
|
||||
}
|
||||
|
||||
.lb-score {
|
||||
text-align: right;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.lb-score-value {
|
||||
font-weight: 800;
|
||||
color: var(--lemon-light);
|
||||
font-size: 14px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.lb-score-label {
|
||||
font-size: 9px;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* ---- Toast ---- */
|
||||
.citron-toast {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--lemon);
|
||||
color: var(--text);
|
||||
padding: 11px 20px;
|
||||
border-radius: 999px;
|
||||
font-size: 13.5px;
|
||||
font-weight: 600;
|
||||
box-shadow: var(--shadow);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.citron-toast i {
|
||||
color: var(--lemon-light);
|
||||
}
|
||||
|
||||
.citron-toast.visible {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -6px);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.lemon-mark-big,
|
||||
.citron-press::before,
|
||||
.citron-float,
|
||||
.citron-drop {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user