Initial production backup
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
RewriteEngine On
|
||||
RewriteBase /blog/
|
||||
|
||||
# Don't rewrite existing files or directories
|
||||
RewriteCond %{REQUEST_FILENAME} -f [OR]
|
||||
RewriteCond %{REQUEST_FILENAME} -d
|
||||
RewriteRule ^ - [L]
|
||||
|
||||
# /blog/admin/* - let admin handle its own routing
|
||||
RewriteRule ^admin/ - [L]
|
||||
|
||||
# /blog/categoria/slug → index.php?cat=slug
|
||||
RewriteRule ^categoria/([a-z0-9-]+)/?$ index.php?cat=$1 [L,QSA]
|
||||
|
||||
# /blog/pagina/2 → index.php?page=2
|
||||
RewriteRule ^pagina/([0-9]+)/?$ index.php?page=$1 [L,QSA]
|
||||
|
||||
# /blog/categoria/slug/pagina/2 → index.php?cat=slug&page=2
|
||||
RewriteRule ^categoria/([a-z0-9-]+)/pagina/([0-9]+)/?$ index.php?cat=$1&page=$2 [L,QSA]
|
||||
|
||||
# /blog/slug-del-post → post.php?slug=slug-del-post
|
||||
RewriteRule ^([a-z0-9-]+)/?$ post.php?slug=$1 [L,QSA]
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin Authentication Middleware
|
||||
*/
|
||||
session_start();
|
||||
|
||||
require_once dirname(__DIR__) . '/config.php';
|
||||
|
||||
function isLoggedIn(): bool {
|
||||
return isset($_SESSION['blog_admin_id']) && !empty($_SESSION['blog_admin_id']);
|
||||
}
|
||||
|
||||
function requireLogin(): void {
|
||||
if (!isLoggedIn()) {
|
||||
header('Location: ' . BLOG_URL . '/admin/login.php');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
function getAdminUser(): ?array {
|
||||
if (!isLoggedIn()) return null;
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("SELECT id, username FROM blog_admins WHERE id = :id LIMIT 1");
|
||||
$stmt->execute([':id' => $_SESSION['blog_admin_id']]);
|
||||
return $stmt->fetch() ?: null;
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/auth.php';
|
||||
requireLogin();
|
||||
|
||||
$db = getDB();
|
||||
$error = '';
|
||||
$success = '';
|
||||
|
||||
// Handle delete
|
||||
if (isset($_GET['delete'])) {
|
||||
$delId = intval($_GET['delete']);
|
||||
// Check if category has posts
|
||||
$stmt = $db->prepare("SELECT COUNT(*) FROM blog_posts WHERE category_id = :id");
|
||||
$stmt->execute([':id' => $delId]);
|
||||
$postCount = (int)$stmt->fetchColumn();
|
||||
|
||||
if ($postCount > 0) {
|
||||
$error = "No se puede eliminar: la categoría tiene $postCount artículo(s) asociado(s).";
|
||||
} else {
|
||||
$stmt = $db->prepare("DELETE FROM blog_categories WHERE id = :id");
|
||||
$stmt->execute([':id' => $delId]);
|
||||
$success = 'Categoría eliminada.';
|
||||
}
|
||||
}
|
||||
|
||||
// Handle create/update
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$catId = intval($_POST['cat_id'] ?? 0);
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$slug = trim($_POST['slug'] ?? '');
|
||||
|
||||
if ($name === '') {
|
||||
$error = 'El nombre es obligatorio.';
|
||||
} else {
|
||||
if ($slug === '') {
|
||||
$slug = slugify($name);
|
||||
}
|
||||
|
||||
if ($catId > 0) {
|
||||
// Update
|
||||
$stmt = $db->prepare("UPDATE blog_categories SET name = :name, slug = :slug WHERE id = :id");
|
||||
$stmt->execute([':name' => $name, ':slug' => $slug, ':id' => $catId]);
|
||||
$success = 'Categoría actualizada.';
|
||||
} else {
|
||||
// Insert
|
||||
$stmt = $db->prepare("INSERT INTO blog_categories (name, slug) VALUES (:name, :slug)");
|
||||
$stmt->execute([':name' => $name, ':slug' => $slug]);
|
||||
$success = 'Categoría creada.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load categories
|
||||
$categories = $db->query("SELECT c.*, COUNT(p.id) AS post_count
|
||||
FROM blog_categories c
|
||||
LEFT JOIN blog_posts p ON c.id = p.category_id
|
||||
GROUP BY c.id
|
||||
ORDER BY c.name")->fetchAll();
|
||||
|
||||
// Edit mode
|
||||
$editing = null;
|
||||
if (isset($_GET['edit'])) {
|
||||
$editId = intval($_GET['edit']);
|
||||
foreach ($categories as $cat) {
|
||||
if (intval($cat['id']) === $editId) { $editing = $cat; break; }
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!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>Categorías - 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;
|
||||
}
|
||||
.cat-card {
|
||||
border: none; border-radius: 12px;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
||||
}
|
||||
.form-control {
|
||||
border-radius: 10px; border: 2px solid #e8eef5; padding: 10px 14px;
|
||||
}
|
||||
.form-control:focus {
|
||||
border-color: #0e55bb; box-shadow: 0 0 0 3px rgba(14,85,187,0.15);
|
||||
}
|
||||
.btn-save {
|
||||
background: linear-gradient(135deg, #0e55bb, #1186cc);
|
||||
border: none; color: white; font-weight: 700; border-radius: 10px;
|
||||
}
|
||||
.btn-save:hover { color: white; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<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>Categorías</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container py-4">
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-danger" style="border-radius: 10px;"><?= e($error) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success" style="border-radius: 10px;"><?= e($success) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row g-4">
|
||||
<!-- Form -->
|
||||
<div class="col-12 col-md-4">
|
||||
<div class="card cat-card p-4">
|
||||
<h6 class="fw-bold mb-3" style="color: #0a1d3b;"><?= $editing ? 'Editar categoría' : 'Nueva categoría' ?></h6>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="cat_id" value="<?= $editing ? $editing['id'] : 0 ?>">
|
||||
<div class="mb-3">
|
||||
<input type="text" name="name" class="form-control" placeholder="Nombre" required
|
||||
value="<?= e($editing['name'] ?? '') ?>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<input type="text" name="slug" class="form-control" placeholder="slug (auto)"
|
||||
value="<?= e($editing['slug'] ?? '') ?>">
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-save px-4"><?= $editing ? 'Actualizar' : 'Crear' ?></button>
|
||||
<?php if ($editing): ?>
|
||||
<a href="categories.php" class="btn btn-outline-secondary" style="border-radius: 10px;">Cancelar</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- List -->
|
||||
<div class="col-12 col-md-8">
|
||||
<div class="card cat-card">
|
||||
<?php if (empty($categories)): ?>
|
||||
<div class="card-body text-center py-5 text-muted">No hay categorías aún.</div>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="ps-3" style="font-size: 0.75rem; text-transform: uppercase; color: #0a1d3b;">Nombre</th>
|
||||
<th style="font-size: 0.75rem; text-transform: uppercase; color: #0a1d3b;">Slug</th>
|
||||
<th style="font-size: 0.75rem; text-transform: uppercase; color: #0a1d3b;">Posts</th>
|
||||
<th class="text-end pe-3" style="font-size: 0.75rem; text-transform: uppercase; color: #0a1d3b;">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($categories as $cat): ?>
|
||||
<tr>
|
||||
<td class="ps-3 fw-bold"><?= e($cat['name']) ?></td>
|
||||
<td class="text-muted"><?= e($cat['slug']) ?></td>
|
||||
<td><span class="badge bg-secondary rounded-pill"><?= $cat['post_count'] ?></span></td>
|
||||
<td class="text-end pe-3">
|
||||
<a href="?edit=<?= $cat['id'] ?>" class="btn btn-sm btn-outline-primary" style="border-radius: 6px;">Editar</a>
|
||||
<a href="?delete=<?= $cat['id'] ?>" class="btn btn-sm btn-outline-danger" style="border-radius: 6px;"
|
||||
onclick="return confirm('¿Eliminar esta categoría?')">Eliminar</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/auth.php';
|
||||
requireLogin();
|
||||
|
||||
$db = getDB();
|
||||
$postId = intval($_GET['id'] ?? 0);
|
||||
|
||||
if ($postId > 0) {
|
||||
// Verify post exists
|
||||
$stmt = $db->prepare("SELECT id, cover_image FROM blog_posts WHERE id = :id LIMIT 1");
|
||||
$stmt->execute([':id' => $postId]);
|
||||
$post = $stmt->fetch();
|
||||
|
||||
if ($post) {
|
||||
// Delete post
|
||||
$stmt = $db->prepare("DELETE FROM blog_posts WHERE id = :id");
|
||||
$stmt->execute([':id' => $postId]);
|
||||
$_SESSION['flash'] = 'Artículo eliminado correctamente.';
|
||||
}
|
||||
}
|
||||
|
||||
header('Location: ' . BLOG_URL . '/admin/');
|
||||
exit;
|
||||
@@ -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>
|
||||
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/auth.php';
|
||||
requireLogin();
|
||||
|
||||
// Handle logout
|
||||
if (isset($_GET['logout'])) {
|
||||
session_destroy();
|
||||
header('Location: ' . BLOG_URL . '/admin/login.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
$admin = getAdminUser();
|
||||
|
||||
// Get all posts
|
||||
$posts = $db->query("SELECT p.*, c.name AS category_name
|
||||
FROM blog_posts p
|
||||
LEFT JOIN blog_categories c ON p.category_id = c.id
|
||||
ORDER BY p.created_at DESC")->fetchAll();
|
||||
|
||||
$totalPublished = 0;
|
||||
$totalDrafts = 0;
|
||||
foreach ($posts as $p) {
|
||||
if ($p['status'] === 'published') $totalPublished++;
|
||||
else $totalDrafts++;
|
||||
}
|
||||
|
||||
$categories = $db->query("SELECT COUNT(*) FROM blog_categories")->fetchColumn();
|
||||
|
||||
// Flash message
|
||||
$flash = $_SESSION['flash'] ?? '';
|
||||
unset($_SESSION['flash']);
|
||||
?>
|
||||
<!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>Dashboard - 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;
|
||||
}
|
||||
.stat-card {
|
||||
border: none; border-radius: 12px;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.stat-card:hover { transform: translateY(-3px); }
|
||||
.stat-card .stat-number {
|
||||
font-size: 2rem; font-weight: 800; color: #0a1d3b;
|
||||
}
|
||||
.stat-card .stat-label {
|
||||
font-size: 0.8rem; color: #888; text-transform: uppercase; letter-spacing: 1px;
|
||||
}
|
||||
.table-posts { border-radius: 12px; overflow: hidden; }
|
||||
.table-posts th {
|
||||
background: #f8fafd; color: #0a1d3b;
|
||||
font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.5px;
|
||||
border: none;
|
||||
}
|
||||
.table-posts td { vertical-align: middle; font-size: 0.9rem; }
|
||||
.badge-pub { background: #d4edda; color: #155724; }
|
||||
.badge-draft { background: #fff3cd; color: #856404; }
|
||||
.btn-new {
|
||||
background: linear-gradient(135deg, #0e55bb, #1186cc);
|
||||
border: none; color: white; font-weight: 700;
|
||||
border-radius: 10px; padding: 10px 24px;
|
||||
}
|
||||
.btn-new:hover { color: white; box-shadow: 0 6px 16px rgba(14,85,187,0.3); }
|
||||
</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">
|
||||
<img src="<?= SITE_URL ?>/assets/img/trimape-logo-512-150x150.png" alt="TRIMAPE" style="height: 36px; border-radius: 6px;">
|
||||
<div>
|
||||
<strong>Blog Admin</strong>
|
||||
<small class="d-block" style="opacity: 0.7; font-size: 0.75rem;">Hola, <?= e($admin['username'] ?? 'Admin') ?></small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="<?= BLOG_URL ?>" class="btn btn-sm btn-outline-light" target="_blank">Ver blog</a>
|
||||
<a href="?logout=1" class="btn btn-sm btn-outline-light">Cerrar sesión</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container py-4">
|
||||
<?php if ($flash): ?>
|
||||
<div class="alert alert-success alert-dismissible fade show" style="border-radius: 10px;">
|
||||
<?= e($flash) ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card stat-card p-3 text-center">
|
||||
<div class="stat-number"><?= $totalPublished ?></div>
|
||||
<div class="stat-label">Publicados</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card stat-card p-3 text-center">
|
||||
<div class="stat-number"><?= $totalDrafts ?></div>
|
||||
<div class="stat-label">Borradores</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card stat-card p-3 text-center">
|
||||
<div class="stat-number"><?= $categories ?></div>
|
||||
<div class="stat-label">Categorías</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card stat-card p-3 text-center">
|
||||
<div class="stat-number"><?= count($posts) ?></div>
|
||||
<div class="stat-label">Total Posts</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions bar -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="fw-bold mb-0" style="color: #0a1d3b;">Artículos</h5>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="categories.php" class="btn btn-sm btn-outline-primary" style="border-radius: 8px;">Categorías</a>
|
||||
<a href="editor.php" class="btn btn-sm btn-new">+ Nuevo artículo</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Posts table -->
|
||||
<div class="card border-0 shadow-sm" style="border-radius: 12px;">
|
||||
<?php if (empty($posts)): ?>
|
||||
<div class="card-body text-center py-5 text-muted">
|
||||
<p>No hay artículos aún.</p>
|
||||
<a href="editor.php" class="btn btn-new">Crear el primero</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-posts table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="ps-3">Título</th>
|
||||
<th>Categoría</th>
|
||||
<th>Estado</th>
|
||||
<th>Fecha</th>
|
||||
<th class="text-end pe-3">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($posts as $p): ?>
|
||||
<tr>
|
||||
<td class="ps-3 fw-bold"><?= e($p['title']) ?></td>
|
||||
<td><span class="text-muted"><?= e($p['category_name'] ?? '—') ?></span></td>
|
||||
<td>
|
||||
<span class="badge rounded-pill <?= $p['status'] === 'published' ? 'badge-pub' : 'badge-draft' ?>">
|
||||
<?= $p['status'] === 'published' ? 'Publicado' : 'Borrador' ?>
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-muted" style="font-size: 0.8rem;">
|
||||
<?= $p['published_at'] ? date('d/m/Y', strtotime($p['published_at'])) : '—' ?>
|
||||
</td>
|
||||
<td class="text-end pe-3">
|
||||
<a href="editor.php?id=<?= $p['id'] ?>" class="btn btn-sm btn-outline-primary" style="border-radius: 6px;">Editar</a>
|
||||
<a href="delete.php?id=<?= $p['id'] ?>" class="btn btn-sm btn-outline-danger" style="border-radius: 6px;"
|
||||
onclick="return confirm('¿Eliminar este artículo?')">Eliminar</a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/auth.php';
|
||||
|
||||
// If already logged in, redirect to dashboard
|
||||
if (isLoggedIn()) {
|
||||
header('Location: ' . BLOG_URL . '/admin/');
|
||||
exit;
|
||||
}
|
||||
|
||||
$error = '';
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$username = trim($_POST['username'] ?? '');
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
if ($username === '' || $password === '') {
|
||||
$error = 'Ingresa usuario y contraseña.';
|
||||
} else {
|
||||
$db = getDB();
|
||||
$stmt = $db->prepare("SELECT id, username, password_hash FROM blog_admins WHERE username = :u LIMIT 1");
|
||||
$stmt->execute([':u' => $username]);
|
||||
$admin = $stmt->fetch();
|
||||
|
||||
if ($admin && password_verify($password, $admin['password_hash'])) {
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['blog_admin_id'] = $admin['id'];
|
||||
header('Location: ' . BLOG_URL . '/admin/');
|
||||
exit;
|
||||
} else {
|
||||
$error = 'Usuario o contraseña incorrectos.';
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!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>Admin Login - Blog TRIMAPE</title>
|
||||
<style>
|
||||
* { font-family: 'Montserrat', sans-serif; }
|
||||
body {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #0a1d3b 0%, #0e55bb 50%, #1186cc 100%);
|
||||
}
|
||||
.login-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 3rem;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||
}
|
||||
.login-card .logo { height: 50px; margin-bottom: 1.5rem; }
|
||||
.login-card h4 { color: #0a1d3b; font-weight: 700; }
|
||||
.form-control {
|
||||
border-radius: 10px;
|
||||
padding: 12px 16px;
|
||||
border: 2px solid #e8eef5;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.form-control:focus {
|
||||
border-color: #0e55bb;
|
||||
box-shadow: 0 0 0 3px rgba(14,85,187,0.15);
|
||||
}
|
||||
.btn-login {
|
||||
background: linear-gradient(135deg, #0e55bb, #1186cc);
|
||||
border: none;
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
width: 100%;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.btn-login:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(14,85,187,0.35);
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-card text-center">
|
||||
<img src="<?= SITE_URL ?>/assets/img/trimape-logo-512-300x300.png" alt="TRIMAPE" class="logo">
|
||||
<h4 class="mb-1">Blog Admin</h4>
|
||||
<p class="text-muted mb-4" style="font-size: 0.85rem;">Inicia sesión para administrar el blog</p>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-danger py-2" style="font-size: 0.85rem; border-radius: 10px;"><?= e($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="POST" autocomplete="off">
|
||||
<div class="mb-3">
|
||||
<input type="text" name="username" class="form-control" placeholder="Usuario" required autofocus
|
||||
value="<?= e($username ?? '') ?>">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<input type="password" name="password" class="form-control" placeholder="Contraseña" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-login">Iniciar sesión</button>
|
||||
</form>
|
||||
|
||||
<a href="<?= BLOG_URL ?>" class="d-block mt-3 text-muted" style="font-size: 0.8rem;">← Volver al blog</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/auth.php';
|
||||
requireLogin();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
echo json_encode(['error' => 'Method not allowed']);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
|
||||
echo json_encode(['error' => 'No se recibió ningún archivo.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$file = $_FILES['file'];
|
||||
|
||||
// Validate size
|
||||
if ($file['size'] > MAX_UPLOAD_SIZE) {
|
||||
echo json_encode(['error' => 'El archivo excede el tamaño máximo de 5MB.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Validate type
|
||||
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml'];
|
||||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||
$mimeType = $finfo->file($file['tmp_name']);
|
||||
|
||||
if (!in_array($mimeType, $allowedTypes, true)) {
|
||||
echo json_encode(['error' => 'Tipo de archivo no permitido. Solo se aceptan imágenes (JPG, PNG, GIF, WebP, SVG).']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Validate extension
|
||||
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'];
|
||||
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
||||
|
||||
if (!in_array($extension, $allowedExtensions, true)) {
|
||||
echo json_encode(['error' => 'Extensión de archivo no permitida.']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Generate unique filename
|
||||
$filename = date('Y-m') . '-' . bin2hex(random_bytes(8)) . '.' . $extension;
|
||||
|
||||
// Create monthly subdirectory
|
||||
$monthDir = UPLOAD_DIR . date('Y') . '/' . date('m') . '/';
|
||||
if (!is_dir($monthDir)) {
|
||||
mkdir($monthDir, 0755, true);
|
||||
}
|
||||
|
||||
$destination = $monthDir . $filename;
|
||||
|
||||
if (move_uploaded_file($file['tmp_name'], $destination)) {
|
||||
$url = UPLOAD_URL . date('Y') . '/' . date('m') . '/' . $filename;
|
||||
echo json_encode(['location' => $url]);
|
||||
} else {
|
||||
echo json_encode(['error' => 'No se pudo guardar el archivo.']);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
/**
|
||||
* Blog Configuration - TRIMAPE
|
||||
* Conexión PDO a la base de datos trimape_blog (separada de producción)
|
||||
*/
|
||||
|
||||
// --- Database ---
|
||||
define('DB_HOST', 'localhost');
|
||||
define('DB_NAME', 'trimape_blog');
|
||||
define('DB_USER', 'trimape_blog_usr');
|
||||
define('DB_PASS', 'Tr1m4p3Bl0g2026!xK');
|
||||
define('DB_CHARSET', 'utf8mb4');
|
||||
|
||||
// --- Blog Settings ---
|
||||
define('BLOG_TITLE', 'Blog - TRIMAPE');
|
||||
define('BLOG_DESCRIPTION', 'Artículos sobre mantenimiento industrial, eficiencia energética y más.');
|
||||
define('BLOG_URL', 'https://trimape.mx/blog');
|
||||
define('SITE_URL', 'https://trimape.mx');
|
||||
define('POSTS_PER_PAGE', 6);
|
||||
define('UPLOAD_DIR', __DIR__ . '/uploads/');
|
||||
define('UPLOAD_URL', BLOG_URL . '/uploads/');
|
||||
define('MAX_UPLOAD_SIZE', 5 * 1024 * 1024); // 5MB
|
||||
|
||||
// --- PDO Connection ---
|
||||
function getDB(): PDO {
|
||||
static $pdo = null;
|
||||
if ($pdo === null) {
|
||||
$dsn = 'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=' . DB_CHARSET;
|
||||
$options = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
];
|
||||
$pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
|
||||
}
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
// --- Helper Functions ---
|
||||
function e(string $str): string {
|
||||
return htmlspecialchars($str, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
}
|
||||
|
||||
function slugify(string $text): string {
|
||||
$text = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $text);
|
||||
$text = preg_replace('/[^a-zA-Z0-9\s-]/', '', $text);
|
||||
$text = strtolower(trim($text));
|
||||
$text = preg_replace('/[\s-]+/', '-', $text);
|
||||
return $text;
|
||||
}
|
||||
|
||||
function truncate(string $text, int $length = 150): string {
|
||||
$text = strip_tags($text);
|
||||
if (mb_strlen($text) <= $length) return $text;
|
||||
return mb_substr($text, 0, $length) . '...';
|
||||
}
|
||||
|
||||
function timeAgo(string $datetime): string {
|
||||
$now = new DateTime();
|
||||
$ago = new DateTime($datetime);
|
||||
$diff = $now->diff($ago);
|
||||
|
||||
if ($diff->y > 0) return $diff->y . ' año' . ($diff->y > 1 ? 's' : '');
|
||||
if ($diff->m > 0) return $diff->m . ' mes' . ($diff->m > 1 ? 'es' : '');
|
||||
if ($diff->d > 0) return $diff->d . ' día' . ($diff->d > 1 ? 's' : '');
|
||||
if ($diff->h > 0) return $diff->h . ' hora' . ($diff->h > 1 ? 's' : '');
|
||||
return 'hace un momento';
|
||||
}
|
||||
|
||||
// Create uploads directory if not exists
|
||||
if (!is_dir(UPLOAD_DIR)) {
|
||||
mkdir(UPLOAD_DIR, 0755, true);
|
||||
}
|
||||
+359
@@ -0,0 +1,359 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
$db = getDB();
|
||||
|
||||
// Get category filter
|
||||
$catSlug = isset($_GET['cat']) ? trim($_GET['cat']) : '';
|
||||
$page = max(1, intval($_GET['page'] ?? 1));
|
||||
$offset = ($page - 1) * POSTS_PER_PAGE;
|
||||
|
||||
// Build query
|
||||
$where = "WHERE p.status = 'published'";
|
||||
$params = [];
|
||||
|
||||
if ($catSlug !== '') {
|
||||
$where .= " AND c.slug = :cat";
|
||||
$params[':cat'] = $catSlug;
|
||||
}
|
||||
|
||||
// Count total
|
||||
$countSql = "SELECT COUNT(*) FROM blog_posts p LEFT JOIN blog_categories c ON p.category_id = c.id $where";
|
||||
$stmt = $db->prepare($countSql);
|
||||
$stmt->execute($params);
|
||||
$totalPosts = (int)$stmt->fetchColumn();
|
||||
$totalPages = max(1, ceil($totalPosts / POSTS_PER_PAGE));
|
||||
|
||||
// Fetch posts
|
||||
$sql = "SELECT p.*, c.name AS category_name, c.slug AS category_slug
|
||||
FROM blog_posts p
|
||||
LEFT JOIN blog_categories c ON p.category_id = c.id
|
||||
$where
|
||||
ORDER BY p.published_at DESC
|
||||
LIMIT :limit OFFSET :offset";
|
||||
$stmt = $db->prepare($sql);
|
||||
foreach ($params as $k => $v) {
|
||||
$stmt->bindValue($k, $v, PDO::PARAM_STR);
|
||||
}
|
||||
$stmt->bindValue(':limit', POSTS_PER_PAGE, PDO::PARAM_INT);
|
||||
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
|
||||
$stmt->execute();
|
||||
$posts = $stmt->fetchAll();
|
||||
|
||||
// Fetch categories for sidebar
|
||||
$categories = $db->query("SELECT c.*, COUNT(p.id) AS post_count
|
||||
FROM blog_categories c
|
||||
LEFT JOIN blog_posts p ON c.id = p.category_id AND p.status = 'published'
|
||||
GROUP BY c.id
|
||||
ORDER BY c.name")->fetchAll();
|
||||
|
||||
// Current category name
|
||||
$currentCatName = '';
|
||||
if ($catSlug !== '') {
|
||||
foreach ($categories as $cat) {
|
||||
if ($cat['slug'] === $catSlug) {
|
||||
$currentCatName = $cat['name'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$pageTitle = $currentCatName ? e($currentCatName) . ' - Blog TRIMAPE' : 'Blog - TRIMAPE | Excelencia en Mantenimientos';
|
||||
$pageDesc = $currentCatName
|
||||
? 'Artículos sobre ' . e($currentCatName) . ' en el blog de TRIMAPE.'
|
||||
: 'Artículos sobre mantenimiento industrial, eficiencia energética y más. Blog de TRIMAPE.';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<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" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
<title><?= $pageTitle ?></title>
|
||||
<meta name="description" content="<?= e($pageDesc) ?>">
|
||||
<link rel="icon" href="<?= SITE_URL ?>/assets/img/favico.png">
|
||||
<link rel="icon" href="<?= SITE_URL ?>/assets/img/trimape-logo-512-150x150.png" sizes="32x32">
|
||||
<link rel="icon" href="<?= SITE_URL ?>/assets/img/trimape-logo-512-300x300.png" sizes="192x192">
|
||||
<link rel="apple-touch-icon" href="<?= SITE_URL ?>/assets/img/trimape-logo-512-300x300.png">
|
||||
|
||||
<meta property="og:title" content="<?= e($pageTitle) ?>">
|
||||
<meta property="og:description" content="<?= e($pageDesc) ?>">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="<?= BLOG_URL ?>">
|
||||
<meta property="og:image" content="<?= SITE_URL ?>/assets/img/trimape-1200.webp">
|
||||
|
||||
<?php include dirname(__DIR__) . '/css-columna.htm'; ?>
|
||||
<style>
|
||||
.hero { position: relative; width: 100%; height: 50vh; overflow: hidden; }
|
||||
.hero .imgback { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; z-index: -1; }
|
||||
|
||||
.blog-card {
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
transition: all 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
|
||||
}
|
||||
.blog-card:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 16px 40px rgba(10, 29, 59, 0.18);
|
||||
}
|
||||
.blog-card .card-img-wrap {
|
||||
overflow: hidden;
|
||||
height: 220px;
|
||||
}
|
||||
.blog-card .card-img-top {
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.5s ease;
|
||||
}
|
||||
.blog-card:hover .card-img-top {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
.blog-card .card-body { padding: 1.5rem; }
|
||||
.blog-card .badge-cat {
|
||||
background: linear-gradient(135deg, #0e55bb, #1186cc);
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
padding: 5px 12px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
.blog-card .card-title {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
color: #0a1d3b;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.blog-card .card-text {
|
||||
font-size: 0.875rem;
|
||||
color: #555;
|
||||
line-height: 1.6;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.blog-card .card-meta {
|
||||
font-size: 0.75rem;
|
||||
color: #888;
|
||||
}
|
||||
.blog-card .read-more {
|
||||
color: #1186cc;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.blog-card .read-more:hover { color: #0e55bb; }
|
||||
|
||||
.cat-sidebar .list-group-item {
|
||||
border: none;
|
||||
padding: 10px 16px;
|
||||
font-size: 0.9rem;
|
||||
border-radius: 8px !important;
|
||||
margin-bottom: 4px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.cat-sidebar .list-group-item:hover,
|
||||
.cat-sidebar .list-group-item.active {
|
||||
background: linear-gradient(135deg, #0e55bb, #1186cc);
|
||||
color: #fff;
|
||||
}
|
||||
.cat-sidebar .list-group-item.active .badge {
|
||||
background: rgba(255,255,255,0.25) !important;
|
||||
}
|
||||
|
||||
.pagination .page-link {
|
||||
border: none;
|
||||
color: #0a1d3b;
|
||||
font-weight: 600;
|
||||
border-radius: 8px !important;
|
||||
margin: 0 3px;
|
||||
}
|
||||
.pagination .page-item.active .page-link {
|
||||
background: linear-gradient(135deg, #0e55bb, #1186cc);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 80px 20px;
|
||||
}
|
||||
.empty-state svg {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
color: #ccc;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
a.blog-link { text-decoration: none; color: inherit; }
|
||||
a.blog-link:hover { color: inherit; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<?php include dirname(__DIR__) . '/menu-general.htm'; ?>
|
||||
|
||||
<!-- Hero -->
|
||||
<div class="hero" id="hero">
|
||||
<video autoplay loop muted playsinline class="w-100 imgback">
|
||||
<source src="<?= SITE_URL ?>/assets/videos/obra-civil-1.webm" type="video/webm">
|
||||
</video>
|
||||
<div class="container position-relative d-flex flex-column justify-content-center align-items-center h-100">
|
||||
<div class="text-center text-white">
|
||||
<h1 class="mb-1 pt-5 texto-sombra fw-bold z-2">BLOG</h1>
|
||||
<p class="texto-sombra fw-bold z-2">TRIMAPE | EXCELENCIA EN MANTENIMIENTOS</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Breadcrumbs -->
|
||||
<div class="container-fluid text-uppercase">
|
||||
<div class="container py-4">
|
||||
<nav style="--bs-breadcrumb-divider: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'%3E%3Cpath d='M2.5 0L1 1.5 3.5 4 1 6.5 2.5 8l4-4-4-4z' fill='currentColor'/%3E%3C/svg%3E");" aria-label="breadcrumb">
|
||||
<ol class="breadcrumb texto-bread">
|
||||
<li class="breadcrumb-item"><a href="<?= SITE_URL ?>">Portada</a></li>
|
||||
<?php if ($currentCatName): ?>
|
||||
<li class="breadcrumb-item"><a href="<?= BLOG_URL ?>">Blog</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page"><?= e($currentCatName) ?></li>
|
||||
<?php else: ?>
|
||||
<li class="breadcrumb-item active" aria-current="page">Blog</li>
|
||||
<?php endif; ?>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Blog Content -->
|
||||
<div class="container pb-5">
|
||||
<div class="row">
|
||||
<!-- Posts -->
|
||||
<div class="col-12 col-lg-8">
|
||||
<?php if ($currentCatName): ?>
|
||||
<h2 class="fw-bold mb-4" style="color: #0a1d3b;"><?= e($currentCatName) ?></h2>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (empty($posts)): ?>
|
||||
<div class="empty-state">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 7.5h1.5m-1.5 3h1.5m-7.5 3h7.5m-7.5 3h7.5m3-9h3.375c.621 0 1.125.504 1.125 1.125V18a2.25 2.25 0 01-2.25 2.25M16.5 7.5V18a2.25 2.25 0 002.25 2.25M16.5 7.5V4.875c0-.621-.504-1.125-1.125-1.125H4.125C3.504 3.75 3 4.254 3 4.875V18a2.25 2.25 0 002.25 2.25h13.5M6 7.5h3v3H6v-3z" />
|
||||
</svg>
|
||||
<h4 class="fw-bold" style="color: #0a1d3b;">Próximamente</h4>
|
||||
<p class="text-muted">Estamos preparando contenido increíble para ti. ¡Vuelve pronto!</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="row g-4">
|
||||
<?php foreach ($posts as $post): ?>
|
||||
<div class="col-12 col-md-6">
|
||||
<a href="<?= BLOG_URL ?>/<?= e($post['slug']) ?>" class="blog-link">
|
||||
<div class="card blog-card h-100">
|
||||
<div class="card-img-wrap">
|
||||
<?php if ($post['cover_image']): ?>
|
||||
<img src="<?= e($post['cover_image']) ?>" class="card-img-top" alt="<?= e($post['title']) ?>">
|
||||
<?php else: ?>
|
||||
<div class="card-img-top d-flex align-items-center justify-content-center" style="background: linear-gradient(135deg, #0a1d3b, #1186cc); height: 100%;">
|
||||
<img src="<?= SITE_URL ?>/assets/img/trimape-logo-512-300x300.png" alt="TRIMAPE" style="width: 80px; opacity: 0.5;">
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column">
|
||||
<?php if ($post['category_name']): ?>
|
||||
<span class="badge badge-cat align-self-start mb-2"><?= e($post['category_name']) ?></span>
|
||||
<?php endif; ?>
|
||||
<h5 class="card-title"><?= e($post['title']) ?></h5>
|
||||
<p class="card-text flex-grow-1"><?= e($post['excerpt'] ?: truncate($post['content'])) ?></p>
|
||||
<div class="d-flex justify-content-between align-items-center mt-3">
|
||||
<span class="card-meta">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-calendar3 me-1" viewBox="0 0 16 16">
|
||||
<path d="M14 0H2a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2M1 3.857C1 3.384 1.448 3 2 3h12c.552 0 1 .384 1 .857v10.286c0 .473-.448.857-1 .857H2c-.552 0-1-.384-1-.857z"/>
|
||||
<path d="M6.5 7a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2m-9 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2m-9 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2"/>
|
||||
</svg>
|
||||
<?= date('d M Y', strtotime($post['published_at'])) ?>
|
||||
</span>
|
||||
<span class="read-more">Leer más →</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<?php if ($totalPages > 1): ?>
|
||||
<nav class="mt-5 d-flex justify-content-center">
|
||||
<ul class="pagination">
|
||||
<?php
|
||||
$baseUrl = $catSlug ? BLOG_URL . "/categoria/$catSlug" : BLOG_URL;
|
||||
?>
|
||||
<?php if ($page > 1): ?>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="<?= $baseUrl ?>/pagina/<?= $page - 1 ?>">← Anterior</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php for ($i = 1; $i <= $totalPages; $i++): ?>
|
||||
<li class="page-item <?= $i === $page ? 'active' : '' ?>">
|
||||
<a class="page-link" href="<?= $i === 1 ? $baseUrl : $baseUrl . '/pagina/' . $i ?>"><?= $i ?></a>
|
||||
</li>
|
||||
<?php endfor; ?>
|
||||
|
||||
<?php if ($page < $totalPages): ?>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="<?= $baseUrl ?>/pagina/<?= $page + 1 ?>">Siguiente →</a>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
</ul>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="col-12 col-lg-4 mt-4 mt-lg-0">
|
||||
<div class="sticky-top" style="top: 100px;">
|
||||
<!-- Categories -->
|
||||
<div class="card border-0 shadow-sm mb-4" style="border-radius: 12px;">
|
||||
<div class="card-body cat-sidebar">
|
||||
<h6 class="fw-bold text-uppercase mb-3" style="color: #0a1d3b; letter-spacing: 1px; font-size: 0.8rem;">Categorías</h6>
|
||||
<div class="list-group list-group-flush">
|
||||
<a href="<?= BLOG_URL ?>" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center <?= $catSlug === '' ? 'active' : '' ?>">
|
||||
Todas
|
||||
<span class="badge bg-secondary rounded-pill"><?= $totalPosts ?></span>
|
||||
</a>
|
||||
<?php foreach ($categories as $cat): ?>
|
||||
<a href="<?= BLOG_URL ?>/categoria/<?= e($cat['slug']) ?>"
|
||||
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center <?= $catSlug === $cat['slug'] ? 'active' : '' ?>">
|
||||
<?= e($cat['name']) ?>
|
||||
<span class="badge bg-secondary rounded-pill"><?= $cat['post_count'] ?></span>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CTA -->
|
||||
<div class="card border-0 text-white text-center p-4" style="border-radius: 12px; background: linear-gradient(135deg, #0a1d3b 0%, #0e55bb 50%, #1186cc 100%);">
|
||||
<div class="card-body">
|
||||
<h5 class="fw-bold mb-3">¿Necesitas mantenimiento industrial?</h5>
|
||||
<p class="mb-3" style="font-size: 0.9rem; opacity: 0.9;">Contáctanos y recibe una cotización sin compromiso.</p>
|
||||
<a href="<?= SITE_URL ?>/contacto" class="btn px-4 py-2 fw-bold" style="background-color: rgb(255, 102, 0); color: white; border-radius: 8px;">CONTACTO</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include dirname(__DIR__) . '/contacto-div.htm'; ?>
|
||||
<?php include dirname(__DIR__) . '/footer.htm'; ?>
|
||||
<?php include dirname(__DIR__) . '/footer-scripts.htm'; ?>
|
||||
</body>
|
||||
</html>
|
||||
+343
@@ -0,0 +1,343 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
$slug = isset($_GET['slug']) ? trim($_GET['slug']) : '';
|
||||
|
||||
if ($slug === '') {
|
||||
header('Location: ' . BLOG_URL);
|
||||
exit;
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
|
||||
// Fetch post
|
||||
$stmt = $db->prepare("SELECT p.*, c.name AS category_name, c.slug AS category_slug
|
||||
FROM blog_posts p
|
||||
LEFT JOIN blog_categories c ON p.category_id = c.id
|
||||
WHERE p.slug = :slug AND p.status = 'published'
|
||||
LIMIT 1");
|
||||
$stmt->execute([':slug' => $slug]);
|
||||
$post = $stmt->fetch();
|
||||
|
||||
if (!$post) {
|
||||
http_response_code(404);
|
||||
$pageTitle = 'Artículo no encontrado - Blog TRIMAPE';
|
||||
$notFound = true;
|
||||
} else {
|
||||
$notFound = false;
|
||||
$pageTitle = e($post['title']) . ' - Blog TRIMAPE';
|
||||
$pageDesc = $post['excerpt'] ? e($post['excerpt']) : e(truncate($post['content'], 160));
|
||||
$coverImage = $post['cover_image'] ?: SITE_URL . '/assets/img/trimape-1200.webp';
|
||||
|
||||
// Related posts (same category, excluding current)
|
||||
$relatedStmt = $db->prepare("SELECT p.*, c.name AS category_name, c.slug AS category_slug
|
||||
FROM blog_posts p
|
||||
LEFT JOIN blog_categories c ON p.category_id = c.id
|
||||
WHERE p.status = 'published' AND p.id != :id AND p.category_id = :catid
|
||||
ORDER BY p.published_at DESC LIMIT 3");
|
||||
$relatedStmt->execute([':id' => $post['id'], ':catid' => $post['category_id']]);
|
||||
$related = $relatedStmt->fetchAll();
|
||||
|
||||
// If not enough related from same category, fill with recent
|
||||
if (count($related) < 3) {
|
||||
$excludeIds = array_merge([$post['id']], array_column($related, 'id'));
|
||||
$placeholders = implode(',', array_fill(0, count($excludeIds), '?'));
|
||||
$fillStmt = $db->prepare("SELECT p.*, c.name AS category_name, c.slug AS category_slug
|
||||
FROM blog_posts p
|
||||
LEFT JOIN blog_categories c ON p.category_id = c.id
|
||||
WHERE p.status = 'published' AND p.id NOT IN ($placeholders)
|
||||
ORDER BY p.published_at DESC LIMIT " . (3 - count($related)));
|
||||
$fillStmt->execute($excludeIds);
|
||||
$related = array_merge($related, $fillStmt->fetchAll());
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<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" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
<title><?= $pageTitle ?></title>
|
||||
<?php if (!$notFound): ?>
|
||||
<meta name="description" content="<?= $pageDesc ?>">
|
||||
<meta property="og:title" content="<?= e($post['title']) ?>">
|
||||
<meta property="og:description" content="<?= $pageDesc ?>">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:url" content="<?= BLOG_URL ?>/<?= e($post['slug']) ?>">
|
||||
<meta property="og:image" content="<?= e($coverImage) ?>">
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="<?= e($post['title']) ?>">
|
||||
<meta name="twitter:description" content="<?= $pageDesc ?>">
|
||||
<meta name="twitter:image" content="<?= e($coverImage) ?>">
|
||||
<?php endif; ?>
|
||||
<link rel="icon" href="<?= SITE_URL ?>/assets/img/favico.png">
|
||||
<link rel="icon" href="<?= SITE_URL ?>/assets/img/trimape-logo-512-150x150.png" sizes="32x32">
|
||||
<link rel="icon" href="<?= SITE_URL ?>/assets/img/trimape-logo-512-300x300.png" sizes="192x192">
|
||||
<link rel="apple-touch-icon" href="<?= SITE_URL ?>/assets/img/trimape-logo-512-300x300.png">
|
||||
|
||||
<?php include dirname(__DIR__) . '/css-columna.htm'; ?>
|
||||
<style>
|
||||
.post-hero {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 45vh;
|
||||
min-height: 350px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.post-hero img {
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.post-hero .overlay {
|
||||
position: absolute;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
background: linear-gradient(to top, rgba(10,29,59,0.85) 0%, rgba(10,29,59,0.3) 60%, transparent 100%);
|
||||
z-index: 1;
|
||||
}
|
||||
.post-hero .hero-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
padding-bottom: 3rem;
|
||||
}
|
||||
|
||||
.post-content {
|
||||
font-size: 1.05rem;
|
||||
line-height: 1.85;
|
||||
color: #333;
|
||||
}
|
||||
.post-content h2, .post-content h3, .post-content h4 {
|
||||
color: #0a1d3b;
|
||||
font-weight: 700;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.post-content p { margin-bottom: 1.25rem; }
|
||||
.post-content img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 10px;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
.post-content blockquote {
|
||||
border-left: 4px solid #0e55bb;
|
||||
padding: 1rem 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
background: #f0f7ff;
|
||||
border-radius: 0 8px 8px 0;
|
||||
font-style: italic;
|
||||
color: #444;
|
||||
}
|
||||
.post-content ul, .post-content ol {
|
||||
padding-left: 1.5rem;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
.post-content li { margin-bottom: 0.5rem; }
|
||||
|
||||
.post-meta {
|
||||
font-size: 0.85rem;
|
||||
color: rgba(255,255,255,0.8);
|
||||
}
|
||||
.post-meta .badge-cat {
|
||||
background: linear-gradient(135deg, #0e55bb, #1186cc);
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
padding: 5px 14px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.author-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 1.25rem;
|
||||
background: #f8fafd;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e8eef5;
|
||||
}
|
||||
.author-avatar {
|
||||
width: 48px; height: 48px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #0e55bb, #1186cc);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
color: white; font-weight: 700; font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.share-buttons a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px; height: 40px;
|
||||
border-radius: 10px;
|
||||
background: #f0f4f8;
|
||||
color: #555;
|
||||
transition: all 0.2s;
|
||||
text-decoration: none;
|
||||
}
|
||||
.share-buttons a:hover {
|
||||
background: #0e55bb;
|
||||
color: white;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.related-card {
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.related-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 12px 30px rgba(10,29,59,0.15);
|
||||
}
|
||||
.related-card img { height: 180px; object-fit: cover; }
|
||||
.related-card .card-title {
|
||||
font-size: 0.95rem; font-weight: 700; color: #0a1d3b;
|
||||
display: -webkit-box; -webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical; overflow: hidden;
|
||||
}
|
||||
a.related-link { text-decoration: none; color: inherit; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<?php include dirname(__DIR__) . '/menu-general.htm'; ?>
|
||||
|
||||
<?php if ($notFound): ?>
|
||||
<!-- 404 -->
|
||||
<div class="container py-5 text-center" style="min-height: 60vh; display: flex; flex-direction: column; justify-content: center;">
|
||||
<h1 class="display-1 fw-bold" style="color: #0a1d3b;">404</h1>
|
||||
<p class="lead">El artículo que buscas no existe o fue eliminado.</p>
|
||||
<a href="<?= BLOG_URL ?>" class="btn px-4 py-2 mt-3" style="background: linear-gradient(135deg, #0e55bb, #1186cc); color: white; border-radius: 8px;">Volver al blog</a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<!-- Post Hero -->
|
||||
<div class="post-hero">
|
||||
<?php if ($post['cover_image']): ?>
|
||||
<img src="<?= e($post['cover_image']) ?>" alt="<?= e($post['title']) ?>">
|
||||
<?php else: ?>
|
||||
<div style="width:100%;height:100%;background: linear-gradient(135deg, #0a1d3b 0%, #0e55bb 50%, #1186cc 100%);"></div>
|
||||
<?php endif; ?>
|
||||
<div class="overlay"></div>
|
||||
<div class="container hero-content">
|
||||
<div class="post-meta mb-2">
|
||||
<?php if ($post['category_name']): ?>
|
||||
<a href="<?= BLOG_URL ?>/categoria/<?= e($post['category_slug']) ?>" class="badge badge-cat text-decoration-none text-white"><?= e($post['category_name']) ?></a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<h1 class="text-white fw-bold mb-2" style="font-size: 2.2rem; line-height: 1.3; max-width: 800px;"><?= e($post['title']) ?></h1>
|
||||
<div class="post-meta d-flex align-items-center gap-3 flex-wrap">
|
||||
<span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-person me-1" viewBox="0 0 16 16">
|
||||
<path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6m2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0m4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4m-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10s-3.516.68-4.168 1.332c-.678.678-.83 1.418-.832 1.664z"/>
|
||||
</svg>
|
||||
<?= e($post['author'] ?: 'TRIMAPE') ?>
|
||||
</span>
|
||||
<span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-calendar3 me-1" viewBox="0 0 16 16">
|
||||
<path d="M14 0H2a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2M1 3.857C1 3.384 1.448 3 2 3h12c.552 0 1 .384 1 .857v10.286c0 .473-.448.857-1 .857H2c-.552 0-1-.384-1-.857z"/>
|
||||
<path d="M6.5 7a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2m-9 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2m-9 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2m3 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2"/>
|
||||
</svg>
|
||||
<?= date('d \d\e F, Y', strtotime($post['published_at'])) ?>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Post Content -->
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-lg-8">
|
||||
<!-- Share buttons -->
|
||||
<div class="share-buttons d-flex gap-2 mb-4">
|
||||
<a href="https://www.facebook.com/sharer/sharer.php?u=<?= urlencode(BLOG_URL . '/' . $post['slug']) ?>" target="_blank" rel="noopener" title="Compartir en Facebook">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M16 8.049c0-4.446-3.582-8.05-8-8.05C3.58 0-.002 3.603-.002 8.05c0 4.017 2.926 7.347 6.75 7.951v-5.625h-2.03V8.05H6.75V6.275c0-2.017 1.195-3.131 3.022-3.131.876 0 1.791.157 1.791.157v1.98h-1.009c-.993 0-1.303.621-1.303 1.258v1.51h2.218l-.354 2.326H9.25V16c3.824-.604 6.75-3.934 6.75-7.951"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://twitter.com/intent/tweet?url=<?= urlencode(BLOG_URL . '/' . $post['slug']) ?>&text=<?= urlencode($post['title']) ?>" target="_blank" rel="noopener" title="Compartir en X">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M12.6.75h2.454l-5.36 6.142L16 15.25h-4.937l-3.867-5.07-4.425 5.07H.316l5.733-6.57L0 .75h5.063l3.495 4.633L12.601.75Zm-.86 13.028h1.36L4.323 2.145H2.865z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/shareArticle?mini=true&url=<?= urlencode(BLOG_URL . '/' . $post['slug']) ?>&title=<?= urlencode($post['title']) ?>" target="_blank" rel="noopener" title="Compartir en LinkedIn">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M0 1.146C0 .513.526 0 1.175 0h13.65C15.474 0 16 .513 16 1.146v13.708c0 .633-.526 1.146-1.175 1.146H1.175C.526 16 0 15.487 0 14.854zm4.943 12.248V6.169H2.542v7.225zm-1.2-8.212c.837 0 1.358-.554 1.358-1.248-.015-.709-.52-1.248-1.342-1.248S2.4 3.226 2.4 3.934c0 .694.521 1.248 1.327 1.248zm4.908 8.212V9.359c0-.216.016-.432.08-.586.173-.431.568-.878 1.232-.878.869 0 1.216.662 1.216 1.634v3.865h2.401V9.25c0-2.22-1.184-3.252-2.764-3.252-1.274 0-1.845.7-2.165 1.193v.025h-.016l.016-.025V6.169h-2.4c.03.678 0 7.225 0 7.225z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://wa.me/?text=<?= urlencode($post['title'] . ' ' . BLOG_URL . '/' . $post['slug']) ?>" target="_blank" rel="noopener" title="Compartir en WhatsApp">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M13.601 2.326A7.85 7.85 0 0 0 7.994 0C3.627 0 .068 3.558.064 7.926c0 1.399.366 2.76 1.057 3.965L0 16l4.204-1.102a7.9 7.9 0 0 0 3.79.965h.004c4.368 0 7.926-3.558 7.93-7.93A7.9 7.9 0 0 0 13.6 2.326zM7.994 14.521a6.6 6.6 0 0 1-3.356-.92l-.24-.144-2.494.654.666-2.433-.156-.251a6.56 6.56 0 0 1-1.007-3.505c0-3.626 2.957-6.584 6.591-6.584a6.56 6.56 0 0 1 4.66 1.931 6.56 6.56 0 0 1 1.928 4.66c-.004 3.639-2.961 6.592-6.592 6.592m3.615-4.934c-.197-.099-1.17-.578-1.353-.646-.182-.065-.315-.099-.445.099-.133.197-.513.646-.627.775-.114.133-.232.148-.43.05-.197-.1-.836-.308-1.592-.985-.59-.525-.985-1.175-1.103-1.372-.114-.198-.011-.304.088-.403.087-.088.197-.232.296-.346.1-.114.133-.198.198-.33.065-.134.034-.248-.015-.347-.05-.099-.445-1.076-.612-1.47-.16-.389-.323-.335-.445-.34-.114-.007-.247-.007-.38-.007a.73.73 0 0 0-.529.247c-.182.198-.691.677-.691 1.654s.71 1.916.81 2.049c.098.133 1.394 2.132 3.383 2.992.47.205.84.326 1.129.418.475.152.904.129 1.246.08.38-.058 1.171-.48 1.338-.943.164-.464.164-.86.114-.943-.049-.084-.182-.133-.38-.232"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Article body -->
|
||||
<article class="post-content">
|
||||
<?= $post['content'] ?>
|
||||
</article>
|
||||
|
||||
<!-- Author -->
|
||||
<div class="author-box mt-5">
|
||||
<div class="author-avatar"><?= strtoupper(mb_substr($post['author'] ?: 'T', 0, 1)) ?></div>
|
||||
<div>
|
||||
<div class="fw-bold" style="color: #0a1d3b;"><?= e($post['author'] ?: 'TRIMAPE') ?></div>
|
||||
<div style="font-size: 0.8rem; color: #888;">Publicado el <?= date('d \d\e F, Y', strtotime($post['published_at'])) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Back to blog -->
|
||||
<div class="text-center mt-5">
|
||||
<a href="<?= BLOG_URL ?>" class="btn px-4 py-2 fw-bold" style="background: linear-gradient(135deg, #0e55bb, #1186cc); color: white; border-radius: 8px;">
|
||||
← Volver al blog
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Related Posts -->
|
||||
<?php if (!empty($related)): ?>
|
||||
<div class="mt-5 pt-4 border-top">
|
||||
<h4 class="fw-bold mb-4 text-center" style="color: #0a1d3b;">Artículos relacionados</h4>
|
||||
<div class="row g-4">
|
||||
<?php foreach ($related as $rel): ?>
|
||||
<div class="col-12 col-md-4">
|
||||
<a href="<?= BLOG_URL ?>/<?= e($rel['slug']) ?>" class="related-link">
|
||||
<div class="card related-card h-100">
|
||||
<?php if ($rel['cover_image']): ?>
|
||||
<img src="<?= e($rel['cover_image']) ?>" class="card-img-top" alt="<?= e($rel['title']) ?>">
|
||||
<?php else: ?>
|
||||
<div class="card-img-top d-flex align-items-center justify-content-center" style="background: linear-gradient(135deg, #0a1d3b, #1186cc); height: 180px;">
|
||||
<img src="<?= SITE_URL ?>/assets/img/trimape-logo-512-300x300.png" alt="TRIMAPE" style="width: 60px; opacity: 0.4;">
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="card-body">
|
||||
<h6 class="card-title"><?= e($rel['title']) ?></h6>
|
||||
<small class="text-muted"><?= date('d M Y', strtotime($rel['published_at'])) ?></small>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php include dirname(__DIR__) . '/contacto-div.htm'; ?>
|
||||
<?php include dirname(__DIR__) . '/footer.htm'; ?>
|
||||
<?php include dirname(__DIR__) . '/footer-scripts.htm'; ?>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user