257 lines
13 KiB
PHP
257 lines
13 KiB
PHP
<?php
|
||
require_once __DIR__ . '/../../inc/auth.php';
|
||
require_once __DIR__ . '/../../inc/functions.php';
|
||
$session = auth_require('owner');
|
||
|
||
$pdo = getDB();
|
||
$message = '';
|
||
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
$id = (int)($_POST['id'] ?? 0);
|
||
$title = trim($_POST['title'] ?? '');
|
||
$slug = trim($_POST['slug'] ?? '');
|
||
$content = $_POST['content'] ?? '';
|
||
$excerpt = trim($_POST['excerpt'] ?? '');
|
||
$category = $_POST['category'] ?? 'risk-engineering';
|
||
$status = $_POST['status'] ?? 'draft';
|
||
$csrf = $_POST['csrf_token'] ?? '';
|
||
|
||
if (!auth_csrf_verify($csrf)) {
|
||
$message = 'Ошибка безопасности.';
|
||
} elseif (!$title || !$content) {
|
||
$message = 'Заголовок и содержание обязательны.';
|
||
} else {
|
||
if (!$slug) {
|
||
$slug = preg_replace('/[^a-z0-9-]/', '', mb_strtolower(str_replace(' ', '-', $title)));
|
||
}
|
||
try {
|
||
if ($id > 0) {
|
||
$stmt = $pdo->prepare("UPDATE blog_posts SET title=?, slug=?, content=?, excerpt=?, category=?, status=? WHERE id=?");
|
||
$stmt->execute([$title, $slug, $content, $excerpt, $category, $status, $id]);
|
||
$message = 'Пост обновлён.';
|
||
} else {
|
||
$stmt = $pdo->prepare("INSERT INTO blog_posts (title, slug, content, excerpt, category, author_id, status, published_at) VALUES (?,?,?,?,?,?,?,?)");
|
||
$published = $status === 'published' ? date('Y-m-d H:i:s') : null;
|
||
$stmt->execute([$title, $slug, $content, $excerpt, $category, $session['user_id'], $status, $published]);
|
||
$message = 'Пост создан.';
|
||
}
|
||
auth_log($session['user_id'], 'blog_save', "Title: $title");
|
||
} catch (\PDOException $e) {
|
||
$message = 'Ошибка: ' . ($e->getCode() === '23000' ? 'Такой slug уже существует.' : $e->getMessage());
|
||
}
|
||
}
|
||
}
|
||
|
||
if (isset($_GET['delete'])) {
|
||
$id = (int)$_GET['delete'];
|
||
if (auth_csrf_verify($_GET['csrf'] ?? '')) {
|
||
$pdo->prepare("DELETE FROM blog_posts WHERE id=?")->execute([$id]);
|
||
$message = 'Пост удалён.';
|
||
}
|
||
}
|
||
|
||
if (isset($_GET['publish'])) {
|
||
$id = (int)$_GET['publish'];
|
||
if (auth_csrf_verify($_GET['csrf'] ?? '')) {
|
||
$pdo->prepare("UPDATE blog_posts SET status='published', published_at=NOW() WHERE id=?")->execute([$id]);
|
||
$message = 'Пост опубликован.';
|
||
}
|
||
}
|
||
|
||
$editPost = null;
|
||
if (isset($_GET['edit'])) {
|
||
$stmt = $pdo->prepare("SELECT * FROM blog_posts WHERE id=?");
|
||
$stmt->execute([(int)$_GET['edit']]);
|
||
$editPost = $stmt->fetch();
|
||
}
|
||
|
||
$posts = $pdo->query("SELECT bp.*, u.full_name as author_name FROM blog_posts bp LEFT JOIN users u ON u.id=bp.author_id ORDER BY bp.created_at DESC")->fetchAll();
|
||
|
||
$csrf = auth_csrf_token();
|
||
|
||
$categories = [
|
||
'audit' => 'Аудит',
|
||
'sla' => 'SLA',
|
||
'incident' => 'Инциденты',
|
||
'supervision' => 'Технадзор',
|
||
'documentation' => 'Документация',
|
||
'risk' => 'Риск-инжиниринг',
|
||
'cases' => 'Кейсы',
|
||
];
|
||
|
||
require __DIR__ . '/../../inc/header.php';
|
||
?>
|
||
|
||
<div class="main-header">
|
||
<div>
|
||
<h1>Управление блогом</h1>
|
||
<div class="breadcrumb"><a href="/service/dashboard.php">Главная</a> / Блог</div>
|
||
</div>
|
||
<button class="btn btn-primary btn-sm" onclick="openModal()" data-tooltip="Создать новую статью">+ Новая статья</button>
|
||
</div>
|
||
|
||
<?php if ($message): ?>
|
||
<div class="alert alert-info"><?= htmlspecialchars($message) ?></div>
|
||
<?php endif; ?>
|
||
|
||
<div class="card">
|
||
<div class="table-wrap">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Заголовок</th>
|
||
<th>Категория</th>
|
||
<th>Автор</th>
|
||
<th>Статус</th>
|
||
<th>Дата</th>
|
||
<th class="actions">Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php if (empty($posts)): ?>
|
||
<tr><td colspan="6" style="text-align:center;color:var(--text-muted);">Нет статей</td></tr>
|
||
<?php endif; ?>
|
||
<?php foreach ($posts as $p): ?>
|
||
<tr>
|
||
<td><strong><?= htmlspecialchars($p['title']) ?></strong></td>
|
||
<td><span class="badge badge-medium"><?= $categories[$p['category']] ?? $p['category'] ?></span></td>
|
||
<td><?= htmlspecialchars($p['author_name'] ?: '—') ?></td>
|
||
<td><span class="badge badge-<?= $p['status'] === 'published' ? 'done' : ($p['status'] === 'draft' ? 'draft' : 'cancelled') ?>"><?= $p['status'] ?></span></td>
|
||
<td><?= $p['published_at'] ? date('d.m.Y', strtotime($p['published_at'])) : date('d.m.Y', strtotime($p['created_at'])) ?></td>
|
||
<td class="actions">
|
||
<a href="/blog/<?= htmlspecialchars($p['slug']) ?>" class="btn btn-secondary btn-sm" target="_blank" data-tooltip="Просмотр">👁️</a>
|
||
<a href="?edit=<?= $p['id'] ?>" class="btn btn-secondary btn-sm" data-tooltip="Редактировать">✏️</a>
|
||
<?php if ($p['status'] !== 'published'): ?>
|
||
<a href="?publish=<?= $p['id'] ?>&csrf=<?= $csrf ?>" class="btn btn-success btn-sm" data-tooltip="Опубликовать">📢</a>
|
||
<?php endif; ?>
|
||
<a href="?delete=<?= $p['id'] ?>&csrf=<?= $csrf ?>" class="btn btn-danger btn-sm" onclick="return confirm('Удалить статью?')" data-tooltip="Удалить">🗑️</a>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-overlay <?= $editPost ? 'open' : '' ?>" id="blogModal">
|
||
<div class="modal" style="max-width:720px;">
|
||
<button class="modal-close" onclick="closeModal()">×</button>
|
||
<h2><?= $editPost ? 'Редактировать' : 'Новая' ?> статья</h2>
|
||
<form method="post">
|
||
<input type="hidden" name="csrf_token" value="<?= $csrf ?>">
|
||
<input type="hidden" name="id" value="<?= $editPost['id'] ?? 0 ?>">
|
||
|
||
<div class="form-group">
|
||
<label for="title">Заголовок *</label>
|
||
<input type="text" id="title" name="title" value="<?= htmlspecialchars($editPost['title'] ?? '') ?>" required>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label for="slug">Slug (оставьте пустым для автогенерации)</label>
|
||
<input type="text" id="slug" name="slug" value="<?= htmlspecialchars($editPost['slug'] ?? '') ?>">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="category">Категория</label>
|
||
<select id="category" name="category">
|
||
<?php foreach ($categories as $key => $label): ?>
|
||
<option value="<?= $key ?>" <?= ($editPost['category'] ?? '') === $key ? 'selected' : '' ?>><?= $label ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label for="status">Статус</label>
|
||
<select id="status" name="status">
|
||
<option value="draft" <?= ($editPost['status'] ?? '') === 'draft' ? 'selected' : '' ?>>Черновик</option>
|
||
<option value="published" <?= ($editPost['status'] ?? '') === 'published' ? 'selected' : '' ?>>Опубликовать</option>
|
||
<option value="archived" <?= ($editPost['status'] ?? '') === 'archived' ? 'selected' : '' ?>>Архив</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="excerpt">Краткое описание</label>
|
||
<input type="text" id="excerpt" name="excerpt" value="<?= htmlspecialchars($editPost['excerpt'] ?? '') ?>">
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="content">Содержание * (HTML)</label>
|
||
<div class="editor-toolbar">
|
||
<button type="button" data-insert="bold" title="Жирный"><b>Ж</b></button>
|
||
<button type="button" data-insert="italic" title="Курсив"><i>К</i></button>
|
||
<button type="button" data-insert="h2" title="Заголовок 2">H2</button>
|
||
<button type="button" data-insert="h3" title="Заголовок 3">H3</button>
|
||
<button type="button" data-insert="ul" title="Маркированный список">• Список</button>
|
||
<button type="button" data-insert="ol" title="Нумерованный список">1. Список</button>
|
||
<button type="button" data-insert="link" title="Ссылка">🔗</button>
|
||
<button type="button" data-insert="image" title="Изображение">🖼️</button>
|
||
</div>
|
||
<textarea id="content" name="content" rows="15" style="font-family:monospace;"><?= htmlspecialchars($editPost['content'] ?? '') ?></textarea>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary">Сохранить</button>
|
||
<button type="button" class="btn btn-secondary" onclick="closeModal()">Отмена</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
function openModal() { document.getElementById('blogModal').classList.add('open'); }
|
||
function closeModal() { document.getElementById('blogModal').classList.remove('open'); window.location.href = 'blog.php'; }
|
||
|
||
// Slug auto-generation from title
|
||
(function(){
|
||
var titleInput = document.getElementById('title');
|
||
var slugInput = document.getElementById('slug');
|
||
if (!titleInput || !slugInput) return;
|
||
var slugTouched = slugInput.value !== '';
|
||
slugInput.addEventListener('input', function(){ slugTouched = true; });
|
||
titleInput.addEventListener('input', function(){
|
||
if (slugTouched) return;
|
||
var val = titleInput.value.toLowerCase().replace(/[^a-zа-яё0-9\s-]/gi, '').replace(/\s+/g, '-');
|
||
// Transliterate Cyrillic
|
||
var map = {'а':'a','б':'b','в':'v','г':'g','д':'d','е':'e','ё':'yo','ж':'zh','з':'z','и':'i','й':'y','к':'k','л':'l','м':'m','н':'n','о':'o','п':'p','р':'r','с':'s','т':'t','у':'u','ф':'f','х':'kh','ц':'ts','ч':'ch','ш':'sh','щ':'shch','ъ':'','ы':'y','ь':'','э':'e','ю':'yu','я':'ya'};
|
||
var result = '';
|
||
for (var i = 0; i < val.length; i++) {
|
||
var ch = val[i];
|
||
result += map[ch] !== undefined ? map[ch] : ch;
|
||
}
|
||
slugInput.value = result.replace(/-+/g, '-').replace(/^-|-$/g, '');
|
||
});
|
||
})();
|
||
|
||
// Editor toolbar
|
||
(function(){
|
||
var textarea = document.getElementById('content');
|
||
if (!textarea) return;
|
||
document.querySelectorAll('.editor-toolbar button').forEach(function(btn){
|
||
btn.addEventListener('click', function(e){
|
||
e.preventDefault();
|
||
var action = btn.getAttribute('data-insert');
|
||
var start = textarea.selectionStart;
|
||
var end = textarea.selectionEnd;
|
||
var selected = textarea.value.substring(start, end);
|
||
var insert = '';
|
||
switch(action) {
|
||
case 'bold': insert = '<strong>' + (selected || 'текст') + '</strong>'; break;
|
||
case 'italic': insert = '<em>' + (selected || 'текст') + '</em>'; break;
|
||
case 'h2': insert = '<h2>' + (selected || 'Заголовок') + '</h2>'; break;
|
||
case 'h3': insert = '<h3>' + (selected || 'Заголовок') + '</h3>'; break;
|
||
case 'ul': insert = '<ul>\n <li>' + (selected || 'Пункт') + '</li>\n</ul>'; break;
|
||
case 'ol': insert = '<ol>\n <li>' + (selected || 'Пункт') + '</li>\n</ol>'; break;
|
||
case 'link': insert = '<a href="">' + (selected || 'текст ссылки') + '</a>'; break;
|
||
case 'image': insert = '<img src="" alt="' + (selected || 'описание') + '">'; break;
|
||
}
|
||
textarea.value = textarea.value.substring(0, start) + insert + textarea.value.substring(end);
|
||
textarea.focus();
|
||
textarea.selectionStart = textarea.selectionEnd = start + insert.length;
|
||
});
|
||
});
|
||
})();
|
||
</script>
|
||
|
||
<?php require __DIR__ . '/../../inc/footer.php'; ?>
|
||
<style>
|
||
.editor-toolbar { display:flex; gap:4px; margin-bottom:8px; flex-wrap:wrap; }
|
||
.editor-toolbar button { padding:4px 10px; background:var(--bg-input); border:1px solid var(--border); border-radius:4px; color:var(--text-primary); cursor:pointer; font-size:12px; transition:all 0.2s; }
|
||
.editor-toolbar button:hover { border-color:var(--accent); background:var(--accent-dim); }
|
||
</style>
|