Initial commit: Proyecto Portal ePromos completo con documentación unificada y seguridad base
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
<?php
|
||||
// landings_public.php
|
||||
// Lista pública de landings con estado 'publicado' + menú + filtros
|
||||
|
||||
require_once __DIR__ . '/admin/db.php'; // ajusta la ruta si tu db.php está en otra carpeta
|
||||
|
||||
// Ruta pública para ver un landing por slug
|
||||
const PUBLIC_LANDING_ROUTE = '/ruleta/landing.php?slug='; // p.ej. /landing.php?slug=mi-landing
|
||||
|
||||
function e($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
|
||||
function valid_date($s){ return (bool)preg_match('/^\d{4}-\d{2}-\d{2}$/', $s ?? ''); }
|
||||
|
||||
// Parámetros de filtros
|
||||
$term = trim($_GET['q'] ?? '');
|
||||
$with_game = isset($_GET['con_juego']) ? 1 : 0;
|
||||
$with_image = isset($_GET['con_imagen']) ? 1 : 0;
|
||||
$desde = $_GET['desde'] ?? '';
|
||||
$hasta = $_GET['hasta'] ?? '';
|
||||
$sort = $_GET['orden'] ?? 'recientes'; // recientes | alfa
|
||||
$pageSize = (int)($_GET['pp'] ?? 12);
|
||||
$page = max(1, (int)($_GET['page'] ?? 1));
|
||||
|
||||
if (!in_array($pageSize, [6,12,24,48,96], true)) $pageSize = 12;
|
||||
$offset = ($page - 1) * $pageSize;
|
||||
|
||||
// Construcción del WHERE dinámico
|
||||
$where = "estado='publicado'";
|
||||
$params = []; $types = '';
|
||||
|
||||
if ($term !== '') {
|
||||
$where .= " AND (titulo LIKE ? OR slug LIKE ? OR meta_title LIKE ? OR meta_desc LIKE ?)";
|
||||
$like = '%'.$term.'%';
|
||||
array_push($params, $like, $like, $like, $like); $types .= 'ssss';
|
||||
}
|
||||
if ($with_game) {
|
||||
$where .= " AND game_url IS NOT NULL AND game_url <> ''";
|
||||
}
|
||||
if ($with_image) {
|
||||
$where .= " AND hero_image IS NOT NULL AND hero_image <> ''";
|
||||
}
|
||||
if (valid_date($desde)) {
|
||||
$where .= " AND (updated_at IS NULL OR updated_at >= ?)";
|
||||
$params[] = $desde . ' 00:00:00';
|
||||
$types .= 's';
|
||||
}
|
||||
if (valid_date($hasta)) {
|
||||
$where .= " AND (updated_at IS NULL OR updated_at <= ?)";
|
||||
$params[] = $hasta . ' 23:59:59';
|
||||
$types .= 's';
|
||||
}
|
||||
|
||||
// Orden
|
||||
switch ($sort) {
|
||||
case 'alfa':
|
||||
$orderSql = "titulo ASC, id DESC";
|
||||
break;
|
||||
case 'recientes':
|
||||
default:
|
||||
$orderSql = "COALESCE(updated_at, '1970-01-01 00:00:00') DESC, id DESC";
|
||||
$sort = 'recientes';
|
||||
}
|
||||
|
||||
// ---- Total para paginación ----
|
||||
$sqlCount = "SELECT COUNT(*) AS total FROM landing_pages WHERE $where";
|
||||
$stmt = $conn->prepare($sqlCount);
|
||||
if ($types) { $stmt->bind_param($types, ...$params); }
|
||||
$stmt->execute();
|
||||
$total = (int)($stmt->get_result()->fetch_assoc()['total'] ?? 0);
|
||||
$stmt->close();
|
||||
|
||||
$total_pages = max(1, (int)ceil($total / $pageSize));
|
||||
if ($page > $total_pages) { $page = $total_pages; $offset = ($page - 1) * $pageSize; }
|
||||
|
||||
// ---- Consulta principal ----
|
||||
$sql = "SELECT id, titulo, slug, hero_image, meta_desc, updated_at, game_url
|
||||
FROM landing_pages
|
||||
WHERE $where
|
||||
ORDER BY $orderSql
|
||||
LIMIT ? OFFSET ?";
|
||||
|
||||
$stmt = $conn->prepare($sql);
|
||||
|
||||
// Bind evitando “positional after unpacking”
|
||||
$limitVal = $pageSize;
|
||||
$offsetVal = $offset;
|
||||
|
||||
if ($types) {
|
||||
$types2 = $types . 'ii';
|
||||
$bindValues = array_merge($params, [$limitVal, $offsetVal]);
|
||||
$stmt->bind_param($types2, ...$bindValues);
|
||||
} else {
|
||||
$stmt->bind_param('ii', $limitVal, $offsetVal);
|
||||
}
|
||||
|
||||
$stmt->execute();
|
||||
$rows = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
|
||||
$stmt->close();
|
||||
|
||||
// Fallback de imagen
|
||||
$fallbackImg = 'https://picsum.photos/seed/landings/800/450';
|
||||
|
||||
// Helper para mantener parámetros en paginación
|
||||
$baseParams = $_GET; unset($baseParams['page']);
|
||||
$base = $_SERVER['PHP_SELF'].'?'.http_build_query($baseParams);
|
||||
$prev = max(1, $page-1); $next = min($total_pages, $page+1);
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Landings activos</title>
|
||||
<meta name="description" content="Explora los landings activos y participa en las campañas disponibles.">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.card-img-top{ aspect-ratio: 16/9; object-fit: cover; }
|
||||
.line-clamp-2{ display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; }
|
||||
.filter-toggle { cursor: pointer; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- MENÚ -->
|
||||
<nav class="navbar navbar-expand-lg bg-dark navbar-dark">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="<?= e($_SERVER['PHP_SELF']) ?>">GLM · Landings</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="mainNav">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
<li class="nav-item"><a class="nav-link active" href="<?= e($_SERVER['PHP_SELF']) ?>">Landings</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/">Inicio</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/contacto.php">Contacto</a></li>
|
||||
</ul>
|
||||
<!-- BÚSQUEDA RÁPIDA EN MENÚ -->
|
||||
<form class="d-flex gap-2" method="get" action="">
|
||||
<input name="q" class="form-control form-control-sm" placeholder="Buscar..." value="<?= e($term) ?>">
|
||||
<button class="btn btn-outline-light btn-sm">Buscar</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container py-4">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center mb-3 gap-2">
|
||||
<h1 class="h4 mb-0">Landings activos</h1>
|
||||
<span class="text-muted"><?= $total ?> resultado<?= $total===1?'':'s' ?></span>
|
||||
</div>
|
||||
|
||||
<!-- PANEL DE FILTROS -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<form method="get" action="">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-12 col-md-4">
|
||||
<label class="form-label">Buscar</label>
|
||||
<input name="q" class="form-control" placeholder="Título, slug, meta..."
|
||||
value="<?= e($term) ?>">
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-2">
|
||||
<div class="form-check mt-4">
|
||||
<input class="form-check-input" type="checkbox" id="con_juego" name="con_juego" <?= $with_game?'checked':'' ?>>
|
||||
<label class="form-check-label" for="con_juego">Con juego</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-2">
|
||||
<div class="form-check mt-4">
|
||||
<input class="form-check-input" type="checkbox" id="con_imagen" name="con_imagen" <?= $with_image?'checked':'' ?>>
|
||||
<label class="form-check-label" for="con_imagen">Con imagen</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-2">
|
||||
<label class="form-label">Desde</label>
|
||||
<input type="date" name="desde" class="form-control" value="<?= e(valid_date($desde)?$desde:'') ?>">
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-2">
|
||||
<label class="form-label">Hasta</label>
|
||||
<input type="date" name="hasta" class="form-control" value="<?= e(valid_date($hasta)?$hasta:'') ?>">
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-2">
|
||||
<label class="form-label">Orden</label>
|
||||
<select name="orden" class="form-select">
|
||||
<option value="recientes" <?= $sort==='recientes'?'selected':'' ?>>Más recientes</option>
|
||||
<option value="alfa" <?= $sort==='alfa'?'selected':'' ?>>Alfabético (A-Z)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-2">
|
||||
<label class="form-label">Por página</label>
|
||||
<select name="pp" class="form-select">
|
||||
<?php foreach ([6,12,24,48,96] as $pp): ?>
|
||||
<option value="<?= $pp ?>" <?= $pageSize===$pp?'selected':'' ?>><?= $pp ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-auto ms-auto">
|
||||
<button class="btn btn-primary">Aplicar</button>
|
||||
<a class="btn btn-outline-secondary" href="<?= e($_SERVER['PHP_SELF']) ?>">Limpiar</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LISTADO -->
|
||||
<?php if (!$rows): ?>
|
||||
<div class="alert alert-info">No hay landings publicados que coincidan con los filtros.</div>
|
||||
<?php else: ?>
|
||||
<div class="row g-4">
|
||||
<?php foreach ($rows as $r): ?>
|
||||
<div class="col-12 col-sm-6 col-lg-4">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<img class="card-img-top" loading="lazy"
|
||||
src="<?= e($r['hero_image'] ?: $fallbackImg) ?>"
|
||||
alt="<?= e($r['titulo']) ?>">
|
||||
<div class="card-body d-flex flex-column">
|
||||
<h2 class="h6 card-title mb-2"><?= e($r['titulo']) ?></h2>
|
||||
<?php if (!empty($r['meta_desc'])): ?>
|
||||
<p class="card-text text-muted small line-clamp-2"><?= e($r['meta_desc']) ?></p>
|
||||
<?php endif; ?>
|
||||
<div class="mt-auto d-flex flex-wrap gap-2">
|
||||
<a class="btn btn-primary btn-sm"
|
||||
href="<?= e(PUBLIC_LANDING_ROUTE . urlencode($r['slug'])) ?>">
|
||||
Ver landing
|
||||
</a>
|
||||
<?php if (!empty($r['game_url'])): ?>
|
||||
<a class="btn btn-outline-success btn-sm" target="_blank" rel="noopener"
|
||||
href="<?= e($r['game_url']) ?>">
|
||||
Jugar
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer bg-white d-flex justify-content-between align-items-center">
|
||||
<small class="text-muted">Actualizado: <?= e($r['updated_at'] ?: '—') ?></small>
|
||||
<?php if (!empty($r['slug'])): ?>
|
||||
<span class="badge text-bg-light"><?= e($r['slug']) ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- Paginación -->
|
||||
<?php if ($total_pages > 1): ?>
|
||||
<nav class="mt-4" aria-label="Paginación">
|
||||
<ul class="pagination justify-content-center">
|
||||
<li class="page-item <?= $page<=1?'disabled':'' ?>">
|
||||
<a class="page-link" href="<?= e($base.'&page='.$prev) ?>">Anterior</a>
|
||||
</li>
|
||||
<?php
|
||||
$start = max(1, $page-2); $end = min($total_pages, $page+2);
|
||||
for ($i=$start; $i<=$end; $i++): ?>
|
||||
<li class="page-item <?= $i===$page?'active':'' ?>">
|
||||
<a class="page-link" href="<?= e($base.'&page='.$i) ?>"><?= $i ?></a>
|
||||
</li>
|
||||
<?php endfor; ?>
|
||||
<li class="page-item <?= $page>=$total_pages?'disabled':'' ?>">
|
||||
<a class="page-link" href="<?= e($base.'&page='.$next) ?>">Siguiente</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</main>
|
||||
|
||||
<footer class="border-top py-4">
|
||||
<div class="container text-center text-muted small">
|
||||
© <?= date('Y') ?> — Landings activos
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user