Initial production backup

This commit is contained in:
Tesscorp
2026-07-15 19:45:08 -06:00
commit 65b7af866f
433 changed files with 24089 additions and 0 deletions
+323
View File
@@ -0,0 +1,323 @@
<?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>