Initial commit: Proyecto Portal ePromos completo con documentación unificada y seguridad base

This commit is contained in:
Isaac Aracena
2026-05-09 12:46:24 -04:00
commit c44f1a69f1
65 changed files with 7223 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
<?php
// memoria/index.php
session_start();
require_once __DIR__ . '/../admin/db.php'; // ajusta la ruta si tu db.php está en otro lugar
$juego_id = (int)($_GET['juego_id'] ?? 1);
// 1) Cargar config del juego (banners, fondo, reverso, icono FS y ruleta_id a la que enlaza)
$stmt = $conn->prepare("SELECT
nombre, banner_1, banner_2, banner_3, banner_4,
background_image, fullscreen_icon, reverso_default, ruleta_id
FROM juegos_config
WHERE juego_id = ? LIMIT 1");
$stmt->bind_param("i", $juego_id);
$stmt->execute();
$config = $stmt->get_result()->fetch_assoc();
$stmt->close();
if (!$config) {
die("Config del juego no encontrada (juego_id=$juego_id).");
}
// 2) Cargar cartas activas (traseras a emparejar). Puedes manejar n pares desde la BD.
$cartas = [];
$res = $conn->prepare("SELECT id, nombre, imagen_trasera, COALESCE(imagen_frontal, '') AS imagen_frontal
FROM cartas
WHERE juego_id = ? AND activo = 1");
$res->bind_param("i", $juego_id);
$res->execute();
$q = $res->get_result();
while ($row = $q->fetch_assoc()) { $cartas[] = $row; }
$res->close();
if (!$cartas) {
die("No hay cartas activas para este juego (juego_id=$juego_id).");
}
// Reverso por defecto (lado frontal que se ve al inicio)
$reverso = $config['reverso_default'] ?: '/memoria/images/back.png';
// Generar parejas: duplico la lista de cartas. Si tienes más de 12, puedes recortar.
$base = $cartas;
shuffle($base);
$max_pairs = 6; // cambia si quieres más o menos pares
$base = array_slice($base, 0, $max_pairs);
// Duplicar para formar pares y asignar una clave de pareja consistente
$deck = [];
$pair_idx = 1;
foreach ($base as $c) {
$key = 'pair_' . $pair_idx++;
$deck[] = ['key'=>$key, 'back'=>$c['imagen_trasera'], 'front'=>$c['imagen_frontal'] ?: $reverso];
$deck[] = ['key'=>$key, 'back'=>$c['imagen_trasera'], 'front'=>$c['imagen_frontal'] ?: $reverso];
}
shuffle($deck);
// Pasar al front la config necesaria
$CONFIG_JS = [
'juego_id' => $juego_id,
'ruleta_id' => (int)$config['ruleta_id'],
'banners' => [
'banner_1' => $config['banner_1'] ?: null,
'banner_2' => $config['banner_2'] ?: null,
'banner_3' => $config['banner_3'] ?: null,
'banner_4' => $config['banner_4'] ?: null,
],
'background_image' => $config['background_image'] ?: null,
'fullscreen_icon' => $config['fullscreen_icon'] ?: null,
];
?>
<!doctype html>
<html lang="es" dir="ltr">
<head>
<meta charset="utf-8">
<title>Juego de Memoria - ePromo</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- Banners dinámicos -->
<div class="banner banner-1"></div>
<div class="banner banner-2"></div>
<div class="banner banner-3"></div>
<div class="banner banner-4"></div>
<div class="wrapper">
<ul class="cards" id="cards">
<?php foreach ($deck as $card): ?>
<li class="card" data-key="<?= htmlspecialchars($card['key']) ?>">
<div class="view front-view">
<img src="<?= htmlspecialchars($card['front']) ?>" alt="front">
</div>
<div class="view back-view">
<img src="<?= htmlspecialchars($card['back']) ?>" alt="back">
</div>
</li>
<?php endforeach; ?>
</ul>
</div>
<!-- Fullscreen -->
<button id="fullscreen-btn"><img id="fullscreen-icon" src="" alt="Pantalla Completa"></button>
<script>
// Inyectar config al front
window.MEMORIA_CONFIG = <?= json_encode($CONFIG_JS, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE) ?>;
// Aplicar banners/bg/icono
(function(){
const C = window.MEMORIA_CONFIG || {};
if (C.background_image) {
document.body.style.backgroundImage = `url("${C.background_image}")`;
document.body.style.backgroundSize = 'cover';
document.body.style.backgroundRepeat = 'no-repeat';
document.body.style.backgroundPosition = 'center center';
}
const b1 = document.querySelector('.banner-1'), b2 = document.querySelector('.banner-2'),
b3 = document.querySelector('.banner-3'), b4 = document.querySelector('.banner-4');
if (b1 && C.banners.banner_1) b1.style.backgroundImage = `url("${C.banners.banner_1}")`;
if (b2 && C.banners.banner_2) b2.style.backgroundImage = `url("${C.banners.banner_2}")`;
if (b3 && C.banners.banner_3) b3.style.backgroundImage = `url("${C.banners.banner_3}")`;
if (b4 && C.banners.banner_4) b4.style.backgroundImage = `url("${C.banners.banner_4}")`;
if (C.fullscreen_icon) document.getElementById('fullscreen-icon').src = C.fullscreen_icon;
})();
</script>
<script src="script.js"></script>
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.5.1/dist/confetti.browser.min.js"></script>
</body>
</html>
+76
View File
@@ -0,0 +1,76 @@
<?php
// memoria/registrar_jugada.php
header('Content-Type: application/json; charset=utf-8');
session_start();
require_once __DIR__ . '/../admin/db.php';
// Leer payload
$payload = json_decode(file_get_contents('php://input'), true) ?: [];
$juego_id = (int)($payload['juego_id'] ?? 1);
$elapsed = (int)($payload['elapsed_ms'] ?? 0);
$aciertos = (int)($payload['aciertos'] ?? 0);
$user_agent = $_SERVER['HTTP_USER_AGENT'] ?? '';
$ip_address = $_SERVER['REMOTE_ADDR'] ?? '';
// 1) Traer la ruleta asociada a este juego
$stmt = $conn->prepare("SELECT ruleta_id FROM juegos_config WHERE juego_id=? LIMIT 1");
$stmt->bind_param("i", $juego_id);
$stmt->execute();
$ruleta_id = (int)($stmt->get_result()->fetch_assoc()['ruleta_id'] ?? 0);
$stmt->close();
// 2) Registrar jugada propia del juego de memoria
$stmt = $conn->prepare("INSERT INTO jugadas_memoria (juego_id, ip_address, user_agent, tiempo_ms, aciertos) VALUES (?,?,?,?,?)");
$stmt->bind_param("issii", $juego_id, $ip_address, $user_agent, $elapsed, $aciertos);
$stmt->execute();
$stmt->close();
// 3) Seleccionar premio usando la misma lógica que la ruleta
$premio_nombre = null;
if ($ruleta_id > 0) {
// Leer premios con inventario y probabilidad
$stmt = $conn->prepare("SELECT id, nombre, cantidad, probabilidad FROM premios WHERE ruleta_id=? AND cantidad > 0");
$stmt->bind_param("i", $ruleta_id);
$stmt->execute();
$res = $stmt->get_result();
$premios = [];
$totalProb = 0.0;
while ($row = $res->fetch_assoc()) {
$row['probabilidad'] = (float)$row['probabilidad'];
$premios[] = $row;
$totalProb += $row['probabilidad'];
}
$stmt->close();
if ($premios && $totalProb > 0) {
// sorteo ponderado
$rnd = mt_rand() / mt_getrandmax() * $totalProb;
$acc = 0.0; $ganador = null;
foreach ($premios as $p) {
$acc += $p['probabilidad'];
if ($rnd <= $acc) { $ganador = $p; break; }
}
if (!$ganador) $ganador = end($premios);
// Guardar jugada general de la ruleta (para tus reportes unificados)
$stmt = $conn->prepare("INSERT INTO jugadas (ruleta_id, premio_id, ip_address, user_agent) VALUES (?,?,?,?)");
$stmt->bind_param("iiss", $ruleta_id, $ganador['id'], $ip_address, $user_agent);
$stmt->execute();
$stmt->close();
// Descontar inventario
$stmt = $conn->prepare("UPDATE premios SET cantidad = cantidad - 1 WHERE id=? AND cantidad > 0");
$stmt->bind_param("i", $ganador['id']);
$stmt->execute();
$stmt->close();
$premio_nombre = $ganador['nombre'];
}
}
echo json_encode([
'ok' => true,
'premio' => $premio_nombre
]);
+101
View File
@@ -0,0 +1,101 @@
// memoria/script.js
(function () {
const cards = document.querySelectorAll(".card");
const totalPairs = cards.length / 2;
let matched = 0;
let cardOne = null, cardTwo = null;
let disableDeck = false;
const startTime = Date.now();
function flipCard({ target: clickedCard }) {
if (disableDeck || clickedCard === cardOne || clickedCard.classList.contains("flip")) return;
clickedCard.classList.add("flip");
if (!cardOne) { cardOne = clickedCard; return; }
cardTwo = clickedCard;
disableDeck = true;
const k1 = cardOne.getAttribute("data-key");
const k2 = cardTwo.getAttribute("data-key");
matchCards(k1, k2);
}
function matchCards(k1, k2) {
if (k1 === k2) {
matched++;
cardOne.removeEventListener("click", flipCard);
cardTwo.removeEventListener("click", flipCard);
cardOne = cardTwo = null;
disableDeck = false;
if (matched === totalPairs) {
// Juego terminado
setTimeout(onGameFinished, 600);
}
return;
}
setTimeout(() => {
cardOne.classList.add("shake");
cardTwo.classList.add("shake");
}, 300);
setTimeout(() => {
cardOne.classList.remove("shake", "flip");
cardTwo.classList.remove("shake", "flip");
cardOne = cardTwo = null;
disableDeck = false;
}, 1000);
}
function onGameFinished() {
const elapsedMs = Date.now() - startTime;
const C = window.MEMORIA_CONFIG || {};
const juego_id = C.juego_id || 1;
// Registrar jugada y obtener premio (usando la ruleta vinculada en la config del juego)
fetch("registrar_jugada.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
juego_id,
elapsed_ms: elapsedMs,
aciertos: matched,
// ruleta_id es opcional aquí; el servidor puede leerlo de la config
}),
})
.then(r => r.json())
.then(data => {
const nombrePremio = data && data.premio ? data.premio : "¡Completado!";
mostrarConfetiConTexto(nombrePremio);
// Si quieres, podrías reiniciar el mazo o redirigir:
// setTimeout(() => location.reload(), 2500);
})
.catch(err => {
console.error(err);
mostrarConfetiConTexto("¡Completado!");
});
}
// Eventos
cards.forEach(card => card.addEventListener("click", flipCard));
function toggleFullScreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(err => console.log(err));
} else {
document.exitFullscreen().catch(err => console.log(err));
}
}
document.getElementById("fullscreen-btn").addEventListener("click", toggleFullScreen);
// Confeti
function mostrarConfetiConTexto(texto = "") {
const duration = 4000, end = Date.now() + duration;
(function frame() {
confetti({ particleCount: 7, angle: 60, spread: 55, origin: { x: 0 }, scalar: 1.1 });
confetti({ particleCount: 7, angle: 120, spread: 55, origin: { x: 1 }, scalar: 1.1 });
if (Date.now() < end) requestAnimationFrame(frame);
})();
if (texto) console.log("🎉 Premio:", texto);
}
})();
+130
View File
@@ -0,0 +1,130 @@
/* Import Google Font - Poppins */
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap');
*{
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Poppins', sans-serif;
}
body{
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
background: #E9AD14 /*url(https://portal.gomezleemarketing.com/wp-content/uploads/2024/02/BANNER-SUPERIOR-GRANDE.png) repeat center center*/;
}
.wrapper{
padding: 10px;
border-radius: 10px;
background: #727272;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}
.cards, .card, .view{
display: flex;
align-items: center;
justify-content: center;
}
.cards{
height: 800px;
width: 800px;
flex-wrap: wrap;
justify-content: space-between;
}
.cards .card{
cursor: pointer;
list-style: none;
user-select: none;
position: relative;
perspective: 1000px;
transform-style: preserve-3d;
height: calc(100% / 4 - 10px);
width: calc(100% / 3 - 10px);
}
.card.shake{
animation: shake 0.35s ease-in-out;
}
@keyframes shake {
0%, 100%{
transform: translateX(0);
}
20%{
transform: translateX(-13px);
}
40%{
transform: translateX(13px);
}
60%{
transform: translateX(-8px);
}
80%{
transform: translateX(8px);
}
}
.card .view{
width: 100%;
height: 100%;
position: absolute;
border-radius: 7px;
background: #fff;
pointer-events: none;
backface-visibility: hidden;
box-shadow: 0 3px 10px rgba(0,0,0,0.1);
transition: transform 0.25s linear;
}
.card .front-view img{
width: 120px;
}
.card .back-view img{
max-width: 150px;
}
.card .back-view{
transform: rotateY(-180deg);
}
.card.flip .back-view{
transform: rotateY(0);
}
.card.flip .front-view{
transform: rotateY(180deg);
}
@media screen and (max-width: 700px) {
.cards{
height: 400px;
width: 400px;
}
.card .front-view img{
width: 70px;
}
.card .back-view img{
max-width: 80px;
}
}
@media screen and (max-width: 530px) {
.cards{
height: 400px;
width: 400px;
}
.card .front-view img{
width: 70px;
}
.card .back-view img{
max-width: 80px;
}
}
#fullscreen-btn {
position: fixed;
top: 10px;
right: 10px;
background: none;
border: none;
cursor: pointer;
z-index: 1000;
}
#fullscreen-btn img {
width: 40px;
height: 40px;
}