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
+241
View File
@@ -0,0 +1,241 @@
-- Tabla principal de landings
CREATE TABLE IF NOT EXISTS landing_pages (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
titulo VARCHAR(200) NOT NULL,
slug VARCHAR(160) NOT NULL UNIQUE,
estado ENUM('borrador','publicado') NOT NULL DEFAULT 'borrador',
hero_image VARCHAR(255) NULL,
contenido_html MEDIUMTEXT NULL, -- HTML renderizable
css_inline MEDIUMTEXT NULL, -- CSS opcional por landing
js_inline MEDIUMTEXT NULL, -- JS opcional por landing
meta_title VARCHAR(200) NULL,
meta_desc VARCHAR(255) NULL,
canonical_url VARCHAR(255) NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Captura de leads del formulario de la landing
CREATE TABLE IF NOT EXISTS landing_forms (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
landing_id INT UNSIGNED NOT NULL,
nombre VARCHAR(150) NULL,
email VARCHAR(180) NULL,
telefono VARCHAR(60) NULL,
mensaje TEXT NULL,
ip_address VARCHAR(64) NULL,
user_agent TEXT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX (landing_id),
CONSTRAINT fk_lf_lp FOREIGN KEY (landing_id) REFERENCES landing_pages(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
-- 1. Bloques reutilizables (biblioteca)
CREATE TABLE IF NOT EXISTS landing_blocks (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(160) NOT NULL,
tipo ENUM('html','css','js') NOT NULL DEFAULT 'html',
contenido MEDIUMTEXT NOT NULL,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY idx_nombre (nombre)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 2. Asignación de bloques a una landing (con orden)
CREATE TABLE IF NOT EXISTS landing_page_blocks (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
landing_id INT UNSIGNED NOT NULL,
block_id INT UNSIGNED NOT NULL,
posicion INT NOT NULL DEFAULT 1, -- orden
zona VARCHAR(40) NOT NULL DEFAULT 'main', -- main/header/footer/etc
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_lp_block (landing_id, block_id, zona),
KEY idx_landing_pos (landing_id, posicion),
CONSTRAINT fk_lpb_lp FOREIGN KEY (landing_id) REFERENCES landing_pages(id) ON DELETE CASCADE,
CONSTRAINT fk_lpb_blk FOREIGN KEY (block_id) REFERENCES landing_blocks(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 3. Variantes A/B de una landing
CREATE TABLE IF NOT EXISTS landing_variants (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
landing_id INT UNSIGNED NOT NULL,
nombre VARCHAR(120) NOT NULL,
peso DECIMAL(6,3) NOT NULL DEFAULT 1.000, -- tráfico relativo
estado ENUM('borrador','activo','pausado') NOT NULL DEFAULT 'activo',
-- contenido específico de la variante (opcional); si NULL, toma el de landing_pages
hero_image VARCHAR(255) NULL,
contenido_html MEDIUMTEXT NULL,
css_inline MEDIUMTEXT NULL,
js_inline MEDIUMTEXT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
KEY idx_lv_landing (landing_id, estado),
CONSTRAINT fk_lv_lp FOREIGN KEY (landing_id) REFERENCES landing_pages(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 4. Programación por fecha (ventanas de publicación)
CREATE TABLE IF NOT EXISTS landing_schedule (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
landing_id INT UNSIGNED NOT NULL,
start_at DATETIME NULL,
end_at DATETIME NULL,
estado ENUM('programada','activa','finalizada') NOT NULL DEFAULT 'programada',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY idx_sched (landing_id, start_at, end_at),
CONSTRAINT fk_ls_lp FOREIGN KEY (landing_id) REFERENCES landing_pages(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 5. Tracking: impresiones y conversiones por variante
CREATE TABLE IF NOT EXISTS landing_variant_stats (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
landing_id INT UNSIGNED NOT NULL,
variant_id INT UNSIGNED NULL, -- NULL = página base (sin variante)
event_type ENUM('impression','conversion') NOT NULL,
session_id VARCHAR(64) NULL,
ip_address VARCHAR(64) NULL,
user_agent TEXT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY idx_lv_evt (landing_id, variant_id, event_type, created_at),
CONSTRAINT fk_lvs_lp FOREIGN KEY (landing_id) REFERENCES landing_pages(id) ON DELETE CASCADE,
CONSTRAINT fk_lvs_lv FOREIGN KEY (variant_id) REFERENCES landing_variants(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
---
## Estructura de Base de Datos
```sql
-- Landings principales
CREATE TABLE landing_pages (
id INT AUTO_INCREMENT PRIMARY KEY,
titulo VARCHAR(255) NOT NULL,
slug VARCHAR(255) NOT NULL UNIQUE,
descripcion TEXT,
activo TINYINT(1) DEFAULT 1,
fecha_creacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Bloques reutilizables
CREATE TABLE landing_blocks (
id INT AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(255) NOT NULL,
tipo ENUM('texto','html','imagen','video') DEFAULT 'texto',
contenido TEXT,
fecha_creacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Relación landing <-> bloques
CREATE TABLE landing_page_blocks (
id INT AUTO_INCREMENT PRIMARY KEY,
landing_id INT NOT NULL,
block_id INT NOT NULL,
zona VARCHAR(40) DEFAULT 'main',
posicion INT DEFAULT 1,
FOREIGN KEY (landing_id) REFERENCES landing_pages(id) ON DELETE CASCADE,
FOREIGN KEY (block_id) REFERENCES landing_blocks(id) ON DELETE CASCADE
);
-- Variantes para A/B testing
CREATE TABLE landing_variants (
id INT AUTO_INCREMENT PRIMARY KEY,
landing_id INT NOT NULL,
nombre VARCHAR(255),
porcentaje_traffic DECIMAL(5,2) DEFAULT 50.00,
activo TINYINT(1) DEFAULT 1,
FOREIGN KEY (landing_id) REFERENCES landing_pages(id) ON DELETE CASCADE
);
-- Programación de publicaciones
CREATE TABLE landing_schedule (
id INT AUTO_INCREMENT PRIMARY KEY,
landing_id INT NOT NULL,
variant_id INT DEFAULT NULL,
fecha_inicio DATETIME,
fecha_fin DATETIME,
activo TINYINT(1) DEFAULT 1,
FOREIGN KEY (landing_id) REFERENCES landing_pages(id) ON DELETE CASCADE,
FOREIGN KEY (variant_id) REFERENCES landing_variants(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS landing_pages (
id INT AUTO_INCREMENT PRIMARY KEY,
titulo VARCHAR(255) NOT NULL,
slug VARCHAR(255) NOT NULL UNIQUE,
descripcion TEXT,
activo TINYINT(1) DEFAULT 1,
fecha_creacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS landing_variants (
id INT AUTO_INCREMENT PRIMARY KEY,
landing_id INT NOT NULL,
nombre VARCHAR(255),
porcentaje_traffic DECIMAL(5,2) DEFAULT 50.00,
activo TINYINT(1) DEFAULT 1,
FOREIGN KEY (landing_id) REFERENCES landing_pages(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS landing_blocks (
id INT AUTO_INCREMENT PRIMARY KEY,
nombre VARCHAR(255) NOT NULL,
tipo ENUM('texto','html','imagen','video') DEFAULT 'texto',
contenido TEXT,
fecha_creacion TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS landing_page_blocks (
id INT AUTO_INCREMENT PRIMARY KEY,
landing_id INT NOT NULL,
block_id INT NOT NULL,
zona VARCHAR(40) DEFAULT 'main',
posicion INT DEFAULT 1,
FOREIGN KEY (landing_id) REFERENCES landing_pages(id) ON DELETE CASCADE,
FOREIGN KEY (block_id) REFERENCES landing_blocks(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS landing_forms (
id INT AUTO_INCREMENT PRIMARY KEY,
landing_id INT NOT NULL,
nombre VARCHAR(200),
email VARCHAR(200),
telefono VARCHAR(50),
mensaje TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ip_address VARCHAR(100),
user_agent TEXT,
FOREIGN KEY (landing_id) REFERENCES landing_pages(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS landing_variant_stats (
id INT AUTO_INCREMENT PRIMARY KEY,
landing_id INT NOT NULL,
variant_id INT NULL,
event_type ENUM('impression','conversion') NOT NULL,
session_id VARCHAR(100) NULL,
ip_address VARCHAR(100) NULL,
user_agent TEXT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX (landing_id),
INDEX (variant_id),
INDEX (event_type),
INDEX (created_at),
FOREIGN KEY (landing_id) REFERENCES landing_pages(id) ON DELETE CASCADE,
FOREIGN KEY (variant_id) REFERENCES landing_variants(id) ON DELETE SET NULL
);
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+22
View File
@@ -0,0 +1,22 @@
# Configuración de base de datos (Sensible)
admin/db.php
# Contenidos de uploads (Mantener carpetas pero ignorar archivos subidos)
uploads/ruletas/*
uploads/rasca/*
uploads/rasca_premios/*
uploads/memoria/*
!uploads/.gitkeep
!uploads/**/.gitkeep
# Archivos de sistema
.DS_Store
Thumbs.db
*.log
*.tmp
# Comprimidos y backups
*.zip
*.rar
*.7z
*.sql.bak
+6
View File
@@ -0,0 +1,6 @@
RewriteEngine On
# Quitar .php de las URLs
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}\.php -f
RewriteRule ^(.+)$ $1.php [L]
+16
View File
@@ -0,0 +1,16 @@
ruleta/
├── rasca_index.html
├── rasca_style.css
├── rasca_app.js
├── rasca_wScratchpad.min.js
├── api/
│ ├── rasca_config.php
│ └── rasca_premio.php
└── admin/
├── rasca_config.php
├── rasca_premios.php
├── rasca_jugadas.php
└── db.php (ya existente en tu admin)
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 475 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 388 KiB

+87
View File
@@ -0,0 +1,87 @@
# Portal ePromos — Hub de Juegos y Landings
Este proyecto es una plataforma gamificada para la captación de leads y aumento de engagement mediante experiencias interactivas. Incluye juegos de **Ruleta**, **Rasca y Gana**, y **Memoria**, además de un potente constructor de **Landing Pages**.
## 🚀 Requisitos del Sistema
- **PHP**: 8.0 o superior (Extensiones: `mysqli`, `mbstring`, `fileinfo`).
- **Base de Datos**: MariaDB/MySQL 10.4+.
- **Servidor Web**: Apache (con `mod_rewrite` habilitado) o Nginx.
---
## 🛠️ Instalación y Configuración
### 1. Preparar el Código
1. Clona el repositorio en tu servidor.
2. Asegúrate de que las carpetas en `uploads/` tengan permisos de escritura:
```bash
chmod -R 775 uploads/
```
### 2. Configurar Base de Datos
1. Crea la base de datos:
```sql
CREATE DATABASE ruleta_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
```
2. Importa el esquema base: `ruleta_db.sql`.
3. Importa el esquema de landings: `-- Tabla principal de landings.txt`.
### 3. Configurar Credenciales
1. Renombra `admin/db.php.example` a `admin/db.php`.
2. Edita los valores de host, usuario y contraseña:
```php
$DB_HOST = 'tu_host';
$DB_USER = 'tu_usuario';
$DB_PASS = 'tu_password';
$DB_NAME = 'ruleta_db';
```
### 4. Ajuste de Rutas (Importante)
El código asume por defecto que el proyecto vive en la subcarpeta `/ruleta/`. Si lo instalas en la raíz o en otra ruta, ajusta las siguientes constantes:
- **Admin**: En `admin/ruletas.php`, `admin/rasca_config.php`, etc., cambia `$PUBLIC_DIR`.
- **Landings**: En `landings_public.php`, cambia `PUBLIC_LANDING_ROUTE`.
---
## 🎮 Módulos Incluidos
### Ruleta
- Configuración de premios por ángulos (0-360°).
- Control de stock y probabilidad ponderada.
- **Ruta**: `index.html?ruleta_id=1`
### Rasca y Gana
- Capas de raspado personalizables.
- Premios dinámicos basados en stock.
- **Ruta**: `rasca_index.html?rasca_id=1`
### Memoria
- Gestión de pares de cartas.
- Posibilidad de enlazar con un premio final de ruleta.
- **Ruta**: `/memoria/?juego_id=1`
### Constructor de Landings
- Creación de landings vía Slug.
- A/B Testing integrado.
- Captura de formularios y exportación CSV.
- **Admin**: `/admin/landing.php`
---
## 🔐 Acceso al Panel de Administración
- **URL**: `/admin/`
- **Usuario por defecto**: `admin@localhost`
- **Contraseña temporal**: Para entrar por primera vez si no conoces la clave, ejecuta:
```sql
UPDATE admin_users SET password_hash='admin123' WHERE email='admin@localhost';
```
*Nota: El sistema re-hasheará la contraseña automáticamente al primer login.*
---
## 📦 Backup y Mantenimiento
- **Archivos**: Respaldar periódicamente la carpeta `uploads/`.
- **Base de Datos**: `mysqldump -u [user] -p ruleta_db > backup.sql`
---
*Desarrollado para GLM por Isaac Aracena.*
+6
View File
@@ -0,0 +1,6 @@
RewriteEngine On
# Quitar .php de las URLs
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME}\.php -f
RewriteRule ^(.+)$ $1.php [L]
+186
View File
@@ -0,0 +1,186 @@
<?php
// admin/cartas.php
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
$UPLOAD_DIR = __DIR__ . '/../uploads/memoria';
$PUBLIC_DIR = '/ruleta/uploads/memoria'; // AJUSTA si tu base no es /ruleta
if (!is_dir($UPLOAD_DIR)) { @mkdir($UPLOAD_DIR, 0775, true); }
function sanitize_filename($name) { $name=preg_replace('/[^A-Za-z0-9._-]/','_',$name); return substr($name,0,180); }
function upload_image($field,$UPLOAD_DIR,$PUBLIC_DIR){
if (!isset($_FILES[$field]) || $_FILES[$field]['error']===UPLOAD_ERR_NO_FILE) return null;
$f=$_FILES[$field]; if($f['error']!==UPLOAD_ERR_OK) throw new RuntimeException("Error subiendo $field.");
$mime=(new finfo(FILEINFO_MIME_TYPE))->file($f['tmp_name']);
$allowed=['image/png'=>'png','image/jpeg'=>'jpg','image/webp'=>'webp']; if(!isset($allowed[$mime])) throw new RuntimeException("Formato no permitido.");
if($f['size']>5*1024*1024) throw new RuntimeException("Archivo muy grande (5MB).");
$ext=$allowed[$mime]; $base=sanitize_filename(pathinfo($f['name'],PATHINFO_FILENAME)); $fname=$base.'-'.uniqid().'.'.$ext;
$dest=rtrim($UPLOAD_DIR,'/\\').DIRECTORY_SEPARATOR.$fname; if(!move_uploaded_file($f['tmp_name'],$dest)) throw new RuntimeException("No se pudo guardar $field.");
return rtrim($PUBLIC_DIR,'/').'/'.$fname;
}
function all_juegos(mysqli $conn){
$res=$conn->query("SELECT juego_id, nombre FROM juegos_config ORDER BY juego_id ASC");
return $res->fetch_all(MYSQLI_ASSOC);
}
function get_carta(mysqli $conn,$id){
$stmt=$conn->prepare("SELECT * FROM cartas WHERE id=?"); $stmt->bind_param("i",$id); $stmt->execute();
return $stmt->get_result()->fetch_assoc();
}
$msg=$err=''; $action=$_GET['action']??''; $id=(int)($_GET['id']??0);
$juegos=all_juegos($conn);
$juego_id=(int)($_GET['juego_id'] ?? ($juegos[0]['juego_id'] ?? 1));
// CREATE/UPDATE
if ($_SERVER['REQUEST_METHOD']==='POST') {
$cid=(int)($_POST['id']??0);
$juego_id=(int)($_POST['juego_id']??0);
$nombre=trim($_POST['nombre']??'');
$activo=isset($_POST['activo'])?1:0;
$old_front=$_POST['old_imagen_frontal']??'';
$old_back =$_POST['old_imagen_trasera']??'';
if ($juego_id<=0 || $nombre==='') {
$err="Selecciona juego y nombre.";
} else {
try{
$new_front=upload_image('imagen_frontal',$UPLOAD_DIR,$PUBLIC_DIR);
$new_back =upload_image('imagen_trasera',$UPLOAD_DIR,$PUBLIC_DIR);
$front=$new_front?:($old_front?:null);
$back =$new_back ?:($old_back ?:null);
if ($cid>0) {
$stmt=$conn->prepare("UPDATE cartas SET juego_id=?, nombre=?, imagen_frontal=?, imagen_trasera=?, activo=? WHERE id=?");
$stmt->bind_param("isssii",$juego_id,$nombre,$front,$back,$activo,$cid);
$stmt->execute(); $msg="Carta actualizada.";
} else {
if(!$back){ throw new RuntimeException("La imagen trasera es obligatoria."); }
$stmt=$conn->prepare("INSERT INTO cartas (juego_id,nombre,imagen_frontal,imagen_trasera,activo) VALUES (?,?,?,?,?)");
$stmt->bind_param("isssi",$juego_id,$nombre,$front,$back,$activo);
$stmt->execute(); $msg="Carta creada.";
}
}catch(Throwable $e){ $err=$e->getMessage(); }
}
}
// DELETE
if ($action==='delete' && $id>0) {
$stmt=$conn->prepare("DELETE FROM cartas WHERE id=?"); $stmt->bind_param("i",$id); $stmt->execute(); $msg="Carta eliminada.";
}
// EDIT
$edit=null;
if ($action==='edit' && $id>0) { $edit=get_carta($conn,$id); if($edit) $juego_id=(int)$edit['juego_id']; }
// LIST
$stmt=$conn->prepare("SELECT c.*, gc.nombre AS juego FROM cartas c LEFT JOIN juegos_config gc ON gc.juego_id=c.juego_id WHERE c.juego_id=? ORDER BY c.id DESC");
$stmt->bind_param("i",$juego_id); $stmt->execute();
$items=$stmt->get_result()->fetch_all(MYSQLI_ASSOC);
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Memoria | Cartas</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>.thumb{width:70px;height:70px;object-fit:cover;border-radius:.5rem;}</style>
</head>
<body>
<nav class="navbar navbar-expand-lg bg-dark navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Home</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="juegos_config.php">Memoria: Juegos</a>
<a class="btn btn-outline-light btn-sm" href="cartas.php">Memoria: Cartas</a>
<a class="btn btn-outline-light btn-sm" href="jugadas_memoria.php">Memoria: Jugadas</a>
<a class="btn btn-outline-light btn-sm" href="../index.php?action=logout">Salir</a>
</div>
</div>
</nav>
<div class="container py-4">
<div class="d-flex align-items-center justify-content-between">
<h3 class="mb-3">Cartas</h3>
<form class="d-flex" method="get">
<select name="juego_id" class="form-select me-2" onchange="this.form.submit()">
<?php foreach($juegos as $j): ?>
<option value="<?= (int)$j['juego_id'] ?>" <?= $juego_id===$j['juego_id']?'selected':'' ?>>
<?= htmlspecialchars($j['nombre']) ?> (ID <?= (int)$j['juego_id'] ?>)
</option>
<?php endforeach; ?>
</select>
<noscript><button class="btn btn-primary">Cambiar</button></noscript>
</form>
</div>
<?php if ($msg): ?><div class="alert alert-success py-2"><?= htmlspecialchars($msg) ?></div><?php endif; ?>
<?php if ($err): ?><div class="alert alert-danger py-2"><?= htmlspecialchars($err) ?></div><?php endif; ?>
<div class="card shadow-sm mb-4"><div class="card-body">
<h5 class="mb-3"><?= $edit ? 'Editar carta' : 'Nueva carta' ?></h5>
<form method="post" enctype="multipart/form-data" class="row g-3">
<input type="hidden" name="id" value="<?= (int)($edit['id'] ?? 0) ?>">
<input type="hidden" name="old_imagen_frontal" value="<?= htmlspecialchars($edit['imagen_frontal'] ?? '') ?>">
<input type="hidden" name="old_imagen_trasera" value="<?= htmlspecialchars($edit['imagen_trasera'] ?? '') ?>">
<div class="col-md-3">
<label class="form-label">Juego</label>
<select name="juego_id" class="form-select" required>
<?php foreach($juegos as $j): ?>
<option value="<?= (int)$j['juego_id'] ?>" <?= ($juego_id===$j['juego_id'])?'selected':'' ?>><?= htmlspecialchars($j['nombre']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Nombre</label>
<input name="nombre" class="form-control" required value="<?= htmlspecialchars($edit['nombre'] ?? '') ?>">
</div>
<div class="col-md-3">
<label class="form-label">Imagen trasera (obligatoria)</label>
<input type="file" name="imagen_trasera" class="form-control" accept=".png,.jpg,.jpeg,.webp">
<?php if (!empty($edit['imagen_trasera'])): ?><div class="mt-2"><img src="<?= htmlspecialchars($edit['imagen_trasera']) ?>" class="thumb"></div><?php endif; ?>
</div>
<div class="col-md-3">
<label class="form-label">Imagen frontal (opcional)</label>
<input type="file" name="imagen_frontal" class="form-control" accept=".png,.jpg,.jpeg,.webp">
<?php if (!empty($edit['imagen_frontal'])): ?><div class="mt-2"><img src="<?= htmlspecialchars($edit['imagen_frontal']) ?>" class="thumb"></div><?php endif; ?>
</div>
<div class="col-md-2">
<label class="form-label">Activo</label><br>
<input type="checkbox" name="activo" value="1" <?= !empty($edit['activo'])?'checked':'' ?>>
</div>
<div class="col-12">
<button class="btn btn-primary"><?= $edit ? 'Actualizar' : 'Crear' ?></button>
<?php if ($edit): ?><a href="cartas.php?juego_id=<?= (int)$juego_id ?>" class="btn btn-secondary">Cancelar</a><?php endif; ?>
</div>
</form>
</div></div>
<div class="card shadow-sm"><div class="card-body table-responsive">
<table class="table table-striped table-sm align-middle">
<thead><tr><th>ID</th><th>Juego</th><th>Nombre</th><th>Trasera</th><th>Frontal</th><th>Activo</th><th>Acciones</th></tr></thead>
<tbody>
<?php foreach($items as $it): ?>
<tr>
<td><?= (int)$it['id'] ?></td>
<td><?= htmlspecialchars($it['juego'].' ('.$it['juego_id'].')') ?></td>
<td><?= htmlspecialchars($it['nombre']) ?></td>
<td><?= $it['imagen_trasera']?'<img class="thumb" src="'.htmlspecialchars($it['imagen_trasera']).'">':'—' ?></td>
<td><?= $it['imagen_frontal']?'<img class="thumb" src="'.htmlspecialchars($it['imagen_frontal']).'">':'—' ?></td>
<td><?= $it['activo']?'Sí':'No' ?></td>
<td class="text-nowrap">
<a class="btn btn-sm btn-outline-primary" href="cartas.php?action=edit&id=<?= (int)$it['id'] ?>&juego_id=<?= (int)$juego_id ?>">Editar</a>
<a class="btn btn-sm btn-outline-danger" href="cartas.php?action=delete&id=<?= (int)$it['id'] ?>&juego_id=<?= (int)$juego_id ?>" onclick="return confirm('¿Eliminar carta?')">Eliminar</a>
</td>
</tr>
<?php endforeach; ?>
<?php if (!$items): ?><tr><td colspan="7" class="text-center text-muted">Sin cartas</td></tr><?php endif; ?>
</tbody>
</table>
</div></div>
</div>
</body>
</html>
+238
View File
@@ -0,0 +1,238 @@
<?php
session_start();
require_once __DIR__ . '/db.php';
if (isset($_GET['action']) && $_GET['action'] === 'logout') {
session_destroy();
header('Location: index.php');
exit;
}
if (empty($_SESSION['admin_id'])) {
header('Location: index.php');
exit;
}
$total_ruletas = (int)$conn->query("SELECT COUNT(*) c FROM ruletas")->fetch_assoc()['c'];
$total_premios = (int)$conn->query("SELECT COUNT(*) c FROM premios")->fetch_assoc()['c'];
$total_jugadas = (int)$conn->query("SELECT COUNT(*) c FROM jugadas")->fetch_assoc()['c'];
$ultimas = $conn->query("
SELECT j.id, j.fecha_juego, r.nombre AS ruleta, p.nombre AS premio, j.ip_address
FROM jugadas j
LEFT JOIN ruletas r ON r.id = j.ruleta_id
LEFT JOIN premios p ON p.id = j.premio_id
ORDER BY j.id DESC
LIMIT 10
");
// --- contadores para Memoria ---
$total_mem_juegos = (int)$conn->query("SELECT COUNT(*) c FROM juegos_config")->fetch_assoc()['c'];
$total_mem_cartas = (int)$conn->query("SELECT COUNT(*) c FROM cartas WHERE activo=1")->fetch_assoc()['c'];
$total_mem_jugadas = (int)$conn->query("SELECT COUNT(*) c FROM jugadas_memoria")->fetch_assoc()['c'];
// últimas 10 jugadas de memoria
$ult_mem = $conn->query("
SELECT jm.id, jm.fecha_juego, jm.juego_id, jm.tiempo_ms, jm.aciertos, gc.nombre AS juego
FROM jugadas_memoria jm
LEFT JOIN juegos_config gc ON gc.juego_id = jm.juego_id
ORDER BY jm.id DESC
LIMIT 10
");
// --- contadores para Rasca y Gana ---
$total_rasca_juegos = (int)$conn->query("SELECT COUNT(*) c FROM rasca_config")->fetch_assoc()['c'];
$total_rasca_premios = (int)$conn->query("SELECT COUNT(*) c FROM rasca_premios WHERE activo=1")->fetch_assoc()['c'];
$total_rasca_jugadas = (int)$conn->query("SELECT COUNT(*) c FROM jugadas_rasca")->fetch_assoc()['c'];
// últimas 10 jugadas de Rasca
$ult_rasca = $conn->query("
SELECT jr.id,
jr.fecha_juego,
rc.nombre AS rasca,
COALESCE(rp.nombre, '—') AS premio,
jr.ip_address AS ip
FROM jugadas_rasca jr
LEFT JOIN rasca_config rc ON rc.rasca_id = jr.rasca_id
LEFT JOIN rasca_premios rp ON rp.id = jr.premio_id
ORDER BY jr.id DESC
LIMIT 10
");
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Dashboard | Admin Ruletas</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg bg-dark navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">Home</a>
<div class="d-flex">
<span class="navbar-text me-3">
<?= htmlspecialchars($_SESSION['admin_nombre'] ?? $_SESSION['admin_email']) ?>
</span>
<a href="?action=logout" class="btn btn-outline-light btn-sm">Salir</a>
</div>
</div>
</nav>
<div class="container py-4">
<?php include __DIR__ . '/dashboard_landing_widgets_adv.php'; ?>
<h5 class="mt-4">Juegos Disponibles</h5>
<div class="row g-3">
<div class="col-md-4"><div class="card shadow-sm"><div class="card-body">
<h6 class="text-muted mb-1">Ruletas</h6>
<div class="display-6"><?= $total_ruletas ?></div>
<a href="ruletas.php" class="btn btn-sm btn-primary mt-2">Gestionar ruletas</a>
</div></div></div>
<div class="col-md-4"><div class="card shadow-sm"><div class="card-body">
<h6 class="text-muted mb-1">Premios</h6>
<div class="display-6"><?= $total_premios ?></div>
<a href="premios.php" class="btn btn-sm btn-primary mt-2">Gestionar premios</a>
</div></div></div>
<div class="col-md-4"><div class="card shadow-sm"><div class="card-body">
<h6 class="text-muted mb-1">Jugadas</h6>
<div class="display-6"><?= $total_jugadas ?></div>
<a href="jugadas.php" class="btn btn-sm btn-primary mt-2">Ver jugadas</a>
</div></div></div>
<div class="col-md-4">
<div class="card shadow-sm"><div class="card-body">
<h6 class="text-muted mb-1">Juegos de Memoria</h6>
<div class="display-6"><?= $total_mem_juegos ?></div>
<a href="juegos_config.php" class="btn btn-sm btn-primary mt-2">Configurar juegos</a>
</div></div>
</div>
<div class="col-md-4">
<div class="card shadow-sm"><div class="card-body">
<h6 class="text-muted mb-1">Cartas activas</h6>
<div class="display-6"><?= $total_mem_cartas ?></div>
<a href="cartas.php" class="btn btn-sm btn-primary mt-2">Gestionar cartas</a>
</div></div>
</div>
<div class="col-md-4">
<div class="card shadow-sm"><div class="card-body">
<h6 class="text-muted mb-1">Jugadas Memoria</h6>
<div class="display-6"><?= $total_mem_jugadas ?></div>
<a href="jugadas_memoria.php" class="btn btn-sm btn-primary mt-2">Ver jugadas</a>
</div></div>
</div>
<div class="col-md-4">
<div class="card shadow-sm"><div class="card-body">
<h6 class="text-muted mb-1">Juegos de Rasca y Gana</h6>
<div class="display-6"><?= $total_rasca_juegos ?></div>
<a href="rasca_config.php" class="btn btn-sm btn-primary mt-2">Configurar juegos</a>
</div></div>
</div>
<div class="col-md-4">
<div class="card shadow-sm"><div class="card-body">
<h6 class="text-muted mb-1">Premios de Rasca y Gana</h6>
<div class="display-6"><?= $total_rasca_premios ?></div>
<a href="rasca_premios.php" class="btn btn-sm btn-primary mt-2">Gestionar premios</a>
</div></div>
</div>
<div class="col-md-4">
<div class="card shadow-sm"><div class="card-body">
<h6 class="text-muted mb-1">Jugadas de Rasca y Gana</h6>
<div class="display-6"><?= $total_rasca_jugadas ?></div>
<a href="rasca_jugadas.php" class="btn btn-sm btn-primary mt-2">Ver jugadas</a>
</div></div>
</div>
</div>
<h5 class="mt-4">Últimas 10 jugadas</h5>
<div class="card shadow-sm">
<div class="card-body table-responsive">
<table class="table table-sm align-middle">
<thead><tr><th>ID</th><th>Fecha</th><th>Ruleta</th><th>Premio</th><th>IP</th></tr></thead>
<tbody>
<?php while($row = $ultimas->fetch_assoc()): ?>
<tr>
<td><?= (int)$row['id'] ?></td>
<td><?= htmlspecialchars($row['fecha_juego']) ?></td>
<td><?= htmlspecialchars($row['ruleta'] ?? '—') ?></td>
<td><?= htmlspecialchars($row['premio'] ?? '—') ?></td>
<td><?= htmlspecialchars($row['ip_address'] ?? '—') ?></td>
</tr>
<?php endwhile; ?>
<?php if ($ultimas->num_rows === 0): ?>
<tr><td colspan="5" class="text-center text-muted">Sin registros</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<h5 class="mt-4">Últimas 10 jugadas — Memoria</h5>
<div class="card shadow-sm">
<div class="card-body table-responsive">
<table class="table table-sm align-middle">
<thead>
<tr><th>ID</th><th>Fecha</th><th>Juego</th><th>Juego ID</th><th>Aciertos</th><th>Tiempo (s)</th></tr>
</thead>
<tbody>
<?php if ($ult_mem && $ult_mem->num_rows): ?>
<?php while($m = $ult_mem->fetch_assoc()): ?>
<tr>
<td><?= (int)$m['id'] ?></td>
<td><?= htmlspecialchars($m['fecha_juego']) ?></td>
<td><?= htmlspecialchars($m['juego'] ?? '—') ?></td>
<td><?= (int)$m['juego_id'] ?></td>
<td><?= (int)$m['aciertos'] ?></td>
<td><?= number_format(($m['tiempo_ms'] ?? 0)/1000, 2) ?></td>
</tr>
<?php endwhile; ?>
<?php else: ?>
<tr><td colspan="6" class="text-center text-muted">Sin registros</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<div class="card shadow-sm mt-4">
<div class="card-body table-responsive">
<h5 class="mb-3">Últimas 10 jugadas (Rasca)</h5>
<table class="table table-sm table-striped align-middle">
<thead>
<tr>
<th>ID</th>
<th>Fecha</th>
<th>Rasca</th>
<th>Premio</th>
<th>IP</th>
</tr>
</thead>
<tbody>
<?php while($row = $ult_rasca->fetch_assoc()): ?>
<tr>
<td><?= (int)$row['id'] ?></td>
<td><?= htmlspecialchars($row['fecha_juego']) ?></td>
<td><?= htmlspecialchars($row['rasca'] ?? '—') ?></td>
<td><?= htmlspecialchars($row['premio'] ?? '—') ?></td>
<td><?= htmlspecialchars($row['ip'] ?? '') ?></td>
</tr>
<?php endwhile; ?>
</tbody>
</table>
</div>
</div>
</div>
</body>
</html>
+171
View File
@@ -0,0 +1,171 @@
<?php
// ruleta/admin/dashboard_landing_widget_data.php
header('Content-Type: application/json; charset=utf-8');
// No HTML en errores:
ini_set('display_errors', '0');
ini_set('log_errors', '1');
// Captura warnings/notices para adjuntarlos al JSON:
$php_warnings = [];
set_error_handler(function($errno, $errstr, $errfile, $errline) use (&$php_warnings) {
if (error_reporting() === 0) return true;
$php_warnings[] = "$errstr in $errfile:$errline";
return true;
});
// Limpia cualquier output previo
while (ob_get_level() > 0) { ob_end_clean(); }
ob_start();
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) {
http_response_code(401);
echo json_encode(['ok'=>false, 'error'=>'auth']);
exit;
}
// Helper seguro para prepares
function must_prepare(mysqli $conn, string $sql): mysqli_stmt {
$st = $conn->prepare($sql);
if (!$st) {
throw new RuntimeException('prepare_failed: '. $conn->error .' | SQL='. preg_replace('/\s+/', ' ', $sql));
}
return $st;
}
function qcount(mysqli $conn, string $sql): int {
$rs = $conn->query($sql);
if (!$rs) throw new RuntimeException('query_failed: '. $conn->error .' | SQL='. preg_replace('/\s+/', ' ', $sql));
$row = $rs->fetch_assoc();
return (int)($row['c'] ?? 0);
}
try {
$from = trim($_GET['from'] ?? '');
$to = trim($_GET['to'] ?? '');
$landing_id = (int)($_GET['landing_id'] ?? 0);
$want_meta = (int)($_GET['meta'] ?? 0);
if ($from === '' || $to === '') {
$to = date('Y-m-d');
$from = date('Y-m-d', strtotime($to.' -30 day'));
}
$from_dt = $from.' 00:00:00';
$to_dt = $to.' 23:59:59';
// --- KPIs
$kpis = [
'landings' => qcount($conn, "SELECT COUNT(*) c FROM landing_pages"),
'variants' => qcount($conn, "SELECT COUNT(*) c FROM landing_variants"),
'blocks' => qcount($conn, "SELECT COUNT(*) c FROM landing_blocks"),
'leads_window' => 0,
'range_label' => date('d M', strtotime($from)).' - '.date('d M', strtotime($to)),
'filtered' => $landing_id > 0
];
if ($landing_id > 0) {
$st = must_prepare($conn, "SELECT COUNT(*) c FROM landing_forms WHERE landing_id=? AND created_at BETWEEN ? AND ?");
$st->bind_param('iss', $landing_id, $from_dt, $to_dt);
} else {
$st = must_prepare($conn, "SELECT COUNT(*) c FROM landing_forms WHERE created_at BETWEEN ? AND ?");
$st->bind_param('ss', $from_dt, $to_dt);
}
$st->execute();
$r = $st->get_result()->fetch_assoc();
$kpis['leads_window'] = (int)($r['c'] ?? 0);
// --- Top + series
$top = [];
if ($landing_id > 0) {
$st = must_prepare($conn, "
SELECT lp.id AS landing_id, lp.titulo,
SUM(lvs.event_type='impression') AS imps,
SUM(lvs.event_type='conversion') AS conv,
ROUND(SUM(lvs.event_type='conversion')/NULLIF(SUM(lvs.event_type='impression'),0)*100,2) AS cr
FROM landing_pages lp
LEFT JOIN landing_variant_stats lvs
ON lvs.landing_id=lp.id
AND lvs.created_at BETWEEN ? AND ?
WHERE lp.id=?
GROUP BY lp.id, lp.titulo
");
$st->bind_param('ssi', $from_dt, $to_dt, $landing_id);
} else {
// --- Top global (landing_id = 0): usar subquery para poder ordenar por 'cr'
$st = must_prepare($conn, "
SELECT landing_id, titulo, imps, conv, cr
FROM (
SELECT
lp.id AS landing_id,
lp.titulo,
SUM(lvs.event_type='impression') AS imps,
SUM(lvs.event_type='conversion') AS conv,
ROUND(
SUM(lvs.event_type='conversion') / NULLIF(SUM(lvs.event_type='impression'), 0) * 100, 2
) AS cr
FROM landing_pages lp
LEFT JOIN landing_variant_stats lvs
ON lvs.landing_id = lp.id
AND lvs.created_at BETWEEN ? AND ?
GROUP BY lp.id, lp.titulo
) t
ORDER BY (cr IS NULL), cr DESC, imps DESC
LIMIT 10
");
$st->bind_param('ss', $from_dt, $to_dt);
}
$st->execute();
$rs = $st->get_result();
while($row = $rs->fetch_assoc()){
$row['imps'] = (int)($row['imps'] ?? 0);
$row['conv'] = (int)($row['conv'] ?? 0);
$row['cr'] = $row['imps'] ? (float)$row['cr'] : null;
// Serie diaria
$st2 = must_prepare($conn, "
SELECT DATE(created_at) d,
SUM(event_type='impression') imp,
SUM(event_type='conversion') conv
FROM landing_variant_stats
WHERE landing_id=? AND created_at BETWEEN ? AND ?
GROUP BY DATE(created_at)
ORDER BY d ASC
");
$lid = (int)$row['landing_id'];
$st2->bind_param('iss', $lid, $from_dt, $to_dt);
$st2->execute();
$ser = $st2->get_result()->fetch_all(MYSQLI_ASSOC);
$row['series'] = array_map(function($p){
return ['d'=>$p['d'], 'imp'=>(int)$p['imp'], 'conv'=>(int)$p['conv']];
}, $ser);
$top[] = $row;
}
// Lista para el select (si meta=1)
$landings_list = [];
if ($want_meta === 1) {
$q = $conn->query("SELECT id, titulo FROM landing_pages ORDER BY titulo ASC");
if (!$q) throw new RuntimeException('query_failed: '. $conn->error .' | SQL=SELECT id,titulo FROM landing_pages ORDER BY titulo ASC');
while($L = $q->fetch_assoc()){
$landings_list[] = ['id'=>(int)$L['id'], 'titulo'=>$L['titulo']];
}
}
$warnings = $php_warnings;
ob_end_clean();
echo json_encode([
'ok'=>true,
'kpis'=>$kpis,
'top'=>$top,
'landings_list'=>$landings_list,
'warnings'=>$warnings
], JSON_UNESCAPED_UNICODE);
} catch (Throwable $e) {
ob_end_clean();
http_response_code(500);
echo json_encode(['ok'=>false, 'error'=>$e->getMessage()], JSON_UNESCAPED_UNICODE);
}
+172
View File
@@ -0,0 +1,172 @@
<?php
// ruleta/admin/dashboard_landing_widgets.php
if (!isset($conn)) { require_once __DIR__ . '/db.php'; }
if (session_status() === PHP_SESSION_NONE) { session_start(); }
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
// ---- Totales generales módulo landing ----
$tot_landings = (int)$conn->query("SELECT COUNT(*) c FROM landing_pages")->fetch_assoc()['c'];
$tot_variants = (int)$conn->query("SELECT COUNT(*) c FROM landing_variants")->fetch_assoc()['c'];
$tot_blocks = (int)$conn->query("SELECT COUNT(*) c FROM landing_blocks")->fetch_assoc()['c'];
$tot_leads_30d = (int)$conn->query("SELECT COUNT(*) c FROM landing_forms WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)")->fetch_assoc()['c'];
// ---- Top landings por CR últimos 30 días (impresiones/conversiones) ----
$top = [];
$st = $conn->prepare("
SELECT
lp.id AS landing_id, lp.titulo,
SUM(lvs.event_type='impression') AS impresiones,
SUM(lvs.event_type='conversion') AS conversiones,
ROUND(SUM(lvs.event_type='conversion')/NULLIF(SUM(lvs.event_type='impression'),0)*100,2) AS cr
FROM landing_pages lp
LEFT JOIN landing_variant_stats lvs
ON lvs.landing_id = lp.id
AND lvs.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY lp.id, lp.titulo
ORDER BY cr DESC IS NULL, cr DESC, impresiones DESC
LIMIT 10
");
$st->execute();
$top_rs = $st->get_result();
while($r = $top_rs->fetch_assoc()){ $top[] = $r; }
// ---- Listado rápido de landings con accesos (últimos 8) ----
$list = $conn->query("
SELECT id, titulo, slug, activo, DATE_FORMAT(fecha_creacion,'%Y-%m-%d %H:%i') AS fc
FROM landing_pages
ORDER BY id DESC
LIMIT 8
");
?>
<div class="container-fluid px-0">
<!-- Resumen de tarjetas -->
<div class="row g-3">
<div class="col-md-3">
<div class="card shadow-sm border-0">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div><h6 class="text-muted mb-1">Landings</h6><h3 class="mb-0"><?= number_format($tot_landings) ?></h3></div>
<div class="fs-3">🧭</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card shadow-sm border-0">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div><h6 class="text-muted mb-1">Variantes A/B</h6><h3 class="mb-0"><?= number_format($tot_variants) ?></h3></div>
<div class="fs-3">🧪</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card shadow-sm border-0">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div><h6 class="text-muted mb-1">Bloques</h6><h3 class="mb-0"><?= number_format($tot_blocks) ?></h3></div>
<div class="fs-3">🧱</div>
</div>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card shadow-sm border-0">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div><h6 class="text-muted mb-1">Leads (30d)</h6><h3 class="mb-0"><?= number_format($tot_leads_30d) ?></h3></div>
<div class="fs-3">📈</div>
</div>
</div>
</div>
</div>
</div>
<!-- Tabla: Top 10 por CR (30d) -->
<div class="card shadow-sm mt-3">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<h5 class="mb-0">Top CR por Landing (30 días)</h5>
<a href="landing.php" class="btn btn-sm btn-outline-primary">Ver todas</a>
</div>
<div class="table-responsive">
<table class="table table-sm align-middle">
<thead>
<tr>
<th>Landing</th>
<th class="text-end">Impresiones</th>
<th class="text-end">Conversiones</th>
<th class="text-end">CR %</th>
<th class="text-nowrap">Acciones</th>
</tr>
</thead>
<tbody>
<?php if($top): foreach($top as $r): ?>
<tr>
<td class="fw-semibold"><?= htmlspecialchars($r['titulo']) ?></td>
<td class="text-end"><?= (int)$r['impresiones'] ?></td>
<td class="text-end"><?= (int)$r['conversiones'] ?></td>
<td class="text-end"><?= $r['cr'] !== null ? number_format((float)$r['cr'],2) : '—' ?></td>
<td class="text-nowrap">
<a class="btn btn-sm btn-outline-secondary" href="landing_stats.php?landing_id=<?= (int)$r['landing_id'] ?>">Stats</a>
<a class="btn btn-sm btn-outline-secondary" href="landing_variants.php?landing_id=<?= (int)$r['landing_id'] ?>">Variantes</a>
<a class="btn btn-sm btn-outline-secondary" href="landing_builder.php?landing_id=<?= (int)$r['landing_id'] ?>">Builder</a>
<a class="btn btn-sm btn-outline-secondary" href="landing_crm.php?landing_id=<?= (int)$r['landing_id'] ?>">CRM</a>
<a class="btn btn-sm btn-outline-secondary" href="landing_forms.php?landing_id=<?= (int)$r['landing_id'] ?>">Leads</a>
</td>
</tr>
<?php endforeach; else: ?>
<tr><td colspan="5" class="text-center text-muted">Sin datos</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Últimas landings creadas (accesos rápidos) -->
<div class="card shadow-sm mt-3">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<h5 class="mb-0">Accesos rápidos</h5>
<a href="landing.php" class="btn btn-sm btn-primary">Nueva landing</a>
</div>
<div class="table-responsive">
<table class="table table-sm align-middle">
<thead>
<tr>
<th>Landing</th>
<th>Slug</th>
<th>Creada</th>
<th>Estado</th>
<th class="text-nowrap">Acciones</th>
</tr>
</thead>
<tbody>
<?php if($list && $list->num_rows): while($r=$list->fetch_assoc()): ?>
<tr>
<td class="fw-semibold"><?= htmlspecialchars($r['titulo']) ?></td>
<td><code><?= htmlspecialchars($r['slug']) ?></code></td>
<td><?= htmlspecialchars($r['fc']) ?></td>
<td><?= $r['activo'] ? '<span class="badge text-bg-success">Activo</span>' : '<span class="badge text-bg-secondary">Inactivo</span>' ?></td>
<td class="text-nowrap d-flex gap-1">
<a class="btn btn-sm btn-outline-primary" href="landing_variants.php?landing_id=<?= (int)$r['id'] ?>">Variantes</a>
<a class="btn btn-sm btn-outline-primary" href="landing_schedule.php?landing_id=<?= (int)$r['id'] ?>">Schedule</a>
<a class="btn btn-sm btn-outline-primary" href="landing_page_blocks.php?landing_id=<?= (int)$r['id'] ?>">Bloques</a>
<a class="btn btn-sm btn-outline-primary" href="landing_builder.php?landing_id=<?= (int)$r['id'] ?>">Builder</a>
<a class="btn btn-sm btn-outline-primary" href="landing_stats.php?landing_id=<?= (int)$r['id'] ?>">Stats</a>
<a class="btn btn-sm btn-outline-primary" href="landing_crm.php?landing_id=<?= (int)$r['id'] ?>">CRM</a>
<a class="btn btn-sm btn-outline-primary" href="landing_forms.php?landing_id=<?= (int)$r['id'] ?>">Leads</a>
<a class="btn btn-sm btn-outline-secondary" target="_blank" href="../landing.php?slug=<?= urlencode($r['slug']) ?>">Ver</a>
</td>
</tr>
<?php endwhile; else: ?>
<tr><td colspan="5" class="text-center text-muted">Aún no hay landings</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
+211
View File
@@ -0,0 +1,211 @@
<?php
// ruleta/admin/dashboard_landing_widgets_adv.php
if (!isset($conn)) { require_once __DIR__ . '/db.php'; }
if (session_status() === PHP_SESSION_NONE) { session_start(); }
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
?>
<div class="container-fluid px-0" id="landing-widget">
<div class="d-flex flex-wrap align-items-end gap-2 mb-2">
<h4 class="mb-0">Landings</h4>
<div class="ms-auto d-flex flex-wrap gap-2">
<div>
<label class="form-label mb-0 small">Landing</label>
<select id="lw-landing" class="form-select form-select-sm" style="min-width:220px">
<option value="0">— Todas las landings —</option>
</select>
</div>
<div>
<label class="form-label mb-0 small">Desde</label>
<input type="date" id="lw-from" class="form-control form-control-sm" />
</div>
<div>
<label class="form-label mb-0 small">Hasta</label>
<input type="date" id="lw-to" class="form-control form-control-sm" />
</div>
<button id="lw-refresh" class="btn btn-sm btn-primary">Actualizar</button>
</div>
</div>
<!-- KPIs -->
<div class="row g-3" id="lw-kpis">
<!-- Se llena por JS -->
</div>
<!-- Top por CR -->
<div class="card shadow-sm mt-3">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<h5 class="mb-0">Top CR por Landing</h5>
<a href="landing.php" class="btn btn-sm btn-outline-primary">Ver todas</a>
</div>
<div class="table-responsive">
<table class="table table-sm align-middle" id="lw-top-table">
<thead>
<tr>
<th>Landing</th>
<th class="text-end">Impresiones</th>
<th class="text-end">Conversiones</th>
<th class="text-end">CR %</th>
<th style="width:180px">Tendencia</th>
<th class="text-nowrap">Acciones</th>
</tr>
</thead>
<tbody>
<!-- Se llena por JS -->
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Chart.js -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script>
(function() {
const $ = (s) => document.querySelector(s);
const API = 'dashboard_landing_widget_data.php';
function fmt(n){ return new Intl.NumberFormat().format(n||0); }
function esc(s){ const d=document.createElement('div'); d.textContent=(s??''); return d.innerHTML; }
function defaultDates(){
const to = new Date();
const from = new Date(); from.setDate(to.getDate()-30);
const f = (d)=>d.toISOString().slice(0,10);
$('#lw-from').value = f(from);
$('#lw-to').value = f(to);
}
async function fetchData(includeList=false){
const params = new URLSearchParams({
from: $('#lw-from').value || '',
to: $('#lw-to').value || '',
landing_id: $('#lw-landing').value || '0',
meta: includeList ? '1' : '0'
});
const res = await fetch(API + '?' + params.toString(), {credentials:'same-origin'});
const txt = await res.text();
try { return JSON.parse(txt); }
catch(e){ console.error('Respuesta no JSON:', txt); throw e; }
}
function renderLandingOptions(list){
const sel = $('#lw-landing');
const cur = sel.value || '0';
sel.innerHTML = '<option value="0">— Todas las landings —</option>';
(list||[]).forEach(r=>{
const opt = document.createElement('option');
opt.value = r.id;
opt.textContent = r.titulo + ' (#'+ r.id +')';
sel.appendChild(opt);
});
// mantener selección si existía
if ([...sel.options].some(o=>o.value===cur)) sel.value = cur;
}
function renderKPIs(k){
$('#lw-kpis').innerHTML = `
<div class="col-md-3"><div class="card shadow-sm border-0"><div class="card-body d-flex justify-content-between">
<div><h6 class="text-muted mb-1">Landings</h6><h3 class="mb-0">${fmt(k.landings)}</h3></div><div class="fs-3">🧭</div></div></div></div>
<div class="col-md-3"><div class="card shadow-sm border-0"><div class="card-body d-flex justify-content-between">
<div><h6 class="text-muted mb-1">Variantes</h6><h3 class="mb-0">${fmt(k.variants)}</h3></div><div class="fs-3">🧪</div></div></div></div>
<div class="col-md-3"><div class="card shadow-sm border-0"><div class="card-body d-flex justify-content-between">
<div><h6 class="text-muted mb-1">Bloques</h6><h3 class="mb-0">${fmt(k.blocks)}</h3></div><div class="fs-3">🧱</div></div></div></div>
<div class="col-md-3"><div class="card shadow-sm border-0"><div class="card-body d-flex justify-content-between">
<div><h6 class="text-muted mb-1">Leads ${k.filtered ? '(landing seleccionada)' : '(rango)'}<br><small class="text-muted">${k.range_label}</small></h6>
<h3 class="mb-0">${fmt(k.leads_window)}</h3></div><div class="fs-3">📈</div></div></div></div>
`;
}
function makeSparkline(canvas, series){
const labels = series.map(p=>p.d.slice(5)); // MM-DD
const imps = series.map(p=>p.imp);
const conv = series.map(p=>p.conv);
new Chart(canvas, {
type: 'line',
data: {
labels,
datasets: [
{ label:'Imps', data: imps, tension: .3, pointRadius: 0 },
{ label:'Conv', data: conv, tension: .3, pointRadius: 0, yAxisID:'y1' }
]
},
options:{
responsive:true, maintainAspectRatio:false,
scales: {
x: { display:false },
y: { display:false, beginAtZero:true },
y1:{ display:false, beginAtZero:true, position:'right' }
},
plugins: { legend:{display:false}, tooltip:{enabled:true} }
}
});
}
function renderTop(rows){
const tbody = $('#lw-top-table tbody');
tbody.innerHTML = '';
rows.forEach(r=>{
const tr = document.createElement('tr');
tr.innerHTML = `
<td class="fw-semibold">${esc(r.titulo)}</td>
<td class="text-end">${fmt(r.imps)}</td>
<td class="text-end">${fmt(r.conv)}</td>
<td class="text-end">${r.cr!==null ? (r.cr.toFixed(2)) : '—'}</td>
<td><div style="height:38px"><canvas id="spk-${r.landing_id}"></canvas></div></td>
<td class="text-nowrap">
<a class="btn btn-sm btn-outline-secondary" href="landing_stats.php?landing_id=${r.landing_id}">Stats</a>
<a class="btn btn-sm btn-outline-secondary" href="landing_variants.php?landing_id=${r.landing_id}">Variantes</a>
<a class="btn btn-sm btn-outline-secondary" href="landing_builder.php?landing_id=${r.landing_id}">Builder</a>
<a class="btn btn-sm btn-outline-secondary" href="landing_crm.php?landing_id=${r.landing_id}">CRM</a>
<a class="btn btn-sm btn-outline-secondary" href="landing_forms.php?landing_id=${r.landing_id}">Leads</a>
</td>
`;
tbody.appendChild(tr);
// sparkline
const cv = document.getElementById(`spk-${r.landing_id}`);
if (cv && r.series && r.series.length) makeSparkline(cv, r.series);
});
if (!rows.length){
tbody.innerHTML = `<tr><td colspan="6" class="text-center text-muted">Sin datos</td></tr>`;
}
}
async function boot(){
defaultDates();
// Primer fetch: pedir datos + lista de landings
try {
const data = await fetchData(true);
if (data.landings_list) renderLandingOptions(data.landings_list);
renderKPIs(data.kpis);
renderTop(data.top);
} catch(e) {
console.error(e); alert('No se pudo cargar el widget de landings.');
}
}
async function refresh(){
$('#lw-refresh').disabled = true;
try{
const data = await fetchData(false);
renderKPIs(data.kpis);
renderTop(data.top);
}catch(e){
console.error(e);
alert('No se pudo cargar el widget de landings.');
}finally{
$('#lw-refresh').disabled = false;
}
}
// eventos
$('#lw-refresh').addEventListener('click', refresh);
$('#lw-landing').addEventListener('change', refresh);
// iniciar
boot();
})();
</script>
+15
View File
@@ -0,0 +1,15 @@
<?php
// admin/db.php
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$DB_HOST = 'localhost';
$DB_USER = 'root';
$DB_PASS = '';
$DB_NAME = 'ruleta_db';
try {
$conn = new mysqli($DB_HOST, $DB_USER, $DB_PASS, $DB_NAME);
$conn->set_charset('utf8mb4');
} catch (Throwable $e) {
die('Fallo conectando a MySQL: ' . $e->getMessage());
}
+94
View File
@@ -0,0 +1,94 @@
<?php
session_start();
require_once __DIR__ . '/db.php';
if (!empty($_SESSION['admin_id'])) {
header('Location: dashboard.php');
exit;
}
$err = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = trim($_POST['email'] ?? '');
$password = $_POST['password'] ?? '';
$stmt = $conn->prepare(
"SELECT id, email, nombre, password_hash
FROM admin_users
WHERE email = ?
LIMIT 1"
);
$stmt->bind_param("s", $email);
$stmt->execute();
$stmt->store_result();
if ($stmt->num_rows === 1) {
$stmt->bind_result($id, $email_db, $nombre, $password_hash);
$stmt->fetch();
$ok = false;
$pareceHash = (strlen((string)$password_hash) >= 20 && substr((string)$password_hash,0,1) === '$');
if ($pareceHash) {
$ok = password_verify($password, $password_hash);
} else {
$ok = hash_equals((string)$password_hash, (string)$password);
if ($ok) {
$nuevoHash = password_hash($password, PASSWORD_DEFAULT);
$up = $conn->prepare("UPDATE admin_users SET password_hash=? WHERE id=?");
$up->bind_param("si", $nuevoHash, $id);
$up->execute();
$up->close();
}
}
if ($ok) {
$_SESSION['admin_id'] = (int)$id;
$_SESSION['admin_email'] = $email_db;
$_SESSION['admin_nombre'] = $nombre;
header('Location: dashboard.php');
exit;
}
}
$stmt->close();
$err = 'Credenciales inválidas';
}
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Login | Admin</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light d-flex align-items-center" style="min-height:100vh;">
<div class="container">
<div class="row justify-content-center">
<div class="col-sm-10 col-md-6 col-lg-4">
<div class="card shadow-sm">
<div class="card-body p-4">
<h1 class="h4 mb-3 text-center">Admin Promo Hub</h1>
<?php if ($err): ?>
<div class="alert alert-danger py-2"><?= htmlspecialchars($err) ?></div>
<?php endif; ?>
<form method="post" autocomplete="off">
<div class="mb-3">
<label class="form-label">Correo</label>
<input type="email" name="email" class="form-control" required autofocus>
</div>
<div class="mb-3">
<label class="form-label">Contraseña</label>
<input type="password" name="password" class="form-control" required>
</div>
<button class="btn btn-primary w-100">Ingresar</button>
</form>
</div>
</div>
<p class="text-center mt-3 text-muted small">© <?= date('Y') ?> GLM</p>
</div>
</div>
</div>
</body>
</html>
+207
View File
@@ -0,0 +1,207 @@
<?php
// admin/juegos_config.php
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
$UPLOAD_DIR = __DIR__ . '/../uploads/memoria'; // físico
$PUBLIC_DIR = '/ruleta/uploads/memoria'; // público: AJUSTA si tu base no es /ruleta
if (!is_dir($UPLOAD_DIR)) { @mkdir($UPLOAD_DIR, 0775, true); }
function sanitize_filename($name) {
$name = preg_replace('/[^A-Za-z0-9._-]/', '_', $name);
return substr($name, 0, 180);
}
function upload_image($field, $UPLOAD_DIR, $PUBLIC_DIR) {
if (!isset($_FILES[$field]) || $_FILES[$field]['error'] === UPLOAD_ERR_NO_FILE) return null;
$f = $_FILES[$field];
if ($f['error'] !== UPLOAD_ERR_OK) throw new RuntimeException("Error subiendo $field (código {$f['error']}).");
$finfo = new finfo(FILEINFO_MIME_TYPE); $mime = $finfo->file($f['tmp_name']);
$allowed = ['image/png'=>'png','image/jpeg'=>'jpg','image/webp'=>'webp'];
if (!isset($allowed[$mime])) throw new RuntimeException("Formato no permitido para $field. Usa PNG/JPG/WEBP.");
if ($f['size'] > 5 * 1024 * 1024) throw new RuntimeException("Archivo muy grande para $field (máx 5MB).");
$ext = $allowed[$mime];
$base = sanitize_filename(pathinfo($f['name'], PATHINFO_FILENAME));
$fname = $base . '-' . uniqid() . '.' . $ext;
$dest = rtrim($UPLOAD_DIR, "/\\") . DIRECTORY_SEPARATOR . $fname;
if (!move_uploaded_file($f['tmp_name'], $dest)) throw new RuntimeException("No se pudo guardar $field.");
return rtrim($PUBLIC_DIR, '/') . '/' . $fname;
}
function get_config(mysqli $conn, $juego_id) {
$stmt = $conn->prepare("SELECT * FROM juegos_config WHERE juego_id=?");
$stmt->bind_param("i", $juego_id); $stmt->execute();
return $stmt->get_result()->fetch_assoc();
}
$msg = $err = '';
$action = $_GET['action'] ?? '';
$juego_id = (int)($_GET['juego_id'] ?? 0);
// CREATE/UPDATE
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$jid = (int)($_POST['juego_id'] ?? 0);
$nombre = trim($_POST['nombre'] ?? '');
$ruleta_id = (int)($_POST['ruleta_id'] ?? 0);
$olds = [
'banner_1' => $_POST['old_banner_1'] ?? '',
'banner_2' => $_POST['old_banner_2'] ?? '',
'banner_3' => $_POST['old_banner_3'] ?? '',
'banner_4' => $_POST['old_banner_4'] ?? '',
'background_image' => $_POST['old_background_image'] ?? '',
'fullscreen_icon' => $_POST['old_fullscreen_icon'] ?? '',
'reverso_default' => $_POST['old_reverso_default'] ?? ''
];
if ($jid <= 0 || $nombre === '') {
$err = "Debes indicar un juego_id (entero) y un nombre.";
} else {
try {
$new = [
'banner_1' => upload_image('banner_1', $UPLOAD_DIR, $PUBLIC_DIR),
'banner_2' => upload_image('banner_2', $UPLOAD_DIR, $PUBLIC_DIR),
'banner_3' => upload_image('banner_3', $UPLOAD_DIR, $PUBLIC_DIR),
'banner_4' => upload_image('banner_4', $UPLOAD_DIR, $PUBLIC_DIR),
'background_image' => upload_image('background_image', $UPLOAD_DIR, $PUBLIC_DIR),
'fullscreen_icon' => upload_image('fullscreen_icon', $UPLOAD_DIR, $PUBLIC_DIR),
'reverso_default' => upload_image('reverso_default', $UPLOAD_DIR, $PUBLIC_DIR),
];
foreach ($new as $k=>$v) { $new[$k] = $v ?: ($olds[$k] ?: null); }
// upsert por juego_id
$stmt = $conn->prepare("INSERT INTO juegos_config
(juego_id, nombre, ruleta_id, banner_1,banner_2,banner_3,banner_4, background_image, fullscreen_icon, reverso_default)
VALUES (?,?,?,?,?,?,?,?,?,?)
ON DUPLICATE KEY UPDATE nombre=VALUES(nombre), ruleta_id=VALUES(ruleta_id),
banner_1=VALUES(banner_1), banner_2=VALUES(banner_2), banner_3=VALUES(banner_3), banner_4=VALUES(banner_4),
background_image=VALUES(background_image), fullscreen_icon=VALUES(fullscreen_icon), reverso_default=VALUES(reverso_default)");
$stmt->bind_param("isisssssss", $jid, $nombre, $ruleta_id,
$new['banner_1'], $new['banner_2'], $new['banner_3'], $new['banner_4'],
$new['background_image'], $new['fullscreen_icon'], $new['reverso_default']);
$stmt->execute();
$msg = "Configuración guardada.";
} catch (Throwable $e) {
$err = $e->getMessage();
}
}
}
// DELETE
if ($action === 'delete' && $juego_id > 0) {
$stmt = $conn->prepare("DELETE FROM juegos_config WHERE juego_id=?");
$stmt->bind_param("i", $juego_id); $stmt->execute();
$msg = "Configuración eliminada.";
}
// EDIT
$edit = null;
if ($action === 'edit' && $juego_id > 0) {
$edit = get_config($conn, $juego_id);
}
// LIST
$list = $conn->query("SELECT * FROM juegos_config ORDER BY juego_id ASC");
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Memoria | Configuración de Juegos</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>.thumb{width:70px;height:70px;object-fit:cover;border-radius:.5rem;}</style>
</head>
<body>
<nav class="navbar navbar-expand-lg bg-dark navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Home</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="juegos_config.php">Memoria: Juegos</a>
<a class="btn btn-outline-light btn-sm" href="cartas.php">Memoria: Cartas</a>
<a class="btn btn-outline-light btn-sm" href="jugadas_memoria.php">Memoria: Jugadas</a>
<a class="btn btn-outline-light btn-sm" href="../index.php?action=logout">Salir</a>
</div>
</div>
</nav>
<div class="container py-4">
<h3 class="mb-3">Config. Juegos de Memoria</h3>
<?php if ($msg): ?><div class="alert alert-success py-2"><?= htmlspecialchars($msg) ?></div><?php endif; ?>
<?php if ($err): ?><div class="alert alert-danger py-2"><?= htmlspecialchars($err) ?></div><?php endif; ?>
<div class="card shadow-sm mb-4"><div class="card-body">
<h5 class="mb-3"><?= $edit ? 'Editar juego' : 'Nuevo/Actualizar juego (por juego_id)' ?></h5>
<form method="post" enctype="multipart/form-data" class="row g-3">
<div class="col-md-2">
<label class="form-label">Juego ID</label>
<input type="number" name="juego_id" class="form-control" required value="<?= htmlspecialchars($edit['juego_id'] ?? '') ?>">
</div>
<div class="col-md-5">
<label class="form-label">Nombre</label>
<input name="nombre" class="form-control" required value="<?= htmlspecialchars($edit['nombre'] ?? '') ?>">
</div>
<?php
// hidden olds
$fields = ['banner_1','banner_2','banner_3','banner_4','background_image','fullscreen_icon','reverso_default'];
foreach ($fields as $f) {
echo '<input type="hidden" name="old_'.$f.'" value="'.htmlspecialchars($edit[$f] ?? '').'">';
}
?>
<div class="col-md-6">
<label class="form-label">Background</label>
<input type="file" name="background_image" class="form-control" accept=".png,.jpg,.jpeg,.webp">
<?php if (!empty($edit['background_image'])): ?><div class="mt-2"><img src="<?= htmlspecialchars($edit['background_image']) ?>" class="thumb"></div><?php endif; ?>
</div>
<div class="col-md-6">
<label class="form-label">Ícono Fullscreen</label>
<input type="file" name="fullscreen_icon" class="form-control" accept=".png,.jpg,.jpeg,.webp">
<?php if (!empty($edit['fullscreen_icon'])): ?><div class="mt-2"><img src="<?= htmlspecialchars($edit['fullscreen_icon']) ?>" class="thumb"></div><?php endif; ?>
</div>
<div class="col-md-6">
<label class="form-label">Reverso por defecto</label>
<input type="file" name="reverso_default" class="form-control" accept=".png,.jpg,.jpeg,.webp">
<?php if (!empty($edit['reverso_default'])): ?><div class="mt-2"><img src="<?= htmlspecialchars($edit['reverso_default']) ?>" class="thumb"></div><?php endif; ?>
</div>
<div class="col-12">
<button class="btn btn-primary">Guardar</button>
<?php if ($edit): ?><a href="juegos_config.php" class="btn btn-secondary">Cancelar</a><?php endif; ?>
</div>
</form>
</div></div>
<div class="card shadow-sm"><div class="card-body table-responsive">
<table class="table table-striped table-sm align-middle">
<thead><tr>
<th>Juego ID</th><th>Nombre</th><th>Ruleta</th>
<th>B1</th><th>B2</th><th>B3</th><th>B4</th>
<th>BG</th><th>FS</th><th>Reverso</th><th>Acciones</th>
</tr></thead>
<tbody>
<?php while($r = $list->fetch_assoc()): ?>
<tr>
<td><?= (int)$r['juego_id'] ?></td>
<td><?= htmlspecialchars($r['nombre']) ?></td>
<td><?= htmlspecialchars($r['ruleta_id'] ?? '') ?></td>
<td><?= $r['banner_1']?'<img class="thumb" src="'.htmlspecialchars($r['banner_1']).'">':'—' ?></td>
<td><?= $r['banner_2']?'<img class="thumb" src="'.htmlspecialchars($r['banner_2']).'">':'—' ?></td>
<td><?= $r['banner_3']?'<img class="thumb" src="'.htmlspecialchars($r['banner_3']).'">':'—' ?></td>
<td><?= $r['banner_4']?'<img class="thumb" src="'.htmlspecialchars($r['banner_4']).'">':'—' ?></td>
<td><?= $r['background_image']?'<img class="thumb" src="'.htmlspecialchars($r['background_image']).'">':'—' ?></td>
<td><?= $r['fullscreen_icon']?'<img class="thumb" src="'.htmlspecialchars($r['fullscreen_icon']).'">':'—' ?></td>
<td><?= $r['reverso_default']?'<img class="thumb" src="'.htmlspecialchars($r['reverso_default']).'">':'—' ?></td>
<td class="text-nowrap">
<a class="btn btn-sm btn-outline-primary" href="juegos_config.php?action=edit&juego_id=<?= (int)$r['juego_id'] ?>">Editar</a>
<a class="btn btn-sm btn-outline-danger" href="juegos_config.php?action=delete&juego_id=<?= (int)$r['juego_id'] ?>" onclick="return confirm('¿Eliminar config?')">Eliminar</a>
<a class="btn-outline-warning" href="/../ruleta/memoria/index.php?juego_id=<?= (int)$r['id'] ?>" target="_blank" rel="noopener noreferrer">Ver</a>
</td>
</tr>
<?php endwhile; ?>
</tbody>
</table>
</div></div>
</div>
</body>
</html>
+223
View File
@@ -0,0 +1,223 @@
<?php
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
/** Cargar ruletas (id, nombre) */
function all_ruletas(mysqli $conn): array {
$res = $conn->query("SELECT id, nombre FROM ruletas ORDER BY id DESC");
return $res ? $res->fetch_all(MYSQLI_ASSOC) : [];
}
/** Helper para bind dinámico (mysqli requiere referencias) */
function stmt_bind_params(mysqli_stmt $stmt, string $types, array &$params): void {
if ($types === '' || !$params) return;
$bind = [$types];
foreach ($params as $k => $v) { $bind[] = &$params[$k]; }
call_user_func_array([$stmt, 'bind_param'], $bind);
}
$ruletas = all_ruletas($conn);
$ruleta_id = (int)($_GET['ruleta_id'] ?? 0);
$q = trim((string)($_GET['q'] ?? ''));
$desde = (string)($_GET['desde'] ?? '');
$hasta = (string)($_GET['hasta'] ?? '');
$where = "1=1";
$params = [];
$types = "";
// Filtros
if ($ruleta_id) {
$where .= " AND j.ruleta_id=?";
$types .= "i";
$params[] = $ruleta_id;
}
if ($q !== '') {
$where .= " AND (p.nombre LIKE CONCAT('%',?,'%') OR j.ip_address LIKE CONCAT('%',?,'%'))";
$types .= "ss";
$params[] = $q;
$params[] = $q;
}
if ($desde !== '') {
$where .= " AND j.fecha_juego >= ?";
$types .= "s";
$params[] = $desde . " 00:00:00";
}
if ($hasta !== '') {
$where .= " AND j.fecha_juego <= ?";
$types .= "s";
$params[] = $hasta . " 23:59:59";
}
// Export CSV
if (($_GET['export'] ?? '') === 'csv') {
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename=jugadas.csv');
$out = fopen('php://output', 'w');
fputcsv($out, ['ID','Fecha','Ruleta','Premio','IP','User Agent']);
$sql = "SELECT j.id, j.fecha_juego, r.nombre AS ruleta, p.nombre AS premio, j.ip_address, j.user_agent
FROM jugadas j
LEFT JOIN ruletas r ON r.id=j.ruleta_id
LEFT JOIN premios p ON p.id=j.premio_id
WHERE $where
ORDER BY j.id DESC";
$stmt = $conn->prepare($sql);
stmt_bind_params($stmt, $types, $params);
$stmt->execute();
$res = $stmt->get_result();
while ($row = $res->fetch_assoc()) {
fputcsv($out, [
$row['id'],
$row['fecha_juego'],
$row['ruleta'],
$row['premio'],
$row['ip_address'],
$row['user_agent']
]);
}
fclose($out);
exit;
}
// Paginación
$page = max(1, (int)($_GET['page'] ?? 1));
$pp = 25;
$off = ($page - 1) * $pp;
// Total
$sqlCount = "SELECT COUNT(*) c
FROM jugadas j
LEFT JOIN ruletas r ON r.id=j.ruleta_id
LEFT JOIN premios p ON p.id=j.premio_id
WHERE $where";
$stmt = $conn->prepare($sqlCount);
stmt_bind_params($stmt, $types, $params);
$stmt->execute();
$total = (int)$stmt->get_result()->fetch_assoc()['c'];
// Página actual
$sql = "SELECT j.id, j.fecha_juego, r.nombre AS ruleta, p.nombre AS premio, j.ip_address, j.user_agent
FROM jugadas j
LEFT JOIN ruletas r ON r.id=j.ruleta_id
LEFT JOIN premios p ON p.id=j.premio_id
WHERE $where
ORDER BY j.id DESC
LIMIT $pp OFFSET $off";
$stmt = $conn->prepare($sql);
stmt_bind_params($stmt, $types, $params);
$stmt->execute();
$rows = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
$pages = (int)ceil($total / $pp);
// Query para export sin "page"
$exportQuery = $_GET;
unset($exportQuery['page']);
$exportQuery['export'] = 'csv';
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Jugadas | Admin Ruletas</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg bg-dark navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Home</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="ruletas.php">Ruletas</a>
<a class="btn btn-outline-light btn-sm" href="premios.php">Premios</a>
</div>
</div>
</nav>
<div class="container py-4">
<h3 class="mb-3">Historial de Jugadas</h3>
<form class="row g-2 mb-3" method="get">
<div class="col-md-3">
<label class="form-label">Ruleta</label>
<select name="ruleta_id" class="form-select">
<option value="">Todas</option>
<?php foreach ($ruletas as $ru): ?>
<?php $rid = (int)$ru['id']; ?>
<option value="<?= $rid ?>" <?= $ruleta_id == $rid ? 'selected' : '' ?>>
<?= htmlspecialchars($ru['nombre']) ?> (ID <?= $rid ?>)
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Desde</label>
<input type="date" name="desde" class="form-control" value="<?= htmlspecialchars($desde) ?>">
</div>
<div class="col-md-2">
<label class="form-label">Hasta</label>
<input type="date" name="hasta" class="form-control" value="<?= htmlspecialchars($hasta) ?>">
</div>
<div class="col-md-3">
<label class="form-label">Buscar (premio/IP)</label>
<input type="text" name="q" class="form-control" value="<?= htmlspecialchars($q) ?>" placeholder="Premio, 192.168...">
</div>
<div class="col-md-2 d-flex align-items-end gap-2">
<button class="btn btn-primary w-100">Filtrar</button>
<a class="btn btn-outline-secondary" href="jugadas.php">Limpiar</a>
</div>
<div class="col-12">
<a class="btn btn-success btn-sm" href="jugadas.php?<?= http_build_query($exportQuery) ?>">Exportar CSV</a>
</div>
</form>
<div class="card shadow-sm">
<div class="card-body table-responsive">
<table class="table table-striped table-sm align-middle">
<thead>
<tr>
<th>ID</th><th>Fecha</th><th>Ruleta</th><th>Premio</th><th>IP</th><th>User Agent</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $row): ?>
<tr>
<td><?= (int)$row['id'] ?></td>
<td><?= htmlspecialchars($row['fecha_juego']) ?></td>
<td><?= htmlspecialchars($row['ruleta'] ?? '—') ?></td>
<td><?= htmlspecialchars($row['premio'] ?? '—') ?></td>
<td><?= htmlspecialchars($row['ip_address'] ?? '—') ?></td>
<td class="text-truncate" style="max-width:400px;" title="<?= htmlspecialchars($row['user_agent'] ?? '') ?>">
<?= htmlspecialchars($row['user_agent'] ?? '') ?>
</td>
</tr>
<?php endforeach; ?>
<?php if (!$rows): ?>
<tr><td colspan="6" class="text-center text-muted">Sin registros</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php if ($pages > 1): ?>
<nav class="mt-3">
<ul class="pagination pagination-sm">
<?php for ($i = 1; $i <= $pages; $i++): ?>
<li class="page-item <?= $i == $page ? 'active' : '' ?>">
<a class="page-link" href="?<?= http_build_query(array_merge($_GET, ['page' => $i])) ?>"><?= $i ?></a>
</li>
<?php endfor; ?>
</ul>
</nav>
<?php endif; ?>
</div>
</body>
</html>
+132
View File
@@ -0,0 +1,132 @@
<?php
// admin/jugadas_memoria.php
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
function all_juegos(mysqli $conn){
$res=$conn->query("SELECT juego_id, nombre FROM juegos_config ORDER BY juego_id ASC");
return $res->fetch_all(MYSQLI_ASSOC);
}
$juegos = all_juegos($conn);
$juego_id = (int)($_GET['juego_id'] ?? 0);
$q = trim($_GET['q'] ?? ''); // buscar por IP o UA
$desde = $_GET['desde'] ?? '';
$hasta = $_GET['hasta'] ?? '';
$where = "1=1"; $types=""; $params=[];
if ($juego_id) { $where.=" AND jm.juego_id=?"; $types.="i"; $params[]=$juego_id; }
if ($q!=='') { $where.=" AND (jm.ip_address LIKE CONCAT('%',?,'%') OR jm.user_agent LIKE CONCAT('%',?,'%'))"; $types.="ss"; $params[]=$q; $params[]=$q; }
if ($desde!==''){ $where.=" AND jm.fecha_juego >= ?"; $types.="s"; $params[]=$desde." 00:00:00"; }
if ($hasta!==''){ $where.=" AND jm.fecha_juego <= ?"; $types.="s"; $params[]=$hasta." 23:59:59"; }
// Export CSV
if (($_GET['export'] ?? '') === 'csv') {
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename=jugadas_memoria.csv');
$out=fopen('php://output','w');
fputcsv($out,['ID','Fecha','Juego','Juego ID','Aciertos','Tiempo (ms)','IP','User Agent']);
$sql="SELECT jm.id, jm.fecha_juego, gc.nombre AS juego, jm.juego_id, jm.aciertos, jm.tiempo_ms, jm.ip_address, jm.user_agent
FROM jugadas_memoria jm
LEFT JOIN juegos_config gc ON gc.juego_id=jm.juego_id
WHERE $where ORDER BY jm.id DESC";
$stmt=$conn->prepare($sql); if($types){ $stmt->bind_param($types, ...$params); }
$stmt->execute(); $res=$stmt->get_result();
while($r=$res->fetch_assoc()){ fputcsv($out, [$r['id'],$r['fecha_juego'],$r['juego'],$r['juego_id'],$r['aciertos'],$r['tiempo_ms'],$r['ip_address'],$r['user_agent']]); }
fclose($out); exit;
}
// Paginación
$page=max(1,(int)($_GET['page']??1)); $pp=25; $off=($page-1)*$pp;
$sqlCount="SELECT COUNT(*) c FROM jugadas_memoria jm LEFT JOIN juegos_config gc ON gc.juego_id=jm.juego_id WHERE $where";
$stmt=$conn->prepare($sqlCount); if($types){ $stmt->bind_param($types, ...$params); } $stmt->execute();
$total=(int)$stmt->get_result()->fetch_assoc()['c'];
$sql="SELECT jm.id, jm.fecha_juego, gc.nombre AS juego, jm.juego_id, jm.aciertos, jm.tiempo_ms, jm.ip_address, jm.user_agent
FROM jugadas_memoria jm
LEFT JOIN juegos_config gc ON gc.juego_id=jm.juego_id
WHERE $where ORDER BY jm.id DESC LIMIT $pp OFFSET $off";
$stmt=$conn->prepare($sql); if($types){ $stmt->bind_param($types, ...$params); } $stmt->execute();
$rows=$stmt->get_result()->fetch_all(MYSQLI_ASSOC);
$pages=(int)ceil($total/$pp);
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Memoria | Jugadas</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg bg-dark navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Home</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="juegos_config.php">Memoria: Juegos</a>
<a class="btn btn-outline-light btn-sm" href="cartas.php">Memoria: Cartas</a>
<a class="btn btn-outline-light btn-sm" href="jugadas_memoria.php">Memoria: Jugadas</a>
<a class="btn btn-outline-light btn-sm" href="../index.php?action=logout">Salir</a>
</div>
</div>
</nav>
<div class="container py-4">
<h3 class="mb-3">Jugadas — Memoria</h3>
<form class="row g-2 mb-3" method="get">
<div class="col-md-3">
<label class="form-label">Juego</label>
<select name="juego_id" class="form-select">
<option value="">Todos</option>
<?php foreach($juegos as $j): ?>
<option value="<?= (int)$j['juego_id'] ?>" <?= $juego_id===$j['juego_id']?'selected':'' ?>><?= htmlspecialchars($j['nombre']) ?> (ID <?= (int)$j['juego_id'] ?>)</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2"><label class="form-label">Desde</label><input type="date" name="desde" class="form-control" value="<?= htmlspecialchars($desde) ?>"></div>
<div class="col-md-2"><label class="form-label">Hasta</label><input type="date" name="hasta" class="form-control" value="<?= htmlspecialchars($hasta) ?>"></div>
<div class="col-md-3"><label class="form-label">Buscar (IP/UA)</label><input type="text" name="q" class="form-control" value="<?= htmlspecialchars($q) ?>"></div>
<div class="col-md-2 d-flex align-items-end gap-2">
<button class="btn btn-primary w-100">Filtrar</button>
<a class="btn btn-outline-secondary" href="jugadas_memoria.php">Limpiar</a>
</div>
<div class="col-12">
<a class="btn btn-success btn-sm" href="jugadas_memoria.php?<?= http_build_query(array_merge($_GET,['export'=>'csv','page'=>null])) ?>">Exportar CSV</a>
</div>
</form>
<div class="card shadow-sm"><div class="card-body table-responsive">
<table class="table table-striped table-sm align-middle">
<thead><tr>
<th>ID</th><th>Fecha</th><th>Juego</th><th>Juego ID</th><th>Aciertos</th><th>Tiempo (s)</th><th>IP</th><th>User Agent</th>
</tr></thead>
<tbody>
<?php foreach($rows as $r): ?>
<tr>
<td><?= (int)$r['id'] ?></td>
<td><?= htmlspecialchars($r['fecha_juego']) ?></td>
<td><?= htmlspecialchars($r['juego'] ?? '—') ?></td>
<td><?= (int)$r['juego_id'] ?></td>
<td><?= (int)$r['aciertos'] ?></td>
<td><?= number_format(($r['tiempo_ms'] ?? 0)/1000, 2) ?></td>
<td><?= htmlspecialchars($r['ip_address'] ?? '—') ?></td>
<td class="text-truncate" style="max-width:400px;" title="<?= htmlspecialchars($r['user_agent'] ?? '') ?>"><?= htmlspecialchars($r['user_agent'] ?? '') ?></td>
</tr>
<?php endforeach; ?>
<?php if (!$rows): ?><tr><td colspan="8" class="text-center text-muted">Sin registros</td></tr><?php endif; ?>
</tbody>
</table>
</div></div>
<?php if ($pages>1): ?>
<nav class="mt-3"><ul class="pagination pagination-sm">
<?php for($i=1;$i<=$pages;$i++): ?>
<li class="page-item <?= $i===$page?'active':'' ?>"><a class="page-link" href="?<?= http_build_query(array_merge($_GET,['page'=>$i])) ?>"><?= $i ?></a></li>
<?php endfor; ?>
</ul></nav>
<?php endif; ?>
</div>
</body>
</html>
+178
View File
@@ -0,0 +1,178 @@
<?php
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
function slugify($s){ $s=trim(strtolower($s)); $s=preg_replace('/[^a-z0-9-]+/','-',$s); return trim($s,'-'); }
$msg=$err=''; $action=$_GET['action']??''; $id=(int)($_GET['id']??0);
if($_SERVER['REQUEST_METHOD']==='POST'){
$pid=(int)($_POST['id']??0);
$titulo=trim($_POST['titulo']??'');
$slug=trim($_POST['slug']??'');
$estado=$_POST['estado']??'borrador';
$hero_image=trim($_POST['hero_image']??'');
$contenido_html=$_POST['contenido_html']??'';
$css_inline=$_POST['css_inline']??'';
$js_inline=$_POST['js_inline']??'';
$meta_title=trim($_POST['meta_title']??'');
$meta_desc=trim($_POST['meta_desc']??'');
$canonical_url=trim($_POST['canonical_url']??'');
$game_url = trim($_POST['game_url'] ?? '');
// (Opcional) Normaliza y valida URL
if ($game_url !== '' && !filter_var($game_url, FILTER_VALIDATE_URL)) {
$err = 'La URL del juego no es válida.';
}
if(!$err){
if($titulo===''){ $err='El título es obligatorio.'; }
else{
if($slug==='') $slug=slugify($titulo);
try{
if($pid>0){
$st=$conn->prepare("UPDATE landing_pages
SET titulo=?, slug=?, estado=?, hero_image=?, contenido_html=?, css_inline=?, js_inline=?, meta_title=?, meta_desc=?, canonical_url=?, game_url=?
WHERE id=?");
$st->bind_param("sssssssssssi",
$titulo,$slug,$estado,$hero_image,$contenido_html,$css_inline,$js_inline,$meta_title,$meta_desc,$canonical_url,$game_url,$pid
);
$st->execute(); $msg='Landing actualizada.';
}else{
$st=$conn->prepare("INSERT INTO landing_pages
(titulo,slug,estado,hero_image,contenido_html,css_inline,js_inline,meta_title,meta_desc,canonical_url,game_url)
VALUES (?,?,?,?,?,?,?,?,?,?,?)");
$st->bind_param("sssssssssss",
$titulo,$slug,$estado,$hero_image,$contenido_html,$css_inline,$js_inline,$meta_title,$meta_desc,$canonical_url,$game_url
);
$st->execute(); $id=$conn->insert_id; $msg='Landing creada.';
}
}catch(Throwable $e){ $err=$e->getMessage(); }
}
}
}
if($action==='delete' && $id>0){
$conn->query("DELETE FROM landing_pages WHERE id=".$id);
$msg='Landing eliminada.'; $id=0;
}
$edit=null; if($action==='edit' && $id>0){ $edit=$conn->query("SELECT * FROM landing_pages WHERE id=".$id)->fetch_assoc(); }
$list=$conn->query("SELECT * FROM landing_pages ORDER BY id DESC");
?>
<!doctype html><html lang="es"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Landing Pages | Admin</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head><body>
<nav class="navbar navbar-dark bg-dark"><div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Admin</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="landing.php">Landings</a>
</div>
</div></nav>
<div class="container py-4">
<?php if($msg):?><div class="alert alert-success py-2"><?=$msg?></div><?php endif;?>
<?php if($err):?><div class="alert alert-danger py-2"><?=$err?></div><?php endif;?>
<div class="card mb-4"><div class="card-body">
<h5 class="mb-3"><?= $edit?'Editar landing':'Nueva landing' ?></h5>
<form method="post" class="row g-3">
<input type="hidden" name="id" value="<?= (int)($edit['id']??0) ?>">
<div class="col-md-6">
<label class="form-label">Título</label>
<input name="titulo" class="form-control" required value="<?= htmlspecialchars($edit['titulo']??'') ?>">
</div>
<div class="col-md-3">
<label class="form-label">Slug</label>
<input name="slug" class="form-control" value="<?= htmlspecialchars($edit['slug']??'') ?>" placeholder="mi-landing">
</div>
<div class="col-md-3">
<label class="form-label">Estado</label>
<select name="estado" class="form-select">
<option value="borrador" <?= (($edit['estado']??'')==='borrador'?'selected':'')?>>Borrador</option>
<option value="publicado" <?= (($edit['estado']??'')==='publicado'?'selected':'')?>>Publicado</option>
</select>
</div>
<div class="col-12">
<label class="form-label">Hero image (URL pública)</label>
<input name="hero_image" class="form-control" value="<?= htmlspecialchars($edit['hero_image']??'') ?>" placeholder="/ruleta/uploads/landings/hero.jpg">
</div>
<div class="col-12">
<label class="form-label">Contenido HTML</label>
<textarea name="contenido_html" class="form-control" rows="8" placeholder="<section>..."></textarea>
<script>document.getElementsByName('contenido_html')[0].value = <?= json_encode($edit['contenido_html']??'') ?>;</script>
</div>
<div class="col-12">
<label class="form-label">CSS inline (opcional)</label>
<textarea name="css_inline" class="form-control" rows="4" placeholder="/* estilos */"></textarea>
<script>document.getElementsByName('css_inline')[0].value = <?= json_encode($edit['css_inline']??'') ?>;</script>
</div>
<div class="col-12">
<label class="form-label">JS inline (opcional)</label>
<textarea name="js_inline" class="form-control" rows="4" placeholder="// scripts"></textarea>
<script>document.getElementsByName('js_inline')[0].value = <?= json_encode($edit['js_inline']??'') ?>;</script>
</div>
<div class="col-md-4">
<label class="form-label">Meta title</label>
<input name="meta_title" class="form-control" value="<?= htmlspecialchars($edit['meta_title']??'') ?>">
</div>
<div class="col-md-5">
<label class="form-label">Meta description</label>
<input name="meta_desc" class="form-control" value="<?= htmlspecialchars($edit['meta_desc']??'') ?>">
</div>
<div class="col-md-3">
<label class="form-label">Canonical URL</label>
<input name="canonical_url" class="form-control" value="<?= htmlspecialchars($edit['canonical_url']??'') ?>">
</div>
<!-- NUEVO CAMPO: URL del juego -->
<div class="col-md-6">
<label class="form-label">URL del juego</label>
<input name="game_url" class="form-control" placeholder="https://ejemplo.com/juego"
value="<?= htmlspecialchars($edit['game_url'] ?? '') ?>">
</div>
<div class="col-12">
<button class="btn btn-primary"><?= $edit?'Actualizar':'Crear' ?></button>
<?php if($edit):?><a href="landing.php" class="btn btn-secondary">Cancelar</a><?php endif;?>
</div>
</form>
</div></div>
<div class="card"><div class="card-body table-responsive">
<table class="table table-sm table-striped align-middle">
<thead>
<tr>
<th>ID</th><th>Título</th><th>Slug</th><th>Estado</th><th>Actualizado</th><th>Acciones</th>
</tr>
</thead>
<tbody>
<?php while($r=$list->fetch_assoc()): ?>
<tr>
<td><?= (int)$r['id'] ?></td>
<td><?= htmlspecialchars($r['titulo']) ?></td>
<td><code><?= htmlspecialchars($r['slug']) ?></code></td>
<td><?= htmlspecialchars($r['estado']) ?></td>
<td><?= htmlspecialchars($r['updated_at']??'') ?></td>
<td class="text-nowrap d-flex gap-1">
<a class="btn btn-sm btn-outline-primary" href="landing.php?action=edit&id=<?= (int)$r['id'] ?>">Editar</a>
<a class="btn btn-sm btn-outline-danger" href="landing.php?action=delete&id=<?= (int)$r['id'] ?>" onclick="return confirm('¿Eliminar landing?')">Eliminar</a>
<a class="btn btn-sm btn-success" target="_blank" href="../landing.php?slug=<?= urlencode($r['slug']) ?>">Ver</a>
<?php if (!empty($r['game_url'])): ?>
<a class="btn btn-sm btn-outline-success" target="_blank" href="<?= htmlspecialchars($r['game_url']) ?>">Juego</a>
<?php endif; ?>
</td>
</tr>
<?php endwhile; ?>
</tbody>
</table>
</div></div>
</div>
</body></html>
+122
View File
@@ -0,0 +1,122 @@
<?php
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
$msg = $err = '';
$action = $_GET['action'] ?? '';
$id = (int)($_GET['id'] ?? 0);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$bid = (int)($_POST['id'] ?? 0);
$nombre= trim($_POST['nombre'] ?? '');
$tipo = $_POST['tipo'] ?? 'html';
$contenido = $_POST['contenido'] ?? '';
if ($nombre === '' || !in_array($tipo, ['html','css','js'], true)) {
$err = 'Nombre y tipo válidos son obligatorios.';
} else {
try {
if ($bid > 0) {
$st = $conn->prepare("UPDATE landing_blocks SET nombre=?, tipo=?, contenido=? WHERE id=?");
$st->bind_param("sssi", $nombre, $tipo, $contenido, $bid);
$st->execute(); $msg = 'Bloque actualizado.';
} else {
$st = $conn->prepare("INSERT INTO landing_blocks (nombre, tipo, contenido) VALUES (?,?,?)");
$st->bind_param("sss", $nombre, $tipo, $contenido);
$st->execute(); $msg = 'Bloque creado.';
}
} catch (Throwable $e) { $err = $e->getMessage(); }
}
}
if ($action === 'delete' && $id > 0) {
try {
$conn->query("DELETE FROM landing_blocks WHERE id=".$id);
$msg = 'Bloque eliminado.';
} catch (Throwable $e) { $err = $e->getMessage(); }
$id = 0;
}
$edit = null;
if ($action === 'edit' && $id > 0) {
$rs = $conn->query("SELECT * FROM landing_blocks WHERE id=".$id);
$edit = $rs ? $rs->fetch_assoc() : null;
}
$list = $conn->query("SELECT * FROM landing_blocks ORDER BY updated_at DESC, id DESC");
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Landing: Bloques reutilizables</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>.mono{font-family:ui-monospace,Menlo,Consolas,monospace;white-space:pre-wrap}</style>
</head>
<body>
<nav class="navbar navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Admin</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="landing.php">Landings</a>
<a class="btn btn-light btn-sm" href="landing_blocks.php">Bloques</a>
<a class="btn btn-outline-light btn-sm" href="../index.php?action=logout">Salir</a>
</div>
</div>
</nav>
<div class="container py-4">
<?php if($msg):?><div class="alert alert-success py-2"><?= htmlspecialchars($msg) ?></div><?php endif;?>
<?php if($err):?><div class="alert alert-danger py-2"><?= htmlspecialchars($err) ?></div><?php endif;?>
<div class="card shadow-sm mb-4"><div class="card-body">
<h5 class="mb-3"><?= $edit ? 'Editar bloque' : 'Nuevo bloque' ?></h5>
<form method="post" class="row g-3">
<input type="hidden" name="id" value="<?= (int)($edit['id'] ?? 0) ?>">
<div class="col-md-6">
<label class="form-label">Nombre</label>
<input name="nombre" class="form-control" required value="<?= htmlspecialchars($edit['nombre'] ?? '') ?>">
</div>
<div class="col-md-3">
<label class="form-label">Tipo</label>
<select name="tipo" class="form-select">
<?php $t=$edit['tipo']??'html'; ?>
<option value="html" <?= $t==='html'?'selected':'' ?>>HTML</option>
<option value="css" <?= $t==='css'?'selected':'' ?>>CSS</option>
<option value="js" <?= $t==='js'?'selected':'' ?>>JS</option>
</select>
</div>
<div class="col-12">
<label class="form-label">Contenido</label>
<textarea name="contenido" rows="10" class="form-control mono"><?= htmlspecialchars($edit['contenido'] ?? '') ?></textarea>
</div>
<div class="col-12">
<button class="btn btn-primary"><?= $edit?'Actualizar':'Crear' ?></button>
<?php if($edit):?><a href="landing_blocks.php" class="btn btn-secondary">Cancelar</a><?php endif;?>
</div>
</form>
</div></div>
<div class="card shadow-sm"><div class="card-body table-responsive">
<table class="table table-sm table-striped align-middle">
<thead><tr><th>ID</th><th>Nombre</th><th>Tipo</th><th>Actualizado</th><th>Acciones</th></tr></thead>
<tbody>
<?php while($r = $list->fetch_assoc()): ?>
<tr>
<td><?= (int)$r['id'] ?></td>
<td><?= htmlspecialchars($r['nombre']) ?></td>
<td><span class="badge text-bg-secondary"><?= htmlspecialchars($r['tipo']) ?></span></td>
<td><?= htmlspecialchars($r['updated_at']) ?></td>
<td class="text-nowrap">
<a class="btn btn-sm btn-outline-primary" href="landing_blocks.php?action=edit&id=<?= (int)$r['id'] ?>">Editar</a>
<a class="btn btn-sm btn-outline-danger" href="landing_blocks.php?action=delete&id=<?= (int)$r['id'] ?>" onclick="return confirm('¿Eliminar bloque?')">Eliminar</a>
</td>
</tr>
<?php endwhile; ?>
</tbody>
</table>
</div></div>
</div>
</body>
</html>
+149
View File
@@ -0,0 +1,149 @@
<?php
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
$landing_id = (int)($_GET['landing_id'] ?? 0);
if ($landing_id <= 0) die('landing_id requerido');
$landing = $conn->query("SELECT * FROM landing_pages WHERE id={$landing_id}")->fetch_assoc();
if (!$landing) die('Landing no existe');
$blocks = $conn->query("SELECT id,nombre,tipo FROM landing_blocks ORDER BY nombre ASC")->fetch_all(MYSQLI_ASSOC);
// bloques ya asignados (por zona y posición)
$assigned = [];
$rs = $conn->query("SELECT lpb.id, lpb.block_id, lpb.zona, lpb.posicion, lb.nombre, lb.tipo
FROM landing_page_blocks lpb
JOIN landing_blocks lb ON lb.id=lpb.block_id
WHERE lpb.landing_id={$landing_id}
ORDER BY lpb.zona, lpb.posicion");
while($r=$rs->fetch_assoc()){ $assigned[$r['zona']][] = $r; }
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Landing Builder (Drag & Drop)</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.js"></script>
<style>
.zone{min-height:80px;border:2px dashed #ddd;border-radius:.5rem;padding:10px}
.chip{cursor:grab;padding:8px 10px;border:1px solid #ddd;border-radius:.5rem;background:#fff;margin-bottom:6px}
.palette{max-height:50vh;overflow:auto;border:1px solid #eee;border-radius:.5rem}
</style>
</head>
<body>
<nav class="navbar navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Admin</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="landing.php">Landings</a>
<a class="btn btn-light btn-sm" href="#">Builder</a>
<a class="btn btn-outline-light btn-sm" href="landing_page_blocks.php?landing_id=<?= (int)$landing_id ?>">Lista</a>
<a class="btn btn-outline-light btn-sm" href="../index.php?action=logout">Salir</a>
</div>
</div>
</nav>
<div class="container py-4">
<div class="mb-3"><a class="btn btn-secondary btn-sm" href="landing.php">← Volver a Landings</a></div>
<h4 class="mb-3">Builder: <strong><?= htmlspecialchars($landing['titulo']) ?></strong></h4>
<div class="row g-3">
<div class="col-md-4">
<div class="card palette">
<div class="card-body">
<h5>Bloques disponibles</h5>
<div id="palette" class="zone">
<?php foreach($blocks as $b): ?>
<div class="chip" data-block-id="<?= (int)$b['id'] ?>">
[<?= htmlspecialchars($b['tipo']) ?>] <?= htmlspecialchars($b['nombre']) ?>
</div>
<?php endforeach; ?>
</div>
<p class="text-muted small mt-2">Arrastra un bloque a una zona.</p>
</div>
</div>
</div>
<div class="col-md-8">
<div class="card mb-3"><div class="card-body">
<h6 class="mb-2">Header</h6>
<div id="zone-header" class="zone"><?php
if(!empty($assigned['header'])) foreach($assigned['header'] as $item){
echo '<div class="chip" data-assign-id="'.(int)$item['id'].'" data-block-id="'.(int)$item['block_id'].'">['.htmlspecialchars($item['tipo']).'] '.htmlspecialchars($item['nombre']).'</div>';
}
?></div>
</div></div>
<div class="card mb-3"><div class="card-body">
<h6 class="mb-2">Main</h6>
<div id="zone-main" class="zone"><?php
if(!empty($assigned['main'])) foreach($assigned['main'] as $item){
echo '<div class="chip" data-assign-id="'.(int)$item['id'].'" data-block-id="'.(int)$item['block_id'].'">['.htmlspecialchars($item['tipo']).'] '.htmlspecialchars($item['nombre']).'</div>';
}
?></div>
</div></div>
<div class="card mb-3"><div class="card-body">
<h6 class="mb-2">Footer</h6>
<div id="zone-footer" class="zone"><?php
if(!empty($assigned['footer'])) foreach($assigned['footer'] as $item){
echo '<div class="chip" data-assign-id="'.(int)$item['id'].'" data-block-id="'.(int)$item['block_id'].'">['.htmlspecialchars($item['tipo']).'] '.htmlspecialchars($item['nombre']).'</div>';
}
?></div>
</div></div>
<button id="save" class="btn btn-primary">Guardar distribución</button>
<div id="msg" class="mt-2"></div>
</div>
</div>
</div>
<script>
function mkSortable(el){ return new Sortable(el, { group:'zones', animation:150, sort:true, pull:true, put:true }); }
['palette','zone-header','zone-main','zone-footer'].forEach(id => mkSortable(document.getElementById(id)));
document.getElementById('save').addEventListener('click', function(){
const landing_id = <?= (int)$landing_id ?>;
const zones = ['header','main','footer'];
const payload = { landing_id, items: [] };
zones.forEach(z => {
const zoneEl = document.getElementById('zone-'+z);
Array.from(zoneEl.children).forEach((chip, idx) => {
payload.items.push({
assign_id: chip.dataset.assignId || null, // si ya existía
block_id: chip.dataset.blockId,
zona: z,
posicion: idx + 1
});
});
});
fetch('landing_builder_save.php', {
method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify(payload)
})
.then(r=>r.json())
.then(res=>{
document.getElementById('msg').innerHTML =
res.ok ? '<div class="alert alert-success py-2">Guardado.</div>' :
'<div class="alert alert-danger py-2">'+(res.error||'Error')+'</div>';
// actualizar assign_id devueltos para chips nuevos
if(res.map_ids){
const map = res.map_ids;
document.querySelectorAll('.chip').forEach(ch=>{
const bid = ch.dataset.blockId;
const key = ch.dataset.assignId ? null : (bid + '@' + ch.parentElement.id.replace('zone-',''));
if(key && map[key]) ch.dataset.assignId = map[key];
});
}
})
.catch(()=>{ document.getElementById('msg').innerHTML='<div class="alert alert-danger py-2">Error de red</div>'; });
});
</script>
</body>
</html>
+43
View File
@@ -0,0 +1,43 @@
<?php
session_start();
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { echo json_encode(['ok'=>false,'error'=>'auth']); exit; }
$raw = file_get_contents('php://input');
$in = json_decode($raw,true);
$landing_id = (int)($in['landing_id'] ?? 0);
$items = $in['items'] ?? [];
if ($landing_id<=0 || !is_array($items)) { echo json_encode(['ok'=>false,'error'=>'input']); exit; }
$conn->begin_transaction();
try{
$map_ids = []; // devolver ids para chips nuevos
foreach($items as $it){
$assign_id = isset($it['assign_id']) ? (int)$it['assign_id'] : 0;
$block_id = (int)$it['block_id'];
$zona = substr(trim($it['zona'] ?? 'main'), 0, 40);
$posicion = (int)($it['posicion'] ?? 1);
if($assign_id>0){
$st=$conn->prepare("UPDATE landing_page_blocks SET zona=?, posicion=? WHERE id=? AND landing_id=?");
$st->bind_param('siii', $zona, $posicion, $assign_id, $landing_id);
$st->execute();
}else{
// crear nueva asignación
$st=$conn->prepare("INSERT INTO landing_page_blocks (landing_id, block_id, zona, posicion) VALUES (?,?,?,?)");
$st->bind_param('iisi', $landing_id, $block_id, $zona, $posicion);
$st->execute();
// clave aproximada para mapear chip -> asignación creada
$map_ids[$block_id.'@'.$zona] = $conn->insert_id;
}
}
$conn->commit();
echo json_encode(['ok'=>true,'map_ids'=>$map_ids]);
}catch(Throwable $e){
$conn->rollback();
echo json_encode(['ok'=>false,'error'=>$e->getMessage()]);
}
+152
View File
@@ -0,0 +1,152 @@
<?php
// ruleta/admin/landing_crm.php
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
$landing_id = (int)($_GET['landing_id'] ?? 0);
if ($landing_id <= 0) { die('landing_id requerido'); }
$landing = $conn->query("SELECT id,titulo FROM landing_pages WHERE id={$landing_id}")->fetch_assoc();
if (!$landing) { die('Landing no existe'); }
$msg = $err = '';
$action = $_GET['action'] ?? '';
$id = (int)($_GET['id'] ?? 0);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$hid = (int)($_POST['id'] ?? 0);
$nombre= trim($_POST['nombre'] ?? '');
$endpoint = trim($_POST['endpoint'] ?? '');
$method = $_POST['method'] ?? 'POST';
$secret = trim($_POST['secret'] ?? '');
$active = isset($_POST['active']) ? 1 : 0;
if ($nombre === '' || $endpoint === '' || !in_array($method, ['POST','GET'], true)) {
$err = 'Nombre, endpoint y método (POST/GET) son obligatorios.';
} else {
try {
if ($hid > 0) {
$st = $conn->prepare("UPDATE landing_crm_hooks SET nombre=?, endpoint=?, method=?, secret=?, active=? WHERE id=? AND landing_id=?");
$st->bind_param("ssssiii", $nombre, $endpoint, $method, $secret, $active, $hid, $landing_id);
$st->execute(); $msg = 'Hook actualizado.';
} else {
$st = $conn->prepare("INSERT INTO landing_crm_hooks (landing_id, nombre, endpoint, method, secret, active) VALUES (?,?,?,?,?,?)");
$st->bind_param("issssi", $landing_id, $nombre, $endpoint, $method, $secret, $active);
$st->execute(); $msg = 'Hook creado.';
}
} catch (Throwable $e) { $err = $e->getMessage(); }
}
}
if ($action === 'delete' && $id > 0) {
try {
$st = $conn->prepare("DELETE FROM landing_crm_hooks WHERE id=? AND landing_id=?");
$st->bind_param("ii", $id, $landing_id);
$st->execute(); $msg = 'Hook eliminado.';
} catch (Throwable $e) { $err = $e->getMessage(); }
$id = 0;
}
$edit = null;
if ($action === 'edit' && $id > 0) {
$st = $conn->prepare("SELECT * FROM landing_crm_hooks WHERE id=? AND landing_id=?");
$st->bind_param("ii", $id, $landing_id);
$st->execute();
$edit = $st->get_result()->fetch_assoc();
}
$list = $conn->query("SELECT * FROM landing_crm_hooks WHERE landing_id={$landing_id} ORDER BY id DESC");
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Landing CRM Hooks</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Admin</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="landing.php">Landings</a>
<a class="btn btn-light btn-sm" href="landing_crm.php?landing_id=<?= (int)$landing_id ?>">CRM Hooks</a>
<a class="btn btn-outline-light btn-sm" href="landing_forms.php?landing_id=<?= (int)$landing_id ?>">Leads</a>
<a class="btn btn-outline-light btn-sm" href="../index.php?action=logout">Salir</a>
</div>
</div>
</nav>
<div class="container py-4">
<div class="mb-3">
<a class="btn btn-secondary btn-sm" href="landing.php">← Volver a Landings</a>
</div>
<h4 class="mb-3">Landing: <strong><?= htmlspecialchars($landing['titulo']) ?></strong> (#<?= (int)$landing_id ?>)</h4>
<?php if($msg):?><div class="alert alert-success py-2"><?= htmlspecialchars($msg) ?></div><?php endif;?>
<?php if($err):?><div class="alert alert-danger py-2"><?= htmlspecialchars($err) ?></div><?php endif;?>
<div class="card shadow-sm mb-4"><div class="card-body">
<h5 class="mb-3"><?= $edit ? 'Editar hook' : 'Nuevo hook' ?></h5>
<form method="post" class="row g-3">
<input type="hidden" name="id" value="<?= (int)($edit['id'] ?? 0) ?>">
<div class="col-md-4">
<label class="form-label">Nombre</label>
<input name="nombre" class="form-control" required value="<?= htmlspecialchars($edit['nombre'] ?? '') ?>">
</div>
<div class="col-md-6">
<label class="form-label">Endpoint (URL)</label>
<input name="endpoint" class="form-control" required placeholder="https://hooks.zapier.com/..." value="<?= htmlspecialchars($edit['endpoint'] ?? '') ?>">
</div>
<div class="col-md-2">
<label class="form-label">Método</label>
<?php $m=$edit['method']??'POST'; ?>
<select name="method" class="form-select">
<option value="POST" <?= $m==='POST'?'selected':'' ?>>POST</option>
<option value="GET" <?= $m==='GET' ?'selected':'' ?>>GET</option>
</select>
</div>
<div class="col-md-8">
<label class="form-label">Secret / Bearer (opcional)</label>
<input name="secret" class="form-control" placeholder="Se enviará como Authorization: Bearer ..." value="<?= htmlspecialchars($edit['secret'] ?? '') ?>">
</div>
<div class="col-md-4 d-flex align-items-end">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="active" name="active" <?= (isset($edit['active']) ? (intval($edit['active']) ? 'checked' : '') : 'checked') ?>>
<label class="form-check-label" for="active">Activo</label>
</div>
</div>
<div class="col-12">
<button class="btn btn-primary"><?= $edit?'Actualizar':'Crear' ?></button>
<?php if($edit):?><a href="landing_crm.php?landing_id=<?= (int)$landing_id ?>" class="btn btn-secondary">Cancelar</a><?php endif;?>
</div>
</form>
</div></div>
<div class="card shadow-sm"><div class="card-body table-responsive">
<table class="table table-sm table-striped align-middle">
<thead><tr><th>ID</th><th>Nombre</th><th>Endpoint</th><th>Método</th><th>Activo</th><th>Acciones</th></tr></thead>
<tbody>
<?php while($r=$list->fetch_assoc()): ?>
<tr>
<td><?= (int)$r['id'] ?></td>
<td><?= htmlspecialchars($r['nombre']) ?></td>
<td><code><?= htmlspecialchars($r['endpoint']) ?></code></td>
<td><span class="badge text-bg-secondary"><?= htmlspecialchars($r['method']) ?></span></td>
<td><?= intval($r['active']) ? 'Sí' : 'No' ?></td>
<td class="text-nowrap">
<a class="btn btn-sm btn-outline-primary" href="landing_crm.php?landing_id=<?= (int)$landing_id ?>&action=edit&id=<?= (int)$r['id'] ?>">Editar</a>
<a class="btn btn-sm btn-outline-danger" href="landing_crm.php?landing_id=<?= (int)$landing_id ?>&action=delete&id=<?= (int)$r['id'] ?>" onclick="return confirm('¿Eliminar hook?')">Eliminar</a>
</td>
</tr>
<?php endwhile; if(!$list || !$list->num_rows): ?>
<tr><td colspan="6" class="text-center text-muted">Sin hooks configurados</td></tr>
<?php endif; ?>
</tbody>
</table>
</div></div>
</div>
</body>
</html>
+100
View File
@@ -0,0 +1,100 @@
<?php
// ruleta/admin/landing_forms.php
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
$landing_id = (int)($_GET['landing_id'] ?? 0);
if ($landing_id <= 0) { die('landing_id requerido'); }
$landing = $conn->query("SELECT id,titulo FROM landing_pages WHERE id={$landing_id}")->fetch_assoc();
if (!$landing) { die('Landing no existe'); }
$from = trim($_GET['from'] ?? '');
$to = trim($_GET['to'] ?? '');
$where = "WHERE lf.landing_id={$landing_id}";
$params = []; $types = '';
if ($from !== '') { $where .= " AND lf.created_at >= ?"; $params[] = $from.' 00:00:00'; $types.='s'; }
if ($to !== '') { $where .= " AND lf.created_at <= ?"; $params[] = $to .' 23:59:59'; $types.='s'; }
$sql = "SELECT lf.id, lf.created_at, lf.nombre, lf.email, lf.telefono, lf.mensaje, lf.ip_address
FROM landing_forms lf
{$where}
ORDER BY lf.id DESC
LIMIT 500";
$st = $conn->prepare($sql);
if (!empty($params)) { $st->bind_param($types, ...$params); }
$st->execute();
$list = $st->get_result();
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Landing Leads</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>.msg{max-width:420px;white-space:pre-wrap}</style>
</head>
<body>
<nav class="navbar navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Admin</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="landing.php">Landings</a>
<a class="btn btn-outline-light btn-sm" href="landing_crm.php?landing_id=<?= (int)$landing_id ?>">CRM Hooks</a>
<a class="btn btn-light btn-sm" href="landing_forms.php?landing_id=<?= (int)$landing_id ?>">Leads</a>
<a class="btn btn-outline-light btn-sm" href="../index.php?action=logout">Salir</a>
</div>
</div>
</nav>
<div class="container py-4">
<div class="mb-3">
<a class="btn btn-secondary btn-sm" href="landing.php">← Volver a Landings</a>
</div>
<h4 class="mb-3">Leads: <strong><?= htmlspecialchars($landing['titulo']) ?></strong></h4>
<form class="row g-2 mb-3" method="get">
<input type="hidden" name="landing_id" value="<?= (int)$landing_id ?>">
<div class="col-md-3">
<label class="form-label">Desde</label>
<input type="date" class="form-control" name="from" value="<?= htmlspecialchars($from) ?>">
</div>
<div class="col-md-3">
<label class="form-label">Hasta</label>
<input type="date" class="form-control" name="to" value="<?= htmlspecialchars($to) ?>">
</div>
<div class="col-md-6 d-flex align-items-end gap-2">
<button class="btn btn-primary">Filtrar</button>
<a class="btn btn-success" target="_blank"
href="landing_forms_export.php?landing_id=<?= (int)$landing_id ?>&from=<?= urlencode($from) ?>&to=<?= urlencode($to) ?>">
Exportar CSV
</a>
</div>
</form>
<div class="card shadow-sm"><div class="card-body table-responsive">
<table class="table table-sm table-striped align-middle">
<thead><tr><th>ID</th><th>Fecha</th><th>Nombre</th><th>Email</th><th>Teléfono</th><th>Mensaje</th><th>IP</th></tr></thead>
<tbody>
<?php while($r=$list->fetch_assoc()): ?>
<tr>
<td><?= (int)$r['id'] ?></td>
<td><?= htmlspecialchars($r['created_at']) ?></td>
<td><?= htmlspecialchars($r['nombre']) ?></td>
<td><?= htmlspecialchars($r['email']) ?></td>
<td><?= htmlspecialchars($r['telefono']) ?></td>
<td class="msg"><?= htmlspecialchars($r['mensaje']) ?></td>
<td><?= htmlspecialchars($r['ip_address']) ?></td>
</tr>
<?php endwhile; if(!$list || !$list->num_rows): ?>
<tr><td colspan="7" class="text-center text-muted">Sin leads</td></tr>
<?php endif; ?>
</tbody>
</table>
</div></div>
</div>
</body>
</html>
+46
View File
@@ -0,0 +1,46 @@
<?php
// ruleta/admin/landing_forms_export.php
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { http_response_code(403); echo 'auth'; exit; }
$landing_id = (int)($_GET['landing_id'] ?? 0);
$from = trim($_GET['from'] ?? '');
$to = trim($_GET['to'] ?? '');
if ($landing_id <= 0) { http_response_code(400); echo 'landing_id requerido'; exit; }
$where = "WHERE lf.landing_id={$landing_id}";
$params = []; $types = '';
if ($from !== '') { $where .= " AND lf.created_at >= ?"; $params[] = $from.' 00:00:00'; $types.='s'; }
if ($to !== '') { $where .= " AND lf.created_at <= ?"; $params[] = $to .' 23:59:59'; $types.='s'; }
$sql = "SELECT lf.id, lf.created_at, lp.titulo AS landing, lf.nombre, lf.email, lf.telefono, lf.mensaje, lf.ip_address
FROM landing_forms lf
JOIN landing_pages lp ON lp.id=lf.landing_id
{$where}
ORDER BY lf.id DESC";
$st = $conn->prepare($sql);
if (!empty($params)) { $st->bind_param($types, ...$params); }
$st->execute();
$res = $st->get_result();
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="landing_'.$landing_id.'_leads.csv"');
$out = fopen('php://output', 'w');
fputcsv($out, ['ID','Fecha','Landing','Nombre','Email','Telefono','Mensaje','IP']);
while($row = $res->fetch_assoc()){
fputcsv($out, [
$row['id'],
$row['created_at'],
$row['landing'],
$row['nombre'],
$row['email'],
$row['telefono'],
preg_replace("/\r|\n/"," ", (string)$row['mensaje']),
$row['ip_address'],
]);
}
fclose($out);
+187
View File
@@ -0,0 +1,187 @@
<?php
// ruleta/admin/landing_page_blocks.php
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
$landing_id = (int)($_GET['landing_id'] ?? 0);
if ($landing_id <= 0) { die('landing_id requerido'); }
// Cargar landing
$landing = $conn->query("SELECT * FROM landing_pages WHERE id=".$landing_id)->fetch_assoc();
if (!$landing) { die('Landing no existe'); }
$msg = $err = '';
$action = $_GET['action'] ?? '';
$id = (int)($_GET['id'] ?? 0);
// Cargar catálogo de bloques para el <select>
$blocks_rs = $conn->query("SELECT id, nombre, tipo FROM landing_blocks ORDER BY nombre ASC");
$blocks = [];
while($b = $blocks_rs->fetch_assoc()){ $blocks[] = $b; }
// Helpers
function valid_zone($z) {
$z = trim($z);
if ($z==='') $z = 'main';
// podrías limitar: ['header','main','footer','sidebar']
return substr($z, 0, 40);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$lpb_id = (int)($_POST['id'] ?? 0);
$block_id = (int)($_POST['block_id'] ?? 0);
$zona = valid_zone($_POST['zona'] ?? 'main');
$posicion = (int)($_POST['posicion'] ?? 1);
if ($block_id <= 0) {
$err = 'Debes seleccionar un bloque.';
} else {
try {
if ($lpb_id > 0) {
$st = $conn->prepare("UPDATE landing_page_blocks SET block_id=?, zona=?, posicion=? WHERE id=? AND landing_id=?");
$st->bind_param("isiii", $block_id, $zona, $posicion, $lpb_id, $landing_id);
$st->execute(); $msg = 'Asignación actualizada.';
} else {
$st = $conn->prepare("INSERT INTO landing_page_blocks (landing_id, block_id, zona, posicion) VALUES (?,?,?,?)");
$st->bind_param("iisi", $landing_id, $block_id, $zona, $posicion);
$st->execute(); $msg = 'Bloque asignado a la landing.';
}
} catch (Throwable $e) { $err = $e->getMessage(); }
}
}
if ($action === 'delete' && $id > 0) {
try {
$st = $conn->prepare("DELETE FROM landing_page_blocks WHERE id=? AND landing_id=?");
$st->bind_param("ii", $id, $landing_id);
$st->execute(); $msg = 'Asignación eliminada.';
} catch (Throwable $e) { $err = $e->getMessage(); }
$id = 0;
}
$edit = null;
if ($action === 'edit' && $id > 0) {
$st = $conn->prepare("SELECT * FROM landing_page_blocks WHERE id=? AND landing_id=?");
$st->bind_param("ii", $id, $landing_id);
$st->execute();
$edit = $st->get_result()->fetch_assoc();
}
// Listado agrupado por zona y ordenado por posición
$list = $conn->query("
SELECT lpb.id, lpb.block_id, lpb.zona, lpb.posicion,
lb.nombre, lb.tipo
FROM landing_page_blocks lpb
JOIN landing_blocks lb ON lb.id = lpb.block_id
WHERE lpb.landing_id = {$landing_id}
ORDER BY lpb.zona ASC, lpb.posicion ASC, lpb.id ASC
");
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Landing: Asignar Bloques</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Admin</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="landing.php">Landings</a>
<a class="btn btn-outline-light btn-sm" href="landing_blocks.php">Bloques</a>
<a class="btn btn-outline-light btn-sm" href="landing_variants.php?landing_id=<?= (int)$landing_id ?>">Variantes</a>
<a class="btn btn-outline-light btn-sm" href="landing_schedule.php?landing_id=<?= (int)$landing_id ?>">Schedule</a>
<a class="btn btn-light btn-sm" href="landing_page_blocks.php?landing_id=<?= (int)$landing_id ?>">Asignar bloques</a>
<a class="btn btn-outline-light btn-sm" href="../index.php?action=logout">Salir</a>
</div>
</div>
</nav>
<div class="container py-4">
<div class="mb-3">
<a class="btn btn-secondary btn-sm" href="landing.php">← Volver a Landings</a>
</div>
<h4 class="mb-3">Landing: <strong><?= htmlspecialchars($landing['titulo']) ?></strong> <small class="text-muted">(#<?= (int)$landing_id ?>)</small></h4>
<?php if($msg):?><div class="alert alert-success py-2"><?= htmlspecialchars($msg) ?></div><?php endif;?>
<?php if($err):?><div class="alert alert-danger py-2"><?= htmlspecialchars($err) ?></div><?php endif;?>
<div class="card shadow-sm mb-4"><div class="card-body">
<h5 class="mb-3"><?= $edit ? 'Editar asignación' : 'Nueva asignación' ?></h5>
<form method="post" class="row g-3">
<input type="hidden" name="id" value="<?= (int)($edit['id'] ?? 0) ?>">
<div class="col-md-6">
<label class="form-label">Bloque</label>
<select name="block_id" class="form-select" required>
<option value="">— Selecciona —</option>
<?php
$sel_block = (int)($edit['block_id'] ?? 0);
foreach ($blocks as $b):
?>
<option value="<?= (int)$b['id'] ?>" <?= $sel_block===(int)$b['id']?'selected':'' ?>>
[<?= htmlspecialchars(strtoupper($b['tipo'])) ?>] <?= htmlspecialchars($b['nombre']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Zona</label>
<input name="zona" class="form-control" placeholder="main / header / footer" value="<?= htmlspecialchars($edit['zona'] ?? 'main') ?>">
</div>
<div class="col-md-3">
<label class="form-label">Posición</label>
<input name="posicion" type="number" class="form-control" value="<?= htmlspecialchars((string)($edit['posicion'] ?? '1')) ?>">
</div>
<div class="col-12">
<button class="btn btn-primary"><?= $edit ? 'Actualizar' : 'Asignar' ?></button>
<?php if($edit):?><a href="landing_page_blocks.php?landing_id=<?= (int)$landing_id ?>" class="btn btn-secondary">Cancelar</a><?php endif;?>
</div>
</form>
</div></div>
<div class="card shadow-sm">
<div class="card-body table-responsive">
<h5 class="mb-3">Bloques asignados</h5>
<table class="table table-sm table-striped align-middle">
<thead>
<tr>
<th>ID</th>
<th>Bloque</th>
<th>Tipo</th>
<th>Zona</th>
<th>Posición</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
<?php if ($list && $list->num_rows): while($r = $list->fetch_assoc()): ?>
<tr>
<td><?= (int)$r['id'] ?></td>
<td><?= htmlspecialchars($r['nombre']) ?></td>
<td><span class="badge text-bg-secondary"><?= htmlspecialchars($r['tipo']) ?></span></td>
<td><code><?= htmlspecialchars($r['zona']) ?></code></td>
<td><?= (int)$r['posicion'] ?></td>
<td class="text-nowrap">
<a class="btn btn-sm btn-outline-primary" href="landing_page_blocks.php?landing_id=<?= (int)$landing_id ?>&action=edit&id=<?= (int)$r['id'] ?>">Editar</a>
<a class="btn btn-sm btn-outline-danger" href="landing_page_blocks.php?landing_id=<?= (int)$landing_id ?>&action=delete&id=<?= (int)$r['id'] ?>" onclick="return confirm('¿Eliminar asignación?')">Eliminar</a>
</td>
</tr>
<?php endwhile; else: ?>
<tr><td colspan="6" class="text-center text-muted">Sin bloques asignados</td></tr>
<?php endif; ?>
</tbody>
</table>
<p class="text-muted small mb-0">* Sugerencia: usa zonas como <code>header</code>, <code>main</code>, <code>footer</code> o <code>sidebar</code>, y ordena con <strong>posiciones</strong> (1,2,3...).</p>
</div>
</div>
</div>
</body>
</html>
+153
View File
@@ -0,0 +1,153 @@
<?php
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
$landing_id = (int)($_GET['landing_id'] ?? 0);
if ($landing_id <= 0) { die('landing_id requerido'); }
$landing = $conn->query("SELECT * FROM landing_pages WHERE id=".$landing_id)->fetch_assoc();
if (!$landing) { die('Landing no existe'); }
$msg = $err = '';
$action = $_GET['action'] ?? '';
$id = (int)($_GET['id'] ?? 0);
function to_mysql_dt($s) {
// recibe "YYYY-MM-DDTHH:MM" y devuelve "YYYY-MM-DD HH:MM:00" o NULL
$s = trim((string)$s);
if ($s === '') return null;
$s = str_replace('T',' ',$s);
if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}/',$s)) return $s.':00';
return null;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$sid = (int)($_POST['id'] ?? 0);
$start= to_mysql_dt($_POST['start_at'] ?? '');
$end = to_mysql_dt($_POST['end_at'] ?? '');
$estado = $_POST['estado'] ?? 'programada';
if (!in_array($estado, ['programada','activa','finalizada'], true)) {
$err = 'Estado inválido.';
} else {
try {
if ($sid > 0) {
$st = $conn->prepare("UPDATE landing_schedule SET start_at=?, end_at=?, estado=? WHERE id=? AND landing_id=?");
$st->bind_param("sssii", $start, $end, $estado, $sid, $landing_id);
$st->execute(); $msg = 'Rango actualizado.';
} else {
$st = $conn->prepare("INSERT INTO landing_schedule (landing_id,start_at,end_at,estado) VALUES (?,?,?,?)");
$st->bind_param("isss", $landing_id, $start, $end, $estado);
$st->execute(); $msg = 'Rango creado.';
}
} catch (Throwable $e) { $err = $e->getMessage(); }
}
}
if ($action === 'delete' && $id > 0) {
try {
$st = $conn->prepare("DELETE FROM landing_schedule WHERE id=? AND landing_id=?");
$st->bind_param("ii", $id, $landing_id);
$st->execute(); $msg = 'Rango eliminado.';
} catch (Throwable $e) { $err = $e->getMessage(); }
$id = 0;
}
$edit = null;
if ($action === 'edit' && $id > 0) {
$st = $conn->prepare("SELECT * FROM landing_schedule WHERE id=? AND landing_id=?");
$st->bind_param("ii", $id, $landing_id);
$st->execute();
$edit = $st->get_result()->fetch_assoc();
}
$list = $conn->query("SELECT * FROM landing_schedule WHERE landing_id=".$landing_id." ORDER BY start_at ASC, id ASC");
// helpers para mostrar en datetime-local
function to_input_dt($s) {
if (!$s) return '';
$s = str_replace(' ', 'T', substr($s,0,16));
return $s;
}
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Landing: Schedule</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Admin</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="landing.php">Landings</a>
<a class="btn btn-outline-light btn-sm" href="landing_variants.php?landing_id=<?= (int)$landing_id ?>">Variantes</a>
<a class="btn btn-light btn-sm" href="landing_schedule.php?landing_id=<?= (int)$landing_id ?>">Schedule</a>
<a class="btn btn-outline-light btn-sm" href="../index.php?action=logout">Salir</a>
</div>
</div>
</nav>
<div class="container py-4">
<div class="mb-3">
<a class="btn btn-secondary btn-sm" href="landing.php">← Volver a Landings</a>
</div>
<h4 class="mb-3">Landing: <strong><?= htmlspecialchars($landing['titulo']) ?></strong></h4>
<?php if($msg):?><div class="alert alert-success py-2"><?= htmlspecialchars($msg) ?></div><?php endif;?>
<?php if($err):?><div class="alert alert-danger py-2"><?= htmlspecialchars($err) ?></div><?php endif;?>
<div class="card shadow-sm mb-4"><div class="card-body">
<h5 class="mb-3"><?= $edit ? 'Editar rango' : 'Nuevo rango' ?></h5>
<form method="post" class="row g-3">
<input type="hidden" name="id" value="<?= (int)($edit['id'] ?? 0) ?>">
<div class="col-md-4">
<label class="form-label">Inicio</label>
<input type="datetime-local" name="start_at" class="form-control" value="<?= htmlspecialchars(to_input_dt($edit['start_at'] ?? '')) ?>">
</div>
<div class="col-md-4">
<label class="form-label">Fin</label>
<input type="datetime-local" name="end_at" class="form-control" value="<?= htmlspecialchars(to_input_dt($edit['end_at'] ?? '')) ?>">
</div>
<div class="col-md-4">
<label class="form-label">Estado</label>
<?php $est=$edit['estado']??'programada'; ?>
<select name="estado" class="form-select">
<option value="programada" <?= $est==='programada'?'selected':'' ?>>Programada</option>
<option value="activa" <?= $est==='activa'?'selected':'' ?>>Activa</option>
<option value="finalizada" <?= $est==='finalizada'?'selected':'' ?>>Finalizada</option>
</select>
</div>
<div class="col-12">
<button class="btn btn-primary"><?= $edit?'Actualizar':'Crear' ?></button>
<?php if($edit):?><a href="landing_schedule.php?landing_id=<?= (int)$landing_id ?>" class="btn btn-secondary">Cancelar</a><?php endif;?>
</div>
</form>
</div></div>
<div class="card shadow-sm"><div class="card-body table-responsive">
<table class="table table-sm table-striped align-middle">
<thead><tr><th>ID</th><th>Inicio</th><th>Fin</th><th>Estado</th><th>Acciones</th></tr></thead>
<tbody>
<?php while($r = $list->fetch_assoc()): ?>
<tr>
<td><?= (int)$r['id'] ?></td>
<td><?= htmlspecialchars($r['start_at'] ?? '—') ?></td>
<td><?= htmlspecialchars($r['end_at'] ?? '—') ?></td>
<td><span class="badge text-bg-secondary"><?= htmlspecialchars($r['estado']) ?></span></td>
<td class="text-nowrap">
<a class="btn btn-sm btn-outline-primary" href="landing_schedule.php?landing_id=<?= (int)$landing_id ?>&action=edit&id=<?= (int)$r['id'] ?>">Editar</a>
<a class="btn btn-sm btn-outline-danger" href="landing_schedule.php?landing_id=<?= (int)$landing_id ?>&action=delete&id=<?= (int)$r['id'] ?>" onclick="return confirm('¿Eliminar rango?')">Eliminar</a>
</td>
</tr>
<?php endwhile; ?>
</tbody>
</table>
</div></div>
</div>
</body>
</html>
+107
View File
@@ -0,0 +1,107 @@
<?php
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
$landing_id = (int)($_GET['landing_id'] ?? 0);
if ($landing_id <= 0) die('landing_id requerido');
$landing = $conn->query("SELECT id,titulo FROM landing_pages WHERE id={$landing_id}")->fetch_assoc();
if (!$landing) die('Landing no existe');
$rows = [];
$st = $conn->prepare("
SELECT lv.id AS variant_id, COALESCE(lv.nombre,'Base') AS nombre,
SUM(lvs.event_type='impression') AS impresiones,
SUM(lvs.event_type='conversion') AS conversiones
FROM landing_variant_stats lvs
LEFT JOIN landing_variants lv ON lv.id=lvs.variant_id
WHERE lvs.landing_id=?
GROUP BY lvs.variant_id, lv.nombre
ORDER BY impresiones DESC
");
$st->bind_param('i',$landing_id);
$st->execute();
$res = $st->get_result();
while($r=$res->fetch_assoc()){
$r['cr'] = $r['impresiones'] ? round(100*$r['conversiones']/$r['impresiones'],2) : 0.0;
$rows[] = $r;
}
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Landing: Stats</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
</head>
<body>
<nav class="navbar navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Admin</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="landing.php">Landings</a>
<a class="btn btn-light btn-sm" href="landing_stats.php?landing_id=<?= (int)$landing_id ?>">Stats</a>
<a class="btn btn-outline-light btn-sm" href="../index.php?action=logout">Salir</a>
</div>
</div>
</nav>
<div class="container py-4">
<div class="mb-3">
<a class="btn btn-secondary btn-sm" href="landing.php">← Volver a Landings</a>
</div>
<h4 class="mb-3">Stats: <strong><?= htmlspecialchars($landing['titulo']) ?></strong></h4>
<div class="card mb-4"><div class="card-body">
<canvas id="chart"></canvas>
</div></div>
<div class="card"><div class="card-body table-responsive">
<table class="table table-sm table-striped align-middle">
<thead><tr><th>Variante</th><th>Impresiones</th><th>Conversiones</th><th>CR %</th></tr></thead>
<tbody>
<?php foreach($rows as $r): ?>
<tr>
<td><?= htmlspecialchars($r['nombre'] ?? 'Base') ?></td>
<td><?= (int)$r['impresiones'] ?></td>
<td><?= (int)$r['conversiones'] ?></td>
<td><?= number_format($r['cr'],2) ?></td>
</tr>
<?php endforeach; ?>
<?php if(!$rows): ?><tr><td colspan="4" class="text-center text-muted">Sin datos</td></tr><?php endif; ?>
</tbody>
</table>
</div></div>
</div>
<script>
const data = <?= json_encode($rows, JSON_UNESCAPED_UNICODE) ?>;
const labels = data.map(r => r.nombre || 'Base');
const imps = data.map(r => +r.impresiones);
const convs = data.map(r => +r.conversiones);
const cr = data.map(r => +r.cr);
new Chart(document.getElementById('chart'), {
type: 'bar',
data: {
labels,
datasets: [
{ label: 'Impresiones', data: imps },
{ label: 'Conversiones', data: convs },
{ label: 'CR %', data: cr, yAxisID: 'y1', type: 'line' }
]
},
options: {
responsive: true,
scales: {
y: { beginAtZero: true, title:{display:true,text:'Impresiones / Conversiones'} },
y1: { beginAtZero: true, position:'right', title:{display:true,text:'CR %'} }
}
}
});
</script>
</body>
</html>
+160
View File
@@ -0,0 +1,160 @@
<?php
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
$landing_id = (int)($_GET['landing_id'] ?? 0);
if ($landing_id <= 0) { die('landing_id requerido'); }
$landing = $conn->query("SELECT * FROM landing_pages WHERE id=".$landing_id)->fetch_assoc();
if (!$landing) { die('Landing no existe'); }
$msg = $err = '';
$action = $_GET['action'] ?? '';
$id = (int)($_GET['id'] ?? 0);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$vid = (int)($_POST['id'] ?? 0);
$nombre= trim($_POST['nombre'] ?? '');
$peso = (float)($_POST['peso'] ?? 1.0);
$estado= $_POST['estado'] ?? 'activo';
$hero_image = trim($_POST['hero_image'] ?? '');
$contenido_html = $_POST['contenido_html'] ?? '';
$css_inline = $_POST['css_inline'] ?? '';
$js_inline = $_POST['js_inline'] ?? '';
if ($nombre === '' || !in_array($estado, ['borrador','activo','pausado'], true)) {
$err = 'Nombre y estado válidos son obligatorios.';
} else {
try {
if ($vid > 0) {
$st = $conn->prepare("UPDATE landing_variants SET nombre=?, peso=?, estado=?, hero_image=?, contenido_html=?, css_inline=?, js_inline=? WHERE id=? AND landing_id=?");
$st->bind_param("sdssssiii", $nombre, $peso, $estado, $hero_image, $contenido_html, $css_inline, $js_inline, $vid, $landing_id);
$st->execute(); $msg = 'Variante actualizada.';
} else {
$st = $conn->prepare("INSERT INTO landing_variants (landing_id, nombre, peso, estado, hero_image, contenido_html, css_inline, js_inline) VALUES (?,?,?,?,?,?,?,?)");
$st->bind_param("isdsssss", $landing_id, $nombre, $peso, $estado, $hero_image, $contenido_html, $css_inline, $js_inline);
$st->execute(); $msg = 'Variante creada.';
}
} catch (Throwable $e) { $err = $e->getMessage(); }
}
}
if ($action === 'delete' && $id > 0) {
try {
$st = $conn->prepare("DELETE FROM landing_variants WHERE id=? AND landing_id=?");
$st->bind_param("ii", $id, $landing_id);
$st->execute(); $msg = 'Variante eliminada.';
} catch (Throwable $e) { $err = $e->getMessage(); }
$id = 0;
}
$edit = null;
if ($action === 'edit' && $id > 0) {
$st = $conn->prepare("SELECT * FROM landing_variants WHERE id=? AND landing_id=?");
$st->bind_param("ii", $id, $landing_id);
$st->execute();
$edit = $st->get_result()->fetch_assoc();
}
$list = $conn->query("SELECT * FROM landing_variants WHERE landing_id=".$landing_id." ORDER BY id DESC");
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Landing: Variantes</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>.mono{font-family:ui-monospace,Menlo,Consolas,monospace;white-space:pre-wrap}</style>
</head>
<body>
<nav class="navbar navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Admin</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="landing.php">Landings</a>
<a class="btn btn-outline-light btn-sm" href="landing_blocks.php">Bloques</a>
<a class="btn btn-light btn-sm" href="landing_variants.php?landing_id=<?= (int)$landing_id ?>">Variantes</a>
<a class="btn btn-outline-light btn-sm" href="landing_schedule.php?landing_id=<?= (int)$landing_id ?>">Schedule</a>
<a class="btn btn-outline-light btn-sm" href="../index.php?action=logout">Salir</a>
</div>
</div>
</nav>
<div class="container py-4">
<div class="mb-3">
<a class="btn btn-secondary btn-sm" href="landing.php">← Volver a Landings</a>
</div>
<h4 class="mb-3">Landing: <strong><?= htmlspecialchars($landing['titulo']) ?></strong></h4>
<?php if($msg):?><div class="alert alert-success py-2"><?= htmlspecialchars($msg) ?></div><?php endif;?>
<?php if($err):?><div class="alert alert-danger py-2"><?= htmlspecialchars($err) ?></div><?php endif;?>
<div class="card shadow-sm mb-4"><div class="card-body">
<h5 class="mb-3"><?= $edit ? 'Editar variante' : 'Nueva variante' ?></h5>
<form method="post" class="row g-3">
<input type="hidden" name="id" value="<?= (int)($edit['id'] ?? 0) ?>">
<div class="col-md-5">
<label class="form-label">Nombre</label>
<input name="nombre" class="form-control" required value="<?= htmlspecialchars($edit['nombre'] ?? '') ?>">
</div>
<div class="col-md-2">
<label class="form-label">Peso (tráfico)</label>
<input name="peso" type="number" step="0.001" min="0" class="form-control" value="<?= htmlspecialchars((string)($edit['peso'] ?? '1.000')) ?>">
</div>
<div class="col-md-3">
<label class="form-label">Estado</label>
<?php $est = $edit['estado'] ?? 'activo'; ?>
<select name="estado" class="form-select">
<option value="borrador" <?= $est==='borrador'?'selected':'' ?>>Borrador</option>
<option value="activo" <?= $est==='activo'?'selected':'' ?>>Activo</option>
<option value="pausado" <?= $est==='pausado'?'selected':'' ?>>Pausado</option>
</select>
</div>
<div class="col-12">
<label class="form-label">Hero image (URL)</label>
<input name="hero_image" class="form-control" placeholder="/ruleta/uploads/landings/hero-b.jpg" value="<?= htmlspecialchars($edit['hero_image'] ?? '') ?>">
</div>
<div class="col-12">
<label class="form-label">Contenido HTML (override)</label>
<textarea name="contenido_html" rows="6" class="form-control mono"><?= htmlspecialchars($edit['contenido_html'] ?? '') ?></textarea>
</div>
<div class="col-12">
<label class="form-label">CSS inline (override)</label>
<textarea name="css_inline" rows="4" class="form-control mono"><?= htmlspecialchars($edit['css_inline'] ?? '') ?></textarea>
</div>
<div class="col-12">
<label class="form-label">JS inline (override)</label>
<textarea name="js_inline" rows="4" class="form-control mono"><?= htmlspecialchars($edit['js_inline'] ?? '') ?></textarea>
</div>
<div class="col-12">
<button class="btn btn-primary"><?= $edit?'Actualizar':'Crear' ?></button>
<?php if($edit):?><a href="landing_variants.php?landing_id=<?= (int)$landing_id ?>" class="btn btn-secondary">Cancelar</a><?php endif;?>
</div>
</form>
</div></div>
<div class="card shadow-sm"><div class="card-body table-responsive">
<table class="table table-sm table-striped align-middle">
<thead><tr><th>ID</th><th>Nombre</th><th>Peso</th><th>Estado</th><th>Actualizado</th><th>Acciones</th></tr></thead>
<tbody>
<?php while($r = $list->fetch_assoc()): ?>
<tr>
<td><?= (int)$r['id'] ?></td>
<td><?= htmlspecialchars($r['nombre']) ?></td>
<td><?= htmlspecialchars($r['peso']) ?></td>
<td><span class="badge text-bg-secondary"><?= htmlspecialchars($r['estado']) ?></span></td>
<td><?= htmlspecialchars($r['updated_at']) ?></td>
<td class="text-nowrap">
<a class="btn btn-sm btn-outline-primary" href="landing_variants.php?landing_id=<?= (int)$landing_id ?>&action=edit&id=<?= (int)$r['id'] ?>">Editar</a>
<a class="btn btn-sm btn-outline-danger" href="landing_variants.php?landing_id=<?= (int)$landing_id ?>&action=delete&id=<?= (int)$r['id'] ?>" onclick="return confirm('¿Eliminar variante?')">Eliminar</a>
</td>
</tr>
<?php endwhile; ?>
</tbody>
</table>
</div></div>
</div>
</body>
</html>
+163
View File
@@ -0,0 +1,163 @@
<?php
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
function all_ruletas(mysqli $conn) {
$res = $conn->query("SELECT id, nombre FROM ruletas ORDER BY id DESC");
return $res->fetch_all(MYSQLI_ASSOC);
}
function find_premio(mysqli $conn, $id) {
$stmt = $conn->prepare("SELECT id, ruleta_id, nombre, cantidad, probabilidad, inicio_angulo, fin_angulo FROM premios WHERE id=?");
$stmt->bind_param("i", $id); $stmt->execute(); return $stmt->get_result()->fetch_assoc();
}
$ruletas = all_ruletas($conn);
$ruleta_id = (int)($_GET['ruleta_id'] ?? ($ruletas[0]['id'] ?? 0));
$msg = $err = "";
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$id = (int)($_POST['id'] ?? 0);
$ruleta_id = (int)($_POST['ruleta_id'] ?? 0);
$nombre = trim($_POST['nombre'] ?? '');
$cantidad = (int)($_POST['cantidad'] ?? 0);
$probabilidad = (float)($_POST['probabilidad'] ?? 0);
$inicio = (float)($_POST['inicio_angulo'] ?? 0);
$fin = (float)($_POST['fin_angulo'] ?? 0);
if (!$ruleta_id || $nombre === '') {
$err = "Completa ruleta y nombre.";
} else {
if ($id > 0) {
$stmt = $conn->prepare("UPDATE premios SET ruleta_id=?, nombre=?, cantidad=?, probabilidad=?, inicio_angulo=?, fin_angulo=? WHERE id=?");
$stmt->bind_param("isidddi", $ruleta_id, $nombre, $cantidad, $probabilidad, $inicio, $fin, $id);
$stmt->execute(); $msg = "Premio actualizado.";
} else {
$stmt = $conn->prepare("INSERT INTO premios (ruleta_id, nombre, cantidad, probabilidad, inicio_angulo, fin_angulo) VALUES (?,?,?,?,?,?)");
$stmt->bind_param("isiddd", $ruleta_id, $nombre, $cantidad, $probabilidad, $inicio, $fin);
$stmt->execute(); $msg = "Premio creado.";
}
}
}
if (($_GET['action'] ?? '') === 'delete') {
$id = (int)($_GET['id'] ?? 0);
if ($id) {
$stmt = $conn->prepare("DELETE FROM premios WHERE id=?");
$stmt->bind_param("i", $id); $stmt->execute(); $msg = "Premio eliminado.";
}
}
$edit = null;
if (($_GET['action'] ?? '') === 'edit') {
$edit = find_premio($conn, (int)($_GET['id'] ?? 0));
if ($edit) $ruleta_id = (int)$edit['ruleta_id'];
}
$stmt = $conn->prepare("SELECT p.id, p.nombre, p.cantidad, p.probabilidad, p.inicio_angulo, p.fin_angulo, r.nombre AS ruleta
FROM premios p INNER JOIN ruletas r ON r.id=p.ruleta_id
WHERE p.ruleta_id=? ORDER BY p.id DESC");
$stmt->bind_param("i", $ruleta_id); $stmt->execute();
$items = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Premios | Admin Ruletas</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg bg-dark navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Home</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="ruletas.php">Ruletas</a>
<a class="btn btn-outline-light btn-sm" href="jugadas.php">Jugadas</a>
</div>
</div>
</nav>
<div class="container py-4">
<div class="d-flex align-items-center justify-content-between">
<h3 class="mb-3">Premios</h3>
<form class="d-flex" method="get">
<select name="ruleta_id" class="form-select me-2" onchange="this.form.submit()">
<?php foreach ($ruletas as $r): ?>
<option value="<?= (int)$r['id'] ?>" <?= $ruleta_id===$r['id']?'selected':'' ?>>
<?= htmlspecialchars($r['nombre']) ?> (ID <?= (int)$r['id'] ?>)
</option>
<?php endforeach; ?>
</select>
<noscript><button class="btn btn-primary">Cambiar</button></noscript>
</form>
</div>
<?php if ($msg): ?><div class="alert alert-success py-2"><?= htmlspecialchars($msg) ?></div><?php endif; ?>
<?php if ($err): ?><div class="alert alert-danger py-2"><?= htmlspecialchars($err) ?></div><?php endif; ?>
<div class="card shadow-sm mb-4"><div class="card-body">
<h5 class="mb-3"><?= $edit ? 'Editar premio' : 'Nuevo premio' ?></h5>
<form method="post" class="row g-3">
<input type="hidden" name="id" value="<?= (int)($edit['id'] ?? 0) ?>">
<div class="col-md-3">
<label class="form-label">Ruleta</label>
<select name="ruleta_id" class="form-select" required>
<?php foreach ($ruletas as $r): ?>
<option value="<?= (int)$r['id'] ?>" <?= ($ruleta_id===$r['id'])?'selected':'' ?>><?= htmlspecialchars($r['nombre']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Nombre</label>
<input name="nombre" class="form-control" required value="<?= htmlspecialchars($edit['nombre'] ?? '') ?>">
</div>
<div class="col-md-2">
<label class="form-label">Cantidad</label>
<input type="number" name="cantidad" class="form-control" min="0" value="<?= (int)($edit['cantidad'] ?? 0) ?>">
</div>
<div class="col-md-2">
<label class="form-label">Probabilidad (%)</label>
<input type="number" step="0.01" name="probabilidad" class="form-control" min="0" value="<?= htmlspecialchars($edit['probabilidad'] ?? '0') ?>">
</div>
<div class="col-md-1">
<label class="form-label">Inicio°</label>
<input type="number" step="0.01" name="inicio_angulo" class="form-control" value="<?= htmlspecialchars($edit['inicio_angulo'] ?? '0') ?>">
</div>
<div class="col-md-1">
<label class="form-label">Fin°</label>
<input type="number" step="0.01" name="fin_angulo" class="form-control" value="<?= htmlspecialchars($edit['fin_angulo'] ?? '0') ?>">
</div>
<div class="col-12">
<button class="btn btn-primary"><?= $edit ? 'Actualizar' : 'Crear' ?></button>
<?php if ($edit): ?><a href="premios.php?ruleta_id=<?= (int)$ruleta_id ?>" class="btn btn-secondary">Cancelar</a><?php endif; ?>
</div>
</form>
</div></div>
<div class="card shadow-sm"><div class="card-body table-responsive">
<table class="table table-striped table-sm align-middle">
<thead><tr><th>ID</th><th>Premio</th><th>Cantidad</th><th>Prob %</th><th>Inicio°</th><th>Fin°</th><th>Acciones</th></tr></thead>
<tbody>
<?php foreach ($items as $it): ?>
<tr>
<td><?= (int)$it['id'] ?></td>
<td><?= htmlspecialchars($it['nombre']) ?></td>
<td><?= (int)$it['cantidad'] ?></td>
<td><?= htmlspecialchars($it['probabilidad']) ?></td>
<td><?= htmlspecialchars($it['inicio_angulo']) ?></td>
<td><?= htmlspecialchars($it['fin_angulo']) ?></td>
<td class="text-nowrap">
<a class="btn btn-sm btn-outline-primary" href="premios.php?action=edit&id=<?= (int)$it['id'] ?>&ruleta_id=<?= (int)$ruleta_id ?>">Editar</a>
<a class="btn btn-sm btn-outline-danger" href="premios.php?action=delete&id=<?= (int)$it['id'] ?>&ruleta_id=<?= (int)$ruleta_id ?>" onclick="return confirm('¿Eliminar premio?')">Eliminar</a>
</td>
</tr>
<?php endforeach; ?>
<?php if (!$items): ?><tr><td colspan="7" class="text-center text-muted">Sin premios</td></tr><?php endif; ?>
</tbody>
</table>
</div></div>
</div>
</body>
</html>
+139
View File
@@ -0,0 +1,139 @@
<?php
session_start();
require_once __DIR__.'/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
$UPLOAD_DIR = __DIR__ . '/../uploads/rasca';
$PUBLIC_DIR = '/ruleta/uploads/rasca';
if (!is_dir($UPLOAD_DIR)) { @mkdir($UPLOAD_DIR, 0775, true); }
function sanitize_filename($name){ $name=preg_replace('/[^A-Za-z0-9._-]/','_',$name); return substr($name,0,180); }
function upload_image($field,$UPLOAD_DIR,$PUBLIC_DIR){
if (!isset($_FILES[$field]) || $_FILES[$field]['error']===UPLOAD_ERR_NO_FILE) return null;
$f=$_FILES[$field]; if($f['error']!==UPLOAD_ERR_OK) throw new RuntimeException("Error subiendo $field.");
$mime=(new finfo(FILEINFO_MIME_TYPE))->file($f['tmp_name']);
$allowed=['image/png'=>'png','image/jpeg'=>'jpg','image/webp'=>'webp'];
if(!isset($allowed[$mime])) throw new RuntimeException("Formato no permitido.");
if($f['size']>5*1024*1024) throw new RuntimeException("Máximo 5MB.");
$ext=$allowed[$mime]; $base=sanitize_filename(pathinfo($f['name'],PATHINFO_FILENAME));
$name=$base.'-'.uniqid().'.'.$ext; $dest=rtrim($UPLOAD_DIR,'/\\').DIRECTORY_SEPARATOR.$name;
if(!move_uploaded_file($f['tmp_name'],$dest)) throw new RuntimeException("No se pudo guardar $field.");
return rtrim($PUBLIC_DIR,'/') . '/' . $name;
}
function upload_or_keep($name,$old,$UPLOAD_DIR,$PUBLIC_DIR){
try{ $v=upload_image($name,$UPLOAD_DIR,$PUBLIC_DIR); return $v?:($old?:null); }
catch(Throwable $e){ return $old?:null; }
}
$msg=$err=''; $action=$_GET['action']??''; $rasca_id=(int)($_GET['rasca_id']??0);
if($_SERVER['REQUEST_METHOD']==='POST'){
$rid=(int)($_POST['rasca_id']??0);
$nombre=trim($_POST['nombre']??'');
$olds=[
'banner_1'=>$_POST['old_banner_1']??'','banner_2'=>$_POST['old_banner_2']??'',
'banner_3'=>$_POST['old_banner_3']??'','banner_4'=>$_POST['old_banner_4']??'',
'background_image'=>$_POST['old_background_image']??'','fullscreen_icon'=>$_POST['old_fullscreen_icon']??'',
'fg_image'=>$_POST['old_fg_image']??'','bg_image'=>$_POST['old_bg_image']??'',
];
if($rid<=0 || $nombre===''){ $err='Indica rasca_id y nombre.'; }
else{
try{
$new=[
'banner_1'=>upload_or_keep('banner_1',$olds['banner_1'],$UPLOAD_DIR,$PUBLIC_DIR),
'banner_2'=>upload_or_keep('banner_2',$olds['banner_2'],$UPLOAD_DIR,$PUBLIC_DIR),
'banner_3'=>upload_or_keep('banner_3',$olds['banner_3'],$UPLOAD_DIR,$PUBLIC_DIR),
'banner_4'=>upload_or_keep('banner_4',$olds['banner_4'],$UPLOAD_DIR,$PUBLIC_DIR),
'background_image'=>upload_or_keep('background_image',$olds['background_image'],$UPLOAD_DIR,$PUBLIC_DIR),
'fullscreen_icon'=>upload_or_keep('fullscreen_icon',$olds['fullscreen_icon'],$UPLOAD_DIR,$PUBLIC_DIR),
'fg_image'=>upload_or_keep('fg_image',$olds['fg_image'],$UPLOAD_DIR,$PUBLIC_DIR),
'bg_image'=>upload_or_keep('bg_image',$olds['bg_image'],$UPLOAD_DIR,$PUBLIC_DIR),
];
$sql="INSERT INTO rasca_config (rasca_id,nombre,banner_1,banner_2,banner_3,banner_4,background_image,fullscreen_icon,fg_image,bg_image)
VALUES (?,?,?,?,?,?,?,?,?,?)
ON DUPLICATE KEY UPDATE nombre=VALUES(nombre),
banner_1=VALUES(banner_1), banner_2=VALUES(banner_2), banner_3=VALUES(banner_3), banner_4=VALUES(banner_4),
background_image=VALUES(background_image), fullscreen_icon=VALUES(fullscreen_icon),
fg_image=VALUES(fg_image), bg_image=VALUES(bg_image)";
$st=$conn->prepare($sql);
$st->bind_param("isssssssss",$rid,$nombre,$new['banner_1'],$new['banner_2'],$new['banner_3'],$new['banner_4'],$new['background_image'],$new['fullscreen_icon'],$new['fg_image'],$new['bg_image']);
$st->execute(); $msg='Configuración guardada.'; $rasca_id=$rid;
}catch(Throwable $e){ $err=$e->getMessage(); }
}
}
if($action==='delete' && $rasca_id>0){ $st=$conn->prepare("DELETE FROM rasca_config WHERE rasca_id=?"); $st->bind_param("i",$rasca_id); $st->execute(); $msg='Configuración eliminada.'; $rasca_id=0; }
$edit=null; if($action==='edit' && $rasca_id>0){ $st=$conn->prepare("SELECT * FROM rasca_config WHERE rasca_id=?"); $st->bind_param("i",$rasca_id); $st->execute(); $edit=$st->get_result()->fetch_assoc(); }
$list=$conn->query("SELECT * FROM rasca_config ORDER BY rasca_id ASC");
?>
<!doctype html>
<html lang="es"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Rasca | Configuración</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>.thumb{width:70px;height:70px;object-fit:cover;border-radius:.5rem}</style>
</head>
<body>
<nav class="navbar navbar-dark bg-dark"><div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Admin</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="rasca_config.php">Config Rasca</a>
<a class="btn btn-outline-light btn-sm" href="rasca_premios.php">Premios Rasca</a>
<a class="btn btn-outline-light btn-sm" href="rasca_jugadas.php">Jugadas Rasca</a>
<a class="btn btn-outline-light btn-sm" href="../index.php?action=logout">Salir</a>
</div>
</div></nav>
<div class="container py-4">
<?php if($msg):?><div class="alert alert-success py-2"><?=htmlspecialchars($msg)?></div><?php endif;?>
<?php if($err):?><div class="alert alert-danger py-2"><?=htmlspecialchars($err)?></div><?php endif;?>
<div class="card shadow-sm mb-4"><div class="card-body">
<h5 class="mb-3"><?= $edit?'Editar':'Nuevo/Actualizar (por rasca_id)' ?></h5>
<form method="post" enctype="multipart/form-data" class="row g-3">
<?php foreach(['banner_1','banner_2','banner_3','banner_4','background_image','fullscreen_icon','fg_image','bg_image'] as $h): ?>
<input type="hidden" name="old_<?= $h ?>" value="<?= htmlspecialchars($edit[$h]??'') ?>">
<?php endforeach; ?>
<div class="col-md-2"><label class="form-label">Rasca ID</label><input type="number" name="rasca_id" class="form-control" required value="<?= htmlspecialchars($edit['rasca_id']??'') ?>"></div>
<div class="col-md-5"><label class="form-label">Nombre</label><input name="nombre" class="form-control" required value="<?= htmlspecialchars($edit['nombre']??'') ?>"></div>
<div class="col-md-6"><label class="form-label">Banner 1</label><input type="file" name="banner_1" class="form-control" accept=".png,.jpg,.jpeg,.webp"><?php if(!empty($edit['banner_1'])):?><div class="mt-2"><img src="<?= htmlspecialchars($edit['banner_1']) ?>" class="thumb"></div><?php endif;?></div>
<div class="col-md-6"><label class="form-label">Banner 2</label><input type="file" name="banner_2" class="form-control" accept=".png,.jpg,.jpeg,.webp"><?php if(!empty($edit['banner_2'])):?><div class="mt-2"><img src="<?= htmlspecialchars($edit['banner_2']) ?>" class="thumb"></div><?php endif;?></div>
<div class="col-md-6"><label class="form-label">Banner 3</label><input type="file" name="banner_3" class="form-control" accept=".png,.jpg,.jpeg,.webp"><?php if(!empty($edit['banner_3'])):?><div class="mt-2"><img src="<?= htmlspecialchars($edit['banner_3']) ?>" class="thumb"></div><?php endif;?></div>
<div class="col-md-6"><label class="form-label">Banner 4</label><input type="file" name="banner_4" class="form-control" accept=".png,.jpg,.jpeg,.webp"><?php if(!empty($edit['banner_4'])):?><div class="mt-2"><img src="<?= htmlspecialchars($edit['banner_4']) ?>" class="thumb"></div><?php endif;?></div>
<div class="col-md-6"><label class="form-label">Background</label><input type="file" name="background_image" class="form-control" accept=".png,.jpg,.jpeg,.webp"><?php if(!empty($edit['background_image'])):?><div class="mt-2"><img src="<?= htmlspecialchars($edit['background_image']) ?>" class="thumb"></div><?php endif;?></div>
<div class="col-md-6"><label class="form-label">Ícono Fullscreen</label><input type="file" name="fullscreen_icon" class="form-control" accept=".png,.jpg,.jpeg,.webp"><?php if(!empty($edit['fullscreen_icon'])):?><div class="mt-2"><img src="<?= htmlspecialchars($edit['fullscreen_icon']) ?>" class="thumb"></div><?php endif;?></div>
<div class="col-md-6"><label class="form-label">Imagen FG (raspable)</label><input type="file" name="fg_image" class="form-control" accept=".png,.jpg,.jpeg,.webp"><?php if(!empty($edit['fg_image'])):?><div class="mt-2"><img src="<?= htmlspecialchars($edit['fg_image']) ?>" class="thumb"></div><?php endif;?></div>
<div class="col-md-6"><label class="form-label">Imagen BG (base/premio)</label><input type="file" name="bg_image" class="form-control" accept=".png,.jpg,.jpeg,.webp"><?php if(!empty($edit['bg_image'])):?><div class="mt-2"><img src="<?= htmlspecialchars($edit['bg_image']) ?>" class="thumb"></div><?php endif;?></div>
<div class="col-12"><button class="btn btn-primary">Guardar</button><?php if($edit):?><a href="rasca_config.php" class="btn btn-secondary">Cancelar</a><?php endif;?></div>
</form>
</div></div>
<div class="card shadow-sm"><div class="card-body table-responsive">
<table class="table table-striped table-sm align-middle">
<thead><tr><th>Rasca ID</th><th>Nombre</th><th>B1</th><th>B2</th><th>B3</th><th>B4</th><th>BG</th><th>FS</th><th>FG</th><th>Base</th><th>Acciones</th></tr></thead>
<tbody>
<?php while($r=$list->fetch_assoc()): ?>
<tr>
<td><?= (int)$r['rasca_id'] ?></td>
<td><?= htmlspecialchars($r['nombre']) ?></td>
<td><?= $r['banner_1']?'<img class="thumb" src="'.htmlspecialchars($r['banner_1']).'">':'—' ?></td>
<td><?= $r['banner_2']?'<img class="thumb" src="'.htmlspecialchars($r['banner_2']).'">':'—' ?></td>
<td><?= $r['banner_3']?'<img class="thumb" src="'.htmlspecialchars($r['banner_3']).'">':'—' ?></td>
<td><?= $r['banner_4']?'<img class="thumb" src="'.htmlspecialchars($r['banner_4']).'">':'—' ?></td>
<td><?= $r['background_image']?'<img class="thumb" src="'.htmlspecialchars($r['background_image']).'">':'—' ?></td>
<td><?= $r['fullscreen_icon']?'<img class="thumb" src="'.htmlspecialchars($r['fullscreen_icon']).'">':'—' ?></td>
<td><?= $r['fg_image']?'<img class="thumb" src="'.htmlspecialchars($r['fg_image']).'">':'—' ?></td>
<td><?= $r['bg_image']?'<img class="thumb" src="'.htmlspecialchars($r['bg_image']).'">':'—' ?></td>
<td class="text-nowrap">
<a class="btn btn-sm btn-outline-primary" href="rasca_config.php?action=edit&rasca_id=<?= (int)$r['rasca_id'] ?>">Editar</a>
<a class="btn btn-sm btn-outline-danger" href="rasca_config.php?action=delete&rasca_id=<?= (int)$r['rasca_id'] ?>" onclick="return confirm('¿Eliminar config?')">Eliminar</a>
<a class="btn-outline-warning" href="/../ruleta/rasca_index.html?rasca_id=<?= (int)$r['rasca_id'] ?>" target="_blank" rel="noopener noreferrer">Ver</a>
</td>
</tr>
<?php endwhile; ?>
</tbody>
</table>
</div></div>
</div>
</body></html>
+87
View File
@@ -0,0 +1,87 @@
<?php
session_start();
require_once __DIR__.'/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
function h($v){ return htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8'); }
$rows = [];
$error = null;
try {
$sql = "
SELECT j.id, j.fecha_juego, j.rasca_id, j.premio_id, j.porcentaje,
j.ip_address, j.user_agent,
p.nombre AS premio_nombre
FROM jugadas_rasca j
LEFT JOIN rasca_premios p
ON p.id = j.premio_id AND p.rasca_id = j.rasca_id
ORDER BY j.id DESC
LIMIT 200
";
$q = $conn->query($sql);
if ($q) {
$rows = $q->fetch_all(MYSQLI_ASSOC);
} else {
$error = 'No se pudo ejecutar la consulta de jugadas.';
}
} catch (Throwable $e) {
$error = $e->getMessage();
}
?>
<!doctype html>
<html lang="es"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Rasca | Jugadas</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-dark bg-dark"><div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Admin</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="rasca_config.php">Config Rasca</a>
<a class="btn btn-outline-light btn-sm" href="rasca_premios.php">Premios Rasca</a>
<a class="btn btn-outline-light btn-sm" href="../index.php?action=logout">Salir</a>
</div>
</div></nav>
<div class="container py-4">
<h4 class="mb-3">Últimas jugadas (Rasca)</h4>
<?php if ($error): ?>
<div class="alert alert-danger"><?= h($error) ?></div>
<?php endif; ?>
<div class="card shadow-sm"><div class="card-body table-responsive">
<table class="table table-striped table-sm align-middle">
<thead>
<tr>
<th>ID</th>
<th>Fecha</th>
<th>Rasca</th>
<th>Premio</th>
<th>%</th>
<th>IP</th>
<th>User Agent</th>
</tr>
</thead>
<tbody>
<?php foreach($rows as $r): ?>
<tr>
<td><?= (int)$r['id'] ?></td>
<td><?= h($r['fecha_juego']) ?></td>
<td><?= (int)$r['rasca_id'] ?></td>
<td><?= h($r['premio_nombre'] ?: '#'.(int)$r['premio_id']) ?></td>
<td><?= h($r['porcentaje']) ?></td>
<td><?= h($r['ip_address'] ?? '—') ?></td>
<td class="text-truncate" style="max-width:360px"><?= h($r['user_agent'] ?? '') ?></td>
</tr>
<?php endforeach; ?>
<?php if(!$rows && !$error): ?>
<tr><td colspan="7" class="text-center text-muted">Sin jugadas</td></tr>
<?php endif; ?>
</tbody>
</table>
</div></div>
</div>
</body></html>
+285
View File
@@ -0,0 +1,285 @@
<?php
/**
* /ruleta/admin/rasca_premios.php
* - Vista 1 (sin rasca_id): lista de rasca configurados para elegir
* - Vista 2 (con rasca_id): CRUD de premios de ese rasca
*/
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
//
// Config de uploads
//
$UPLOAD_DIR = __DIR__ . '/../uploads/rasca_premios';
$PUBLIC_DIR = '/ruleta/uploads/rasca_premios'; // ajusta si tu raíz no es /ruleta
if (!is_dir($UPLOAD_DIR)) { @mkdir($UPLOAD_DIR, 0775, true); }
//
// Helpers
//
function sanitize_filename($n){ $n = preg_replace('/[^A-Za-z0-9._-]/','_', $n); return substr($n,0,180); }
function upload_image($field,$UPLOAD_DIR,$PUBLIC_DIR){
if (!isset($_FILES[$field]) || $_FILES[$field]['error']===UPLOAD_ERR_NO_FILE) return null;
$f = $_FILES[$field];
if ($f['error']!==UPLOAD_ERR_OK) throw new RuntimeException("Error subiendo $field (código {$f['error']}).");
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($f['tmp_name']);
$allowed = ['image/png'=>'png','image/jpeg'=>'jpg','image/webp'=>'webp'];
if (!isset($allowed[$mime])) throw new RuntimeException("Formato no permitido. Usa PNG/JPG/WEBP.");
if ($f['size'] > 5*1024*1024) throw new RuntimeException("Archivo muy grande (máx 5MB).");
$ext = $allowed[$mime]; $base = sanitize_filename(pathinfo($f['name'], PATHINFO_FILENAME));
$name = $base.'-'.uniqid().'.'.$ext; $dest = rtrim($UPLOAD_DIR,'/\\').DIRECTORY_SEPARATOR.$name;
if (!move_uploaded_file($f['tmp_name'],$dest)) throw new RuntimeException("No se pudo guardar el archivo.");
return rtrim($PUBLIC_DIR,'/').'/'.$name;
}
//
// Routing params
//
$action = $_GET['action'] ?? '';
$id = (int)($_GET['id'] ?? 0);
$rasca_id = isset($_GET['rasca_id']) ? (int)$_GET['rasca_id'] : null;
$msg = $err = '';
//
// Form handling (solo si hay rasca_id seleccionado)
//
if ($rasca_id && $_SERVER['REQUEST_METHOD']==='POST') {
$pid = (int)($_POST['id'] ?? 0);
$rasca_id = (int)($_POST['rasca_id'] ?? 0);
$nombre = trim($_POST['nombre'] ?? '');
$prob = (float)($_POST['probabilidad'] ?? 0);
$stock = ($_POST['stock'] !== '' ? (int)$_POST['stock'] : null);
$activo = isset($_POST['activo']) ? 1 : 0;
$old_img = $_POST['old_imagen_url'] ?? '';
if ($rasca_id<=0 || $nombre==='') {
$err = 'Indica rasca_id y nombre.';
} else {
try {
$img = upload_image('imagen',$UPLOAD_DIR,$PUBLIC_DIR) ?? ($old_img ?: null);
if ($pid > 0) {
$st = $conn->prepare("UPDATE rasca_premios SET rasca_id=?, nombre=?, probabilidad=?, stock=?, activo=?, imagen_url=? WHERE id=?");
// stock puede ser NULL: usa bind con "d" para probabilidad y "i" para stock; si es NULL, usa null y types "isdiisi" sigue válido porque mysqli convierte nulls
$st->bind_param("isdiisi", $rasca_id, $nombre, $prob, $stock, $activo, $img, $pid);
$st->execute();
$msg = 'Premio actualizado.';
} else {
$st = $conn->prepare("INSERT INTO rasca_premios (rasca_id, nombre, probabilidad, stock, activo, imagen_url) VALUES (?,?,?,?,?,?)");
$st->bind_param("isdiis", $rasca_id, $nombre, $prob, $stock, $activo, $img);
$st->execute();
$msg = 'Premio creado.';
}
} catch (Throwable $e) {
$err = $e->getMessage();
}
}
}
//
// Delete premio
//
if ($rasca_id && $action==='delete' && $id>0) {
$st = $conn->prepare("DELETE FROM rasca_premios WHERE id=? AND rasca_id=?");
$st->bind_param("ii", $id, $rasca_id);
$st->execute();
$msg = 'Premio eliminado.';
}
//
// Datos para vistas
//
$rasca_list = []; // para Vista 1
$rasca_info = null; // info del rasca seleccionado
$edit = null; // en Vista 2, premio a editar
$premios = []; // lista de premios del rasca
if (!$rasca_id) {
// Vista 1: listar rasca_config + conteo de premios
$sql = "SELECT r.rasca_id, r.nombre,
COALESCE(p.cnt,0) AS total_premios
FROM rasca_config r
LEFT JOIN (
SELECT rasca_id, COUNT(*) cnt
FROM rasca_premios
GROUP BY rasca_id
) p ON p.rasca_id = r.rasca_id
ORDER BY r.rasca_id ASC";
$rs = $conn->query($sql);
if ($rs) $rasca_list = $rs->fetch_all(MYSQLI_ASSOC);
} else {
// Vista 2: CRUD de premios para rasca_id
$st = $conn->prepare("SELECT rasca_id, nombre FROM rasca_config WHERE rasca_id=?");
$st->bind_param("i", $rasca_id);
$st->execute();
$rasca_info = $st->get_result()->fetch_assoc();
if ($action==='edit' && $id>0) {
$st = $conn->prepare("SELECT * FROM rasca_premios WHERE id=? AND rasca_id=?");
$st->bind_param("ii", $id, $rasca_id);
$st->execute();
$edit = $st->get_result()->fetch_assoc();
}
$st = $conn->prepare("SELECT * FROM rasca_premios WHERE rasca_id=? ORDER BY id DESC");
$st->bind_param("i", $rasca_id);
$st->execute();
$premios = $st->get_result()->fetch_all(MYSQLI_ASSOC);
}
?>
<!doctype html>
<html lang="es"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Rasca | Premios</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
.thumb{width:70px;height:70px;object-fit:cover;border-radius:.5rem}
.card-hover:hover{box-shadow:0 0.5rem 1rem rgba(0,0,0,.15);}
</style>
</head>
<body>
<nav class="navbar navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Admin</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="rasca_config.php">Config Rasca</a>
<a class="btn btn-outline-light btn-sm" href="rasca_premios.php">Premios Rasca</a>
<a class="btn btn-outline-light btn-sm" href="rasca_jugadas.php">Jugadas Rasca</a>
<a class="btn btn-outline-light btn-sm" href="../index.php?action=logout">Salir</a>
</div>
</div>
</nav>
<div class="container py-4">
<?php if($msg):?><div class="alert alert-success py-2"><?= htmlspecialchars($msg) ?></div><?php endif; ?>
<?php if($err):?><div class="alert alert-danger py-2"><?= htmlspecialchars($err) ?></div><?php endif; ?>
<?php if(!$rasca_id): ?>
<!-- VISTA 1: Selector de juegos Rasca -->
<div class="d-flex align-items-center justify-content-between mb-3">
<h4 class="mb-0">Selecciona un juego de Rasca</h4>
<a class="btn btn-primary" href="rasca_config.php">Crear/editar Rasca</a>
</div>
<?php if(!$rasca_list): ?>
<div class="alert alert-info">No hay juegos de rasca configurados todavía.</div>
<?php else: ?>
<div class="row g-3">
<?php foreach ($rasca_list as $rc): ?>
<div class="col-md-4">
<a href="rasca_premios.php?rasca_id=<?= (int)$rc['rasca_id'] ?>" class="text-decoration-none text-dark">
<div class="card card-hover h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">Rasca #<?= (int)$rc['rasca_id'] ?></h5>
<span class="badge text-bg-secondary"><?= (int)$rc['total_premios'] ?> premios</span>
</div>
<p class="mt-2 mb-0 text-muted"><?= htmlspecialchars($rc['nombre'] ?? '') ?></p>
</div>
<div class="card-footer bg-transparent border-0 pt-0">
<button class="btn btn-sm btn-outline-primary">Administrar premios</button>
</div>
</div>
</a>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php else: ?>
<!-- VISTA 2: CRUD premios de un Rasca -->
<div class="d-flex align-items-center justify-content-between mb-3">
<div>
<a href="rasca_premios.php" class="btn btn-link">&larr; Volver</a>
<h4 class="d-inline-block ms-2 mb-0">Premios Rasca #<?= (int)$rasca_id ?> <?= $rasca_info ? '— '.htmlspecialchars($rasca_info['nombre']) : '' ?></h4>
</div>
</div>
<div class="card shadow-sm mb-4">
<div class="card-body">
<h5 class="mb-3"><?= $edit ? 'Editar premio' : 'Nuevo premio' ?></h5>
<form method="post" enctype="multipart/form-data" class="row g-3">
<input type="hidden" name="id" value="<?= (int)($edit['id'] ?? 0) ?>">
<input type="hidden" name="old_imagen_url" value="<?= htmlspecialchars($edit['imagen_url'] ?? '') ?>">
<div class="col-md-2">
<label class="form-label">Rasca ID</label>
<input type="number" name="rasca_id" class="form-control" required value="<?= htmlspecialchars($rasca_id) ?>">
</div>
<div class="col-md-4">
<label class="form-label">Nombre</label>
<input name="nombre" class="form-control" required value="<?= htmlspecialchars($edit['nombre'] ?? '') ?>">
</div>
<div class="col-md-3">
<label class="form-label">Probabilidad (peso)</label>
<input type="number" step="0.001" name="probabilidad" class="form-control" required value="<?= htmlspecialchars($edit['probabilidad'] ?? '0') ?>">
</div>
<div class="col-md-3">
<label class="form-label">Stock (vacío = ilimitado)</label>
<input type="number" name="stock" class="form-control" value="<?= htmlspecialchars(isset($edit['stock']) ? $edit['stock'] : '') ?>">
</div>
<div class="col-md-6">
<label class="form-label">Imagen del premio</label>
<input type="file" name="imagen" class="form-control" accept=".png,.jpg,.jpeg,.webp">
<?php if(!empty($edit['imagen_url'])): ?>
<div class="mt-2"><img src="<?= htmlspecialchars($edit['imagen_url']) ?>" class="thumb"></div>
<?php endif; ?>
</div>
<div class="col-md-2 d-flex align-items-end">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="activo" id="activo" <?= (isset($edit['activo']) ? ((int)$edit['activo']===1) : true) ? 'checked' : '' ?>>
<label class="form-check-label" for="activo">Activo</label>
</div>
</div>
<div class="col-12">
<button class="btn btn-primary"><?= $edit ? 'Actualizar' : 'Crear' ?></button>
<?php if($edit): ?><a href="rasca_premios.php?rasca_id=<?= (int)$rasca_id ?>" class="btn btn-secondary">Cancelar</a><?php endif; ?>
</div>
</form>
</div>
</div>
<div class="card shadow-sm">
<div class="card-body table-responsive">
<table class="table table-striped table-sm align-middle">
<thead>
<tr>
<th>ID</th>
<th>Nombre</th>
<th>Prob</th>
<th>Stock</th>
<th>Activo</th>
<th>Imagen</th>
<th class="text-nowrap">Acciones</th>
</tr>
</thead>
<tbody>
<?php foreach($premios as $r): ?>
<tr>
<td><?= (int)$r['id'] ?></td>
<td><?= htmlspecialchars($r['nombre']) ?></td>
<td><?= htmlspecialchars($r['probabilidad']) ?></td>
<td><?= is_null($r['stock']) ? '∞' : (int)$r['stock'] ?></td>
<td><?= ((int)$r['activo']===1 ? 'Sí' : 'No') ?></td>
<td><?= $r['imagen_url'] ? '<img class="thumb" src="'.htmlspecialchars($r['imagen_url']).'">' : '—' ?></td>
<td class="text-nowrap">
<a class="btn btn-sm btn-outline-primary" href="rasca_premios.php?rasca_id=<?= (int)$rasca_id ?>&action=edit&id=<?= (int)$r['id'] ?>">Editar</a>
<a class="btn btn-sm btn-outline-danger" href="rasca_premios.php?rasca_id=<?= (int)$rasca_id ?>&action=delete&id=<?= (int)$r['id'] ?>" onclick="return confirm('¿Eliminar premio?')">Eliminar</a>
</td>
</tr>
<?php endforeach; ?>
<?php if(!$premios): ?>
<tr><td colspan="7" class="text-center text-muted">Sin premios</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<?php endif; ?>
</div>
</body>
</html>
+303
View File
@@ -0,0 +1,303 @@
<?php
// admin/ruletas.php (con banners, background e icono fullscreen)
session_start();
require_once __DIR__ . '/db.php';
if (empty($_SESSION['admin_id'])) { header('Location: ../index.php'); exit; }
// Dónde guardar físicamente y cómo se verán públicamente (URL)
$UPLOAD_DIR = __DIR__ . '/../uploads/ruletas'; // físico
$PUBLIC_DIR = '/ruleta/uploads/ruletas'; // público (ajusta si tu base no es /ruleta)
if (!is_dir($UPLOAD_DIR)) { @mkdir($UPLOAD_DIR, 0775, true); }
// ---------- helpers ----------
function sanitize_filename($name) {
$name = preg_replace('/[^A-Za-z0-9._-]/', '_', $name);
return substr($name, 0, 180);
}
function upload_image($field, $UPLOAD_DIR, $PUBLIC_DIR) {
if (!isset($_FILES[$field]) || $_FILES[$field]['error'] === UPLOAD_ERR_NO_FILE) return null;
$f = $_FILES[$field];
if ($f['error'] !== UPLOAD_ERR_OK) throw new RuntimeException("Error subiendo $field (código {$f['error']}).");
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($f['tmp_name']);
$allowed = ['image/png'=>'png','image/jpeg'=>'jpg','image/webp'=>'webp'];
if (!isset($allowed[$mime])) throw new RuntimeException("Formato no permitido para $field. Usa PNG/JPG/WEBP.");
if ($f['size'] > 5 * 1024 * 1024) throw new RuntimeException("Archivo muy grande para $field (máx 5MB).");
$ext = $allowed[$mime];
$base = sanitize_filename(pathinfo($f['name'], PATHINFO_FILENAME));
$fname = $base . '-' . uniqid() . '.' . $ext;
$dest = rtrim($UPLOAD_DIR, "/\\") . DIRECTORY_SEPARATOR . $fname;
if (!move_uploaded_file($f['tmp_name'], $dest)) throw new RuntimeException("No se pudo guardar el archivo de $field.");
return rtrim($PUBLIC_DIR, '/') . '/' . $fname; // ruta pública para <img src="">
}
function get_ruleta(mysqli $conn, $id) {
$stmt = $conn->prepare("SELECT id, nombre, descripcion,
imagen_ruleta, imagen_puntero,
banner_1, banner_2, banner_3, banner_4,
background_image, fullscreen_icon
FROM ruletas WHERE id=?");
$stmt->bind_param("i", $id); $stmt->execute();
return $stmt->get_result()->fetch_assoc();
}
$msg = $err = '';
$action = $_GET['action'] ?? '';
$id = (int)($_GET['id'] ?? 0);
// ---------- create / update ----------
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$rid = (int)($_POST['id'] ?? 0);
$nombre = trim($_POST['nombre'] ?? '');
$descripcion = trim($_POST['descripcion'] ?? '');
// valores previos (para mantener si no se sube nuevo archivo)
$old = [
'imagen_ruleta' => trim($_POST['old_imagen_ruleta'] ?? ''),
'imagen_puntero' => trim($_POST['old_imagen_puntero'] ?? ''),
'banner_1' => trim($_POST['old_banner_1'] ?? ''),
'banner_2' => trim($_POST['old_banner_2'] ?? ''),
'banner_3' => trim($_POST['old_banner_3'] ?? ''),
'banner_4' => trim($_POST['old_banner_4'] ?? ''),
'background_image'=> trim($_POST['old_background_image'] ?? ''),
'fullscreen_icon' => trim($_POST['old_fullscreen_icon'] ?? ''),
];
if ($nombre === '') {
$err = "El nombre es obligatorio.";
} else {
try {
// nuevas subidas (si vienen)
$new = [
'imagen_ruleta' => upload_image('imagen_ruleta', $UPLOAD_DIR, $PUBLIC_DIR),
'imagen_puntero' => upload_image('imagen_puntero', $UPLOAD_DIR, $PUBLIC_DIR),
'banner_1' => upload_image('banner_1', $UPLOAD_DIR, $PUBLIC_DIR),
'banner_2' => upload_image('banner_2', $UPLOAD_DIR, $PUBLIC_DIR),
'banner_3' => upload_image('banner_3', $UPLOAD_DIR, $PUBLIC_DIR),
'banner_4' => upload_image('banner_4', $UPLOAD_DIR, $PUBLIC_DIR),
'background_image'=> upload_image('background_image',$UPLOAD_DIR, $PUBLIC_DIR),
'fullscreen_icon' => upload_image('fullscreen_icon', $UPLOAD_DIR, $PUBLIC_DIR),
];
// decidir final (nuevo o viejo o null)
$vals = [];
foreach ($new as $k => $v) {
$vals[$k] = $v ?: ($old[$k] ?: null);
}
if ($rid > 0) {
$stmt = $conn->prepare("UPDATE ruletas SET
nombre=?, descripcion=?,
imagen_ruleta=?, imagen_puntero=?,
banner_1=?, banner_2=?, banner_3=?, banner_4=?,
background_image=?, fullscreen_icon=?
WHERE id=?");
$stmt->bind_param(
"ssssssssssi",
$nombre, $descripcion,
$vals['imagen_ruleta'], $vals['imagen_puntero'],
$vals['banner_1'], $vals['banner_2'], $vals['banner_3'], $vals['banner_4'],
$vals['background_image'], $vals['fullscreen_icon'],
$rid
);
$stmt->execute();
$msg = "Ruleta actualizada.";
} else {
$stmt = $conn->prepare("INSERT INTO ruletas
(nombre, descripcion, imagen_ruleta, imagen_puntero,
banner_1, banner_2, banner_3, banner_4,
background_image, fullscreen_icon)
VALUES (?,?,?,?,?,?,?,?,?,?)");
$stmt->bind_param(
"ssssssssss",
$nombre, $descripcion,
$vals['imagen_ruleta'], $vals['imagen_puntero'],
$vals['banner_1'], $vals['banner_2'], $vals['banner_3'], $vals['banner_4'],
$vals['background_image'], $vals['fullscreen_icon']
);
$stmt->execute();
$msg = "Ruleta creada.";
}
} catch (Throwable $e) {
$err = $e->getMessage();
}
}
}
// ---------- delete ----------
if ($action === 'delete' && $id > 0) {
$stmt = $conn->prepare("DELETE FROM ruletas WHERE id=?");
$stmt->bind_param("i", $id); $stmt->execute();
$msg = "Ruleta eliminada.";
}
// ---------- edit load + list ----------
$edit = null;
if ($action === 'edit' && $id > 0) { $edit = get_ruleta($conn, $id); }
$res = $conn->query("SELECT id, nombre, descripcion,
imagen_ruleta, imagen_puntero,
banner_1, banner_2, banner_3, banner_4,
background_image, fullscreen_icon, fecha_creacion
FROM ruletas ORDER BY id DESC");
$items = $res->fetch_all(MYSQLI_ASSOC);
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Ruletas | Admin Ruletas</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>.thumb{width:70px;height:70px;object-fit:cover;border-radius:.5rem;}</style>
</head>
<body>
<nav class="navbar navbar-expand-lg bg-dark navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="dashboard.php">Home</a>
<div class="d-flex gap-2">
<a class="btn btn-outline-light btn-sm" href="premios.php">Premios</a>
<a class="btn btn-outline-light btn-sm" href="jugadas.php">Jugadas</a>
<a class="btn btn-outline-light btn-sm" href="../index.php?action=logout">Salir</a>
</div>
</div>
</nav>
<div class="container py-4">
<h3 class="mb-3">Ruletas</h3>
<?php if ($msg): ?><div class="alert alert-success py-2"><?= htmlspecialchars($msg) ?></div><?php endif; ?>
<?php if ($err): ?><div class="alert alert-danger py-2"><?= htmlspecialchars($err) ?></div><?php endif; ?>
<div class="card shadow-sm mb-4"><div class="card-body">
<h5 class="mb-3"><?= $edit ? 'Editar ruleta' : 'Nueva ruleta' ?></h5>
<form method="post" enctype="multipart/form-data" class="row g-3">
<input type="hidden" name="id" value="<?= (int)($edit['id'] ?? 0) ?>">
<!-- old values para conservar si no se sube -->
<input type="hidden" name="old_imagen_ruleta" value="<?= htmlspecialchars($edit['imagen_ruleta'] ?? '') ?>">
<input type="hidden" name="old_imagen_puntero" value="<?= htmlspecialchars($edit['imagen_puntero'] ?? '') ?>">
<input type="hidden" name="old_banner_1" value="<?= htmlspecialchars($edit['banner_1'] ?? '') ?>">
<input type="hidden" name="old_banner_2" value="<?= htmlspecialchars($edit['banner_2'] ?? '') ?>">
<input type="hidden" name="old_banner_3" value="<?= htmlspecialchars($edit['banner_3'] ?? '') ?>">
<input type="hidden" name="old_banner_4" value="<?= htmlspecialchars($edit['banner_4'] ?? '') ?>">
<input type="hidden" name="old_background_image"value="<?= htmlspecialchars($edit['background_image'] ?? '') ?>">
<input type="hidden" name="old_fullscreen_icon" value="<?= htmlspecialchars($edit['fullscreen_icon'] ?? '') ?>">
<div class="col-md-4">
<label class="form-label">Nombre</label>
<input name="nombre" class="form-control" required value="<?= htmlspecialchars($edit['nombre'] ?? '') ?>">
</div>
<div class="col-md-8">
<label class="form-label">Descripción</label>
<input name="descripcion" class="form-control" value="<?= htmlspecialchars($edit['descripcion'] ?? '') ?>">
</div>
<div class="col-md-6">
<label class="form-label">Imagen de la ruleta</label>
<input type="file" name="imagen_ruleta" class="form-control" accept=".png,.jpg,.jpeg,.webp">
<?php if (!empty($edit['imagen_ruleta'])): ?>
<div class="mt-2"><img src="<?= htmlspecialchars($edit['imagen_ruleta']) ?>" class="thumb" alt="ruleta">
<small class="text-muted ms-2">Se mantiene si no subes otra.</small></div>
<?php endif; ?>
</div>
<div class="col-md-6">
<label class="form-label">Imagen del puntero</label>
<input type="file" name="imagen_puntero" class="form-control" accept=".png,.jpg,.jpeg,.webp">
<?php if (!empty($edit['imagen_puntero'])): ?>
<div class="mt-2"><img src="<?= htmlspecialchars($edit['imagen_puntero']) ?>" class="thumb" alt="puntero"></div>
<?php endif; ?>
</div>
<div class="col-12"><hr></div>
<div class="col-md-6">
<label class="form-label">Banner 1</label>
<input type="file" name="banner_1" class="form-control" accept=".png,.jpg,.jpeg,.webp">
<?php if (!empty($edit['banner_1'])): ?>
<div class="mt-2"><img src="<?= htmlspecialchars($edit['banner_1']) ?>" class="thumb" alt="b1"></div>
<?php endif; ?>
</div>
<div class="col-md-6">
<label class="form-label">Banner 2</label>
<input type="file" name="banner_2" class="form-control" accept=".png,.jpg,.jpeg,.webp">
<?php if (!empty($edit['banner_2'])): ?>
<div class="mt-2"><img src="<?= htmlspecialchars($edit['banner_2']) ?>" class="thumb" alt="b2"></div>
<?php endif; ?>
</div>
<div class="col-md-6">
<label class="form-label">Banner 3</label>
<input type="file" name="banner_3" class="form-control" accept=".png,.jpg,.jpeg,.webp">
<?php if (!empty($edit['banner_3'])): ?>
<div class="mt-2"><img src="<?= htmlspecialchars($edit['banner_3']) ?>" class="thumb" alt="b3"></div>
<?php endif; ?>
</div>
<div class="col-md-6">
<label class="form-label">Banner 4</label>
<input type="file" name="banner_4" class="form-control" accept=".png,.jpg,.jpeg,.webp">
<?php if (!empty($edit['banner_4'])): ?>
<div class="mt-2"><img src="<?= htmlspecialchars($edit['banner_4']) ?>" class="thumb" alt="b4"></div>
<?php endif; ?>
</div>
<div class="col-md-6">
<label class="form-label">Background (imagen)</label>
<input type="file" name="background_image" class="form-control" accept=".png,.jpg,.jpeg,.webp">
<?php if (!empty($edit['background_image'])): ?>
<div class="mt-2"><img src="<?= htmlspecialchars($edit['background_image']) ?>" class="thumb" alt="bg"></div>
<?php endif; ?>
</div>
<div class="col-md-6">
<label class="form-label">Ícono botón Fullscreen</label>
<input type="file" name="fullscreen_icon" class="form-control" accept=".png,.jpg,.jpeg,.webp">
<?php if (!empty($edit['fullscreen_icon'])): ?>
<div class="mt-2"><img src="<?= htmlspecialchars($edit['fullscreen_icon']) ?>" class="thumb" alt="fs"></div>
<?php endif; ?>
</div>
<div class="col-12">
<button class="btn btn-primary"><?= $edit ? 'Actualizar' : 'Crear' ?></button>
<?php if ($edit): ?><a href="ruletas.php" class="btn btn-secondary">Cancelar</a><?php endif; ?>
</div>
</form>
</div></div>
<div class="card shadow-sm"><div class="card-body table-responsive">
<table class="table table-striped table-sm align-middle">
<thead>
<tr>
<th>ID</th><th>Nombre</th>
<th>Ruleta</th><th>Puntero</th><th>BG</th>
<th>B1</th><th>B2</th><th>B3</th><th>B4</th>
<th>FS</th><th>Creado</th><th>Acciones</th>
</tr>
</thead>
<tbody>
<?php foreach ($items as $r): ?>
<tr>
<td><?= (int)$r['id'] ?></td>
<td><?= htmlspecialchars($r['nombre']) ?></td>
<td><?php if ($r['imagen_ruleta']): ?><img class="thumb" src="<?= htmlspecialchars($r['imagen_ruleta']) ?>"><?php else: ?>—<?php endif; ?></td>
<td><?php if ($r['imagen_puntero']): ?><img class="thumb" src="<?= htmlspecialchars($r['imagen_puntero']) ?>"><?php else: ?>—<?php endif; ?></td>
<td><?php if ($r['background_image']): ?><img class="thumb" src="<?= htmlspecialchars($r['background_image']) ?>"><?php else: ?>—<?php endif; ?></td>
<td><?php if ($r['banner_1']): ?><img class="thumb" src="<?= htmlspecialchars($r['banner_1']) ?>"><?php else: ?>—<?php endif; ?></td>
<td><?php if ($r['banner_2']): ?><img class="thumb" src="<?= htmlspecialchars($r['banner_2']) ?>"><?php else: ?>—<?php endif; ?></td>
<td><?php if ($r['banner_3']): ?><img class="thumb" src="<?= htmlspecialchars($r['banner_3']) ?>"><?php else: ?>—<?php endif; ?></td>
<td><?php if ($r['banner_4']): ?><img class="thumb" src="<?= htmlspecialchars($r['banner_4']) ?>"><?php else: ?>—<?php endif; ?></td>
<td><?php if ($r['fullscreen_icon']): ?><img class="thumb" src="<?= htmlspecialchars($r['fullscreen_icon']) ?>"><?php else: ?>—<?php endif; ?></td>
<td><?= htmlspecialchars($r['fecha_creacion'] ?? '') ?></td>
<td class="text-nowrap">
<a class="btn btn-sm btn-outline-primary" href="ruletas.php?action=edit&id=<?= (int)$r['id'] ?>">Editar</a>
<a class="btn btn-sm btn-outline-danger" href="ruletas.php?action=delete&id=<?= (int)$r['id'] ?>" onclick="return confirm('¿Eliminar ruleta?')">Eliminar</a>
<a class="btn btn-sm btn-outline-success" href="premios.php?ruleta_id=<?= (int)$r['id'] ?>">Premios</a>
<a class="btn-outline-warning" href="/../ruleta/index.html?ruleta_id=<?= (int)$r['id'] ?>" target="_blank" rel="noopener noreferrer">Ver</a>
</td>
</tr>
<?php endforeach; ?>
<?php if (!$items): ?>
<tr><td colspan="12" class="text-center text-muted">Sin ruletas</td></tr>
<?php endif; ?>
</tbody>
</table>
</div></div>
</div>
</body>
</html>
+24
View File
@@ -0,0 +1,24 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../admin/db.php';
$rasca_id = (int)($_GET['rasca_id'] ?? 0);
if ($rasca_id <= 0) { echo json_encode(['error'=>'rasca_id inválido']); exit; }
$stmt = $conn->prepare("SELECT rasca_id,nombre,banner_1,banner_2,banner_3,banner_4,background_image,fullscreen_icon,fg_image,bg_image FROM rasca_config WHERE rasca_id=?");
$stmt->bind_param("i", $rasca_id);
$stmt->execute();
$cfg = $stmt->get_result()->fetch_assoc();
if(!$cfg){
// Fallbacks por defecto
$cfg = [
'rasca_id'=>$rasca_id,'nombre'=>'Rasca',
'banner_1'=>'/ruleta/img/BANNER-1.png','banner_2'=>'/ruleta/img/BANNER-2.png',
'banner_3'=>'/ruleta/img/BANNER-3.png','banner_4'=>'/ruleta/img/BANNER-4.png',
'background_image'=>null,'fullscreen_icon'=>'/ruleta/img/fullscreen-icon.png',
'fg_image'=>null,'bg_image'=>null
];
}
echo json_encode($cfg, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+100
View File
@@ -0,0 +1,100 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../admin/db.php';
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
try {
// JSON o form
$raw = file_get_contents('php://input');
$data = json_decode($raw, true);
if (!is_array($data)) $data = $_POST;
$rasca_id = isset($data['rasca_id']) ? (int)$data['rasca_id'] : 0;
$porcentaje = isset($data['percent']) ? (float)$data['percent'] : null; // opcional
if ($rasca_id <= 0) { echo json_encode(['ok'=>false,'error'=>'rasca_id inválido']); exit; }
// Premios activos con stock (NULL = ilimitado)
$sql = "SELECT id, nombre, probabilidad, stock, activo, imagen_url
FROM rasca_premios
WHERE rasca_id=? AND activo=1
AND (stock IS NULL OR stock > 0)
AND probabilidad > 0";
$st = $conn->prepare($sql);
$st->bind_param('i', $rasca_id);
$st->execute();
$st->bind_result($id, $nombre, $prob, $stock, $activo, $img);
$premios = []; $total = 0.0;
while ($st->fetch()) {
$prob = (float)$prob;
$premios[] = [
'id' => (int)$id,
'nombre' => $nombre,
'prob' => $prob,
'stock' => is_null($stock) ? null : (int)$stock,
'imagen_url' => $img ?: null
];
$total += max(0.0, $prob);
}
$st->close();
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? null;
$ua = $_SERVER['HTTP_USER_AGENT'] ?? null;
if (!$premios) {
// Sin premios disponibles: registra jugada sin premio
$conn->prepare("INSERT INTO jugadas_rasca (rasca_id, ip_address, user_agent, porcentaje, premio_id)
VALUES (?,?,?,?,NULL)")
->bind_param('issd', $rasca_id, $ip, $ua, $porcentaje)
->execute();
echo json_encode(['ok'=>true,'sin_premio'=>true,'mensaje'=>'No hay premios activos o con stock']);
exit;
}
if ($total <= 0) { echo json_encode(['ok'=>false,'error'=>'Probabilidades inválidas']); exit; }
// Selección ponderada
$rnd = mt_rand() / mt_getrandmax() * $total;
$acc = 0.0; $pick = null;
foreach ($premios as $p) { $acc += max(0.0, $p['prob']); if ($rnd <= $acc) { $pick = $p; break; } }
if ($pick === null) $pick = $premios[0];
// Transacción: descontar stock (si aplica) y registrar jugada
$conn->begin_transaction();
if ($pick['stock'] !== null) {
$upd = $conn->prepare("UPDATE rasca_premios SET stock = stock - 1 WHERE id=? AND stock > 0");
$upd->bind_param('i', $pick['id']);
$upd->execute();
if ($upd->affected_rows === 0) {
$conn->rollback();
// stock se agotó en carrera; registra jugada sin premio
$conn->prepare("INSERT INTO jugadas_rasca (rasca_id, ip_address, user_agent, porcentaje, premio_id)
VALUES (?,?,?,?,NULL)")
->bind_param('issd', $rasca_id, $ip, $ua, $porcentaje)
->execute();
echo json_encode(['ok'=>true,'sin_premio'=>true,'mensaje'=>'Stock agotado']);
exit;
}
}
$ins = $conn->prepare("INSERT INTO jugadas_rasca (rasca_id, ip_address, user_agent, porcentaje, premio_id)
VALUES (?,?,?,?,?)");
$ins->bind_param('issdi', $rasca_id, $ip, $ua, $porcentaje, $pick['id']);
$ins->execute();
$conn->commit();
echo json_encode([
'ok' => true,
'premio' => $pick['nombre'],
'premio_id' => $pick['id'],
'imagen_url' => $pick['imagen_url']
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
} catch (Throwable $e) {
@mysqli_rollback($conn);
echo json_encode(['ok'=>false,'error'=>'No hay articulos en stock.']);
//echo json_encode(['ok'=>false,'error'=>'Sorteo falló: '.$e->getMessage()]);
}
+42
View File
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../admin/db.php';
$id = (int)($_GET['ruleta_id'] ?? 1);
$stmt = $conn->prepare("SELECT
imagen_ruleta, imagen_puntero,
banner_1, banner_2, banner_3, banner_4,
background_image, fullscreen_icon
FROM ruletas WHERE id=? LIMIT 1");
$stmt->bind_param("i", $id);
$stmt->execute();
$res = $stmt->get_result();
if (!$row = $res->fetch_assoc()) {
http_response_code(404);
echo json_encode(['error' => 'Ruleta no encontrada']);
exit;
}
function norm(?string $p): ?string {
if (!$p) return null;
$p = str_replace('\\', '/', $p);
if ($p[0] !== '/') $p = '/' . ltrim($p, '/');
return $p;
}
$docroot = rtrim($_SERVER['DOCUMENT_ROOT'] ?? '', '/');
$fields = [
'imagen_ruleta','imagen_puntero',
'banner_1','banner_2','banner_3','banner_4',
'background_image','fullscreen_icon'
];
$out = [];
foreach ($fields as $f) {
$v = norm($row[$f] ?? null);
$out[$f] = ($v && file_exists($docroot . $v)) ? $v : null;
}
echo json_encode($out);
+58
View File
@@ -0,0 +1,58 @@
<?php
// api/seleccionar_premio.php - DB-driven selection
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../admin/db.php';
$payload = json_decode(file_get_contents('php://input'), true) ?: [];
$ruleta_id = isset($payload['ruleta_id']) ? (int)$payload['ruleta_id'] : (int)($_GET['ruleta_id'] ?? 1);
$user_agent = $payload['user_agent'] ?? ($_SERVER['HTTP_USER_AGENT'] ?? '');
$ip_address = $payload['ip_address'] ?? ($_SERVER['REMOTE_ADDR'] ?? '');
// Leer premios disponibles
$stmt = $conn->prepare("SELECT id, nombre, cantidad, probabilidad, inicio_angulo, fin_angulo 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'];
$totalProb += $row['probabilidad'];
$premios[] = $row;
}
$stmt->close();
if (!$premios || $totalProb <= 0) {
http_response_code(422);
echo json_encode(['error'=>'No hay premios configurados o probabilidades en cero']);
exit;
}
// Sorteo ponderado
$pick = mt_rand() / mt_getrandmax() * $totalProb;
$acc = 0.0;
$ganador = null;
foreach ($premios as $p) {
$acc += $p['probabilidad'];
if ($pick <= $acc) { $ganador = $p; break; }
}
if (!$ganador) $ganador = end($premios);
// Guardar jugada
$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();
echo json_encode([
'ok' => true,
'nombre' => $ganador['nombre'],
'inicio' => (float)$ganador['inicio_angulo'],
'fin' => (float)$ganador['fin_angulo']
]);
+78
View File
@@ -0,0 +1,78 @@
<?php
$servername = "208.109.71.144";
$username = "ruletaadmin1990";
$password = "Nd;GfPL#}*5s";
$dbname = "ruleta_db";
// Crear la conexión
$conn = new mysqli($servername, $username, $password, $dbname);
// Verificar la conexión
if ($conn->connect_error) {
die(json_encode(["error" => "Conexión fallida: " . $conn->connect_error]));
}
// Obtener la IP del cliente en el servidor
$ip_address = $_SERVER['REMOTE_ADDR'];
// Recibir el user_agent desde el frontend
$data = json_decode(file_get_contents("php://input"), true);
$user_agent = $data['user_agent'] ?? null;
// Establecer las variables de sesión en MariaDB
$conn->query("SET @user_agent = '$user_agent'");
$conn->query("SET @ip_address = '$ip_address'");
// Consultar los premios con cantidad mayor que 0
$sql = "SELECT id, nombre, probabilidad, cantidad, inicio, final FROM premios3 WHERE cantidad > 0";
$result = $conn->query($sql);
$premios = array();
$totalProbabilidad = 0;
if ($result->num_rows > 0) {
// Calcular el total de probabilidades
while($row = $result->fetch_assoc()) {
$premios[] = $row;
$totalProbabilidad += $row['probabilidad'];
}
// Generar un número aleatorio entre 0 y el total de probabilidades
$random = mt_rand(0, $totalProbabilidad * 1000) / 1000; // Se multiplica por 1000 para trabajar con 3 decimales
$suma = 0;
// Seleccionar el premio basado en la probabilidad acumulada
foreach ($premios as $premio) {
$suma += $premio['probabilidad'];
if ($random <= $suma) {
// copiar esta parte Establecer las variables de sesión en MariaDB
$conn->query("SET @user_agent = '$user_agent'");
$conn->query("SET @ip_address = '$ip_address'");
// Actualizar la cantidad del premio seleccionado
$stmt = $conn->prepare("UPDATE premios3 SET cantidad = cantidad - 1 WHERE id = ?");
if ($stmt) {
$stmt->bind_param("i", $premio['id']);
$stmt->execute();
$stmt->close();
// Devolver el premio seleccionado con sus rangos de ángulo
echo json_encode([
'id' => $premio['id'],
'nombre' => $premio['nombre'],
'inicio' => $premio['inicio'],
'final' => $premio['final']
]);
exit(); // Salir inmediatamente después de devolver el premio seleccionado
} else {
echo json_encode(["error" => "Error en la preparación de la actualización"]);
}
break;
}
}
} else {
echo json_encode(["error" => "No hay premios disponibles"]);
}
$conn->close();
?>
+31
View File
@@ -0,0 +1,31 @@
ruleta/ <-- Carpeta raíz del proyecto
├── index.html <-- Juego (frontend)
├── script.js <-- Lógica del juego
├── style.css <-- Estilos del juego
├── img/ <-- Imágenes por defecto del juego
│ ├── ruleta.png
│ ├── puntero.png
│ ├── BANNER-1.png
│ ├── BANNER-2.png
│ ├── BANNER-3.png
│ └── BANNER-4.png
├── uploads/ <-- Carpeta pública para subir imágenes
│ └── ruletas/ <-- Imágenes de ruletas y punteros personalizadas
│ ├── ruleta-abc123.png
│ └── puntero-xyz456.png
├── api/ <-- Endpoints de datos para el juego
│ ├── ruleta_config.php <-- Devuelve imágenes y config de ruleta según ID
│ └── seleccionar_premio.php <-- Devuelve premio ganador según ruleta_id
└── admin/ <-- Panel de administración (PHP)
├── index.php <-- Login de admin
├── dashboard.php <-- Panel principal
├── ruletas.php <-- CRUD de ruletas
├── premios.php <-- CRUD de premios
├── jugadas.php <-- Historial de jugadas
├── db.php <-- Conexión a la BD
└── (otros scripts .php según necesidad)
+35
View File
@@ -0,0 +1,35 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>¡Gracias por participar!</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
background: linear-gradient(135deg, #00b09b, #96c93d);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
text-align: center;
}
.card {
background: rgba(255, 255, 255, 0.1);
border: none;
backdrop-filter: blur(8px);
}
</style>
</head>
<body>
<div class="container">
<div class="card p-4 shadow-lg mx-auto" style="max-width: 500px;">
<h1 class="mb-3">🎉 ¡Gracias por participar!</h1>
<p class="lead">Tu registro ha sido enviado correctamente.</p>
<p>En breve podrás acceder al juego o recibirás más instrucciones.</p>
<!-- <a href="/" class="btn btn-light mt-3">Volver al inicio</a>-->
</div>
</div>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 486 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

+108
View File
@@ -0,0 +1,108 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<title>GLM - Ruleta</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Estilos del juego -->
<link rel="stylesheet" href="style.css" />
</head>
<body>
<!-- Banners (se rellenan dinámicamente) -->
<div class="banner banner-1"></div>
<div class="banner banner-2"></div>
<div class="banner banner-3"></div>
<div class="banner banner-4"></div>
<!-- Contenido principal -->
<div class="content">
<h1></h1>
<div class="game">
<!-- Se asignan src dinámicamente -->
<img id="ruleta" alt="Ruleta" />
<img id="puntero" alt="Puntero" />
</div>
</div>
<!-- Botón de pantalla completa (icono dinámico) -->
<div class="fullscreen-btn" id="fullscreen-btn"></div>
<!-- Mensaje para orientación vertical -->
<div class="landscape-message">
Por favor, gira tu dispositivo para ver el contenido en horizontal.
</div>
<!-- Modal de confirmación -->
<div id="confirm-modal" class="modal">
<div class="modal-content">
<p>¿Deseas participar en la promoción?</p>
<div class="modal-buttons">
<button id="confirm-yes"></button>
<button id="confirm-no">No</button>
</div>
</div>
</div>
<!-- Modal de agradecimiento -->
<div id="thank-you-modal" class="popup-modal" style="display: none;">
<div class="popup-content">
<h2 id="thank-you-message"></h2>
<p id="thank-you-details"></p>
<button id="redirect-button">Cerrar</button>
</div>
</div>
<!-- Confeti -->
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.5.1/dist/confetti.browser.min.js"></script>
<!-- Cargar imágenes/estilos por ruleta desde la API -->
<script>
(function () {
const params = new URLSearchParams(window.location.search);
const ruletaId = params.get("ruleta_id") || 1;
fetch(`api/ruleta_config.php?ruleta_id=${ruletaId}`)
.then(r => r.json())
.then(data => {
// Ruleta & Puntero
const imgRuleta = document.getElementById("ruleta");
const imgPuntero = document.getElementById("puntero");
imgRuleta.src = data.imagen_ruleta || "img/ruleta.png";
imgPuntero.src = data.imagen_puntero || "img/puntero.png";
// Banners
if (data.banner_1) document.querySelector(".banner-1").style.backgroundImage = `url("${data.banner_1}")`;
if (data.banner_2) document.querySelector(".banner-2").style.backgroundImage = `url("${data.banner_2}")`;
if (data.banner_3) document.querySelector(".banner-3").style.backgroundImage = `url("${data.banner_3}")`;
if (data.banner_4) document.querySelector(".banner-4").style.backgroundImage = `url("${data.banner_4}")`;
// Background de la página
if (data.background_image) {
document.body.style.backgroundImage = `url("${data.background_image}")`;
document.body.style.backgroundSize = "cover";
document.body.style.backgroundRepeat = "no-repeat";
document.body.style.backgroundPosition= "center center";
}
// Icono del botón Fullscreen
if (data.fullscreen_icon) {
const fs = document.getElementById("fullscreen-btn");
fs.style.backgroundImage = `url("${data.fullscreen_icon}")`;
fs.style.backgroundRepeat = "no-repeat";
fs.style.backgroundSize = "contain";
fs.style.backgroundPosition= "center";
}
})
.catch(err => {
console.error("Error cargando configuración de ruleta:", err);
// Fallback mínimo si falla
document.getElementById("ruleta").src = "img/ruleta.png";
document.getElementById("puntero").src = "img/puntero.png";
});
})();
</script>
<!-- Lógica del juego (usa seleccionar_premio.php y respeta ruleta_id) -->
<script src="script.js"></script>
</body>
</html>
+118
View File
@@ -0,0 +1,118 @@
<?php
require_once __DIR__ . '/admin/db.php';
require_once __DIR__ . '/lib/landing_helpers.php';
$slug = trim($_GET['slug'] ?? '');
if ($slug==='') { http_response_code(404); echo "Landing no encontrada."; exit; }
$st = $conn->prepare("SELECT * FROM landing_pages WHERE slug=? AND estado IN ('publicado','borrador') LIMIT 1");
$st->bind_param('s',$slug);
$st->execute();
$landing = $st->get_result()->fetch_assoc();
if(!$landing){ http_response_code(404); echo "Landing no encontrada."; exit; }
$landing_id = (int)$landing['id'];
// validar schedule
if ($landing['estado']!=='publicado' || !lp_schedule_active($conn, $landing_id)) {
http_response_code(404); echo "Landing no publicada."; exit;
}
// variante A/B
$variant = lp_pick_variant($conn, $landing_id); // null = base
// contenido final = variante override o base
$hero = $variant['hero_image'] ?? ($landing['hero_image'] ?? '');
$html = $variant['contenido_html'] ?? ($landing['contenido_html'] ?? '');
$css = ($landing['css_inline'] ?? '') . "\n" . ($variant['css_inline'] ?? '');
$js = ($landing['js_inline'] ?? '') . "\n" . ($variant['js_inline'] ?? '');
// bloques
$bloques = lp_load_blocks($conn, $landing_id);
// tracking impresión
$session_id = session_id() ?: bin2hex(random_bytes(8));
$vid = $variant['id'] ?? null;
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? null;
$ua = $_SERVER['HTTP_USER_AGENT'] ?? null;
$ins = $conn->prepare("INSERT INTO landing_variant_stats (landing_id, variant_id, event_type, session_id, ip_address, user_agent)
VALUES (?,?,?,?,?,?)");
$evt='impression';
$ins->bind_param('iissss',$landing_id,$vid,$evt,$session_id,$ip,$ua);
$ins->execute();
?>
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title><?= htmlspecialchars($landing['meta_title'] ?: $landing['titulo']) ?></title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<?php if(!empty($landing['meta_desc'])): ?>
<meta name="description" content="<?= htmlspecialchars($landing['meta_desc']) ?>">
<?php endif; ?>
<?php if(!empty($landing['canonical_url'])): ?>
<link rel="canonical" href="<?= htmlspecialchars($landing['canonical_url']) ?>">
<?php endif; ?>
<style>
body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto;line-height:1.5;color:#222}
.hero{min-height:50vh;display:flex;align-items:center;justify-content:center;background-size:cover;background-position:center;text-align:center;padding:4rem 1rem}
.container{max-width:980px;margin:0 auto;padding:2rem 1rem}
.btn{display:inline-block;padding:.75rem 1.25rem;border-radius:.5rem;background:#0d6efd;color:#fff;text-decoration:none}
<?= $css ?>
</style>
</head>
<body>
<header class="hero" style="background-image:url('<?= htmlspecialchars($hero) ?>');">
<div class="container">
<h1><?= htmlspecialchars($landing['titulo']) ?></h1>
<p><?= htmlspecialchars($landing['meta_desc'] ?? '') ?></p>
<a class="btn" href="#form">Quiero más info</a>
</div>
</header>
<main class="container">
<!-- Bloques zona=header -->
<?php if(!empty($bloques['header'])): foreach($bloques['header'] as $b): ?>
<?php if($b['tipo']==='html') echo $b['contenido']; ?>
<?php endforeach; endif; ?>
<!-- Contenido administrable (base/variante) -->
<?= $html ?>
<!-- Bloques zona=main -->
<?php if(!empty($bloques['main'])): foreach($bloques['main'] as $b): ?>
<?php if($b['tipo']==='html') echo $b['contenido']; ?>
<?php endforeach; endif; ?>
<hr id="form">
<h3>Contáctanos</h3>
<form method="post" action="landing_form.php" onsubmit="lpConv();">
<input type="hidden" name="landing_id" value="<?= (int)$landing_id ?>">
<input type="hidden" name="variant_id" value="<?= (int)($variant['id'] ?? 0) ?>">
<div><input name="nombre" placeholder="Nombre" style="width:100%;padding:.6rem;margin:.25rem 0"></div>
<div><input name="email" placeholder="Email" style="width:100%;padding:.6rem;margin:.25rem 0"></div>
<div><input name="telefono" placeholder="Teléfono" style="width:100%;padding:.6rem;margin:.25rem 0"></div>
<div><textarea name="mensaje" placeholder="Mensaje" rows="4" style="width:100%;padding:.6rem;margin:.25rem 0"></textarea></div>
<button class="btn">Enviar</button>
</form>
<!-- Bloques zona=footer -->
<?php if(!empty($bloques['footer'])): foreach($bloques['footer'] as $b): ?>
<?php if($b['tipo']==='html') echo $b['contenido']; ?>
<?php endforeach; endif; ?>
</main>
<script>
// registra conversión al enviar (fire-and-forget)
function lpConv(){
navigator.sendBeacon('landing_track.php', new URLSearchParams({
landing_id: '<?= (int)$landing_id ?>',
variant_id: '<?= (int)($variant['id'] ?? 0) ?>',
event_type: 'conversion',
session_id: '<?= htmlspecialchars($session_id) ?>'
}));
}
<?= $js ?>
</script>
</body>
</html>
+114
View File
@@ -0,0 +1,114 @@
<?php
session_start();
require_once __DIR__ . '/admin/db.php';
function post_json($url, array $payload, $secret=null){
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => array_filter([
'Content-Type: application/json',
$secret ? 'Authorization: Bearer '.$secret : null
]),
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES),
CURLOPT_TIMEOUT => 8
]);
$res = curl_exec($ch);
$err = curl_error($ch);
curl_close($ch);
return $err ?: $res;
}
function safe_redirect(string $url, int $status = 303){
$url = str_replace(["\r","\n"], '', $url);
header("Location: ".$url, true, $status);
exit;
}
function is_abs_url(string $url): bool {
return (bool) filter_var($url, FILTER_VALIDATE_URL);
}
function is_rel_path(string $url): bool {
// /ruta o ruta (sin esquema)
return preg_match('#^/[^/]|^[^:]+$#', $url) === 1;
}
if($_SERVER['REQUEST_METHOD']==='POST'){
$landing_id = (int)($_POST['landing_id'] ?? 0);
$variant_id = (int)($_POST['variant_id'] ?? 0);
$nombre = trim($_POST['nombre'] ?? '');
$email = trim($_POST['email'] ?? '');
$telefono= trim($_POST['telefono'] ?? '');
$mensaje = trim($_POST['mensaje'] ?? '');
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? ($_SERVER['REMOTE_ADDR'] ?? null);
$ua = $_SERVER['HTTP_USER_AGENT'] ?? null;
if($landing_id>0){
// 1) Guardar lead local
$st=$conn->prepare("INSERT INTO landing_forms (landing_id,nombre,email,telefono,mensaje,ip_address,user_agent) VALUES (?,?,?,?,?,?,?)");
$st->bind_param('issssss',$landing_id,$nombre,$email,$telefono,$mensaje,$ip,$ua);
$st->execute();
// 2) Track conversión para la variante elegida
$evt='conversion';
$sess = session_id() ?: null;
$vid = $variant_id > 0 ? $variant_id : null;
$st2 = $conn->prepare("INSERT INTO landing_variant_stats (landing_id, variant_id, event_type, session_id, ip_address, user_agent) VALUES (?,?,?,?,?,?)");
$st2->bind_param('iissss',$landing_id,$vid,$evt,$sess,$ip,$ua);
$st2->execute();
// 3) Enviar a CRMs configurados
$hooks = $conn->query("SELECT endpoint, method, secret FROM landing_crm_hooks WHERE landing_id={$landing_id} AND active=1")->fetch_all(MYSQLI_ASSOC);
if($hooks){
$payload = [
'landing_id'=>$landing_id,
'variant_id'=>$vid,
'lead'=>[
'nombre'=>$nombre,'email'=>$email,'telefono'=>$telefono,'mensaje'=>$mensaje,
'ip'=>$ip,'user_agent'=>$ua,'created_at'=>date('c')
],
'utm'=>[
'source'=>$_POST['utm_source']??null,
'medium'=>$_POST['utm_medium']??null,
'campaign'=>$_POST['utm_campaign']??null
]
];
foreach($hooks as $h){
if (strtoupper($h['method'])==='POST') {
post_json($h['endpoint'], $payload, $h['secret'] ?? null);
} else {
@file_get_contents($h['endpoint']);
}
}
}
// 4) Redirigir al juego (game_url) si existe; si no, fallback a gracias.html
try {
$st3 = $conn->prepare("SELECT game_url FROM landing_pages WHERE id=? LIMIT 1");
$st3->bind_param('i', $landing_id);
$st3->execute();
$res = $st3->get_result()->fetch_assoc();
$game_url = trim((string)($res['game_url'] ?? ''));
if ($game_url !== '') {
if (is_abs_url($game_url)) {
safe_redirect($game_url, 303);
} elseif (is_rel_path($game_url)) {
// Normaliza si deseas forzar slash inicial:
// $game_url = '/' . ltrim($game_url, '/');
safe_redirect($game_url, 303);
}
}
} catch (Throwable $e) {
// Si hay error, continua a fallback
}
// Fallback
safe_redirect('gracias.html', 303);
}
}
http_response_code(400);
echo "Solicitud inválida.";
+20
View File
@@ -0,0 +1,20 @@
<?php
require_once __DIR__ . '/admin/db.php';
header('Content-Type: text/plain; charset=utf-8');
$landing_id = (int)($_POST['landing_id'] ?? $_GET['landing_id'] ?? 0);
$variant_id = (int)($_POST['variant_id'] ?? $_GET['variant_id'] ?? 0);
$event_type = $_POST['event_type'] ?? $_GET['event_type'] ?? 'conversion';
$session_id = $_POST['session_id'] ?? $_GET['session_id'] ?? null;
if($landing_id<=0 || !in_array($event_type, ['impression','conversion'], true)) { http_response_code(400); echo "bad"; exit; }
$vid = $variant_id>0 ? $variant_id : null;
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? null;
$ua = $_SERVER['HTTP_USER_AGENT'] ?? null;
$st = $conn->prepare("INSERT INTO landing_variant_stats (landing_id, variant_id, event_type, session_id, ip_address, user_agent) VALUES (?,?,?,?,?,?)");
$st->bind_param('iissss', $landing_id, $vid, $event_type, $session_id, $ip, $ua);
$st->execute();
echo "ok";
+282
View File
@@ -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>
+73
View File
@@ -0,0 +1,73 @@
<?php
function lp_now() { return new DateTime('now'); }
function lp_schedule_active(mysqli $conn, int $landing_id): bool {
// activa si NO hay schedule o si hay una ventana vigente
$now = lp_now()->format('Y-m-d H:i:s');
$st = $conn->prepare("SELECT 1 FROM landing_schedule
WHERE landing_id=? AND (start_at IS NULL OR start_at<=?) AND (end_at IS NULL OR end_at>=?)
LIMIT 1");
$st->bind_param('iss', $landing_id, $now, $now);
$st->execute(); $st->store_result();
return $st->num_rows > 0 || $conn->query("SELECT COUNT(*) c FROM landing_schedule WHERE landing_id={$landing_id}")->fetch_assoc()['c'] == 0;
}
function lp_pick_variant(mysqli $conn, int $landing_id): ?array {
// cookie stickiness
$cookieName = "lpv_".$landing_id;
if (!empty($_COOKIE[$cookieName])) {
$vid = (int)$_COOKIE[$cookieName];
$st = $conn->prepare("SELECT id, nombre, peso, estado, hero_image, contenido_html, css_inline, js_inline
FROM landing_variants WHERE id=? AND landing_id=? AND estado='activo' LIMIT 1");
$st->bind_param('ii', $vid, $landing_id);
$st->execute();
$res = $st->get_result()->fetch_assoc();
if ($res) return $res;
}
// carga variantes activas
$st = $conn->prepare("SELECT id, nombre, peso, estado, hero_image, contenido_html, css_inline, js_inline
FROM landing_variants WHERE landing_id=? AND estado='activo'");
$st->bind_param('i', $landing_id);
$st->execute();
$r = $st->get_result();
$vars = []; $total = 0.0;
while ($row = $r->fetch_assoc()) {
$w = max(0.0, (float)$row['peso']);
$row['_w'] = $w;
$vars[] = $row;
$total += $w;
}
if (!$vars || $total <= 0) return null;
// ruleta ponderada
$rnd = mt_rand() / mt_getrandmax() * $total;
$acc = 0.0;
foreach ($vars as $v) {
$acc += $v['_w'];
if ($rnd <= $acc) {
// cookie 7 días
setcookie($cookieName, (string)$v['id'], time()+7*86400, "/");
return $v;
}
}
return $vars[0];
}
function lp_load_blocks(mysqli $conn, int $landing_id): array {
// bloques por zonas (main/header/footer, etc.)
$sql = "SELECT lb.id, lb.nombre, lb.tipo, lb.contenido, lpb.zona, lpb.posicion
FROM landing_page_blocks lpb
JOIN landing_blocks lb ON lb.id=lpb.block_id
WHERE lpb.landing_id=?
ORDER BY lpb.zona, lpb.posicion ASC";
$st=$conn->prepare($sql);
$st->bind_param('i',$landing_id);
$st->execute();
$res=$st->get_result();
$out=[];
while($row=$res->fetch_assoc()){
$out[$row['zona']][]=$row; // agrupa por zona
}
return $out;
}
+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;
}
+70
View File
@@ -0,0 +1,70 @@
// Rasca y Gana (premios separados)
// Requiere: window.RASCA_BOOT, jQuery 2.x y rasca_wScratchpad.min.js
(function ($) {
"use strict";
const BOOT = window.RASCA_BOOT || { rascaId: "1", api:{config:"/api/rasca_config.php?rasca_id=1", premio:"/api/rasca_premio.php"} };
let CFG = null;
let confirmado = false, enviado = false, scratchInit = false;
function setBanner(id, url){
const el = document.getElementById(id);
if(!el) return;
if(url){ el.style.backgroundImage = "url('"+url+"')"; el.style.display='block'; }
else el.style.display='none';
}
function aplicarVisual(cfg){
if(cfg.background_image){ document.body.style.backgroundImage = "url('"+cfg.background_image+"')"; }
setBanner("banner-1", cfg.banner_1 || "img/BANNER-1.png");
setBanner("banner-2", cfg.banner_2 || "img/BANNER-2.png");
setBanner("banner-3", cfg.banner_3 || "img/BANNER-3.png");
setBanner("banner-4", cfg.banner_4 || "img/BANNER-4.png");
if(cfg.fullscreen_icon && document.getElementById('fullscreen-icon')){
document.getElementById('fullscreen-icon').src = cfg.fullscreen_icon;
}
}
function toggleFullScreen(){
if(!document.fullscreenElement){ document.documentElement.requestFullscreen().catch(()=>{}); }
else { document.exitFullscreen().catch(()=>{}); }
}
function initScratch(){
if(scratchInit) return; scratchInit = true;
const bg = (CFG && CFG.bg_image) || "img/ruleta.png"; // fallback
const fg = (CFG && CFG.fg_image) || "img/puntero.png"; // fallback (capa raspable)
$("#card").wScratchPad({
size: 80, bg: bg, fg: fg, cursor: "pointer",
scratchMove: function(e, percent){
if(!confirmado){
if(!window.confirm("¿Deseas participar en la promoción?")) return;
confirmado = true;
}
if(percent > 0 && !enviado){
enviado = true;
$("#card").wScratchPad("clear");
$.ajax({
url: BOOT.api.premio, method: "POST",
contentType: "application/json; charset=utf-8", dataType: "json",
data: JSON.stringify({rasca_id: parseInt(BOOT.rascaId,10), percent: Math.round(percent)})
})
.done(function(res){
if(res && res.ok){
if(res.sin_premio){ $("#premio").text("Gracias por participar."); return; }
//$("#premio").text(res.premio ? ("¡Felicidades! " + res.premio) : "¡Felicidades!");
if(res.imagen_url){ $("#card").wScratchPad("bg", res.imagen_url); }
else if (CFG && CFG.bg_image){ $("#card").wScratchPad("bg", CFG.bg_image); }
}else{
$("#premio").text(res && res.error ? res.error : "No se pudo completar la jugada."); enviado = false;
}
})
.fail(function(){ $("#premio").text("Error de red. Intenta nuevamente."); enviado = false; });
}
}
});
}
function cargarConfig(){
$.ajax({ url: BOOT.api.config, method: "GET", dataType: "json" })
.done(function(cfg){ CFG = cfg || {}; aplicarVisual(CFG); initScratch(); })
.fail(function(){ CFG = {}; initScratch(); });
}
$(function(){ $("#fullscreen-btn").on("click", toggleFullScreen); cargarConfig(); });
})(jQuery);
+54
View File
@@ -0,0 +1,54 @@
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Rasca y Gana</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="rasca_style.css">
<!-- jQuery 2.x (necesario para wScratchpad) -->
<script src="https://code.jquery.com/jquery-2.2.4.min.js" crossorigin="anonymous"></script>
<!-- Cambia el nombre del archivo que ya tienes a rasca_wScratchpad.min.js y colócalo junto a este HTML -->
<script src="rasca_wScratchpad.min.js"></script>
<!-- Boot: define rutas API y rasca_id (por URL ?rasca_id=1) -->
<script>
(function () {
const q = new URLSearchParams(location.search);
const rascaId = q.get('rasca_id') || '1';
const API_BASE = '/ruleta/api'; // 🔧 AJUSTA si tu raíz no es /ruleta
window.RASCA_BOOT = {
rascaId,
api: {
config: API_BASE + '/rasca_config.php?rasca_id=' + encodeURIComponent(rascaId),
premio: API_BASE + '/rasca_premio.php'
}
};
})();
</script>
<script src="rasca_app.js" defer></script>
</head>
<body>
<!-- Banners (opcionales; se cargan desde la config) -->
<div id="banner-1" class="banner banner-1"></div>
<div id="banner-2" class="banner banner-2"></div>
<div id="banner-3" class="banner banner-3"></div>
<div id="banner-4" class="banner banner-4"></div>
<!-- Juego -->
<div id="container">
<div id="card"></div>
<div id="premio" class="premio"></div>
</div>
<!-- Pantalla completa -->
<button id="fullscreen-btn" aria-label="Pantalla completa">
<img id="fullscreen-icon" src="img/fullscreen-icon.png" alt="Pantalla Completa">
</button>
</body>
</html>
+25
View File
@@ -0,0 +1,25 @@
*{box-sizing:border-box;margin:0;padding:0}
body{
width:100%;height:100vh;background:#111;color:#fff;
display:flex;align-items:center;justify-content:center;overflow:hidden;
background-position:center;background-size:cover;background-repeat:no-repeat;
font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,"Noto Sans",sans-serif
}
.banner{position:fixed;background-size:contain;background-repeat:no-repeat;z-index:10}
.banner-1{top:1rem;left:1rem;width:140px;height:140px}
.banner-2{top:1rem;right:1rem;width:260px;height:260px}
.banner-3{bottom:1rem;left:1rem;width:180px;height:90px}
.banner-4{bottom:1rem;right:1rem;width:320px;height:90px}
#container{position:relative;width:100%;height:100%;display:flex;align-items:center;justify-content:center}
#card{width:420px;height:520px;position:relative;user-select:none;border-radius:12px;box-shadow:0 10px 40px rgba(0,0,0,.35)}
#card:active{transform:scale(1.02)}
#premio{position:absolute;top:calc(50% + 290px);left:50%;transform:translate(-50%,-50%);
background:rgba(0,0,0,.55);padding:.6rem 1rem;border-radius:10px;font-weight:600;min-width:260px;text-align:center}
canvas,img{border-radius:12px}
#fullscreen-btn{position:fixed;top:10px;right:10px;background:none;border:none;cursor:pointer;z-index:20}
#fullscreen-btn img{width:42px;height:42px;filter:drop-shadow(0 2px 6px rgba(0,0,0,.5))}
@media (max-width:768px){
#card{width:320px;height:420px}
.banner-2{width:180px;height:180px}
.banner-4{width:240px}
}
+255
View File
@@ -0,0 +1,255 @@
/*! wScratchPad - v2.1.0 - 2014-04-14 */ !(function (a) {
"use strict";
function b(b, c) {
(this.$el = a(b)),
(this.options = c),
(this.init = !1),
(this.enabled = !0),
this._generate();
}
(b.prototype = {
_generate: function () {
return a.support.canvas
? ((this.canvas = document.createElement("canvas")),
(this.ctx = this.canvas.getContext("2d")),
"static" === this.$el.css("position") &&
this.$el.css("position", "relative"),
(this.$img = a('<img src=""/>')
.attr("crossOrigin", "")
.css({ position: "absolute", width: "100%", height: "100%" })),
(this.$scratchpad = a(this.canvas).css({
position: "absolute",
width: "100%",
height: "100%",
})),
this.$scratchpad.bindMobileEvents(),
this.$scratchpad
.mousedown(
a.proxy(function (b) {
return this.enabled
? ((this.canvasOffset = a(this.canvas).offset()),
(this.scratch = !0),
void this._scratchFunc(b, "Down"))
: !0;
}, this)
)
.mousemove(
a.proxy(function (a) {
this.scratch && this._scratchFunc(a, "Move");
}, this)
)
.mouseup(
a.proxy(function (a) {
this.scratch &&
((this.scratch = !1), this._scratchFunc(a, "Up"));
}, this)
),
this._setOptions(),
this.$el.append(this.$img).append(this.$scratchpad),
(this.init = !0),
void this.reset())
: (this.$el.append("Canvas is not supported in this browser."), !0);
},
reset: function () {
var b = this,
c = Math.ceil(this.$el.innerWidth()),
d = Math.ceil(this.$el.innerHeight()),
e = window.devicePixelRatio || 1;
(this.pixels = c * d),
this.$scratchpad.attr("width", c).attr("height", d),
this.canvas.setAttribute("width", c * e),
this.canvas.setAttribute("height", d * e),
this.ctx.scale(e, e),
(this.pixels = c * e * d * e),
this.$img.hide(),
this.options.bg &&
("#" === this.options.bg.charAt(0)
? this.$el.css("backgroundColor", this.options.bg)
: (this.$el.css("backgroundColor", ""),
this.$img.attr("src", this.options.bg))),
this.options.fg &&
("#" === this.options.fg.charAt(0)
? ((this.ctx.fillStyle = this.options.fg),
this.ctx.beginPath(),
this.ctx.rect(0, 0, c, d),
this.ctx.fill(),
this.$img.show())
: a(new Image())
.attr("src", this.options.fg)
.load(function () {
b.ctx.drawImage(this, 0, 0, c, d), b.$img.show();
}));
},
clear: function () {
this.ctx.clearRect(
0,
0,
Math.ceil(this.$el.innerWidth()),
Math.ceil(this.$el.innerHeight())
);
},
enable: function (a) {
this.enabled = a === !0 ? !0 : !1;
},
destroy: function () {
this.$el.children().remove(), a.removeData(this.$el, "wScratchPad");
},
_setOptions: function () {
var a, b;
for (a in this.options)
(this.options[a] = this.$el.attr("data-" + a) || this.options[a]),
(b = "set" + a.charAt(0).toUpperCase() + a.substring(1)),
this[b] && this[b](this.options[a]);
},
setBg: function () {
this.init && this.reset();
},
setFg: function () {
this.setBg();
},
setCursor: function (a) {
this.$el.css("cursor", a);
},
_scratchFunc: function (a, b) {
(a.pageX = Math.floor(a.pageX - this.canvasOffset.left)),
(a.pageY = Math.floor(a.pageY - this.canvasOffset.top)),
this["_scratch" + b](a),
(this.options.realtime || "Up" === b) &&
this.options["scratch" + b] &&
this.options["scratch" + b].apply(this, [a, this._scratchPercent()]);
},
_scratchPercent: function () {
for (
var a = 0,
b = this.ctx.getImageData(
0,
0,
this.canvas.width,
this.canvas.height
),
c = 0,
d = b.data.length;
d > c;
c += 4
)
0 === b.data[c] &&
0 === b.data[c + 1] &&
0 === b.data[c + 2] &&
0 === b.data[c + 3] &&
a++;
return (a / this.pixels) * 100;
},
_scratchDown: function (a) {
(this.ctx.globalCompositeOperation = "destination-out"),
(this.ctx.lineJoin = "round"),
(this.ctx.lineCap = "round"),
(this.ctx.strokeStyle = this.options.color),
(this.ctx.lineWidth = this.options.size),
this.ctx.beginPath(),
this.ctx.arc(
a.pageX,
a.pageY,
this.options.size / 2,
0,
2 * Math.PI,
!0
),
this.ctx.closePath(),
this.ctx.fill(),
this.ctx.beginPath(),
this.ctx.moveTo(a.pageX, a.pageY);
},
_scratchMove: function (a) {
this.ctx.lineTo(a.pageX, a.pageY), this.ctx.stroke();
},
_scratchUp: function () {
this.ctx.closePath();
},
}),
(a.support.canvas = document.createElement("canvas").getContext),
(a.fn.wScratchPad = function (c, d) {
function e() {
var d = a.data(this, "wScratchPad");
return (
d ||
((d = new b(this, a.extend(!0, {}, c))),
a.data(this, "wScratchPad", d)),
d
);
}
if ("string" == typeof c) {
var f,
g = [],
h =
(void 0 !== d ? "set" : "get") +
c.charAt(0).toUpperCase() +
c.substring(1),
i = function () {
f.options[c] && (f.options[c] = d), f[h] && f[h].apply(f, [d]);
},
j = function () {
return f[h]
? f[h].apply(f, [d])
: f.options[c]
? f.options[c]
: void 0;
},
k = function () {
(f = a.data(this, "wScratchPad")),
f &&
(f[c] ? f[c].apply(f, [d]) : void 0 !== d ? i() : g.push(j()));
};
return this.each(k), g.length ? (1 === g.length ? g[0] : g) : this;
}
return (c = a.extend({}, a.fn.wScratchPad.defaults, c)), this.each(e);
}),
(a.fn.wScratchPad.defaults = {
size: 5,
bg: "#cacaca",
fg: "#6699ff",
realtime: !0,
scratchDown: null,
scratchUp: null,
scratchMove: null,
cursor: "crosshair",
}),
(a.fn.bindMobileEvents = function () {
a(this).on("touchstart touchmove touchend touchcancel", function (a) {
var b = a.changedTouches || a.originalEvent.targetTouches,
c = b[0],
d = "";
switch (a.type) {
case "touchstart":
d = "mousedown";
break;
case "touchmove":
(d = "mousemove"), a.preventDefault();
break;
case "touchend":
d = "mouseup";
break;
default:
return;
}
var e = document.createEvent("MouseEvent");
e.initMouseEvent(
d,
!0,
!0,
window,
1,
c.screenX,
c.screenY,
c.clientX,
c.clientY,
!1,
!1,
!1,
!1,
0,
null
),
c.target.dispatchEvent(e);
});
});
})(jQuery);
+477
View File
@@ -0,0 +1,477 @@
-- phpMyAdmin SQL Dump
-- version 5.2.1
-- https://www.phpmyadmin.net/
--
-- Servidor: 127.0.0.1
-- Tiempo de generación: 09-08-2025 a las 21:23:36
-- Versión del servidor: 10.4.32-MariaDB
-- Versión de PHP: 8.0.30
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
--
-- Base de datos: `ruleta_db`
--
-- --------------------------------------------------------
--
-- Estructura de tabla para la tabla `admin_users`
--
CREATE TABLE `admin_users` (
`id` int(11) NOT NULL,
`nombre` varchar(100) NOT NULL,
`email` varchar(150) NOT NULL,
`password_hash` varchar(255) NOT NULL,
`created_at` timestamp NOT NULL DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Volcado de datos para la tabla `admin_users`
--
INSERT INTO `admin_users` (`id`, `nombre`, `email`, `password_hash`, `created_at`) VALUES
(1, 'Administrador', 'admin@localhost', '$2y$10$14EWwemmWzjU4Vr9fZXvduxHgsfnHrpk5ziLFN3IGJ5Pw5QzSorm6', '2025-08-08 21:23:15');
-- --------------------------------------------------------
--
-- Estructura de tabla para la tabla `cartas`
--
CREATE TABLE `cartas` (
`id` int(11) NOT NULL,
`juego_id` int(11) NOT NULL,
`nombre` varchar(100) NOT NULL,
`imagen_frontal` varchar(255) DEFAULT NULL,
`imagen_trasera` varchar(255) NOT NULL,
`activo` tinyint(1) NOT NULL DEFAULT 1,
`created_at` timestamp NOT NULL DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Volcado de datos para la tabla `cartas`
--
INSERT INTO `cartas` (`id`, `juego_id`, `nombre`, `imagen_frontal`, `imagen_trasera`, `activo`, `created_at`) VALUES
(7, 1, 'carta1', '/ruleta/uploads/memoria/back-6896886e0c5fa.png', '/ruleta/uploads/memoria/img-1-6896886e0c89e.png', 1, '2025-08-08 23:29:50'),
(8, 1, 'carta2', '/ruleta/uploads/memoria/back-689688b64d392.png', '/ruleta/uploads/memoria/img-2-689688b64d806.png', 1, '2025-08-08 23:31:02'),
(9, 2, 'primera', NULL, '/ruleta/uploads/memoria/img-1-689689e0611fb.png', 1, '2025-08-08 23:36:00'),
(10, 2, 'imagen2', NULL, '/ruleta/uploads/memoria/img-2-68968c7021030.png', 1, '2025-08-08 23:46:56'),
(11, 2, 'image3', NULL, '/ruleta/uploads/memoria/img-3-68968ca3dab11.png', 1, '2025-08-08 23:47:47'),
(12, 2, 'imagen4', NULL, '/ruleta/uploads/memoria/img-4-68968cb31a8ad.png', 1, '2025-08-08 23:48:03'),
(13, 2, 'imagen5', NULL, '/ruleta/uploads/memoria/img-5-68968cc1091b2.png', 1, '2025-08-08 23:48:17'),
(14, 2, 'imagen6', NULL, '/ruleta/uploads/memoria/img-6-68968ccee8e67.png', 1, '2025-08-08 23:48:30');
-- --------------------------------------------------------
--
-- Estructura de tabla para la tabla `juegos_config`
--
CREATE TABLE `juegos_config` (
`id` int(11) NOT NULL,
`juego_id` int(11) NOT NULL,
`nombre` varchar(100) NOT NULL,
`banner_1` varchar(255) DEFAULT NULL,
`banner_2` varchar(255) DEFAULT NULL,
`banner_3` varchar(255) DEFAULT NULL,
`banner_4` varchar(255) DEFAULT NULL,
`background_image` varchar(255) DEFAULT NULL,
`fullscreen_icon` varchar(255) DEFAULT NULL,
`reverso_default` varchar(255) DEFAULT NULL,
`ruleta_id` int(11) DEFAULT NULL,
`created_at` timestamp NOT NULL DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Volcado de datos para la tabla `juegos_config`
--
INSERT INTO `juegos_config` (`id`, `juego_id`, `nombre`, `banner_1`, `banner_2`, `banner_3`, `banner_4`, `background_image`, `fullscreen_icon`, `reverso_default`, `ruleta_id`, `created_at`) VALUES
(2, 2, 'Test', NULL, NULL, NULL, NULL, '/ruleta/uploads/memoria/BANNER-SUPERIOR-GRANDE-68978a66db94e.png', '/ruleta/uploads/memoria/fullscreen-icon-68978a66dc1b1.png', '/ruleta/uploads/memoria/front-68978a66dcac1.jpg', 0, '2025-08-08 23:35:30');
-- --------------------------------------------------------
--
-- Estructura de tabla para la tabla `jugadas`
--
CREATE TABLE `jugadas` (
`id` int(11) NOT NULL,
`ruleta_id` int(11) NOT NULL,
`premio_id` int(11) DEFAULT NULL,
`ip_address` varchar(45) DEFAULT NULL,
`user_agent` text DEFAULT NULL,
`fecha_juego` timestamp NOT NULL DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Volcado de datos para la tabla `jugadas`
--
INSERT INTO `jugadas` (`id`, `ruleta_id`, `premio_id`, `ip_address`, `user_agent`, `fecha_juego`) VALUES
(1, 2, 2, '148.101.50.128', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', '2025-08-08 21:39:31'),
(6, 2, 5, '148.101.50.128', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', '2025-08-08 21:57:34'),
(7, 2, 5, '148.101.50.128', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', '2025-08-08 22:01:31'),
(8, 3, 6, '148.101.50.128', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', '2025-08-08 22:08:40'),
(9, 3, 6, '148.101.50.128', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', '2025-08-08 22:09:44'),
(10, 4, 7, '148.101.50.128', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', '2025-08-08 22:27:31'),
(11, 4, 7, '148.101.50.128', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15', '2025-08-08 22:29:41'),
(12, 4, 7, '148.101.50.128', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15', '2025-08-08 22:30:04'),
(13, 4, 7, '148.101.50.128', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15', '2025-08-08 22:41:26'),
(14, 4, 7, '148.101.50.128', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', '2025-08-08 23:38:35'),
(15, 4, 7, '148.101.50.128', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', '2025-08-08 23:38:53'),
(16, 4, 7, '148.101.50.128', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', '2025-08-08 23:39:32'),
(17, 4, 8, '148.101.50.128', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', '2025-08-08 23:42:29'),
(18, 4, 8, '148.101.50.128', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', '2025-08-08 23:42:39'),
(19, 4, 7, '148.101.50.128', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', '2025-08-08 23:43:58'),
(20, 4, 7, '148.101.50.128', 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1', '2025-08-09 17:59:24');
-- --------------------------------------------------------
--
-- Estructura de tabla para la tabla `jugadas_memoria`
--
CREATE TABLE `jugadas_memoria` (
`id` int(11) NOT NULL,
`juego_id` int(11) NOT NULL,
`ip_address` varchar(45) DEFAULT NULL,
`user_agent` text DEFAULT NULL,
`tiempo_ms` int(11) DEFAULT 0,
`aciertos` int(11) DEFAULT 0,
`fecha_juego` timestamp NOT NULL DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Volcado de datos para la tabla `jugadas_memoria`
--
INSERT INTO `jugadas_memoria` (`id`, `juego_id`, `ip_address`, `user_agent`, `tiempo_ms`, `aciertos`, `fecha_juego`) VALUES
(1, 2, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 12864, 1, '2025-08-08 23:36:28'),
(2, 2, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 8170, 1, '2025-08-08 23:45:59'),
(3, 2, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 14532, 2, '2025-08-08 23:47:15'),
(4, 2, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 38886, 6, '2025-08-08 23:49:16'),
(5, 2, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 1644391, 6, '2025-08-09 14:24:29'),
(6, 2, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 35695, 6, '2025-08-09 14:25:13'),
(7, 2, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 36400, 6, '2025-08-09 14:26:00'),
(8, 2, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 231321, 6, '2025-08-09 17:54:32');
-- --------------------------------------------------------
--
-- Estructura de tabla para la tabla `jugadas_rasca`
--
CREATE TABLE `jugadas_rasca` (
`id` int(11) NOT NULL,
`rasca_id` int(11) NOT NULL,
`ip_address` varchar(45) DEFAULT NULL,
`user_agent` text DEFAULT NULL,
`porcentaje` int(11) DEFAULT 0,
`premio_id` int(11) DEFAULT NULL,
`fecha_juego` timestamp NOT NULL DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Volcado de datos para la tabla `jugadas_rasca`
--
INSERT INTO `jugadas_rasca` (`id`, `rasca_id`, `ip_address`, `user_agent`, `porcentaje`, `premio_id`, `fecha_juego`) VALUES
(1, 10, '::1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15', 50, 2, '2025-08-09 18:26:37'),
(2, 10, '::1', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15', 50, 2, '2025-08-09 18:26:50'),
(3, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 51, 2, '2025-08-09 18:27:29'),
(4, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 51, 2, '2025-08-09 18:27:44'),
(5, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 50, 2, '2025-08-09 18:29:45'),
(6, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 50, 2, '2025-08-09 18:30:24'),
(7, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 50, 2, '2025-08-09 18:31:29'),
(8, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 50, 2, '2025-08-09 18:32:14'),
(9, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 51, 2, '2025-08-09 18:32:36'),
(10, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 51, 2, '2025-08-09 18:35:47'),
(11, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 50, 2, '2025-08-09 18:37:16'),
(12, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 50, 2, '2025-08-09 18:37:45'),
(13, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 2, 2, '2025-08-09 18:39:13'),
(14, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 2, 2, '2025-08-09 18:39:25'),
(15, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 9, 2, '2025-08-09 18:39:50'),
(16, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 5, 2, '2025-08-09 18:40:43'),
(17, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 27, 2, '2025-08-09 18:43:22'),
(18, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 3, 2, '2025-08-09 19:22:04'),
(19, 10, '::1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36', 2, 2, '2025-08-09 19:22:42');
-- --------------------------------------------------------
--
-- Estructura de tabla para la tabla `premios`
--
CREATE TABLE `premios` (
`id` int(11) NOT NULL,
`ruleta_id` int(11) NOT NULL,
`nombre` varchar(100) NOT NULL,
`cantidad` int(11) DEFAULT 0,
`probabilidad` decimal(6,2) NOT NULL DEFAULT 0.00,
`inicio_angulo` decimal(7,2) NOT NULL DEFAULT 0.00,
`fin_angulo` decimal(7,2) NOT NULL DEFAULT 0.00
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Volcado de datos para la tabla `premios`
--
INSERT INTO `premios` (`id`, `ruleta_id`, `nombre`, `cantidad`, `probabilidad`, `inicio_angulo`, `fin_angulo`) VALUES
(2, 2, 'Freidora de aire', 0, 1.00, 100.00, 150.00),
(5, 2, 'correa', 98, 1.00, 180.00, 190.00),
(6, 3, 'bono de compra', 8, 1.00, 180.00, 182.00),
(7, 4, 'plancha', 12, 0.75, 180.00, 190.00),
(8, 4, 'Abanico de Piso', 8, 0.25, 15.00, 30.00);
-- --------------------------------------------------------
--
-- Estructura de tabla para la tabla `rasca_config`
--
CREATE TABLE `rasca_config` (
`id` int(11) NOT NULL,
`rasca_id` int(11) NOT NULL,
`nombre` varchar(100) NOT NULL,
`ruleta_id` int(11) DEFAULT NULL,
`banner_1` varchar(255) DEFAULT NULL,
`banner_2` varchar(255) DEFAULT NULL,
`banner_3` varchar(255) DEFAULT NULL,
`banner_4` varchar(255) DEFAULT NULL,
`background_image` varchar(255) DEFAULT NULL,
`fullscreen_icon` varchar(255) DEFAULT NULL,
`fg_image` varchar(255) DEFAULT NULL,
`bg_image` varchar(255) DEFAULT NULL,
`created_at` timestamp NOT NULL DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Volcado de datos para la tabla `rasca_config`
--
INSERT INTO `rasca_config` (`id`, `rasca_id`, `nombre`, `ruleta_id`, `banner_1`, `banner_2`, `banner_3`, `banner_4`, `background_image`, `fullscreen_icon`, `fg_image`, `bg_image`, `created_at`) VALUES
(8, 10, 'Test2', NULL, NULL, NULL, NULL, NULL, '/ruleta/uploads/rasca/BANNER-SUPERIOR-GRANDE-689784d6b20a2.png', '/ruleta/uploads/rasca/fullscreen-icon-689784d6b2a9e.png', '/ruleta/uploads/rasca/front-689784d6b3331.jpg', '/ruleta/uploads/rasca/back-68979308bd719.jpg', '2025-08-09 17:26:46');
-- --------------------------------------------------------
--
-- Estructura de tabla para la tabla `rasca_premios`
--
CREATE TABLE `rasca_premios` (
`id` int(11) NOT NULL,
`rasca_id` int(11) NOT NULL,
`nombre` varchar(150) NOT NULL,
`probabilidad` decimal(6,3) NOT NULL DEFAULT 0.000,
`stock` int(11) DEFAULT NULL,
`activo` tinyint(1) NOT NULL DEFAULT 1,
`imagen_url` varchar(255) DEFAULT NULL,
`created_at` timestamp NOT NULL DEFAULT current_timestamp()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Volcado de datos para la tabla `rasca_premios`
--
INSERT INTO `rasca_premios` (`id`, `rasca_id`, `nombre`, `probabilidad`, `stock`, `activo`, `imagen_url`, `created_at`) VALUES
(2, 10, 'laptop', 1.000, 97, 1, '/ruleta/uploads/rasca_premios/Gpay_Card_2-689784f9ba2c3.jpg', '2025-08-09 17:27:21');
-- --------------------------------------------------------
--
-- Estructura de tabla para la tabla `ruletas`
--
CREATE TABLE `ruletas` (
`id` int(11) NOT NULL,
`nombre` varchar(100) NOT NULL,
`descripcion` text DEFAULT NULL,
`imagen_ruleta` varchar(255) DEFAULT NULL,
`imagen_puntero` varchar(255) DEFAULT NULL,
`fecha_creacion` timestamp NOT NULL DEFAULT current_timestamp(),
`banner_1` varchar(255) DEFAULT NULL,
`banner_2` varchar(255) DEFAULT NULL,
`banner_3` varchar(255) DEFAULT NULL,
`banner_4` varchar(255) DEFAULT NULL,
`background_image` varchar(255) DEFAULT NULL,
`fullscreen_icon` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Volcado de datos para la tabla `ruletas`
--
INSERT INTO `ruletas` (`id`, `nombre`, `descripcion`, `imagen_ruleta`, `imagen_puntero`, `fecha_creacion`, `banner_1`, `banner_2`, `banner_3`, `banner_4`, `background_image`, `fullscreen_icon`) VALUES
(2, 'Test', '1', '/ruleta/uploads/ruletas/ruleta1-68966beb88079.png', '/ruleta/uploads/ruletas/puntero11-68966beb88ae7.png', '2025-08-08 21:28:11', '/ruleta/uploads/ruletas/BANNER-1-689671662b081.png', '/ruleta/uploads/ruletas/BANNER-2-689671662c041.png', NULL, NULL, '/ruleta/uploads/ruletas/background1-6896708085ee4.png', '/ruleta/uploads/ruletas/fullscreen-icon-689671662c540.png'),
(3, 'ruleta2', 'jlsgksjgkjfg', '/ruleta/uploads/ruletas/ruleta-68967520e286f.png', '/ruleta/uploads/ruletas/puntero-68967520e2a68.png', '2025-08-08 22:07:28', NULL, '/ruleta/uploads/ruletas/puntero2-68967520e2c3d.png', NULL, NULL, '/ruleta/uploads/ruletas/background-68967520e3c87.png', '/ruleta/uploads/ruletas/fullscreen-icon-68967520e3e76.png'),
(4, 'promotion 1', 'My first rule', '/ruleta/uploads/ruletas/ruleta-68967960bad31.png', '/ruleta/uploads/ruletas/puntero-68967960bafae.png', '2025-08-08 22:25:36', '/ruleta/uploads/ruletas/BANNER-1-68967960bb15c.png', '/ruleta/uploads/ruletas/BANNER-2-68967960bb380.png', NULL, '/ruleta/uploads/ruletas/BANNER-4-68967960bb67a.png', '/ruleta/uploads/ruletas/Strong-68967960bbc33.png', '/ruleta/uploads/ruletas/fullscreen-icon-68967960bbed3.png');
--
-- Índices para tablas volcadas
--
--
-- Indices de la tabla `admin_users`
--
ALTER TABLE `admin_users`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `email` (`email`);
--
-- Indices de la tabla `cartas`
--
ALTER TABLE `cartas`
ADD PRIMARY KEY (`id`);
--
-- Indices de la tabla `juegos_config`
--
ALTER TABLE `juegos_config`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `juego_id` (`juego_id`);
--
-- Indices de la tabla `jugadas`
--
ALTER TABLE `jugadas`
ADD PRIMARY KEY (`id`),
ADD KEY `ruleta_id` (`ruleta_id`),
ADD KEY `premio_id` (`premio_id`);
--
-- Indices de la tabla `jugadas_memoria`
--
ALTER TABLE `jugadas_memoria`
ADD PRIMARY KEY (`id`);
--
-- Indices de la tabla `jugadas_rasca`
--
ALTER TABLE `jugadas_rasca`
ADD PRIMARY KEY (`id`);
--
-- Indices de la tabla `premios`
--
ALTER TABLE `premios`
ADD PRIMARY KEY (`id`),
ADD KEY `ruleta_id` (`ruleta_id`);
--
-- Indices de la tabla `rasca_config`
--
ALTER TABLE `rasca_config`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `rasca_id` (`rasca_id`);
--
-- Indices de la tabla `rasca_premios`
--
ALTER TABLE `rasca_premios`
ADD PRIMARY KEY (`id`),
ADD KEY `rasca_id` (`rasca_id`),
ADD KEY `activo` (`activo`);
--
-- Indices de la tabla `ruletas`
--
ALTER TABLE `ruletas`
ADD PRIMARY KEY (`id`);
--
-- AUTO_INCREMENT de las tablas volcadas
--
--
-- AUTO_INCREMENT de la tabla `admin_users`
--
ALTER TABLE `admin_users`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
--
-- AUTO_INCREMENT de la tabla `cartas`
--
ALTER TABLE `cartas`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=15;
--
-- AUTO_INCREMENT de la tabla `juegos_config`
--
ALTER TABLE `juegos_config`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6;
--
-- AUTO_INCREMENT de la tabla `jugadas`
--
ALTER TABLE `jugadas`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=21;
--
-- AUTO_INCREMENT de la tabla `jugadas_memoria`
--
ALTER TABLE `jugadas_memoria`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=9;
--
-- AUTO_INCREMENT de la tabla `jugadas_rasca`
--
ALTER TABLE `jugadas_rasca`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=20;
--
-- AUTO_INCREMENT de la tabla `premios`
--
ALTER TABLE `premios`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=9;
--
-- AUTO_INCREMENT de la tabla `rasca_config`
--
ALTER TABLE `rasca_config`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=11;
--
-- AUTO_INCREMENT de la tabla `rasca_premios`
--
ALTER TABLE `rasca_premios`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3;
--
-- AUTO_INCREMENT de la tabla `ruletas`
--
ALTER TABLE `ruletas`
MODIFY `id` int(11) NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=5;
--
-- Restricciones para tablas volcadas
--
--
-- Filtros para la tabla `jugadas`
--
ALTER TABLE `jugadas`
ADD CONSTRAINT `jugadas_ibfk_1` FOREIGN KEY (`ruleta_id`) REFERENCES `ruletas` (`id`),
ADD CONSTRAINT `jugadas_ibfk_2` FOREIGN KEY (`premio_id`) REFERENCES `premios` (`id`);
--
-- Filtros para la tabla `premios`
--
ALTER TABLE `premios`
ADD CONSTRAINT `premios_ibfk_1` FOREIGN KEY (`ruleta_id`) REFERENCES `ruletas` (`id`) ON DELETE CASCADE;
COMMIT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
+160
View File
@@ -0,0 +1,160 @@
(function () {
const ruleta = document.getElementById("ruleta");
const puntero = document.getElementById("puntero");
// Modal confirmación
const confirmModal = document.getElementById("confirm-modal");
const confirmYes = document.getElementById("confirm-yes");
const confirmNo = document.getElementById("confirm-no");
// Modal agradecimiento
const thankModal = document.getElementById("thank-you-modal");
const thankTitle = document.getElementById("thank-you-message");
const thankDetails = document.getElementById("thank-you-details");
const thankCloseBtn = document.getElementById("redirect-button");
// Fullscreen
const fullscreenBtn = document.getElementById("fullscreen-btn");
let gira = 0;
let articuloGanador = "";
let estaGirando = false;
const params = new URLSearchParams(window.location.search);
const ruletaId = parseInt(params.get("ruleta_id") || "1", 10) || 1;
// Fallback local (solo si backend no manda ángulos)
const rangosFallback = [
{ articulo: "Freidora de aire", inicio: 330, final: 16 },
{ articulo: "Articulo plastico", inicio: 15, final: 16 },
{ articulo: "Sandwichera", inicio: 150, final: 196 },
{ articulo: "Bandana", inicio: 170, final: 16 },
{ articulo: "Cubeta", inicio: 20, final: 52 },
{ articulo: "Bandana1", inicio: 42, final: 88 },
{ articulo: "Beggin'", inicio: 130, final: 160 },
{ articulo: "Sobre Pro Plan", inicio: 220, final: 232 },
{ articulo: "Plancha", inicio: 150, final: 232 },
{ articulo: "Caldero", inicio: 30, final: 194 },
{ articulo: "Abanico de piso", inicio: 296, final: 328 },
];
function normaliza(a){ return ((a % 360) + 360) % 360; }
function centro(inicio, fin){
const i = normaliza(inicio), f = normaliza(fin);
if (i === f) return i;
if (i < f) return i + (f - i)/2;
const span = 360 - i + f;
return normaliza(i + span/2);
}
function buscarRangoLocal(nombre){
return rangosFallback.find(r => r.articulo.toLowerCase() === String(nombre).toLowerCase());
}
function mostrarConfirmacion(){ confirmModal.style.display = "block"; }
function cerrarConfirmacion(){ confirmModal.style.display = "none"; }
function mostrarAgradecimiento(nombre){
thankTitle.textContent = "🎊🎉¡Felicidades!🎉🎊";
thankDetails.textContent = `🎁 Has ganado "${nombre}" 🎁`;
thankModal.style.display = "flex";
}
function ocultarAgradecimiento(){ thankModal.style.display = "none"; }
function girarA(angulo){
gira = 1440 + normaliza(angulo); // 4 vueltas + objetivo
estaGirando = true;
puntero.style.pointerEvents = "none";
ruleta.style.transition = "all 5s ease-out";
ruleta.style.transform = `rotate(${gira}deg)`;
}
ruleta.addEventListener("transitionend", function () {
ruleta.style.transition = "none";
ruleta.style.transform = `rotate(${normaliza(gira)}deg)`;
estaGirando = false;
puntero.style.pointerEvents = "auto";
lanzarConfeti();
mostrarAgradecimiento(articuloGanador);
});
async function obtenerIP() {
try {
const r = await fetch("https://api.ipify.org?format=json");
const d = await r.json();
return d.ip || "0.0.0.0";
} catch { return "0.0.0.0"; }
}
async function obtenerArticuloGanadorYSpin() {
const userAgent = navigator.userAgent || "unknown";
const ip = await obtenerIP();
try {
const resp = await fetch("api/seleccionar_premio.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
user_agent: userAgent,
ip_address: ip,
ruleta_id: ruletaId,
}),
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const data = await resp.json();
// data esperado:
// { ok:true, nombre:"X", inicio:120, fin:160 } (inicio/fin opcionales)
articuloGanador = data.nombre || "Premio";
let angulo = null;
if (typeof data.inicio === "number" && typeof data.fin === "number") {
angulo = centro(data.inicio, data.fin);
} else {
const rango = buscarRangoLocal(articuloGanador);
angulo = rango ? centro(rango.inicio, rango.final) : Math.floor(Math.random() * 360);
}
girarA(angulo);
} catch (err) {
console.error("Error al obtener el artículo ganador:", err);
estaGirando = false;
puntero.style.pointerEvents = "auto";
alert("Hubo un problema al obtener el premio. Intenta de nuevo.");
}
}
// Interacciones
puntero.addEventListener("mousedown", function () {
if (estaGirando) return;
mostrarConfirmacion();
});
confirmYes.addEventListener("click", function () {
cerrarConfirmacion();
if (estaGirando) return;
obtenerArticuloGanadorYSpin();
});
confirmNo.addEventListener("click", function () {
cerrarConfirmacion();
});
thankCloseBtn.addEventListener("click", ocultarAgradecimiento);
// Fullscreen
fullscreenBtn.addEventListener("click", () => {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(console.error);
} else {
document.exitFullscreen().catch(console.error);
}
});
// Confeti
function lanzarConfeti() {
const duration = 4000;
const end = Date.now() + duration;
(function frame() {
confetti({ particleCount: 7, angle: 60, spread: 55, origin: { x: 0 }, scalar: 1.2 });
confetti({ particleCount: 7, angle: 120, spread: 55, origin: { x: 1 }, scalar: 1.2 });
if (Date.now() < end) requestAnimationFrame(frame);
})();
}
})();
+262
View File
@@ -0,0 +1,262 @@
/* Estilos generales existentes */
body, html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background: #000000;
background-size: cover;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Source Sans Pro', sans-serif;
overflow: hidden;
}
.content {
text-align: center;
font-weight: bold;
font-size: 48px;
color: #ffffff;
text-shadow:
4px 4px 0 #e60000;
}
.game {
position: relative;
display: inline-block;
margin-left: -400px;
}
#ruleta {
max-width: 100%;
height: auto;
}
#puntero {
position: absolute;
top: 47%;
left: 50%;
width: 200px;
transform: translate(-50%, -50%);
}
/* Estilos para dispositivos con pantallas más pequeñas */
@media (max-width: 1024px) {
#ruleta {
width: 70%;
}
#puntero {
width: 200px;
}
}
@media (max-width: 768px) {
#ruleta, #puntero {
width: 50%;
}
}
@media (max-width: 480px) {
#ruleta, #puntero {
width: 80%;
}
#puntero {
width: 100px;
}
}
/* Nueva clase para manejar orientación vertical */
.landscape-message {
display: none;
text-align: center;
font-size: 24px;
color: red;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #ffffff;
padding: 20px;
border: 2px solid red;
z-index: 9999;
}
@media (orientation: portrait) {
.landscape-message {
display: block;
}
#ruleta, #puntero, .content, .game {
display: none;
}
}
.fullscreen-btn {
position: absolute;
top: 5%;
right: 3%;
width: 40px;
height: 40px;
background: url('img/fullscreen-icon.png') no-repeat center center;
background-size: contain;
cursor: pointer;
z-index: 1000;
}
@media (max-width: 1024px) {
.fullscreen-btn {
top: 3%;
right: 5%;
width: 35px;
height: 35px;
}
}
@media (max-width: 768px) {
.fullscreen-btn {
top: 3%;
right: 5%;
width: 30px;
height: 30px;
}
}
@media (max-width: 480px) {
.fullscreen-btn {
top: 2%;
right: 8%;
width: 25px;
height: 25px;
}
}
canvas {
position: fixed !important;
top: 0;
left: 0;
width: 100% !important;
height: 100% !important;
z-index: 99999 !important; /* Mayor que cualquier otro elemento */
pointer-events: none; /* No bloquear interacciones */
}
/* Estilos de los banners */
.banner {
position: fixed;
background-size: contain;
background-repeat: no-repeat;
z-index: 1000;
}
.banner-1 {
top: 0%;
left: 5%;
width: 150px;
height: 150px;
background-image: url('img/BANNER-1.png');
}
.banner-4 {
bottom: 0%;
right: 5%;
width: 400px;
height: 100px;
background-image: url('img/BANNER-4.png');
}
.banner-2 {
top: 5%;
right: 5%;
width: 400px;
height: 400px;
background-image: url('img/BANNER-2.png');
}
/* Estilos para el modal */
.modal {
display: none; /* Oculto por defecto */
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.5); /* Fondo semitransparente */
}
.modal-content {
background-color: #fff;
margin: 15% auto;
padding: 20px;
border-radius: 8px;
width: 300px;
text-align: center;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.modal-buttons {
display: flex;
justify-content: space-around;
margin-top: 20px;
}
.modal-buttons button {
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
#confirm-yes {
background-color: #28a745;
color: white;
}
#confirm-no {
background-color: #dc3545;
color: white;
}
.modal-buttons button:hover {
opacity: 0.9;
}
/*PopUp Agradecimiento*/
#thank-you-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
justify-content: center;
align-items: center;
z-index: 1000;
color: #ffffff;
}
#thank-you-modal .popup-content {
background: #C10F1A;
padding: 20px;
border-radius: 8px;
text-align: center;
}
#redirect-button {
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
#redirect-button:hover {
background-color: #0056b3;
}
View File
View File
View File
View File