File: /homepages/35/d993672390/htdocs/dermiteseborrheique/admin/blog.php
<?php
define('DS_APP', true);
require_once dirname(__DIR__) . '/includes/init.php';
$user = auth_require();
if (!$user['is_admin'] && !$user['is_author']) {
flash('error', 'Accès non autorisé.');
redirect('app/dashboard.php');
}
// ── Actions ───────────────────────────────────────────────────────────────────
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
csrf_verify();
$action = post('action');
if ($action === 'save_article') {
$id = (int)(post('id') ?: 0);
$title = sanitize_string(post('title'), 300);
$intro = sanitize_string(post('intro'), 1000);
$body = post('body'); // HTML TinyMCE — pas de sanitize strict
$catId = (int)post('category_id');
$status = in_array(post('status'), ['draft','published','archived']) ? post('status') : 'draft';
$isFeatured = !empty(post('is_featured')) ? 1 : 0;
$readTime = max(1, min(60, (int)post('read_time')));
$metaTitle = sanitize_string(post('meta_title'), 300);
$metaDesc = sanitize_string(post('meta_desc'), 300);
if (empty($title) || empty($body) || !$catId) {
flash('error', 'Titre, contenu et catégorie sont obligatoires.');
redirect('admin/blog.php?action=edit&id=' . $id);
}
// Slug
$slug = $id
? db_fetch("SELECT slug FROM blog_articles WHERE id=?", [$id])['slug']
: null;
if (!$slug) {
$baseSlug = mb_strtolower($title);
$baseSlug = str_replace(
['é','è','ê','à','â','î','ô','ù','û','ç'],
['e','e','e','a','a','i','o','u','u','c'],
$baseSlug
);
$baseSlug = preg_replace('/[^a-z0-9\s-]/', '', $baseSlug);
$baseSlug = preg_replace('/[\s-]+/', '-', trim($baseSlug));
$baseSlug = mb_substr($baseSlug, 0, 80);
$slug = $baseSlug;
// Unicité
$n = 1;
while (db_count("SELECT COUNT(*) FROM blog_articles WHERE slug=?", [$slug]) > 0) {
$slug = $baseSlug . '-' . $n++;
}
}
$publishedAt = ($status === 'published')
? ($id ? db_fetch("SELECT published_at FROM blog_articles WHERE id=?",[$id])['published_at'] ?? date('Y-m-d H:i:s') : date('Y-m-d H:i:s'))
: null;
// Upload image de couverture
$coverImage = $id
? db_fetch("SELECT cover_image FROM blog_articles WHERE id=?", [$id])['cover_image']
: null;
if (!empty($_FILES['cover_image']['tmp_name'])) {
$mime = mime_content_type($_FILES['cover_image']['tmp_name']);
if (in_array($mime, ['image/jpeg','image/png','image/webp']) && $_FILES['cover_image']['size'] <= UPLOAD_MAX_SIZE) {
$ext = match($mime){'image/jpeg'=>'jpg','image/png'=>'png','image/webp'=>'webp',default=>'jpg'};
$fn = 'blog_' . uniqid() . '.' . $ext;
@mkdir(DS_UPLOADS . 'blog/', 0755, true);
if (move_uploaded_file($_FILES['cover_image']['tmp_name'], DS_UPLOADS . 'blog/' . $fn)) {
$coverImage = url('uploads/blog/' . $fn);
}
}
}
if ($id) {
db_execute(
"UPDATE blog_articles SET
category_id=?, title=?, intro=?, body=?, cover_image=?,
status=?, is_featured=?, read_time=?,
meta_title=?, meta_desc=?, published_at=?
WHERE id=? AND user_id=?",
[$catId, $title, $intro, $body, $coverImage,
$status, $isFeatured, $readTime,
$metaTitle ?: null, $metaDesc ?: null, $publishedAt,
$id, $user['id']]
);
} else {
$id = db_insert(
"INSERT INTO blog_articles
(user_id, category_id, slug, title, intro, body, cover_image,
status, is_featured, read_time, meta_title, meta_desc, published_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
[$user['id'], $catId, $slug, $title, $intro, $body, $coverImage,
$status, $isFeatured, $readTime,
$metaTitle ?: null, $metaDesc ?: null, $publishedAt]
);
}
// Mettre à jour le compteur catégorie
db_execute(
"UPDATE blog_categories SET article_count = (
SELECT COUNT(*) FROM blog_articles WHERE category_id=blog_categories.id AND status='published'
)"
);
// Créer un post social si publication et pas déjà fait
if ($status === 'published') {
$article = db_fetch("SELECT * FROM blog_articles WHERE id=?", [$id]);
if (!$article['social_post_id']) {
// Accroche courte pour le social (pas le corps complet !)
$socialBody = "📖 Nouvel article : **{$title}**\n\n"
. mb_substr(strip_tags($intro), 0, 200) . "...\n\n"
. "🔗 Lire l'article complet → " . url('ressources/' . $slug . '/');
$postId = db_insert(
"INSERT INTO social_posts (user_id, slug, title, body, tags, category_id)
VALUES (?, ?, ?, ?, ?, ?)",
[
$user['id'],
\Social::makeSlug("Article: {$title}", 0),
"📖 " . mb_substr($title, 0, 100),
$socialBody,
json_encode(['article', 'ressources']),
null
]
);
// Fixer le slug avec l'ID
$postSlug = \Social::makeSlug("Article: {$title}", $postId);
db_execute("UPDATE social_posts SET slug=? WHERE id=?", [$postSlug, $postId]);
// Lier
db_execute("UPDATE blog_articles SET social_post_id=? WHERE id=?", [$postId, $id]);
}
}
flash('success', 'Article sauvegardé.');
redirect('admin/blog.php?action=edit&id=' . $id);
}
if ($action === 'delete') {
$id = (int)post('id');
db_execute("DELETE FROM blog_articles WHERE id=? AND user_id=?", [$id, $user['id']]);
db_execute("UPDATE blog_categories SET article_count = (SELECT COUNT(*) FROM blog_articles WHERE category_id=blog_categories.id AND status='published')");
flash('success', 'Article supprimé.');
redirect('admin/blog.php');
}
}
// ── Listes et édition ─────────────────────────────────────────────────────────
$action = $_GET['action'] ?? 'list';
$editId = (int)($_GET['id'] ?? 0);
$editArticle = null;
if ($action === 'edit' && $editId) {
$editArticle = db_fetch(
"SELECT * FROM blog_articles WHERE id=? AND (user_id=? OR ?=1)",
[$editId, $user['id'], $user['is_admin']]
);
}
$articles = db_fetch_all(
"SELECT a.*, c.name as cat_name, c.icon as cat_icon
FROM blog_articles a JOIN blog_categories c ON a.category_id=c.id
WHERE a.user_id=? OR ?=1
ORDER BY a.updated_at DESC",
[$user['id'], $user['is_admin']]
);
$categories = db_fetch_all("SELECT * FROM blog_categories WHERE is_active=1 ORDER BY sort_order");
$pageTitle = 'Gestion du blog';
$activePage = 'admin';
ob_start();
?>
<style>
.blog-admin{display:grid;grid-template-columns:240px 1fr;gap:20px;align-items:start;}
.blog-admin-nav{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:10px;position:sticky;top:80px;}
.ban-item{display:flex;align-items:center;gap:8px;padding:8px 12px;border-radius:8px;text-decoration:none;color:var(--text-mid);font-size:0.85rem;font-weight:600;transition:all 0.2s;cursor:pointer;}
.ban-item:hover,.ban-item.active{background:var(--cyan-dim);color:var(--cyan);}
.blog-admin-content{min-width:0;}
.articles-table{width:100%;border-collapse:collapse;}
.articles-table th{font-size:0.68rem;font-weight:700;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.08em;padding:8px 12px;text-align:left;border-bottom:1px solid var(--border);}
.articles-table td{font-size:0.82rem;padding:10px 12px;border-bottom:1px solid var(--border);color:var(--text-mid);vertical-align:middle;}
.articles-table tr:last-child td{border:none;}
.articles-table td:first-child{color:var(--text);font-weight:600;}
.status-badge{font-size:0.65rem;font-weight:700;padding:2px 7px;border-radius:100px;}
.s-published{background:var(--emerald-dim);color:var(--emerald);}
.s-draft{background:rgba(255,181,71,0.1);color:var(--amber);}
.s-archived{background:rgba(255,255,255,0.05);color:var(--text-dim);}
.edit-form{background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:24px;}
.ef-grid{display:grid;grid-template-columns:1fr 280px;gap:20px;align-items:start;}
.fg{margin-bottom:16px;}
.fl{display:block;font-size:0.7rem;font-weight:700;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.08em;margin-bottom:7px;}
.fi{width:100%;background:var(--surface2);border:1px solid var(--border-bright);border-radius:9px;padding:9px 12px;color:var(--text);font-size:0.88rem;font-family:inherit;transition:border-color 0.2s;}
.fi:focus{outline:none;border-color:rgba(0,212,200,0.4);}
.fta{width:100%;background:var(--surface2);border:1px solid var(--border-bright);border-radius:9px;padding:9px 12px;color:var(--text);font-size:0.88rem;font-family:inherit;resize:vertical;min-height:80px;}
.fta:focus{outline:none;border-color:rgba(0,212,200,0.4);}
.fsel{width:100%;background:var(--surface2);border:1px solid var(--border-bright);border-radius:9px;padding:9px 12px;color:var(--text);font-size:0.88rem;font-family:inherit;cursor:pointer;}
.save-btn{background:var(--cyan);color:var(--bg);border:none;padding:12px 24px;border-radius:9px;font-size:0.9rem;font-weight:800;font-family:inherit;cursor:pointer;transition:all 0.2s;}
.save-btn:hover{background:#fff;}
.meta-card{background:var(--surface2);border:1px solid var(--border);border-radius:10px;padding:16px;}
.tinymce-wrap{border:1px solid var(--border-bright);border-radius:9px;overflow:hidden;min-height:400px;}
#articleBody{min-height:400px;}
</style>
<?php if ($action === 'edit'): ?>
<!-- Éditeur d'article -->
<div class="edit-form">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;">
<div style="font-size:1rem;font-weight:800;"><?= $editId ? 'Modifier l\'article' : 'Nouvel article' ?></div>
<a href="<?= url('admin/blog.php') ?>" style="font-size:0.82rem;color:var(--text-dim);text-decoration:none;">← Retour à la liste</a>
</div>
<form method="POST" enctype="multipart/form-data">
<?= csrf_field() ?>
<input type="hidden" name="action" value="save_article">
<?php if ($editId): ?><input type="hidden" name="id" value="<?= $editId ?>"><?php endif; ?>
<div class="ef-grid">
<!-- Colonne principale -->
<div>
<div class="fg">
<label class="fl">Titre *</label>
<input type="text" name="title" class="fi" value="<?= e($editArticle['title'] ?? post('title')) ?>" required placeholder="Titre de l'article">
</div>
<div class="fg">
<label class="fl">Accroche (chapeau) *</label>
<textarea name="intro" class="fta" required placeholder="2-3 phrases qui donnent envie de lire. Apparaît dans les cards et en intro de l'article."><?= e($editArticle['intro'] ?? post('intro')) ?></textarea>
</div>
<div class="fg">
<label class="fl">Contenu *</label>
<!-- TinyMCE sera initialisé ici -->
<div class="tinymce-wrap">
<textarea name="body" id="articleBody"><?= $editArticle['body'] ?? '' ?></textarea>
</div>
</div>
</div>
<!-- Colonne latérale -->
<div>
<div class="meta-card" style="margin-bottom:14px;">
<div style="font-size:0.72rem;font-weight:700;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.08em;margin-bottom:14px;">Publication</div>
<div class="fg">
<label class="fl">Statut</label>
<select name="status" class="fsel">
<option value="draft" <?= ($editArticle['status']??'draft')==='draft'?'selected':'' ?>>📝 Brouillon</option>
<option value="published" <?= ($editArticle['status']??'')==='published'?'selected':'' ?>>✅ Publié</option>
<option value="archived" <?= ($editArticle['status']??'')==='archived'?'selected':'' ?>>📦 Archivé</option>
</select>
</div>
<div class="fg">
<label class="fl">Catégorie *</label>
<select name="category_id" class="fsel" required>
<option value="">-- Choisir --</option>
<?php foreach ($categories as $cat): ?>
<option value="<?= $cat['id'] ?>" <?= ($editArticle['category_id']??0)==$cat['id']?'selected':'' ?>>
<?= $cat['icon'] ?> <?= e($cat['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="fg">
<label class="fl">Temps de lecture (min)</label>
<input type="number" name="read_time" class="fi" min="1" max="60" value="<?= $editArticle['read_time'] ?? 5 ?>">
</div>
<div style="display:flex;align-items:center;gap:8px;padding:10px 0;border-top:1px solid var(--border);">
<input type="checkbox" name="is_featured" id="isFeatured" <?= ($editArticle['is_featured']??0)?'checked':'' ?>>
<label for="isFeatured" style="font-size:0.82rem;cursor:pointer;">⭐ Article à la une</label>
</div>
<button type="submit" class="save-btn" style="width:100%;margin-top:10px;">💾 Enregistrer</button>
<?php if ($editId): ?>
<a href="<?= url('ressources/' . ($editArticle['slug'] ?? '') . '/') ?>" target="_blank"
style="display:block;text-align:center;margin-top:8px;font-size:0.75rem;color:var(--cyan);text-decoration:none;">
👁 Voir l'article →
</a>
<?php endif; ?>
</div>
<div class="meta-card" style="margin-bottom:14px;">
<div style="font-size:0.72rem;font-weight:700;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.08em;margin-bottom:14px;">Image de couverture</div>
<?php if (!empty($editArticle['cover_image'])): ?>
<img src="<?= e($editArticle['cover_image']) ?>" style="width:100%;border-radius:8px;margin-bottom:10px;">
<?php endif; ?>
<input type="file" name="cover_image" accept="image/*" style="width:100%;font-size:0.78rem;color:var(--text-mid);">
</div>
<div class="meta-card">
<div style="font-size:0.72rem;font-weight:700;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.08em;margin-bottom:14px;">SEO (optionnel)</div>
<div class="fg">
<label class="fl">Meta title</label>
<input type="text" name="meta_title" class="fi" value="<?= e($editArticle['meta_title'] ?? '') ?>" placeholder="Laissez vide pour utiliser le titre">
</div>
<div class="fg">
<label class="fl">Meta description</label>
<textarea name="meta_desc" class="fta" style="min-height:60px;" placeholder="Laissez vide pour utiliser l'accroche"><?= e($editArticle['meta_desc'] ?? '') ?></textarea>
</div>
</div>
</div>
</div>
</form>
</div>
<!-- TinyMCE -->
<script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>
<script>
tinymce.init({
selector: '#articleBody',
height: 500,
menubar: false,
language: 'fr_FR',
skin: document.documentElement.getAttribute('data-theme') === 'light' ? 'oxide' : 'oxide-dark',
content_css: document.documentElement.getAttribute('data-theme') === 'light' ? 'default' : 'dark',
plugins: [
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap',
'preview', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
'insertdatetime', 'media', 'table', 'help', 'wordcount'
],
toolbar: 'undo redo | blocks | bold italic underline | alignleft aligncenter alignright | bullist numlist | link image | blockquote | code | fullscreen',
content_style: 'body { font-family: Cabinet Grotesk, sans-serif; font-size: 16px; line-height: 1.8; }',
block_formats: 'Paragraphe=p; Titre 2=h2; Titre 3=h3; Citation=blockquote; Code=pre',
// Auto-générer les IDs sur les H2 pour la TOC
setup: function(editor) {
editor.on('NodeChange', function() {
const content = editor.getContent();
const newContent = content.replace(/<h2(?![^>]*id=)[^>]*>/g, (match) => {
const text = match.replace(/<[^>]*>/g, '').toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
return match.replace('<h2', '<h2 id="' + text + '"');
});
if (content !== newContent) editor.setContent(newContent);
});
}
});
</script>
<?php else: ?>
<!-- Liste des articles -->
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;">
<div style="font-size:1rem;font-weight:800;">Mes articles (<?= count($articles) ?>)</div>
<a href="?action=edit" style="background:var(--cyan);color:var(--bg);padding:9px 18px;border-radius:8px;text-decoration:none;font-size:0.82rem;font-weight:800;">+ Nouvel article</a>
</div>
<div style="background:var(--surface);border:1px solid var(--border);border-radius:14px;overflow:hidden;">
<table class="articles-table">
<thead>
<tr>
<th>Titre</th>
<th>Catégorie</th>
<th>Statut</th>
<th>Vues</th>
<th>Modifié</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php if (empty($articles)): ?>
<tr><td colspan="6" style="text-align:center;padding:30px;color:var(--text-dim);">Aucun article — <a href="?action=edit" style="color:var(--cyan);">créez le premier →</a></td></tr>
<?php else: ?>
<?php foreach ($articles as $a): ?>
<tr>
<td><?= e(mb_substr($a['title'], 0, 60)) ?><?= mb_strlen($a['title'])>60?'...':'' ?></td>
<td><?= $a['cat_icon'] ?> <?= e($a['cat_name']) ?></td>
<td><span class="status-badge s-<?= $a['status'] ?>"><?= match($a['status']){'published'=>'✅ Publié','draft'=>'📝 Brouillon',default=>'📦 Archivé'} ?></span></td>
<td><?= number_format($a['views']) ?></td>
<td style="font-size:0.75rem;"><?= date('d/m/Y', strtotime($a['updated_at'])) ?></td>
<td style="display:flex;gap:6px;">
<a href="?action=edit&id=<?= $a['id'] ?>" style="background:var(--surface2);border:1px solid var(--border);color:var(--text-mid);padding:4px 10px;border-radius:6px;font-size:0.75rem;text-decoration:none;">✏️ Éditer</a>
<?php if ($a['status'] === 'published'): ?>
<a href="<?= url('ressources/' . $a['slug'] . '/') ?>" target="_blank" style="background:var(--cyan-dim);border:1px solid rgba(0,212,200,0.2);color:var(--cyan);padding:4px 10px;border-radius:6px;font-size:0.75rem;text-decoration:none;">👁 Voir</a>
<?php endif; ?>
<form method="POST" style="display:inline;" onsubmit="return confirm('Supprimer cet article ?')">
<?= csrf_field() ?>
<input type="hidden" name="action" value="delete">
<input type="hidden" name="id" value="<?= $a['id'] ?>">
<button type="submit" style="background:rgba(255,107,138,0.08);border:1px solid rgba(255,107,138,0.2);color:var(--rose);padding:4px 10px;border-radius:6px;font-size:0.75rem;cursor:pointer;font-family:inherit;">🗑</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<?php
$content = ob_get_clean();
require DS_VIEWS . 'layouts/app.php';