blog+contacto php listo

This commit is contained in:
2026-09-20 13:28:57 -03:00
parent cd67fca3dc
commit cfb97b486e
24 changed files with 8970 additions and 165 deletions
+19
View File
@@ -0,0 +1,19 @@
<?php
require_once '../config/db.php';
// Verificar si el usuario está logueado
function verificarAuth() {
if (!isset($_SESSION['admin_id'])) {
header('Location: login.php');
exit;
}
}
// Obtener datos del admin logueado
function getAdminActual() {
global $pdo;
$stmt = $pdo->prepare("SELECT id, nombre, email FROM admin_users WHERE id = ?");
$stmt->execute([$_SESSION['admin_id']]);
return $stmt->fetch();
}
?>
+176
View File
@@ -0,0 +1,176 @@
<?php
require_once '../config/db.php';
require_once 'auth.php';
verificarAuth();
// Obtener todos los artículos
$stmt = $pdo->prepare("
SELECT p.*, u.nombre as autor_nombre
FROM blog_posts p
LEFT JOIN admin_users u ON p.autor_id = u.id
ORDER BY p.created_at DESC
");
$stmt->execute();
$posts = $stmt->fetchAll();
$admin = getAdminActual();
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard - Blog Amor y Roma</title>
<link rel="stylesheet" href="../style/main.css">
<style>
.admin-header {
background: white;
padding: 1.5rem 2rem;
border-bottom: 1px solid rgba(0, 63, 82, 0.12);
display: flex;
justify-content: space-between;
align-items: center;
}
.admin-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.btn-nuevo {
background: #ba124d;
color: white;
padding: 0.75rem 1.5rem;
border-radius: 24px;
text-decoration: none;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-nuevo:hover {
background: #9c0f40;
}
.posts-table {
width: 100%;
border-collapse: collapse;
margin-top: 2rem;
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
}
.posts-table th,
.posts-table td {
padding: 1rem;
text-align: left;
border-bottom: 1px solid rgba(0, 63, 82, 0.12);
}
.posts-table th {
background: #f5f9f8;
font-weight: 600;
color: #003f52;
font-size: 0.85rem;
text-transform: uppercase;
}
.posts-table tr:hover {
background: #f9f9f9;
}
.estado-badge {
padding: 0.25rem 0.75rem;
border-radius: 999px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
}
.estado-publicado {
background: #d4edda;
color: #155724;
}
.estado-borrador {
background: #fff3cd;
color: #856404;
}
.acciones {
display: flex;
gap: 0.5rem;
}
.btn-accion {
padding: 0.4rem 0.8rem;
border-radius: 6px;
text-decoration: none;
font-size: 0.85rem;
font-weight: 600;
}
.btn-editar {
background: #003f52;
color: white;
}
.btn-eliminar {
background: #dc3545;
color: white;
}
.logout-link {
color: #ba124d;
text-decoration: none;
font-weight: 600;
}
</style>
</head>
<body>
<div class="admin-header">
<div>
<h2 style="margin: 0; font-family: 'Literata', serif;">Hola, <?= htmlspecialchars($admin['nombre']) ?></h2>
<p style="margin: 0.25rem 0 0; color: #666; font-size: 0.9rem;">Gestión de artículos del blog</p>
</div>
<div style="display: flex; gap: 1rem; align-items: center;">
<a href="../blog.php" target="_blank" class="btn-accion" style="background: #eaf0ee; color: #003f52;">Ver Blog</a>
<a href="editor.php" class="btn-nuevo">+ Nuevo Artículo</a>
<a href="logout.php" class="logout-link">Cerrar sesión</a>
</div>
</div>
<div class="admin-container">
<table class="posts-table">
<thead>
<tr>
<th>Título</th>
<th>Categoría</th>
<th>Autor</th>
<th>Estado</th>
<th>Fecha</th>
<th>Vistas</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
<?php if (empty($posts)): ?>
<tr>
<td colspan="7" style="text-align: center; padding: 3rem; color: #666;">
No hay artículos creados aún. <a href="editor.php" style="color: #ba124d;">Crear el primero</a>
</td>
</tr>
<?php else: ?>
<?php foreach ($posts as $post): ?>
<tr>
<td><?= htmlspecialchars($post['titulo']) ?></td>
<td><?= htmlspecialchars($post['categoria']) ?></td>
<td><?= htmlspecialchars($post['autor_nombre']) ?></td>
<td>
<span class="estado-badge estado-<?= $post['estado'] ?>">
<?= $post['estado'] ?>
</span>
</td>
<td><?= date('d/m/Y', strtotime($post['created_at'])) ?></td>
<td><?= $post['vistas'] ?></td>
<td class="acciones">
<a href="editor.php?id=<?= $post['id'] ?>" class="btn-accion btn-editar">Editar</a>
<a href="eliminar.php?id=<?= $post['id'] ?>" class="btn-accion btn-eliminar" onclick="return confirm('¿Eliminar este artículo?')">Eliminar</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</body>
</html>
+294
View File
@@ -0,0 +1,294 @@
<?php
require_once '../config/db.php';
require_once 'auth.php';
verificarAuth();
$admin = getAdminActual();
$post = null;
$modo_edicion = false;
// Si viene ID, es edición
if (isset($_GET['id'])) {
$stmt = $pdo->prepare("SELECT * FROM blog_posts WHERE id = ?");
$stmt->execute([$_GET['id']]);
$post = $stmt->fetch();
if (!$post) {
die('Artículo no encontrado');
}
$modo_edicion = true;
}
// Procesar formulario
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$titulo = trim($_POST['titulo']);
$slug = strtolower(trim(preg_replace('/[^A-Za-z0-9-]+/', '-', $titulo)));
$extracto = trim($_POST['extracto']);
$contenido = $_POST['contenido'];
$categoria = $_POST['categoria'];
$tags = trim($_POST['tags']);
$destacado = isset($_POST['destacado']) ? 1 : 0;
$estado = $_POST['estado'];
$tiempo_lectura = (int)$_POST['tiempo_lectura'];
$autor_id = $admin['id'];
// Subida de imagen
$imagen = $post['imagen'] ?? '';
if (isset($_FILES['imagen']) && $_FILES['imagen']['error'] === UPLOAD_ERR_OK) {
$allowed = ['image/jpeg', 'image/png', 'image/webp'];
$max_size = 5 * 1024 * 1024; // 5MB
if (!in_array($_FILES['imagen']['type'], $allowed)) {
$error = 'Solo se permiten imágenes JPG, PNG o WebP';
} elseif ($_FILES['imagen']['size'] > $max_size) {
$error = 'La imagen no puede superar los 5MB';
} else {
$extension = pathinfo($_FILES['imagen']['name'], PATHINFO_EXTENSION);
$nombre_imagen = $slug . '-' . time() . '.' . $extension;
$ruta_destino = '../assets/blog/' . $nombre_imagen;
if (!is_dir('../assets/blog')) {
mkdir('../assets/blog', 0755, true);
}
if (move_uploaded_file($_FILES['imagen']['tmp_name'], $ruta_destino)) {
$imagen = 'assets/blog/' . $nombre_imagen;
} else {
$error = 'Error al subir la imagen';
}
}
}
if (!isset($error)) {
if ($modo_edicion) {
$stmt = $pdo->prepare("
UPDATE blog_posts SET
titulo = ?, slug = ?, extracto = ?, contenido = ?,
categoria = ?, tags = ?, imagen = ?, destacado = ?,
estado = ?, tiempo_lectura = ?
WHERE id = ?
");
$stmt->execute([
$titulo, $slug, $extracto, $contenido,
$categoria, $tags, $imagen, $destacado,
$estado, $tiempo_lectura, $post['id']
]);
} else {
$stmt = $pdo->prepare("
INSERT INTO blog_posts
(titulo, slug, extracto, contenido, categoria, tags, imagen, autor_id, destacado, estado, tiempo_lectura)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$titulo, $slug, $extracto, $contenido,
$categoria, $tags, $imagen, $autor_id, $destacado,
$estado, $tiempo_lectura
]);
}
header('Location: dashboard.php');
exit;
}
}
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= $modo_edicion ? 'Editar' : 'Nuevo' ?> Artículo - Amor y Roma</title>
<script src="https://cdn.tiny.mce.com/1/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>
<link rel="stylesheet" href="../style/main.css">
<style>
.admin-header {
background: white;
padding: 1.5rem 2rem;
border-bottom: 1px solid rgba(0, 63, 82, 0.12);
display: flex;
justify-content: space-between;
align-items: center;
}
.editor-container {
max-width: 1000px;
margin: 0 auto;
padding: 2rem;
}
.form-group {
margin-bottom: 1.5rem;
}
label {
display: block;
margin-bottom: 0.5rem;
color: #003f52;
font-weight: 600;
}
input[type="text"],
input[type="number"],
select,
textarea {
width: 100%;
padding: 0.75rem;
border: 1.5px solid rgba(0, 63, 82, 0.2);
border-radius: 8px;
font-size: 1rem;
box-sizing: border-box;
font-family: 'Raleway', sans-serif;
}
textarea {
resize: vertical;
min-height: 100px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.checkbox-group input[type="checkbox"] {
width: 20px;
height: 20px;
cursor: pointer;
}
.btn-guardar {
background: #ba124d;
color: white;
padding: 0.85rem 2rem;
border: none;
border-radius: 24px;
font-weight: 600;
cursor: pointer;
font-size: 1rem;
}
.btn-guardar:hover {
background: #9c0f40;
}
.btn-cancelar {
background: #eaf0ee;
color: #003f52;
padding: 0.85rem 2rem;
border: none;
border-radius: 24px;
font-weight: 600;
cursor: pointer;
font-size: 1rem;
text-decoration: none;
display: inline-block;
}
.imagen-preview {
margin-top: 1rem;
max-width: 300px;
}
.imagen-preview img {
width: 100%;
border-radius: 8px;
}
.error {
background: #fee;
color: #c00;
padding: 1rem;
border-radius: 8px;
margin-bottom: 1rem;
}
</style>
<script>
tinymce.init({
selector: '#contenido',
height: 500,
plugins: 'anchor autolink charmap codesample emoticons image link lists media searchreplace table visualblocks wordcount',
toolbar: 'undo redo | blocks fontfamily fontsize | bold italic underline strikethrough | link image media table | align lineheight | numlist bullist indent outdent | emoticons charmap | removeformat',
content_style: 'body { font-family: Raleway, sans-serif; font-size: 16px; }',
language: 'es'
});
</script>
</head>
<body>
<div class="admin-header">
<h2 style="margin: 0; font-family: 'Literata', serif;"><?= $modo_edicion ? 'Editar' : 'Nuevo' ?> Artículo</h2>
<a href="dashboard.php" class="btn-cancelar">← Volver al dashboard</a>
</div>
<div class="editor-container">
<?php if (isset($error)): ?>
<div class="error"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<form method="POST" enctype="multipart/form-data">
<div class="form-group">
<label for="titulo">Título del artículo *</label>
<input type="text" id="titulo" name="titulo" required value="<?= htmlspecialchars($post['titulo'] ?? '') ?>">
</div>
<div class="form-group">
<label for="extracto">Extracto (resumen corto) *</label>
<textarea id="extracto" name="extracto" required><?= htmlspecialchars($post['extracto'] ?? '') ?></textarea>
</div>
<div class="form-group">
<label for="contenido">Contenido completo *</label>
<textarea id="contenido" name="contenido" required><?= htmlspecialchars($post['contenido'] ?? '') ?></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="categoria">Categoría *</label>
<select id="categoria" name="categoria" required>
<option value="">Seleccionar...</option>
<option value="branding" <?= ($post['categoria'] ?? '') === 'branding' ? 'selected' : '' ?>>Branding</option>
<option value="web" <?= ($post['categoria'] ?? '') === 'web' ? 'selected' : '' ?>>Web</option>
<option value="redes" <?= ($post['categoria'] ?? '') === 'redes' ? 'selected' : '' ?>>Redes</option>
<option value="automatizaciones" <?= ($post['categoria'] ?? '') === 'automatizaciones' ? 'selected' : '' ?>>Automatizaciones</option>
<option value="paid" <?= ($post['categoria'] ?? '') === 'paid' ? 'selected' : '' ?>>Paid Media</option>
<option value="estrategia" <?= ($post['categoria'] ?? '') === 'estrategia' ? 'selected' : '' ?>>Estrategia</option>
<option value="casos" <?= ($post['categoria'] ?? '') === 'casos' ? 'selected' : '' ?>>Casos</option>
</select>
</div>
<div class="form-group">
<label for="estado">Estado *</label>
<select id="estado" name="estado" required>
<option value="borrador" <?= ($post['estado'] ?? 'borrador') === 'borrador' ? 'selected' : '' ?>>Borrador</option>
<option value="publicado" <?= ($post['estado'] ?? '') === 'publicado' ? 'selected' : '' ?>>Publicado</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="tags">Tags (separados por coma)</label>
<input type="text" id="tags" name="tags" value="<?= htmlspecialchars($post['tags'] ?? '') ?>" placeholder="ej: branding, identidad, logo">
</div>
<div class="form-group">
<label for="tiempo_lectura">Tiempo de lectura (minutos)</label>
<input type="number" id="tiempo_lectura" name="tiempo_lectura" min="1" value="<?= $post['tiempo_lectura'] ?? 5 ?>">
</div>
</div>
<div class="form-group">
<label for="imagen">Imagen destacada *</label>
<input type="file" id="imagen" name="imagen" accept="image/*" <?= !$modo_edicion ? 'required' : '' ?>>
<?php if ($post && $post['imagen']): ?>
<div class="imagen-preview">
<img src="../<?= htmlspecialchars($post['imagen']) ?>" alt="Imagen actual">
<p style="font-size: 0.85rem; color: #666; margin-top: 0.5rem;">Imagen actual: <?= basename($post['imagen']) ?></p>
</div>
<?php endif; ?>
</div>
<div class="form-group">
<div class="checkbox-group">
<input type="checkbox" id="destacado" name="destacado" value="1" <?= ($post['destacado'] ?? 0) ? 'checked' : '' ?>>
<label for="destacado" style="margin: 0;">Marcar como destacado (aparece en la sección "Más leídos")</label>
</div>
</div>
<div style="display: flex; gap: 1rem; margin-top: 2rem;">
<button type="submit" class="btn-guardar"> Guardar Artículo</button>
<a href="dashboard.php" class="btn-cancelar">Cancelar</a>
</div>
</form>
</div>
</body>
</html>
+29
View File
@@ -0,0 +1,29 @@
<?php
require_once '../config/db.php';
require_once 'auth.php';
verificarAuth();
if (!isset($_GET['id'])) {
header('Location: dashboard.php');
exit;
}
// Obtener el artículo para borrar la imagen
$stmt = $pdo->prepare("SELECT imagen FROM blog_posts WHERE id = ?");
$stmt->execute([$_GET['id']]);
$post = $stmt->fetch();
if ($post && $post['imagen']) {
$ruta_imagen = '../' . $post['imagen'];
if (file_exists($ruta_imagen)) {
unlink($ruta_imagen);
}
}
// Eliminar el artículo
$stmt = $pdo->prepare("DELETE FROM blog_posts WHERE id = ?");
$stmt->execute([$_GET['id']]);
header('Location: dashboard.php');
exit;
?>
+130
View File
@@ -0,0 +1,130 @@
<?php
require_once '../config/db.php';
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = trim($_POST['email']);
$password = $_POST['password'];
$stmt = $pdo->prepare("SELECT id, nombre, password FROM admin_users WHERE email = ?");
$stmt->execute([$email]);
$user = $stmt->fetch();
// Comparación directa sin hash (como pediste)
if ($user && $password === $user['password']) {
$_SESSION['admin_id'] = $user['id'];
$_SESSION['admin_nombre'] = $user['nombre'];
header('Location: dashboard.php');
exit;
} else {
$error = 'Email o contraseña incorrectos';
}
}
?>
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Login - Amor y Roma</title>
<style>
body {
font-family: 'Raleway', sans-serif;
background: #eaf0ee;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
}
.login-container {
background: white;
padding: 3rem;
border-radius: 16px;
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
width: 100%;
max-width: 400px;
}
h1 {
color: #221728;
margin-bottom: 0.5rem;
font-family: 'Literata', serif;
font-style: italic;
}
.subtitle {
color: #666;
margin-bottom: 2rem;
font-size: 0.9rem;
}
.form-group {
margin-bottom: 1.5rem;
}
label {
display: block;
margin-bottom: 0.5rem;
color: #003f52;
font-weight: 600;
font-size: 0.9rem;
}
input[type="email"],
input[type="password"] {
width: 100%;
padding: 0.75rem;
border: 1.5px solid rgba(0, 63, 82, 0.2);
border-radius: 8px;
font-size: 1rem;
box-sizing: border-box;
}
input:focus {
outline: none;
border-color: #ba124d;
}
.btn-login {
width: 100%;
padding: 0.85rem;
background: #ba124d;
color: white;
border: none;
border-radius: 24px;
font-weight: 600;
cursor: pointer;
font-size: 1rem;
transition: background 0.3s;
}
.btn-login:hover {
background: #9c0f40;
}
.error {
background: #fee;
color: #c00;
padding: 0.75rem;
border-radius: 8px;
margin-bottom: 1rem;
font-size: 0.9rem;
}
</style>
</head>
<body>
<div class="login-container">
<h1>Amor y Roma</h1>
<p class="subtitle">Panel de Administración del Blog</p>
<?php if ($error): ?>
<div class="error"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<form method="POST">
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" required autofocus>
</div>
<div class="form-group">
<label for="password">Contraseña</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit" class="btn-login">Ingresar</button>
</form>
</div>
</body>
</html>
+6
View File
@@ -0,0 +1,6 @@
<?php
session_start();
session_destroy();
header('Location: login.php');
exit;
?>
+117
View File
@@ -0,0 +1,117 @@
<?php
require_once 'config/db.php';
if (!isset($_GET['id'])) {
header('Location: blog.php');
exit;
}
// Obtener artículo
$stmt = $pdo->prepare("
SELECT p.*, u.nombre as autor_nombre
FROM blog_posts p
LEFT JOIN admin_users u ON p.autor_id = u.id
WHERE p.id = ? AND p.estado = 'publicado'
");
$stmt->execute([$_GET['id']]);
$post = $stmt->fetch();
if (!$post) {
header('Location: blog.php');
exit;
}
// Incrementar vistas
$stmt = $pdo->prepare("UPDATE blog_posts SET vistas = vistas + 1 WHERE id = ?");
$stmt->execute([$post['id']]);
?>
<!DOCTYPE html>
<html lang="es" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="<?= htmlspecialchars($post['extracto']) ?>">
<title><?= htmlspecialchars($post['titulo']) ?> | Blog Amor y Roma</title>
<link rel="stylesheet" href="style/main.css">
<style>
.articulo-header {
max-width: 900px;
margin: 0 auto;
padding: 4rem 2rem 2rem;
}
.articulo-imagen {
width: 100%;
max-height: 500px;
object-fit: cover;
border-radius: 16px;
margin: 2rem 0;
}
.articulo-contenido {
max-width: 800px;
margin: 0 auto;
padding: 0 2rem 4rem;
font-size: 1.1rem;
line-height: 1.8;
}
.articulo-contenido h2,
.articulo-contenido h3 {
margin-top: 2.5rem;
margin-bottom: 1rem;
}
.articulo-contenido p {
margin-bottom: 1.5rem;
}
.articulo-contenido img {
max-width: 100%;
height: auto;
border-radius: 8px;
margin: 2rem 0;
}
.articulo-meta {
display: flex;
gap: 2rem;
color: #666;
font-size: 0.9rem;
margin: 1rem 0 2rem;
padding: 1rem 0;
border-top: 1px solid rgba(0, 63, 82, 0.12);
border-bottom: 1px solid rgba(0, 63, 82, 0.12);
}
.volver-link {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: #ba124d;
text-decoration: none;
font-weight: 600;
margin-bottom: 2rem;
}
</style>
</head>
<body>
<div id="menu-container"></div>
<main>
<article>
<div class="articulo-header">
<a href="blog.php" class="volver-link">← Volver al blog</a>
<span class="badge-filosofia"><?= ucfirst(htmlspecialchars($post['categoria'])) ?></span>
<h1 style="font-size: clamp(2rem, 4vw, 3rem); margin: 1rem 0;"><?= htmlspecialchars($post['titulo']) ?></h1>
<div class="articulo-meta">
<span>📅 <?= date('d de F Y', strtotime($post['created_at'])) ?></span>
<span> <?= $post['tiempo_lectura'] ?> min de lectura</span>
<span>✍️ <?= htmlspecialchars($post['autor_nombre']) ?></span>
</div>
<img src="<?= htmlspecialchars($post['imagen']) ?>" alt="<?= htmlspecialchars($post['titulo']) ?>" class="articulo-imagen">
</div>
<div class="articulo-contenido">
<?= $post['contenido'] ?>
</div>
</article>
</main>
<div id="footer-container"></div>
<script src="scripts/main.js"></script>
</body>
</html>
+1
View File
@@ -4,6 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Blog de Amor y Roma. Estrategia, branding, desarrollo web y marketing digital con esencia y solidez."> <meta name="description" content="Blog de Amor y Roma. Estrategia, branding, desarrollo web y marketing digital con esencia y solidez.">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.0/css/all.min.css">
<title>Blog | Amor y Roma — Recursos y Estrategias</title> <title>Blog | Amor y Roma — Recursos y Estrategias</title>
<link rel="stylesheet" href="style/main.css"> <link rel="stylesheet" href="style/main.css">
</head> </head>
+194
View File
@@ -0,0 +1,194 @@
<?php
require_once 'config/db.php';
// Obtener artículos publicados
$stmt = $pdo->prepare("
SELECT p.*, u.nombre as autor_nombre
FROM blog_posts p
LEFT JOIN admin_users u ON p.autor_id = u.id
WHERE p.estado = 'publicado'
ORDER BY p.created_at DESC
");
$stmt->execute();
$posts = $stmt->fetchAll();
// Obtener destacados
$stmt = $pdo->prepare("
SELECT p.*, u.nombre as autor_nombre
FROM blog_posts p
LEFT JOIN admin_users u ON p.autor_id = u.id
WHERE p.estado = 'publicado' AND p.destacado = 1
ORDER BY p.vistas DESC
LIMIT 3
");
$stmt->execute();
$destacados = $stmt->fetchAll();
?>
<!DOCTYPE html>
<html lang="es" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Blog de Amor y Roma. Estrategia, branding, desarrollo web y marketing digital con esencia y solidez.">
<title>Blog | Amor y Roma — Recursos y Estrategias</title>
<link rel="stylesheet" href="style/main.css">
<link rel="stylesheet" href="style/blog.css">
</head>
<body>
<div id="menu-container"></div>
<main>
<!-- HERO -->
<section class="blog-hero-compact">
<div class="hero-compact-content">
<span class="badge-filosofia">Ideas que construyen marcas</span>
<h1 class="hero-compact-title">Estrategia, diseño y tecnología explicados sin tecnicismos</h1>
<p class="hero-compact-subtitle">
Consejos prácticos de branding, web y marketing digital para hacer crecer tu negocio con esencia y estructura.
</p>
</div>
</section>
<!-- SECCIÓN 2: HEADER DE FILTROS -->
<section class="blog-filters-section" id="blog-filters-header">
<div class="filters-container">
<!-- Fila superior: Buscador + Filtros principales -->
<div class="filters-top-row">
<div class="search-wrapper-compact">
<span class="search-icon">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/>
<path d="m21 21-4.35-4.35"/>
</svg>
</span>
<input type="text" id="blog-search" placeholder="Buscá por tema: branding, web, redes..." autocomplete="off" aria-label="Buscar artículos">
<button class="search-clear" id="search-clear" type="button" aria-label="Limpiar búsqueda">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 6 6 18M6 6l12 12"/>
</svg>
</button>
</div>
<div class="filter-group-compact">
<button class="filter-btn active" data-filter="all">Todos</button>
<button class="filter-btn" data-filter="destacados">Destacados</button>
</div>
</div>
<!-- Fila inferior: Categorías + Ordenamiento -->
<div class="filters-bottom-row">
<div class="filter-categories-compact">
<button class="category-btn active" data-category="all">Todas</button>
<button class="category-btn" data-category="branding">Branding</button>
<button class="category-btn" data-category="web">Web</button>
<button class="category-btn" data-category="redes">Redes</button>
<button class="category-btn" data-category="automatizaciones">Automatizaciones</button>
<button class="category-btn" data-category="paid">Paid Media</button>
<button class="category-btn" data-category="estrategia">Estrategia</button>
<button class="category-btn" data-category="casos">Casos</button>
</div>
<div class="filter-secondary-compact">
<select id="sort-select" class="sort-select-compact">
<option value="recent">Más recientes</option>
<option value="popular">Más populares</option>
</select>
</div>
</div>
<!-- Info de resultados -->
<div class="results-info-compact">
<span id="results-count">Mostrando <strong>todos</strong> los artículos</span>
<button class="reset-filters-compact" id="reset-filters">Limpiar filtros</button>
</div>
</div>
</section>
<!-- DESTACADOS -->
<?php if (!empty($destacados)): ?>
<section class="featured-posts" id="featured-posts">
<div class="section-header">
<h2 class="section-title">Más leídos</h2>
<p class="section-subtitle">Los artículos que más están ayudando a marcas como la tuya.</p>
</div>
<div class="featured-grid">
<?php foreach ($destacados as $post): ?>
<article class="featured-card" data-categories="<?= htmlspecialchars($post['categoria']) ?>">
<div class="featured-image">
<img src="<?= htmlspecialchars($post['imagen']) ?>" alt="<?= htmlspecialchars($post['titulo']) ?>">
<span class="featured-badge">Destacado</span>
</div>
<div class="featured-content">
<div class="featured-meta">
<span class="category-tag <?= htmlspecialchars($post['categoria']) ?>"><?= ucfirst(htmlspecialchars($post['categoria'])) ?></span>
<time datetime="<?= $post['created_at'] ?>"><?= date('d M Y', strtotime($post['created_at'])) ?></time>
</div>
<h3 class="featured-title"><?= htmlspecialchars($post['titulo']) ?></h3>
<p class="featured-excerpt"><?= htmlspecialchars($post['extracto']) ?></p>
<div class="featured-footer">
<span class="reading-time">⏱ <?= $post['tiempo_lectura'] ?> min de lectura</span>
<a href="articulo.php?id=<?= $post['id'] ?>" class="btn-read">Leer artículo →</a>
</div>
</div>
</article>
<?php endforeach; ?>
</div>
</section>
<?php endif; ?>
<!-- GRID DE ARTÍCULOS -->
<section class="blog-grid-section">
<div class="blog-grid" id="blog-grid">
<?php foreach ($posts as $post): ?>
<article class="blog-card" data-categories="<?= htmlspecialchars($post['categoria']) ?>">
<div class="card-image">
<img src="<?= htmlspecialchars($post['imagen']) ?>" alt="<?= htmlspecialchars($post['titulo']) ?>">
<span class="card-category <?= htmlspecialchars($post['categoria']) ?>"><?= ucfirst(htmlspecialchars($post['categoria'])) ?></span>
</div>
<div class="card-content">
<div class="card-meta">
<time datetime="<?= $post['created_at'] ?>"><?= date('d M Y', strtotime($post['created_at'])) ?></time>
<span class="reading-time-mini"><?= $post['tiempo_lectura'] ?> min</span>
</div>
<h3 class="card-title"><?= htmlspecialchars($post['titulo']) ?></h3>
<p class="card-excerpt"><?= htmlspecialchars($post['extracto']) ?></p>
<a href="articulo.php?id=<?= $post['id'] ?>" class="card-link">Leer artículo</a>
</div>
</article>
<?php endforeach; ?>
</div>
<?php if (empty($posts)): ?>
<div class="empty-state" id="empty-state">
<div class="empty-icon">📝</div>
<h3>Aún no hay artículos publicados</h3>
<p>Volvé pronto para encontrar contenido de valor sobre branding, web y marketing digital.</p>
</div>
<?php endif; ?>
</section>
<!-- NEWSLETTER -->
<section class="newsletter-section">
<div class="newsletter-card">
<div class="newsletter-content">
<h2 class="newsletter-title">Ideas frescas en tu inbox</h2>
<p class="newsletter-subtitle">
Suscribite y recibí nuestras mejores estrategias de branding, web y marketing digital.
</p>
</div>
<form class="newsletter-form" id="newsletter-form">
<input type="email" placeholder="Tu email profesional" required style="padding: 0.85rem; border: 1.5px solid rgba(0, 63, 82, 0.2); border-radius: 24px; min-width: 300px;">
<button type="submit" class="btn btn-primary">Suscribirme</button>
</form>
</div>
</section>
</main>
<div id="footer-container"></div>
<script src="scripts/main.js"></script>
<script src="scripts/blog.js"></script>
</body>
</html>
+28
View File
@@ -0,0 +1,28 @@
<?php
/**
* Configuración general del formulario de contacto.
* NO subir este archivo a repos públicos.
*/
// ─── SMTP (DonWeb / Ferozo) ────────────────────────────────────────────
// Reemplazá los valores por los reales de tu casilla contacto@amoryroma.com.ar
define('SMTP_HOST', 'dtc036.ferozo.com'); // o el que te dio DonWeb
define('SMTP_PORT', 465); // 587 (TLS) o 465 (SSL)
define('SMTP_SECURE', 'ssl'); // 'tls' o 'ssl'
define('SMTP_USER', 'contacto@amoryroma.com.ar');
define('SMTP_PASS', '1J@oUz7f');
define('SMTP_FROM', 'contacto@amoryroma.com.ar');
define('SMTP_FROM_NAME', 'Amor & Roma');
// ─── Destinatarios ─────────────────────────────────────────────────────
define('MAIL_TO', 'contacto@amoryroma.com.ar');
define('MAIL_TO_NAME', 'Amor & Roma');
// ─── Redirección post-éxito ────────────────────────────────────────────
define('REDIRECT_URL', 'inicio.html');
define('REDIRECT_MS', 4000); // milisegundos antes de redirigir
// ─── Modo debug ────────────────────────────────────────────────────────
// true → muestra errores SMTP en la respuesta JSON (solo para probar)
// false → producción (recomendado)
define('DEBUG_MODE', false);
+29
View File
@@ -0,0 +1,29 @@
<?php
// Configuración de base de datos
define('DB_HOST', 'localhost');
define('DB_NAME', 'w360284_blog');
define('DB_USER', 'w360284_blog');
define('DB_PASS', 'f@IuVS8sjLLm7aN');
define('DB_CHARSET', 'utf8mb4');
// Crear conexión PDO
try {
$pdo = new PDO(
"mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET,
DB_USER,
DB_PASS,
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]
);
} catch (PDOException $e) {
die("Error de conexión: " . $e->getMessage());
}
// Iniciar sesión si no está iniciada
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
?>
+17 -3
View File
@@ -4,8 +4,11 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Contactá a Amor y Roma. Contanos sobre tu proyecto y coordinemos un diagnóstico estratégico sin compromiso."> <meta name="description" content="Contactá a Amor y Roma. Contanos sobre tu proyecto y coordinemos un diagnóstico estratégico sin compromiso.">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.0/css/all.min.css">
<title>Contacto | Amor y Roma — Agencia Boutique</title> <title>Contacto | Amor y Roma — Agencia Boutique</title>
<link rel="stylesheet" href="style/main.css"> <link rel="stylesheet" href="style/main.css">
<link rel="stylesheet" href="style/contacto.css">
<link rel="stylesheet" href="style/modal.css">
</head> </head>
<body> <body>
<div id="menu-container"></div> <div id="menu-container"></div>
@@ -174,6 +177,11 @@
Acepto que mis datos sean utilizados para contactarme respecto a mi consulta. <span class="form-required">*</span> Acepto que mis datos sean utilizados para contactarme respecto a mi consulta. <span class="form-required">*</span>
</label> </label>
</div> </div>
<!-- Honeypot anti-spam: invisible para humanos -->
<div class="form-honeypot" aria-hidden="true">
<label for="website">No completar este campo</label>
<input type="text" id="website" name="website" tabindex="-1" autocomplete="off">
</div>
<button type="submit" class="btn btn-primary btn-form-enviar"> <button type="submit" class="btn btn-primary btn-form-enviar">
Enviar consulta Enviar consulta
@@ -243,14 +251,20 @@
</div> </div>
</div> </div>
</section> </section>
<!-- MODAL DE ÉXITO -->
<div id="modal-contacto" role="dialog" aria-modal="true" aria-hidden="true" aria-labelledby="modal-titulo">
<div class="modal-card">
<div class="modal-icono" aria-hidden="true">✓</div>
<h2 id="modal-titulo" class="modal-titulo">¡Mensaje enviado!</h2>
<p class="modal-mensaje">Gracias por escribirnos. Te contactaremos en menos de 48 horas hábiles.</p>
<button type="button" class="modal-cerrar">Volver al inicio</button>
</div>
</div>
</main> </main>
<div id="footer-container"></div> <div id="footer-container"></div>
<script src="scripts/main.js"></script> <script src="scripts/main.js"></script>
<script src="scripts/efecto.js"></script>
<script src="scripts/contacto.js"></script> <script src="scripts/contacto.js"></script>
<link rel="stylesheet" href="style/contacto.css">
</body> </body>
</html> </html>
+171
View File
@@ -0,0 +1,171 @@
<?php
/**
* Endpoint AJAX del formulario de contacto.
* Recibe POST y responde JSON.
*/
// ── Silenciar salida directa, solo JSON ────────────────────────────────
if (ob_get_length() > 0) { ob_clean(); }
header('Content-Type: application/json; charset=UTF-8');
// ── Errores a log, no a pantalla ───────────────────────────────────────
ini_set('display_errors', 0);
ini_set('display_startup_errors', 0);
ini_set('log_errors', 1);
error_reporting(E_ALL);
require_once __DIR__ . '/config.php';
require_once __DIR__ . '/validar.php';
// ── Respuesta helper ───────────────────────────────────────────────────
function responder($status, $message, $code = 200, $extra = []) {
http_response_code($code);
echo json_encode(array_merge([
'status' => $status,
'message' => $message,
], $extra));
exit;
}
// ── Método ─────────────────────────────────────────────────────────────
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
responder('error', 'Método no permitido.', 405);
}
// ── Honeypot ───────────────────────────────────────────────────────────
if (!empty($_POST['website'])) {
// Fingimos éxito para no darle pistas al bot
responder('success', '¡Gracias! Te contactaremos a la brevedad.');
}
// ── Validación ─────────────────────────────────────────────────────────
$resultado = validar_contacto($_POST);
if (!$resultado['ok']) {
responder('error', implode(' ', $resultado['errores']), 400);
}
$d = $resultado['datos'];
// ── PHPMailer ──────────────────────────────────────────────────────────
require_once __DIR__ . '/lib/PHPMailer.php';
require_once __DIR__ . '/lib/SMTP.php';
require_once __DIR__ . '/lib/Exception.php';
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
use PHPMailer\PHPMailer\SMTP;
// ── Mapas para mostrar etiquetas legibles ─────────────────────────────
$etiquetas = [
'servicio' => [
'branding' => 'Branding & Identidad',
'web' => 'Desarrollo Web a Medida',
'rrss' => 'Gestión de Redes Sociales',
'automatizaciones'=> 'Automatizaciones & Flujos',
'paid' => 'Paid Media & Campañas',
'ecosistema' => 'Ecosistema Digital Completo',
'otro' => 'Otro / Aún no definido',
],
'etapa' => [
'idea' => 'Solo tengo la idea',
'inicio' => 'Estoy empezando',
'marcha' => 'En marcha, necesito orden',
'redisenio' => 'Rediseñar / reposicionar',
'escalar' => 'Escalar lo existente',
],
'presupuesto' => [
'esencial' => 'Hasta $250.000 (Esencial)',
'impulso' => '$250.000 – $500.000 (Impulso)',
'premium' => '$500.000 – $800.000 (Premium)',
'elite' => 'Más de $800.000 (Élite)',
'proyecto' => 'Proyecto cerrado (one-off)',
'no-se' => 'A evaluar por el equipo',
],
'origen' => [
'instagram' => 'Instagram',
'linkedin' => 'LinkedIn',
'referido' => 'Referido',
'google' => 'Google',
'otro' => 'Otro',
],
];
$mail = new PHPMailer(true);
try {
// SMTP
$mail->isSMTP();
$mail->Host = SMTP_HOST;
$mail->SMTPAuth = true;
$mail->Username = SMTP_USER;
$mail->Password = SMTP_PASS;
$mail->SMTPSecure = SMTP_SECURE;
$mail->Port = SMTP_PORT;
$mail->CharSet = 'UTF-8';
$mail->SMTPDebug = 0;
// De / Para
$mail->setFrom(SMTP_FROM, SMTP_FROM_NAME);
$mail->addAddress(MAIL_TO, MAIL_TO_NAME);
$mail->addReplyTo($d['email'], $d['nombre']);
// Contenido
$mail->Subject = 'Nuevo contacto: ' . $d['nombre'] . ($d['empresa'] ? ' (' . $d['empresa'] . ')' : '');
$mail->isHTML(true);
$servicio_txt = $etiquetas['servicio'][$d['servicio']] ?? '—';
$etapa_txt = $etiquetas['etapa'][$d['etapa']] ?? '—';
$presupuesto_txt = $etiquetas['presupuesto'][$d['presupuesto']] ?? '—';
$origen_txt = $etiquetas['origen'][$d['origen']] ?? '—';
$rows = [
'Nombre' => $d['nombre'],
'Empresa / Marca' => $d['empresa'] ?: '—',
'Email' => $d['email'],
'Teléfono' => $d['telefono'] ?: '—',
'Servicio' => $servicio_txt,
'Etapa' => $etapa_txt,
'Presupuesto' => $presupuesto_txt,
'Cómo nos conoció'=> $origen_txt,
];
$html = '<div style="font-family:Arial,sans-serif;font-size:14px;color:#222;max-width:640px">';
$html .= '<h2 style="margin:0 0 16px;color:#111">Nueva consulta desde el sitio</h2>';
$html .= '<table cellpadding="8" cellspacing="0" style="border-collapse:collapse;width:100%">';
foreach ($rows as $label => $valor) {
$html .= '<tr>';
$html .= '<td style="background:#f5f2ec;border:1px solid #e5e0d5;font-weight:bold;width:180px">' . htmlspecialchars($label) . '</td>';
$html .= '<td style="border:1px solid #e5e0d5">' . htmlspecialchars($valor) . '</td>';
$html .= '</tr>';
}
$html .= '</table>';
$html .= '<h3 style="margin:24px 0 8px;color:#111">Mensaje</h3>';
$html .= '<div style="white-space:pre-wrap;background:#fafafa;border:1px solid #eee;padding:12px;border-radius:6px">'
. htmlspecialchars($d['mensaje']) . '</div>';
$html .= '</div>';
$mail->Body = $html;
$mail->AltBody = "Nueva consulta\n\n"
. "Nombre: {$d['nombre']}\n"
. "Empresa: {$d['empresa']}\n"
. "Email: {$d['email']}\n"
. "Teléfono: {$d['telefono']}\n"
. "Servicio: $servicio_txt\n"
. "Etapa: $etapa_txt\n"
. "Presupuesto: $presupuesto_txt\n"
. "Origen: $origen_txt\n\n"
. "Mensaje:\n{$d['mensaje']}\n";
$mail->send();
responder('success', '¡Gracias! Te contactaremos en menos de 48 horas hábiles.', 200, [
'redirect' => REDIRECT_URL,
'delay' => REDIRECT_MS,
]);
} catch (Exception $e) {
error_log('[contacto] Falló el envío: ' . $mail->ErrorInfo);
$msg = DEBUG_MODE
? 'Error SMTP: ' . $mail->ErrorInfo
: 'No pudimos enviar tu mensaje. Probá por WhatsApp o intentá más tarde.';
responder('error', $msg, 500);
}
+4 -2
View File
@@ -15,8 +15,9 @@
<!-- Sección Hero --> <!-- Sección Hero -->
<section id="hero" class="hero-section"> <section id="hero" class="hero-section">
<div class="hero-content"> <div class="hero-content">
<h1 class="hero-title">Marcas con esencia, construidas con estrategia</h1> <span class="badge-filosofia">Agencia Boutique de Marketing Digital</span>
<p class="hero-subtitle">Diseñamos servicios que se adaptan a tu realidad, para ayudarte a crecer sin perder lo que te hace único</p> <h1 class="hero-title">En un mundo de ruido, solo lo auténtico resuena</h1>
<p class="hero-subtitle">Combinamos la pasión creativa con la ingeniería de procesos para desarrollar tu identidad, sitio web y automatizaciones. <br>Porque lo auténtico, cuando está bien construido, siempre deja eco.</p>
<div class="hero-cta-group"> <div class="hero-cta-group">
<a href="#" class="btn btn-primary hero-cta-primary">Hablemos</a> <a href="#" class="btn btn-primary hero-cta-primary">Hablemos</a>
<a href="inicio.html#servicios" class="btn btn-secondary hero-cta-secondary">Conocé más</a> <a href="inicio.html#servicios" class="btn btn-secondary hero-cta-secondary">Conocé más</a>
@@ -531,6 +532,7 @@
<div id="footer-container"></div> <div id="footer-container"></div>
<script src="scripts/main.js"></script> <script src="scripts/main.js"></script>
<script src="scripts/fluid.js"></script>
</body> </body>
</html> </html>
+40
View File
@@ -0,0 +1,40 @@
<?php
/**
* PHPMailer Exception class.
* PHP Version 5.5.
*
* @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
*
* @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
* @author Jim Jagielski (jimjag) <jimjag@gmail.com>
* @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
* @author Brent R. Matzelle (original founder)
* @copyright 2012 - 2020 Marcus Bointon
* @copyright 2010 - 2012 Jim Jagielski
* @copyright 2004 - 2009 Andy Prevost
* @license https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html GNU Lesser General Public License
* @note This program is distributed in the hope that it will be useful - WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE.
*/
namespace PHPMailer\PHPMailer;
/**
* PHPMailer exception handler.
*
* @author Marcus Bointon <phpmailer@synchromedia.co.uk>
*/
class Exception extends \Exception
{
/**
* Prettify error message output.
*
* @return string
*/
public function errorMessage()
{
return '<strong>' . htmlspecialchars($this->getMessage(), ENT_COMPAT | ENT_HTML401) . "</strong><br />\n";
}
}
+5586
View File
File diff suppressed because it is too large Load Diff
+1617
View File
File diff suppressed because it is too large Load Diff
+84 -159
View File
@@ -1,185 +1,110 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
console.log('Blog JS cargado');
const searchInput = document.getElementById('blog-search'); const searchInput = document.getElementById('blog-search');
const searchClear = document.getElementById('search-clear'); const searchClear = document.getElementById('search-clear');
const blogGrid = document.getElementById('blog-grid'); const categoryBtns = document.querySelectorAll('.category-btn');
const emptyState = document.getElementById('empty-state'); const filterBtns = document.querySelectorAll('.filter-btn');
const resetBtn = document.getElementById('reset-filters');
const resultsCount = document.getElementById('results-count'); const resultsCount = document.getElementById('results-count');
const filtersHeader = document.getElementById('blog-filters-header'); const emptyState = document.getElementById('empty-state');
if (!searchInput || !blogGrid) { const allCards = document.querySelectorAll('.blog-card, .featured-card');
console.error('Faltan elementos del DOM'); let activeCategory = 'all';
return; let activeFilter = 'all';
} let searchQuery = '';
let currentFilters = { function filterArticles() {
search: '',
category: 'all',
featured: 'all',
sort: 'recent'
};
// ===== EFECTO SCROLL EN FILTROS =====
window.addEventListener('scroll', () => {
if (window.scrollY > 100) {
filtersHeader.classList.add('is-scrolled');
} else {
filtersHeader.classList.remove('is-scrolled');
}
});
// ===== BUSCADOR =====
searchInput.addEventListener('input', (e) => {
const value = e.target.value.trim();
currentFilters.search = value;
if (searchClear) {
searchClear.classList.toggle('visible', value.length > 0);
}
filterArticles();
});
if (searchClear) {
searchClear.addEventListener('click', () => {
searchInput.value = '';
searchClear.classList.remove('visible');
currentFilters.search = '';
filterArticles();
searchInput.focus();
});
}
// ===== FILTROS PRINCIPALES =====
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentFilters.featured = btn.dataset.filter;
filterArticles();
});
});
// ===== CATEGORÍAS =====
document.querySelectorAll('.category-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.category-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentFilters.category = btn.dataset.category;
filterArticles();
});
});
// ===== ORDENAMIENTO =====
const sortSelect = document.getElementById('sort-select');
if (sortSelect) {
sortSelect.addEventListener('change', (e) => {
currentFilters.sort = e.target.value;
sortArticles();
});
}
// ===== RESET =====
const resetFilters = document.getElementById('reset-filters');
if (resetFilters) {
resetFilters.addEventListener('click', () => {
currentFilters = { search: '', category: 'all', featured: 'all', sort: 'recent' };
searchInput.value = '';
searchClear?.classList.remove('visible');
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
document.querySelector('.filter-btn[data-filter="all"]')?.classList.add('active');
document.querySelectorAll('.category-btn').forEach(b => b.classList.remove('active'));
document.querySelector('.category-btn[data-category="all"]')?.classList.add('active');
if (sortSelect) sortSelect.value = 'recent';
filterArticles();
});
}
// ===== FUNCIÓN DE FILTRADO =====
const filterArticles = () => {
const articles = document.querySelectorAll('.blog-card, .featured-card');
let visibleCount = 0; let visibleCount = 0;
articles.forEach(article => { allCards.forEach(card => {
const categories = article.dataset.categories || ''; const categories = card.dataset.categories || '';
const tags = article.dataset.tags || ''; const isFeatured = card.dataset.featured === 'true';
const isFeatured = article.dataset.featured === 'true'; const title = card.querySelector('.featured-title, .card-title')?.textContent.toLowerCase() || '';
const excerpt = card.querySelector('.featured-excerpt, .card-excerpt')?.textContent.toLowerCase() || '';
const title = article.querySelector('.card-title, .featured-title')?.textContent.toLowerCase() || ''; // 1. Filtro de categoría
const excerpt = article.querySelector('.card-excerpt, .featured-excerpt')?.textContent.toLowerCase() || ''; const matchCategory = activeCategory === 'all' || categories.includes(activeCategory);
const searchTerm = currentFilters.search.toLowerCase(); // 2. Filtro de tipo (Todos / Destacados)
const matchSearch = searchTerm === '' || const matchFilter = activeFilter === 'all' || (activeFilter === 'destacados' && isFeatured);
title.includes(searchTerm) ||
excerpt.includes(searchTerm) ||
categories.toLowerCase().includes(searchTerm) ||
tags.toLowerCase().includes(searchTerm);
const matchCategory = currentFilters.category === 'all' || // 3. Filtro de búsqueda
categories.toLowerCase().includes(currentFilters.category.toLowerCase()); const matchSearch = searchQuery === '' || title.includes(searchQuery) || excerpt.includes(searchQuery);
let matchFeatured = true; if (matchCategory && matchFilter && matchSearch) {
if (currentFilters.featured === 'destacados') { card.style.display = 'flex';
matchFeatured = isFeatured;
}
if (matchSearch && matchCategory && matchFeatured) {
article.style.display = '';
visibleCount++; visibleCount++;
} else { } else {
article.style.display = 'none'; card.style.display = 'none';
} }
}); });
sortArticles(); // Actualizar contador y estado vacío
updateResultsCount(visibleCount); if (resultsCount) {
resultsCount.innerHTML = `Mostrando <strong>${visibleCount}</strong> artículo${visibleCount !== 1 ? 's' : ''}`;
}
if (emptyState) { if (emptyState) {
emptyState.style.display = visibleCount === 0 ? 'block' : 'none'; emptyState.style.display = visibleCount === 0 ? 'block' : 'none';
} }
};
const sortArticles = () => { // Mostrar/ocultar botón de limpiar búsqueda
const articles = Array.from(document.querySelectorAll('.blog-card')); if (searchClear) {
const container = blogGrid; searchClear.classList.toggle('visible', searchQuery.length > 0);
}
switch(currentFilters.sort) {
case 'recent':
articles.sort((a, b) => {
const dateA = new Date(a.querySelector('time')?.datetime || 0);
const dateB = new Date(b.querySelector('time')?.datetime || 0);
return dateB - dateA;
});
break;
case 'alphabetical':
articles.sort((a, b) => {
const titleA = a.querySelector('.card-title')?.textContent || '';
const titleB = b.querySelector('.card-title')?.textContent || '';
return titleA.localeCompare(titleB);
});
break;
} }
articles.forEach(article => container.appendChild(article)); // Event Listeners
}; if (searchInput) {
searchInput.addEventListener('input', (e) => {
const updateResultsCount = (count) => { searchQuery = e.target.value.toLowerCase().trim();
if (!resultsCount) return; filterArticles();
});
if (count === 0) { }
resultsCount.innerHTML = 'No se encontraron artículos';
} else if (currentFilters.category === 'all' && currentFilters.search === '') { if (searchClear) {
resultsCount.innerHTML = `Mostrando <strong>${count}</strong> artículos`; searchClear.addEventListener('click', () => {
} else { searchInput.value = '';
resultsCount.innerHTML = `Mostrando <strong>${count}</strong> artículo${count !== 1 ? 's' : ''}`; searchQuery = '';
} searchInput.focus();
}; filterArticles();
});
// Inicializar }
categoryBtns.forEach(btn => {
btn.addEventListener('click', () => {
categoryBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
activeCategory = btn.dataset.category;
filterArticles();
});
});
filterBtns.forEach(btn => {
btn.addEventListener('click', () => {
filterBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
activeFilter = btn.dataset.filter;
filterArticles();
});
});
if (resetBtn) {
resetBtn.addEventListener('click', () => {
activeCategory = 'all';
activeFilter = 'all';
searchQuery = '';
if (searchInput) searchInput.value = '';
categoryBtns.forEach(b => b.classList.remove('active'));
document.querySelector('.category-btn[data-category="all"]')?.classList.add('active');
filterBtns.forEach(b => b.classList.remove('active'));
document.querySelector('.filter-btn[data-filter="all"]')?.classList.add('active');
filterArticles();
});
}
// Ejecutar al inicio
filterArticles(); filterArticles();
}); });
+103
View File
@@ -0,0 +1,103 @@
/**
* Envío AJAX del formulario de contacto + modal de éxito.
*/
(function () {
'use strict';
const form = document.getElementById('form-contacto');
const modal = document.getElementById('modal-contacto');
const btnCerrar = modal ? modal.querySelector('.modal-cerrar') : null;
const btnEnviar = form ? form.querySelector('button[type="submit"]') : null;
if (!form || !modal) return;
let redirectTimer = null;
// ── Helpers ────────────────────────────────────────────────────────
function abrirModal(mensaje) {
const texto = modal.querySelector('.modal-mensaje');
if (texto && mensaje) texto.textContent = mensaje;
modal.classList.add('is-visible');
modal.setAttribute('aria-hidden', 'false');
document.body.style.overflow = 'hidden';
}
function cerrarModal(redirigir = false) {
modal.classList.remove('is-visible');
modal.setAttribute('aria-hidden', 'true');
document.body.style.overflow = '';
if (redirectTimer) clearTimeout(redirectTimer);
if (redirigir) window.location.href = 'inicio.html';
}
function mostrarError(mensaje) {
// Reutilizamos el modal con estética distinta
modal.classList.add('is-error');
abrirModal(mensaje);
}
// ── Cerrar modal ───────────────────────────────────────────────────
if (btnCerrar) {
btnCerrar.addEventListener('click', () => cerrarModal(true));
}
modal.addEventListener('click', (e) => {
if (e.target === modal) cerrarModal(true);
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && modal.classList.contains('is-visible')) {
cerrarModal(true);
}
});
// ── Submit ─────────────────────────────────────────────────────────
form.addEventListener('submit', async (e) => {
e.preventDefault();
// Validación nativa HTML5
if (!form.checkValidity()) {
form.reportValidity();
return;
}
// Estado de carga
const textoOriginal = btnEnviar ? btnEnviar.innerHTML : '';
if (btnEnviar) {
btnEnviar.disabled = true;
btnEnviar.innerHTML = 'Enviando…';
}
try {
const data = new FormData(form);
const res = await fetch('enviar.php', {
method: 'POST',
body: data,
headers: { 'X-Requested-With': 'XMLHttpRequest' },
});
const json = await res.json().catch(() => ({
status: 'error',
message: 'Respuesta inválida del servidor.',
}));
if (json.status === 'success') {
modal.classList.remove('is-error');
abrirModal(json.message || '¡Gracias! Te contactaremos en breve.');
form.reset();
const delay = Number(json.delay) || 4000;
redirectTimer = setTimeout(() => {
window.location.href = json.redirect || 'inicio.html';
}, delay);
} else {
mostrarError(json.message || 'No pudimos enviar tu mensaje.');
}
} catch (err) {
mostrarError('Hubo un problema de conexión. Probá nuevamente.');
} finally {
if (btnEnviar) {
btnEnviar.disabled = false;
btnEnviar.innerHTML = textoOriginal;
}
}
});
})();
+133
View File
@@ -839,6 +839,128 @@
color: var(--brand-teal); color: var(--brand-teal);
border-color: #ffffff; border-color: #ffffff;
} }
/* ============================================
PÁGINA DE ARTÍCULO INDIVIDUAL (articulo.php)
============================================ */
.articulo-header {
max-width: 900px;
margin: 0 auto;
padding: calc(var(--spacing-lg) + 2rem) var(--spacing-md) var(--spacing-md);
text-align: center;
}
.articulo-header .badge-filosofia {
display: inline-block;
padding: 0.4rem 1rem;
border-radius: 999px;
background-color: var(--badge-bg);
color: var(--badge-text);
font-family: var(--font-small);
font-weight: 600;
font-size: 0.85rem;
letter-spacing: 0.5px;
text-transform: uppercase;
margin-bottom: var(--spacing-sm);
}
.articulo-header h1 {
font-family: var(--font-title);
font-style: italic;
font-weight: 700;
font-size: clamp(2rem, 5vw, 3.5rem);
line-height: 1.15;
color: var(--text-primary);
margin-bottom: var(--spacing-md);
}
.articulo-meta {
display: flex;
justify-content: center;
gap: var(--spacing-md);
color: var(--text-muted);
font-family: var(--font-small);
font-size: 0.9rem;
margin-bottom: var(--spacing-md);
flex-wrap: wrap;
}
.articulo-imagen {
width: 100%;
max-height: 500px;
object-fit: cover;
border-radius: 16px;
margin-top: var(--spacing-md);
box-shadow: var(--shadow);
}
.articulo-contenido {
max-width: 800px;
margin: 0 auto;
padding: var(--spacing-lg) var(--spacing-md);
font-family: var(--font-body);
font-size: 1.1rem;
line-height: 1.8;
color: var(--text-primary);
}
.articulo-contenido h2,
.articulo-contenido h3 {
font-family: var(--font-title);
font-style: italic;
color: var(--text-primary);
margin-top: 2.5rem;
margin-bottom: 1rem;
}
.articulo-contenido p {
margin-bottom: 1.5rem;
}
.articulo-contenido ul,
.articulo-contenido ol {
margin-bottom: 1.5rem;
padding-left: 1.5rem;
}
.articulo-contenido li {
margin-bottom: 0.5rem;
}
.articulo-contenido img {
max-width: 100%;
height: auto;
border-radius: 12px;
margin: 2rem 0;
box-shadow: var(--shadow);
}
.articulo-contenido blockquote {
border-left: 4px solid var(--brand-crimson);
padding-left: var(--spacing-md);
font-style: italic;
color: var(--text-secondary);
margin: 2rem 0;
font-size: 1.2rem;
}
.volver-link {
display: inline-flex;
align-items: center;
gap: 0.5rem;
color: var(--brand-crimson);
text-decoration: none;
font-family: var(--font-small);
font-weight: 600;
font-size: 0.95rem;
margin-bottom: var(--spacing-md);
transition: all 0.2s ease;
}
.volver-link:hover {
color: var(--brand-teal);
gap: 0.7rem;
}
/* ============================================ /* ============================================
RESPONSIVE BLOG RESPONSIVE BLOG
@@ -855,6 +977,17 @@
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.articulo-header {
padding: var(--spacing-lg) var(--spacing-sm) var(--spacing-sm);
}
.articulo-contenido {
padding: var(--spacing-md) var(--spacing-sm);
font-size: 1rem;
}
.articulo-meta {
flex-direction: column;
gap: 0.5rem;
}
.blog-hero-compact { .blog-hero-compact {
padding: var(--spacing-lg) var(--spacing-sm) var(--spacing-md); padding: var(--spacing-lg) var(--spacing-sm) var(--spacing-md);
} }
+7
View File
@@ -420,6 +420,13 @@
color: var(--text-muted); color: var(--text-muted);
line-height: 1.6; line-height: 1.6;
} }
.form-honeypot {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
}
/* ============================================ /* ============================================
RESPONSIVE CONTACTO RESPONSIVE CONTACTO
+14
View File
@@ -156,6 +156,20 @@ ul { list-style: none; }
z-index: 100; z-index: 100;
} }
.badge-filosofia {
display: inline-block;
padding: 0.4rem 1rem;
border-radius: 999px;
color: var(--badge-text);
font-family: var(--font-small);
font-weight: 400;
font-size: 1rem;
letter-spacing: 0.5px;
text-transform: uppercase;
opacity: 0;
animation: fadeInUp 0.9s ease-out 0s forwards;
}
.site-header { .site-header {
background-color: var(--bg-surface); background-color: var(--bg-surface);
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
+94
View File
@@ -0,0 +1,94 @@
/* ── Modal de contacto ──────────────────────────────────────────────── */
#modal-contacto {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(20, 18, 15, 0.55);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
opacity: 0;
visibility: hidden;
transition: opacity .35s ease, visibility .35s ease;
z-index: 9999;
padding: 1.5rem;
}
#modal-contacto.is-visible {
opacity: 1;
visibility: visible;
}
#modal-contacto .modal-card {
background: #fff;
color: #1c1a17;
border-radius: 14px;
padding: 2.5rem 2rem;
max-width: 460px;
width: 100%;
text-align: center;
box-shadow: 0 24px 60px rgba(0, 0, 0, .25);
transform: translateY(20px) scale(.97);
transition: transform .35s cubic-bezier(.2, .8, .2, 1);
position: relative;
}
#modal-contacto.is-visible .modal-card {
transform: translateY(0) scale(1);
}
#modal-contacto .modal-icono {
width: 64px;
height: 64px;
border-radius: 50%;
background: #f1ece1;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 1.25rem;
font-size: 1.75rem;
color: #8b7355;
}
#modal-contacto .modal-titulo {
font-size: 1.4rem;
margin: 0 0 .75rem;
letter-spacing: -0.01em;
}
#modal-contacto .modal-mensaje {
font-size: .98rem;
line-height: 1.55;
color: #4a453d;
margin: 0 0 1.75rem;
}
#modal-contacto .modal-cerrar {
display: inline-block;
background: #1c1a17;
color: #fff;
border: none;
padding: .85rem 1.75rem;
border-radius: 999px;
font-size: .95rem;
cursor: pointer;
transition: background .25s ease, transform .25s ease;
font-family: inherit;
}
#modal-contacto .modal-cerrar:hover {
background: #8b7355;
transform: translateY(-1px);
}
/* Variante error */
#modal-contacto.is-error .modal-icono {
background: #fdecec;
color: #c0392b;
}
@media (max-width: 480px) {
#modal-contacto .modal-card { padding: 2rem 1.25rem; }
#modal-contacto .modal-titulo { font-size: 1.2rem; }
}
+76
View File
@@ -0,0 +1,76 @@
<?php
/**
* Funciones de sanitización y validación reutilizables.
*/
function limpiar_texto($valor, $max = 500) {
$valor = trim((string)$valor);
$valor = strip_tags($valor);
$valor = preg_replace('/[\r\n\t]+/', ' ', $valor);
if (mb_strlen($valor) > $max) {
$valor = mb_substr($valor, 0, $max);
}
return $valor;
}
function limpiar_mensaje($valor, $max = 5000) {
$valor = trim((string)$valor);
$valor = strip_tags($valor);
if (mb_strlen($valor) > $max) {
$valor = mb_substr($valor, 0, $max);
}
return $valor;
}
function limpiar_email($valor) {
return filter_var(trim((string)$valor), FILTER_SANITIZE_EMAIL);
}
function email_valido($valor) {
return (bool) filter_var($valor, FILTER_VALIDATE_EMAIL);
}
function en_lista($valor, array $permitidos) {
return in_array($valor, $permitidos, true) ? $valor : '';
}
/**
* Valida los datos del formulario y devuelve
* ['ok' => bool, 'errores' => [], 'datos' => []]
*/
function validar_contacto(array $post) {
$errores = [];
$datos = [
'nombre' => limpiar_texto($post['nombre'] ?? '', 120),
'empresa' => limpiar_texto($post['empresa'] ?? '', 120),
'email' => limpiar_email($post['email'] ?? ''),
'telefono' => limpiar_texto($post['telefono'] ?? '', 40),
'servicio' => en_lista($post['servicio'] ?? '', [
'branding','web','rrss','automatizaciones','paid','ecosistema','otro'
]),
'etapa' => en_lista($post['etapa'] ?? '', [
'idea','inicio','marcha','redisenio','escalar'
]),
'presupuesto' => en_lista($post['presupuesto'] ?? '', [
'esencial','impulso','premium','elite','proyecto','no-se'
]),
'mensaje' => limpiar_mensaje($post['mensaje'] ?? '', 5000),
'origen' => en_lista($post['origen'] ?? '', [
'instagram','linkedin','referido','google','otro'
]),
'consentimiento'=> !empty($post['consentimiento']),
];
if ($datos['nombre'] === '') $errores[] = 'El nombre es obligatorio.';
if (!email_valido($datos['email'])) $errores[] = 'El email no es válido.';
if ($datos['servicio'] === '') $errores[] = 'Seleccioná un servicio de interés.';
if ($datos['mensaje'] === '') $errores[] = 'El mensaje es obligatorio.';
if (!$datos['consentimiento']) $errores[] = 'Debés aceptar el uso de tus datos.';
return [
'ok' => empty($errores),
'errores' => $errores,
'datos' => $datos,
];
}