324 lines
14 KiB
PHP
324 lines
14 KiB
PHP
<?php
|
|
require_once __DIR__ . '/auth.php';
|
|
requireLogin();
|
|
|
|
$db = getDB();
|
|
$error = '';
|
|
$success = '';
|
|
|
|
// Load post if editing
|
|
$postId = intval($_GET['id'] ?? 0);
|
|
$post = null;
|
|
if ($postId > 0) {
|
|
$stmt = $db->prepare("SELECT * FROM blog_posts WHERE id = :id LIMIT 1");
|
|
$stmt->execute([':id' => $postId]);
|
|
$post = $stmt->fetch();
|
|
if (!$post) {
|
|
header('Location: ' . BLOG_URL . '/admin/');
|
|
exit;
|
|
}
|
|
}
|
|
|
|
// Load categories
|
|
$categories = $db->query("SELECT * FROM blog_categories ORDER BY name")->fetchAll();
|
|
|
|
// Handle form submission
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
$title = trim($_POST['title'] ?? '');
|
|
$slug = trim($_POST['slug'] ?? '');
|
|
$excerpt = trim($_POST['excerpt'] ?? '');
|
|
$content = $_POST['content'] ?? '';
|
|
$categoryId = intval($_POST['category_id'] ?? 0);
|
|
$author = trim($_POST['author'] ?? 'TRIMAPE');
|
|
$status = ($_POST['status'] ?? 'draft') === 'published' ? 'published' : 'draft';
|
|
$coverImage = trim($_POST['cover_image'] ?? '');
|
|
|
|
if ($title === '') {
|
|
$error = 'El título es obligatorio.';
|
|
} else {
|
|
if ($slug === '') {
|
|
$slug = slugify($title);
|
|
}
|
|
|
|
// Ensure unique slug
|
|
$slugCheck = $db->prepare("SELECT id FROM blog_posts WHERE slug = :slug AND id != :id LIMIT 1");
|
|
$slugCheck->execute([':slug' => $slug, ':id' => $postId]);
|
|
if ($slugCheck->fetch()) {
|
|
$slug .= '-' . time();
|
|
}
|
|
|
|
$publishedAt = $status === 'published' ? ($post['published_at'] ?? date('Y-m-d H:i:s')) : null;
|
|
|
|
if ($post) {
|
|
// Update
|
|
$stmt = $db->prepare("UPDATE blog_posts SET
|
|
title = :title, slug = :slug, excerpt = :excerpt, content = :content,
|
|
category_id = :cat, author = :author, status = :status,
|
|
cover_image = :cover, published_at = :pub, updated_at = NOW()
|
|
WHERE id = :id");
|
|
$stmt->execute([
|
|
':title' => $title, ':slug' => $slug, ':excerpt' => $excerpt,
|
|
':content' => $content, ':cat' => $categoryId ?: null,
|
|
':author' => $author, ':status' => $status, ':cover' => $coverImage,
|
|
':pub' => $publishedAt, ':id' => $postId
|
|
]);
|
|
$_SESSION['flash'] = 'Artículo actualizado correctamente.';
|
|
} else {
|
|
// Insert
|
|
$stmt = $db->prepare("INSERT INTO blog_posts
|
|
(title, slug, excerpt, content, category_id, author, status, cover_image, published_at)
|
|
VALUES (:title, :slug, :excerpt, :content, :cat, :author, :status, :cover, :pub)");
|
|
$stmt->execute([
|
|
':title' => $title, ':slug' => $slug, ':excerpt' => $excerpt,
|
|
':content' => $content, ':cat' => $categoryId ?: null,
|
|
':author' => $author, ':status' => $status, ':cover' => $coverImage,
|
|
':pub' => $publishedAt
|
|
]);
|
|
$_SESSION['flash'] = 'Artículo creado correctamente.';
|
|
}
|
|
header('Location: ' . BLOG_URL . '/admin/');
|
|
exit;
|
|
}
|
|
}
|
|
|
|
// Prefill form
|
|
$formTitle = $post['title'] ?? ($_POST['title'] ?? '');
|
|
$formSlug = $post['slug'] ?? ($_POST['slug'] ?? '');
|
|
$formExcerpt = $post['excerpt'] ?? ($_POST['excerpt'] ?? '');
|
|
$formContent = $post['content'] ?? ($_POST['content'] ?? '');
|
|
$formCatId = $post['category_id'] ?? ($_POST['category_id'] ?? 0);
|
|
$formAuthor = $post['author'] ?? ($_POST['author'] ?? 'TRIMAPE');
|
|
$formStatus = $post['status'] ?? ($_POST['status'] ?? 'draft');
|
|
$formCover = $post['cover_image'] ?? ($_POST['cover_image'] ?? '');
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="es">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@100..900&display=swap" rel="stylesheet">
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<title><?= $post ? 'Editar' : 'Nuevo' ?> artículo - Blog Admin</title>
|
|
<style>
|
|
* { font-family: 'Montserrat', sans-serif; }
|
|
body { background: #f4f6fa; }
|
|
.admin-header {
|
|
background: linear-gradient(135deg, #0a1d3b, #0e55bb);
|
|
color: white; padding: 1rem 0;
|
|
}
|
|
.editor-card {
|
|
border: none; border-radius: 12px;
|
|
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
|
}
|
|
.form-control, .form-select {
|
|
border-radius: 10px; border: 2px solid #e8eef5;
|
|
padding: 10px 14px;
|
|
}
|
|
.form-control:focus, .form-select:focus {
|
|
border-color: #0e55bb;
|
|
box-shadow: 0 0 0 3px rgba(14,85,187,0.15);
|
|
}
|
|
.form-label { font-weight: 600; font-size: 0.85rem; color: #0a1d3b; }
|
|
.btn-save {
|
|
background: linear-gradient(135deg, #0e55bb, #1186cc);
|
|
border: none; color: white; font-weight: 700;
|
|
border-radius: 10px; padding: 10px 30px;
|
|
}
|
|
.btn-save:hover { color: white; box-shadow: 0 6px 16px rgba(14,85,187,0.3); }
|
|
.btn-draft {
|
|
background: #f0f4f8; color: #0a1d3b; font-weight: 600;
|
|
border: none; border-radius: 10px; padding: 10px 24px;
|
|
}
|
|
.cover-preview {
|
|
width: 100%; max-height: 200px; object-fit: cover;
|
|
border-radius: 10px; margin-top: 10px;
|
|
}
|
|
.tox-tinymce { border-radius: 10px !important; border: 2px solid #e8eef5 !important; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- Header -->
|
|
<div class="admin-header">
|
|
<div class="container d-flex justify-content-between align-items-center">
|
|
<div class="d-flex align-items-center gap-3">
|
|
<a href="<?= BLOG_URL ?>/admin/" class="text-white text-decoration-none">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
|
|
<path fill-rule="evenodd" d="M15 8a.5.5 0 0 0-.5-.5H2.707l3.147-3.146a.5.5 0 1 0-.708-.708l-4 4a.5.5 0 0 0 0 .708l4 4a.5.5 0 0 0 .708-.708L2.707 8.5H14.5A.5.5 0 0 0 15 8"/>
|
|
</svg>
|
|
</a>
|
|
<strong><?= $post ? 'Editar artículo' : 'Nuevo artículo' ?></strong>
|
|
</div>
|
|
<a href="<?= BLOG_URL ?>/admin/" class="btn btn-sm btn-outline-light">Dashboard</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container py-4">
|
|
<?php if ($error): ?>
|
|
<div class="alert alert-danger" style="border-radius: 10px;"><?= e($error) ?></div>
|
|
<?php endif; ?>
|
|
|
|
<form method="POST" id="postForm">
|
|
<div class="row g-4">
|
|
<!-- Main content -->
|
|
<div class="col-12 col-lg-8">
|
|
<div class="card editor-card p-4 mb-4">
|
|
<div class="mb-3">
|
|
<label class="form-label">Título</label>
|
|
<input type="text" name="title" class="form-control" value="<?= e($formTitle) ?>" required
|
|
placeholder="Escribe el título del artículo..." id="titleInput">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Slug (URL)</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text" style="border-radius: 10px 0 0 10px; border: 2px solid #e8eef5; border-right: none; font-size: 0.8rem; color: #888;">/blog/</span>
|
|
<input type="text" name="slug" class="form-control" value="<?= e($formSlug) ?>"
|
|
placeholder="se-genera-automaticamente" id="slugInput" style="border-radius: 0 10px 10px 0;">
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Extracto <small class="text-muted fw-normal">(opcional, se muestra en el listado)</small></label>
|
|
<textarea name="excerpt" class="form-control" rows="2" placeholder="Breve descripción del artículo..."><?= e($formExcerpt) ?></textarea>
|
|
</div>
|
|
<div class="mb-0">
|
|
<label class="form-label">Contenido</label>
|
|
<textarea name="content" id="editor"><?= e($formContent) ?></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sidebar -->
|
|
<div class="col-12 col-lg-4">
|
|
<!-- Publish -->
|
|
<div class="card editor-card p-4 mb-3">
|
|
<h6 class="fw-bold mb-3" style="color: #0a1d3b;">Publicación</h6>
|
|
<div class="mb-3">
|
|
<label class="form-label">Estado</label>
|
|
<select name="status" class="form-select">
|
|
<option value="draft" <?= $formStatus === 'draft' ? 'selected' : '' ?>>Borrador</option>
|
|
<option value="published" <?= $formStatus === 'published' ? 'selected' : '' ?>>Publicado</option>
|
|
</select>
|
|
</div>
|
|
<div class="d-grid gap-2">
|
|
<button type="submit" class="btn btn-save"><?= $post ? 'Actualizar' : 'Guardar' ?></button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Category -->
|
|
<div class="card editor-card p-4 mb-3">
|
|
<h6 class="fw-bold mb-3" style="color: #0a1d3b;">Categoría</h6>
|
|
<select name="category_id" class="form-select">
|
|
<option value="0">Sin categoría</option>
|
|
<?php foreach ($categories as $cat): ?>
|
|
<option value="<?= $cat['id'] ?>" <?= intval($formCatId) === intval($cat['id']) ? 'selected' : '' ?>>
|
|
<?= e($cat['name']) ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Author -->
|
|
<div class="card editor-card p-4 mb-3">
|
|
<h6 class="fw-bold mb-3" style="color: #0a1d3b;">Autor</h6>
|
|
<input type="text" name="author" class="form-control" value="<?= e($formAuthor) ?>">
|
|
</div>
|
|
|
|
<!-- Cover Image -->
|
|
<div class="card editor-card p-4 mb-3">
|
|
<h6 class="fw-bold mb-3" style="color: #0a1d3b;">Imagen de portada</h6>
|
|
<input type="text" name="cover_image" class="form-control mb-2" id="coverInput"
|
|
value="<?= e($formCover) ?>" placeholder="URL de la imagen...">
|
|
<input type="file" id="coverFile" class="form-control" accept="image/*">
|
|
<small class="text-muted mt-1 d-block">Sube una imagen o pega una URL</small>
|
|
<?php if ($formCover): ?>
|
|
<img src="<?= e($formCover) ?>" class="cover-preview" id="coverPreview" alt="Preview">
|
|
<?php else: ?>
|
|
<img src="" class="cover-preview d-none" id="coverPreview" alt="Preview">
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>
|
|
<script>
|
|
// TinyMCE
|
|
tinymce.init({
|
|
selector: '#editor',
|
|
height: 500,
|
|
menubar: 'file edit view insert format tools table',
|
|
plugins: 'advlist autolink lists link image charmap preview anchor searchreplace visualblocks code fullscreen insertdatetime media table help wordcount',
|
|
toolbar: 'undo redo | blocks | bold italic forecolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image | removeformat | code',
|
|
content_style: "body { font-family: 'Montserrat', sans-serif; font-size: 15px; line-height: 1.7; color: #333; }",
|
|
branding: false,
|
|
promotion: false,
|
|
language: 'es',
|
|
images_upload_url: 'upload.php',
|
|
automatic_uploads: true,
|
|
file_picker_types: 'image',
|
|
images_upload_handler: function(blobInfo) {
|
|
return new Promise(function(resolve, reject) {
|
|
var formData = new FormData();
|
|
formData.append('file', blobInfo.blob(), blobInfo.filename());
|
|
fetch('upload.php', { method: 'POST', body: formData })
|
|
.then(function(res) { return res.json(); })
|
|
.then(function(data) {
|
|
if (data.location) resolve(data.location);
|
|
else reject('Upload failed: ' + (data.error || 'Unknown error'));
|
|
})
|
|
.catch(function() { reject('Upload error'); });
|
|
});
|
|
}
|
|
});
|
|
|
|
// Auto-generate slug from title
|
|
var titleInput = document.getElementById('titleInput');
|
|
var slugInput = document.getElementById('slugInput');
|
|
var slugEdited = <?= $post ? 'true' : 'false' ?>;
|
|
|
|
slugInput.addEventListener('input', function() { slugEdited = true; });
|
|
titleInput.addEventListener('input', function() {
|
|
if (!slugEdited) {
|
|
var text = this.value.toLowerCase()
|
|
.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
|
|
.replace(/[^a-z0-9\s-]/g, '')
|
|
.trim().replace(/[\s]+/g, '-');
|
|
slugInput.value = text;
|
|
}
|
|
});
|
|
|
|
// Cover image upload
|
|
document.getElementById('coverFile').addEventListener('change', function() {
|
|
var file = this.files[0];
|
|
if (!file) return;
|
|
var formData = new FormData();
|
|
formData.append('file', file);
|
|
fetch('upload.php', { method: 'POST', body: formData })
|
|
.then(function(res) { return res.json(); })
|
|
.then(function(data) {
|
|
if (data.location) {
|
|
document.getElementById('coverInput').value = data.location;
|
|
var preview = document.getElementById('coverPreview');
|
|
preview.src = data.location;
|
|
preview.classList.remove('d-none');
|
|
} else {
|
|
alert('Error: ' + (data.error || 'No se pudo subir'));
|
|
}
|
|
});
|
|
});
|
|
|
|
// Preview cover from URL input
|
|
document.getElementById('coverInput').addEventListener('change', function() {
|
|
var preview = document.getElementById('coverPreview');
|
|
if (this.value) {
|
|
preview.src = this.value;
|
|
preview.classList.remove('d-none');
|
|
} else {
|
|
preview.classList.add('d-none');
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|