feat: déplacer 'À lire aussi' après les réactions dans la colonne principale

This commit is contained in:
Cedric Abonnel
2026-05-13 01:32:03 +02:00
parent 0a44ab9da2
commit 78d6c656be
7 changed files with 355 additions and 21 deletions
+6
View File
@@ -36,6 +36,12 @@ RewriteRule ^admin/role/([a-z0-9_-]+)/?$ /index.php?action=admin_role_edit&role_
RewriteRule ^admin/([a-z0-9-]+)/?$ /index.php?action=admin&tab=$1 [L,QSA]
RewriteRule ^admin/?$ /index.php?action=admin [L,QSA]
# Réactions et commentaires
RewriteRule ^react/?$ /index.php?action=react [L,QSA]
RewriteRule ^comment/?$ /index.php?action=comment [L,QSA]
RewriteRule ^comment-moderate/?$ /index.php?action=comment_moderate [L,QSA]
RewriteRule ^verify-comment/([0-9]{6})/?$ /index.php?action=verify_comment&code=$1 [L,QSA]
# Pages de gestion
RewriteRule ^categories/?$ /index.php?action=categories [L,QSA]
RewriteRule ^profile/?$ /index.php?action=profile [L,QSA]
+30
View File
@@ -681,6 +681,36 @@ textarea.form-control {
border-color: var(--vl-accent);
}
.also-read-title {
font-size: .7rem;
font-weight: 700;
letter-spacing: .08em;
text-transform: uppercase;
color: var(--vl-muted);
margin-bottom: .75rem;
}
.also-read-grid {
display: flex;
flex-wrap: wrap;
gap: .75rem;
}
.also-read-grid .related-card {
flex: 1 1 200px;
max-width: 280px;
background: var(--vl-surface);
border-radius: var(--vl-radius);
padding: .65rem;
box-shadow: var(--vl-shadow-sm);
transition: box-shadow .15s, transform .15s;
}
.also-read-grid .related-card:hover {
box-shadow: var(--vl-shadow-md);
transform: translateY(-1px);
}
.related-card {
display: flex;
gap: 0.75rem;
+212 -1
View File
@@ -22,7 +22,7 @@ $action = $_GET['action'] ?? 'list';
$uuid = $_GET['uuid'] ?? '';
$slug = $_GET['slug'] ?? '';
$_noindexActions = ['create', 'edit', 'admin', 'categories', 'diff', 'add_files', 'import_image', 'import_image_step2', 'sources', 'profile', 'delete_file', 'delete_external_link', 'rename_category', 'delete_category', 'toggle_private_category', 'admin_save_site', 'not_found', 'add_feed', 'delete_feed', 'add_link', 'delete_link', 'reorder_links'];
$_noindexActions = ['create', 'edit', 'admin', 'categories', 'diff', 'add_files', 'import_image', 'import_image_step2', 'sources', 'profile', 'delete_file', 'delete_external_link', 'rename_category', 'delete_category', 'toggle_private_category', 'admin_save_site', 'not_found', 'add_feed', 'delete_feed', 'add_link', 'delete_link', 'reorder_links', 'react', 'comment', 'verify_comment', 'comment_moderate'];
$metaRobots = in_array($action, $_noindexActions, true) ? 'noindex, nofollow' : null;
unset($_noindexActions);
@@ -635,6 +635,38 @@ switch ($action) {
$backlinks = $articles->getBacklinks($article['slug'] ?? '', $article['uuid']);
// Réactions et commentaires
require_once BASE_PATH . '/src/ReactionManager.php';
require_once BASE_PATH . '/src/CommentManager.php';
$reactionStats = array_fill_keys(ReactionManager::TYPES, 0);
$visitorReactions = [];
$comments = [];
$commentFlash = isset($_GET['commented']);
$commentVerified = isset($_GET['verified']);
$commentError = null;
if ($pdo) {
$reactionMgr = new ReactionManager($pdo);
$commentMgr = new CommentManager($pdo);
// Cookie visiteur (fingerprint anti-doublon)
if (empty($_COOKIE['vl_vid'])) {
$vid = bin2hex(random_bytes(16));
setcookie('vl_vid', $vid, [
'expires' => time() + 365 * 86400,
'path' => '/',
'secure' => !empty($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Lax',
]);
} else {
$vid = $_COOKIE['vl_vid'];
}
$reactionStats = $reactionMgr->statsForArticle($article['uuid']);
$visitorReactions = $reactionMgr->visitorReactions($article['uuid'], $vid);
$comments = $commentMgr->forArticle($article['uuid']);
}
include BASE_PATH . '/templates/post_view.php';
break;
@@ -1616,6 +1648,161 @@ switch ($action) {
header('Location: /edit/' . rawurlencode($uuid));
exit;
case 'react':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: /');
exit;
}
$reactUuid = trim($_POST['uuid'] ?? '');
$reactType = trim($_POST['type'] ?? '');
$isAjax = ($_POST['_ajax'] ?? '') === '1';
// Cookie visiteur
if (empty($_COOKIE['vl_vid'])) {
$vid = bin2hex(random_bytes(16));
setcookie('vl_vid', $vid, [
'expires' => time() + 365 * 86400,
'path' => '/',
'secure' => !empty($_SERVER['HTTPS']),
'httponly' => true,
'samesite' => 'Lax',
]);
} else {
$vid = $_COOKIE['vl_vid'];
}
$pdo = dbPdo();
if ($pdo && $reactUuid !== '') {
require_once BASE_PATH . '/src/ReactionManager.php';
$rm = new ReactionManager($pdo);
$added = $rm->toggle($reactUuid, $reactType, $vid);
$count = $rm->statsForArticle($reactUuid)[$reactType] ?? 0;
if ($isAjax) {
header('Content-Type: application/json');
echo json_encode(['ok' => true, 'active' => $added, 'count' => $count]);
exit;
}
}
$reactBack = $_POST['_back'] ?? '/';
header('Location: ' . $reactBack);
exit;
case 'comment':
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: /');
exit;
}
// Honeypot
if (($_POST['website'] ?? '') !== '') {
header('Location: /');
exit;
}
// CSRF
$csrfOk = isset($_POST['_token'], $_SESSION['comment_csrf'])
&& hash_equals($_SESSION['comment_csrf'], $_POST['_token']);
unset($_SESSION['comment_csrf']);
if (!$csrfOk) {
header('Location: /');
exit;
}
$cmtUuid = trim($_POST['uuid'] ?? '');
$cmtName = trim($_POST['author_name'] ?? '');
$cmtEmail = trim($_POST['author_email'] ?? '');
$cmtContent = trim($_POST['content'] ?? '');
$cmtArticle = $cmtUuid !== '' ? $articles->getByUuid($cmtUuid) : null;
$cmtBack = $cmtArticle ? '/post/' . rawurlencode($cmtArticle['slug'] ?? $cmtUuid) : '/';
$pdo = dbPdo();
if (!$pdo || !$cmtArticle || $cmtName === '' || !filter_var($cmtEmail, FILTER_VALIDATE_EMAIL) || $cmtContent === '') {
header('Location: ' . $cmtBack . '#comment-form-card');
exit;
}
if (mb_strlen($cmtContent) > 2000) {
header('Location: ' . $cmtBack . '#comment-form-card');
exit;
}
require_once BASE_PATH . '/src/CommentManager.php';
require_once BASE_PATH . '/src/mailer.php';
$cm = new CommentManager($pdo);
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? '';
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
$code = $cm->submit($cmtUuid, $cmtName, $cmtEmail, $cmtContent, $ip, $ua);
$verifyUrl = rtrim(APP_URL, '/') . '/verify-comment/' . $code;
$articleUrl = rtrim(APP_URL, '/') . $cmtBack;
$subject = '[' . siteTitle() . '] Confirmez votre commentaire';
$html = '<!DOCTYPE html><html><body style="font-family:sans-serif;max-width:560px;margin:0 auto">'
. '<p>Bonjour ' . htmlspecialchars($cmtName) . ',</p>'
. '<p>Cliquez sur le bouton ci-dessous pour confirmer votre commentaire sur <em>' . htmlspecialchars($cmtArticle['title']) . '</em> :</p>'
. '<p><a href="' . htmlspecialchars($verifyUrl) . '" style="display:inline-block;padding:10px 20px;background:#0d6efd;color:#fff;text-decoration:none;border-radius:4px">Confirmer mon commentaire</a></p>'
. '<p style="color:#888;font-size:.875em">Ou copiez ce lien dans votre navigateur :<br>' . htmlspecialchars($verifyUrl) . '</p>'
. '<p style="color:#888;font-size:.875em">Ce lien expire dans 24 heures. Si vous n\'êtes pas à l\'origine de ce message, ignorez-le.</p>'
. '</body></html>';
try {
envoyer_mail_smtp($cmtEmail, $subject, $html);
} catch (\RuntimeException) {
// Taux limité ou erreur SMTP : on continue sans planter le visiteur
}
header('Location: ' . $cmtBack . '?commented=1#comments');
exit;
case 'verify_comment':
$vcCode = trim($_GET['code'] ?? '');
$pdo = dbPdo();
if ($pdo && preg_match('/^[0-9]{6}$/', $vcCode)) {
require_once BASE_PATH . '/src/CommentManager.php';
$cm = new CommentManager($pdo);
$vcUuid = $cm->verify($vcCode);
if ($vcUuid !== null) {
$vcArticle = $articles->getByUuid($vcUuid);
$vcSlug = $vcArticle ? ($vcArticle['slug'] ?? $vcUuid) : $vcUuid;
header('Location: /post/' . rawurlencode($vcSlug) . '?verified=1#comments');
exit;
}
}
// Code invalide ou expiré
http_response_code(404);
ob_start();
?>
<div class="container py-5 text-center">
<h1 class="h2 mb-3">Lien invalide ou expiré</h1>
<p class="text-muted mb-4">Ce lien de confirmation n'est plus valide (expiré après 24 h) ou a déjà été utilisé.</p>
<a href="/" class="btn btn-primary">← Retour à l'accueil</a>
</div>
<?php
$content = ob_get_clean();
$title = 'Lien invalide — ' . siteTitle();
$metaRobots = 'noindex, nofollow';
include BASE_PATH . '/templates/layout.php';
break;
case 'comment_moderate':
requireAuth();
if (!isAdmin() || $_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(403);
exit;
}
$modId = (int)($_POST['id'] ?? 0);
$modPub = (int)($_POST['pub'] ?? 1);
$pdo = dbPdo();
if ($pdo && $modId > 0) {
require_once BASE_PATH . '/src/CommentManager.php';
(new CommentManager($pdo))->setPublished($modId, $modPub === 1);
}
header('Location: /admin/comments');
exit;
case 'rate':
requireAuth();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
@@ -1780,6 +1967,30 @@ switch ($action) {
}
}
if ($tab === 'comments') {
if (!isAdmin()) {
http_response_code(403);
exit;
}
$pdo = dbPdo();
if ($pdo) {
require_once BASE_PATH . '/src/CommentManager.php';
$adminData['comments'] = (new CommentManager($pdo))->allForAdmin();
// Enrichit avec le slug de chaque article pour les liens
$adminData['articleSlugs'] = [];
foreach ($adminData['comments'] as $cmtRow) {
$uuid = $cmtRow['article_uuid'];
if (!isset($adminData['articleSlugs'][$uuid])) {
$a = $articles->getByUuid($uuid);
$adminData['articleSlugs'][$uuid] = $a ? ($a['slug'] ?? null) : null;
}
}
} else {
$adminData['comments'] = [];
$adminData['articleSlugs'] = [];
}
}
include BASE_PATH . '/templates/admin.php';
break;