commit c44f1a69f154dccc1f85dcb43e87b780bbe3fb12 Author: Isaac Aracena Date: Sat May 9 12:46:24 2026 -0400 Initial commit: Proyecto Portal ePromos completo con documentación unificada y seguridad base diff --git a/-- Tabla principal de landings.txt b/-- Tabla principal de landings.txt new file mode 100644 index 0000000..fd90948 --- /dev/null +++ b/-- Tabla principal de landings.txt @@ -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 +); + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3e05275 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..20492b1 --- /dev/null +++ b/.htaccess @@ -0,0 +1,6 @@ +RewriteEngine On + +# Quitar .php de las URLs +RewriteCond %{REQUEST_FILENAME} !-d +RewriteCond %{REQUEST_FILENAME}\.php -f +RewriteRule ^(.+)$ $1.php [L] \ No newline at end of file diff --git a/Estructura Rasca.txt b/Estructura Rasca.txt new file mode 100644 index 0000000..b5e6cd7 --- /dev/null +++ b/Estructura Rasca.txt @@ -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) diff --git a/Grados 2.png b/Grados 2.png new file mode 100644 index 0000000..93682f5 Binary files /dev/null and b/Grados 2.png differ diff --git a/Grados Originales.png b/Grados Originales.png new file mode 100644 index 0000000..f4808f9 Binary files /dev/null and b/Grados Originales.png differ diff --git a/Grados.png b/Grados.png new file mode 100644 index 0000000..481dcc5 Binary files /dev/null and b/Grados.png differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..fa508ff --- /dev/null +++ b/README.md @@ -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.* diff --git a/admin/.htaccess b/admin/.htaccess new file mode 100644 index 0000000..20492b1 --- /dev/null +++ b/admin/.htaccess @@ -0,0 +1,6 @@ +RewriteEngine On + +# Quitar .php de las URLs +RewriteCond %{REQUEST_FILENAME} !-d +RewriteCond %{REQUEST_FILENAME}\.php -f +RewriteRule ^(.+)$ $1.php [L] \ No newline at end of file diff --git a/admin/cartas.php b/admin/cartas.php new file mode 100644 index 0000000..3f9cb36 --- /dev/null +++ b/admin/cartas.php @@ -0,0 +1,186 @@ +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); +?> + + + + + Memoria | Cartas + + + + + + + +
+
+

Cartas

+
+ + +
+
+ +
+
+ +
+
+
+ + + + +
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
+ > +
+ +
+ + Cancelar +
+
+
+ +
+ + + + + + + + + + + + + + + + +
IDJuegoNombreTraseraFrontalActivoAcciones
':'—' ?>':'—' ?> + Editar + Eliminar +
Sin cartas
+
+
+ + diff --git a/admin/dashboard.php b/admin/dashboard.php new file mode 100644 index 0000000..23d25c5 --- /dev/null +++ b/admin/dashboard.php @@ -0,0 +1,238 @@ +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 +"); + + +?> + + + + + Dashboard | Admin Ruletas + + + + + + +
+ +
Juegos Disponibles
+ +
+
+
Ruletas
+
+ Gestionar ruletas +
+ +
+
Premios
+
+ Gestionar premios +
+ +
+
Jugadas
+
+ Ver jugadas +
+ +
+
+
Juegos de Memoria
+
+ Configurar juegos +
+
+ +
+
+
Cartas activas
+
+ Gestionar cartas +
+
+ +
+
+
Jugadas Memoria
+
+ Ver jugadas +
+
+ +
+
+
Juegos de Rasca y Gana
+
+ Configurar juegos +
+
+ +
+
+
Premios de Rasca y Gana
+
+ Gestionar premios +
+
+ +
+
+
Jugadas de Rasca y Gana
+
+ Ver jugadas +
+
+
+
Últimas 10 jugadas
+
+
+ + + + fetch_assoc()): ?> + + + + + + + + + num_rows === 0): ?> + + + +
IDFechaRuletaPremioIP
Sin registros
+
+
+ +
Últimas 10 jugadas — Memoria
+
+
+ + + + + + num_rows): ?> + fetch_assoc()): ?> + + + + + + + + + + + + + +
IDFechaJuegoJuego IDAciertosTiempo (s)
Sin registros
+
+
+ +
+
+
Últimas 10 jugadas (Rasca)
+ + + + + + + + + + + + fetch_assoc()): ?> + + + + + + + + + +
IDFechaRascaPremioIP
+
+
+ + +
+ + diff --git a/admin/dashboard_landing_widget_data.php b/admin/dashboard_landing_widget_data.php new file mode 100644 index 0000000..e5bee34 --- /dev/null +++ b/admin/dashboard_landing_widget_data.php @@ -0,0 +1,171 @@ + 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); +} diff --git a/admin/dashboard_landing_widgets.php b/admin/dashboard_landing_widgets.php new file mode 100644 index 0000000..5ae48f4 --- /dev/null +++ b/admin/dashboard_landing_widgets.php @@ -0,0 +1,172 @@ +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 +"); +?> +
+ +
+
+
+
+
+
Landings

+
🧭
+
+
+
+
+
+
+
+
+
Variantes A/B

+
🧪
+
+
+
+
+
+
+
+
+
Bloques

+
🧱
+
+
+
+
+
+
+
+
+
Leads (30d)

+
📈
+
+
+
+
+
+ + +
+
+
+
Top CR por Landing (30 días)
+ Ver todas +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
LandingImpresionesConversionesCR %Acciones
+ Stats + Variantes + Builder + CRM + Leads +
Sin datos
+
+
+
+ + +
+
+
+
Accesos rápidos
+ Nueva landing +
+
+ + + + + + + + + + + + num_rows): while($r=$list->fetch_assoc()): ?> + + + + + + + + + + + +
LandingSlugCreadaEstadoAcciones
Activo' : 'Inactivo' ?> + Variantes + Schedule + Bloques + Builder + Stats + CRM + Leads + Ver +
Aún no hay landings
+
+
+
+
diff --git a/admin/dashboard_landing_widgets_adv.php b/admin/dashboard_landing_widgets_adv.php new file mode 100644 index 0000000..a055582 --- /dev/null +++ b/admin/dashboard_landing_widgets_adv.php @@ -0,0 +1,211 @@ + +
+
+

Landings

+ +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+ +
+ + +
+
+
+
Top CR por Landing
+ Ver todas +
+
+ + + + + + + + + + + + + + +
LandingImpresionesConversionesCR %TendenciaAcciones
+
+
+
+
+ + + + diff --git a/admin/db.php.example b/admin/db.php.example new file mode 100644 index 0000000..f9b8a7c --- /dev/null +++ b/admin/db.php.example @@ -0,0 +1,15 @@ +set_charset('utf8mb4'); +} catch (Throwable $e) { + die('Fallo conectando a MySQL: ' . $e->getMessage()); +} diff --git a/admin/index.php b/admin/index.php new file mode 100644 index 0000000..5e118c2 --- /dev/null +++ b/admin/index.php @@ -0,0 +1,94 @@ +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'; +} +?> + + + + + Login | Admin + + + + +
+
+
+
+
+

Admin Promo Hub

+ +
+ +
+
+ + +
+
+ + +
+ +
+
+
+

© GLM

+
+
+
+ + diff --git a/admin/juegos_config.php b/admin/juegos_config.php new file mode 100644 index 0000000..a64e669 --- /dev/null +++ b/admin/juegos_config.php @@ -0,0 +1,207 @@ +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"); +?> + + + + + Memoria | Configuración de Juegos + + + + + + + +
+

Config. Juegos de Memoria

+
+
+ +
+
+
+
+ + +
+
+ + +
+ + '; + } + ?> +
+ + +
+
+
+ + +
+
+
+ + +
+
+ +
+ + Cancelar +
+
+
+ +
+ + + + + + + + fetch_assoc()): ?> + + + + + + + + + + + + + + + +
Juego IDNombreRuletaB1B2B3B4BGFSReversoAcciones
':'—' ?>':'—' ?>':'—' ?>':'—' ?>':'—' ?>':'—' ?>':'—' ?> + Editar + Eliminar + Ver +
+
+
+ + diff --git a/admin/jugadas.php b/admin/jugadas.php new file mode 100644 index 0000000..b018c9c --- /dev/null +++ b/admin/jugadas.php @@ -0,0 +1,223 @@ +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'; +?> + + + + + Jugadas | Admin Ruletas + + + + + + +
+

Historial de Jugadas

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + Limpiar +
+ + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + +
IDFechaRuletaPremioIPUser Agent
+ +
Sin registros
+
+
+ + 1): ?> + + +
+ + diff --git a/admin/jugadas_memoria.php b/admin/jugadas_memoria.php new file mode 100644 index 0000000..00df0bb --- /dev/null +++ b/admin/jugadas_memoria.php @@ -0,0 +1,132 @@ +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); +?> + + + + + Memoria | Jugadas + + + + + + +
+

Jugadas — Memoria

+
+
+ + +
+
+
+
+
+ + Limpiar +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
IDFechaJuegoJuego IDAciertosTiempo (s)IPUser Agent
Sin registros
+
+ + 1): ?> + + +
+ + diff --git a/admin/landing.php b/admin/landing.php new file mode 100644 index 0000000..0e82524 --- /dev/null +++ b/admin/landing.php @@ -0,0 +1,178 @@ +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"); +?> + + +Landing Pages | Admin + + + + +
+
+
+ +
+
+
+ +
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + +
+
+ + +
+
+ + +
+ + +
+ + +
+ +
+ + Cancelar +
+
+
+ +
+ + + + + + + + fetch_assoc()): ?> + + + + + + + + + + +
IDTítuloSlugEstadoActualizadoAcciones
+ Editar + Eliminar + Ver + + Juego + +
+
+
+ diff --git a/admin/landing_blocks.php b/admin/landing_blocks.php new file mode 100644 index 0000000..51ec816 --- /dev/null +++ b/admin/landing_blocks.php @@ -0,0 +1,122 @@ + 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"); +?> + + + + +Landing: Bloques reutilizables + + + + + + +
+
+
+ +
+
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + Cancelar +
+
+
+ +
+ + + + fetch_assoc()): ?> + + + + + + + + + +
IDNombreTipoActualizadoAcciones
+ Editar + Eliminar +
+
+
+ + diff --git a/admin/landing_builder.php b/admin/landing_builder.php new file mode 100644 index 0000000..972a966 --- /dev/null +++ b/admin/landing_builder.php @@ -0,0 +1,149 @@ +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; } +?> + + + + +Landing Builder (Drag & Drop) + + + + + + + +
+ +

Builder:

+ +
+
+
+
+
Bloques disponibles
+
+ +
+ [] +
+ +
+

Arrastra un bloque a una zona.

+
+
+
+ +
+
+
Header
+
['.htmlspecialchars($item['tipo']).'] '.htmlspecialchars($item['nombre']).'
'; + } + ?>
+
+ +
+
Main
+
['.htmlspecialchars($item['tipo']).'] '.htmlspecialchars($item['nombre']).'
'; + } + ?>
+
+ +
+
Footer
+ '; + } + ?>
+
+ + +
+ + + + + + + diff --git a/admin/landing_builder_save.php b/admin/landing_builder_save.php new file mode 100644 index 0000000..8610623 --- /dev/null +++ b/admin/landing_builder_save.php @@ -0,0 +1,43 @@ +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()]); +} diff --git a/admin/landing_crm.php b/admin/landing_crm.php new file mode 100644 index 0000000..830d6dd --- /dev/null +++ b/admin/landing_crm.php @@ -0,0 +1,152 @@ +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"); +?> + + + + +Landing CRM Hooks + + + + + +
+ + +

Landing: (#)

+ +
+
+ +
+
+
+ +
+ + +
+
+ + +
+
+ + + +
+
+ + +
+
+
+ > + +
+
+
+ + Cancelar +
+
+
+ +
+ + + + fetch_assoc()): ?> + + + + + + + + + num_rows): ?> + + + +
IDNombreEndpointMétodoActivoAcciones
+ Editar + Eliminar +
Sin hooks configurados
+
+
+ + diff --git a/admin/landing_forms.php b/admin/landing_forms.php new file mode 100644 index 0000000..cc733c3 --- /dev/null +++ b/admin/landing_forms.php @@ -0,0 +1,100 @@ +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(); +?> + + + + +Landing Leads + + + + + + +
+ + +

Leads:

+ +
+ +
+ + +
+
+ + +
+
+ + + Exportar CSV + +
+
+ +
+ + + + fetch_assoc()): ?> + + + + + + + + + + num_rows): ?> + + + +
IDFechaNombreEmailTeléfonoMensajeIP
Sin leads
+
+
+ + diff --git a/admin/landing_forms_export.php b/admin/landing_forms_export.php new file mode 100644 index 0000000..80bb5ff --- /dev/null +++ b/admin/landing_forms_export.php @@ -0,0 +1,46 @@ += ?"; $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); diff --git a/admin/landing_page_blocks.php b/admin/landing_page_blocks.php new file mode 100644 index 0000000..a12ff32 --- /dev/null +++ b/admin/landing_page_blocks.php @@ -0,0 +1,187 @@ +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 + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + Cancelar +
+ + + +
+
+
Bloques asignados
+ + + + + + + + + + + + + num_rows): while($r = $list->fetch_assoc()): ?> + + + + + + + + + + + + +
IDBloqueTipoZonaPosiciónAcciones
+ Editar + Eliminar +
Sin bloques asignados
+

* Sugerencia: usa zonas como header, main, footer o sidebar, y ordena con posiciones (1,2,3...).

+
+
+ + + diff --git a/admin/landing_schedule.php b/admin/landing_schedule.php new file mode 100644 index 0000000..ef54262 --- /dev/null +++ b/admin/landing_schedule.php @@ -0,0 +1,153 @@ +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; +} +?> + + + + +Landing: Schedule + + + + + +
+ + +

Landing:

+ +
+
+ +
+
+
+ +
+ + +
+
+ + +
+
+ + + +
+
+ + Cancelar +
+
+
+ +
+ + + + fetch_assoc()): ?> + + + + + + + + + +
IDInicioFinEstadoAcciones
+ Editar + Eliminar +
+
+
+ + diff --git a/admin/landing_stats.php b/admin/landing_stats.php new file mode 100644 index 0000000..3b27090 --- /dev/null +++ b/admin/landing_stats.php @@ -0,0 +1,107 @@ +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; +} +?> + + + + +Landing: Stats + + + + + + +
+ + +

Stats:

+ +
+ +
+ +
+ + + + + + + + + + + + + +
VarianteImpresionesConversionesCR %
Sin datos
+
+
+ + + + diff --git a/admin/landing_variants.php b/admin/landing_variants.php new file mode 100644 index 0000000..a809944 --- /dev/null +++ b/admin/landing_variants.php @@ -0,0 +1,160 @@ +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"); +?> + + + + +Landing: Variantes + + + + + + +
+ + +

Landing:

+ +
+
+ +
+
+
+ +
+ + +
+
+ + +
+
+ + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + Cancelar +
+
+
+ +
+ + + + fetch_assoc()): ?> + + + + + + + + + + +
IDNombrePesoEstadoActualizadoAcciones
+ Editar + Eliminar +
+
+
+ + diff --git a/admin/premios.php b/admin/premios.php new file mode 100644 index 0000000..21b804a --- /dev/null +++ b/admin/premios.php @@ -0,0 +1,163 @@ +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); +?> + + + + + Premios | Admin Ruletas + + + + + + +
+
+

Premios

+
+ + +
+
+ +
+
+ +
+
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + Cancelar +
+
+
+ +
+ + + + + + + + + + + + + + + + +
IDPremioCantidadProb %Inicio°Fin°Acciones
+ Editar + Eliminar +
Sin premios
+
+
+ + diff --git a/admin/rasca_config.php b/admin/rasca_config.php new file mode 100644 index 0000000..ceb6ddd --- /dev/null +++ b/admin/rasca_config.php @@ -0,0 +1,139 @@ +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"); +?> + + + +Rasca | Configuración + + + + + + +
+
+
+ +
+
+
+ + + +
+
+ +
+
+
+
+
+
+
+
+ +
Cancelar
+
+
+ +
+ + + + fetch_assoc()): ?> + + + + + + + + + + + + + + + +
Rasca IDNombreB1B2B3B4BGFSFGBaseAcciones
':'—' ?>':'—' ?>':'—' ?>':'—' ?>':'—' ?>':'—' ?>':'—' ?>':'—' ?> + Editar + Eliminar + Ver +
+
+
+ diff --git a/admin/rasca_jugadas.php b/admin/rasca_jugadas.php new file mode 100644 index 0000000..5c739ca --- /dev/null +++ b/admin/rasca_jugadas.php @@ -0,0 +1,87 @@ +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(); +} +?> + + + +Rasca | Jugadas + + + + + +
+

Últimas jugadas (Rasca)

+ + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDFechaRascaPremio%IPUser Agent
Sin jugadas
+
+
+ diff --git a/admin/rasca_premios.php b/admin/rasca_premios.php new file mode 100644 index 0000000..e0f9825 --- /dev/null +++ b/admin/rasca_premios.php @@ -0,0 +1,285 @@ +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); +} +?> + + + +Rasca | Premios + + + + + + +
+ +
+
+ + + +
+

Selecciona un juego de Rasca

+ Crear/editar Rasca +
+ + +
No hay juegos de rasca configurados todavía.
+ + + + + + +
+
+ ← Volver +

Premios Rasca #

+
+
+ +
+
+
+
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + + +
+ +
+
+
+ > + +
+
+ +
+ + Cancelar +
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDNombreProbStockActivoImagenAcciones
' : '—' ?> + Editar + Eliminar +
Sin premios
+
+
+ + +
+ + diff --git a/admin/ruletas.php b/admin/ruletas.php new file mode 100644 index 0000000..deb712c --- /dev/null +++ b/admin/ruletas.php @@ -0,0 +1,303 @@ +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 +} +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); +?> + + + + + Ruletas | Admin Ruletas + + + + + + + +
+

Ruletas

+ +
+
+ +
+
+
+ + + + + + + + + + + +
+ + +
+
+ + +
+ +
+ + + +
ruleta + Se mantiene si no subes otra.
+ +
+
+ + + +
puntero
+ +
+ +

+ +
+ + + +
b1
+ +
+
+ + + +
b2
+ +
+
+ + + +
b3
+ +
+
+ + + +
b4
+ +
+ +
+ + + +
bg
+ +
+
+ + + +
fs
+ +
+ +
+ + Cancelar +
+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDNombreRuletaPunteroBGB1B2B3B4FSCreadoAcciones
+ Editar + Eliminar + Premios + Ver +
Sin ruletas
+
+
+ + diff --git a/api/rasca_config.php b/api/rasca_config.php new file mode 100644 index 0000000..1c9d805 --- /dev/null +++ b/api/rasca_config.php @@ -0,0 +1,24 @@ +'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); diff --git a/api/rasca_premio.php b/api/rasca_premio.php new file mode 100644 index 0000000..1515034 --- /dev/null +++ b/api/rasca_premio.php @@ -0,0 +1,100 @@ +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()]); +} diff --git a/api/ruleta_config.php b/api/ruleta_config.php new file mode 100644 index 0000000..6abb850 --- /dev/null +++ b/api/ruleta_config.php @@ -0,0 +1,42 @@ +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); diff --git a/api/seleccionar_premio.php b/api/seleccionar_premio.php new file mode 100644 index 0000000..a73674d --- /dev/null +++ b/api/seleccionar_premio.php @@ -0,0 +1,58 @@ +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'] +]); diff --git a/api/seleccionar_premio.php.user b/api/seleccionar_premio.php.user new file mode 100644 index 0000000..770176f --- /dev/null +++ b/api/seleccionar_premio.php.user @@ -0,0 +1,78 @@ +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(); +?> diff --git a/estrucutraadminjuego.txt b/estrucutraadminjuego.txt new file mode 100644 index 0000000..a6176d2 --- /dev/null +++ b/estrucutraadminjuego.txt @@ -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) diff --git a/gracias.html b/gracias.html new file mode 100644 index 0000000..798e292 --- /dev/null +++ b/gracias.html @@ -0,0 +1,35 @@ + + + + + + ¡Gracias por participar! + + + + +
+
+

🎉 ¡Gracias por participar!

+

Tu registro ha sido enviado correctamente.

+

En breve podrás acceder al juego o recibirás más instrucciones.

+ +
+
+ + diff --git a/imagen de grados sin fondo.png b/imagen de grados sin fondo.png new file mode 100644 index 0000000..1b2febe Binary files /dev/null and b/imagen de grados sin fondo.png differ diff --git a/imagen de grados.png b/imagen de grados.png new file mode 100644 index 0000000..db4137a Binary files /dev/null and b/imagen de grados.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..86d3a39 --- /dev/null +++ b/index.html @@ -0,0 +1,108 @@ + + + + + GLM - Ruleta + + + + + + + + + + + + +
+

+
+ + Ruleta + Puntero +
+
+ + +
+ + +
+ Por favor, gira tu dispositivo para ver el contenido en horizontal. +
+ + + + + + + + + + + + + + + + + diff --git a/landing.php b/landing.php new file mode 100644 index 0000000..9b09cb2 --- /dev/null +++ b/landing.php @@ -0,0 +1,118 @@ +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(); +?> + + + + + <?= htmlspecialchars($landing['meta_title'] ?: $landing['titulo']) ?> + + + + + + + + + + + +
+ +
+ +
+ + + + + + + + + + + + + +
+

Contáctanos

+
+ + +
+
+
+
+ +
+ + + + + +
+ + + + diff --git a/landing_form.php b/landing_form.php new file mode 100644 index 0000000..53c90da --- /dev/null +++ b/landing_form.php @@ -0,0 +1,114 @@ + 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."; diff --git a/landing_track.php b/landing_track.php new file mode 100644 index 0000000..d89aa5b --- /dev/null +++ b/landing_track.php @@ -0,0 +1,20 @@ +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"; diff --git a/landings_public.php b/landings_public.php new file mode 100644 index 0000000..b4b156a --- /dev/null +++ b/landings_public.php @@ -0,0 +1,282 @@ + ''"; +} +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); +?> + + + + + + Landings activos + + + + + + + + +
+
+

Landings activos

+ resultado +
+ + +
+
+
+
+
+ + +
+ +
+
+ > + +
+
+ +
+
+ > + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + Limpiar +
+
+
+
+
+ + + +
No hay landings publicados que coincidan con los filtros.
+ +
+ +
+
+ <?= e($r['titulo']) ?> +
+

+ +

+ + +
+ +
+
+ +
+ + + 1): ?> + + + +
+ + + + + + diff --git a/lib/landing_helpers.php b/lib/landing_helpers.php new file mode 100644 index 0000000..42bddf9 --- /dev/null +++ b/lib/landing_helpers.php @@ -0,0 +1,73 @@ +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; +} diff --git a/memoria/index.php b/memoria/index.php new file mode 100644 index 0000000..2dcf4c9 --- /dev/null +++ b/memoria/index.php @@ -0,0 +1,129 @@ +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, +]; +?> + + + + + Juego de Memoria - ePromo + + + + + + + + + + +
+ +
+ + + + + + + + + + diff --git a/memoria/registrar_jugada.php b/memoria/registrar_jugada.php new file mode 100644 index 0000000..e87bd50 --- /dev/null +++ b/memoria/registrar_jugada.php @@ -0,0 +1,76 @@ +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 +]); diff --git a/memoria/script.js b/memoria/script.js new file mode 100644 index 0000000..9f52c79 --- /dev/null +++ b/memoria/script.js @@ -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); + } +})(); diff --git a/memoria/style.css b/memoria/style.css new file mode 100644 index 0000000..e05d9c3 --- /dev/null +++ b/memoria/style.css @@ -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; +} + diff --git a/rasca_app.js b/rasca_app.js new file mode 100644 index 0000000..0db7a62 --- /dev/null +++ b/rasca_app.js @@ -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); diff --git a/rasca_index.html b/rasca_index.html new file mode 100644 index 0000000..1ea86f8 --- /dev/null +++ b/rasca_index.html @@ -0,0 +1,54 @@ + + + + + Rasca y Gana + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + + diff --git a/rasca_style.css b/rasca_style.css new file mode 100644 index 0000000..039de60 --- /dev/null +++ b/rasca_style.css @@ -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} +} diff --git a/rasca_wScratchpad.min.js b/rasca_wScratchpad.min.js new file mode 100644 index 0000000..2157098 --- /dev/null +++ b/rasca_wScratchpad.min.js @@ -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('') + .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); diff --git a/ruleta_db.sql b/ruleta_db.sql new file mode 100644 index 0000000..51c5cf5 --- /dev/null +++ b/ruleta_db.sql @@ -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 */; diff --git a/script.js b/script.js new file mode 100644 index 0000000..4e42112 --- /dev/null +++ b/script.js @@ -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); + })(); + } +})(); diff --git a/style.css b/style.css new file mode 100644 index 0000000..2e72582 --- /dev/null +++ b/style.css @@ -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; +} \ No newline at end of file diff --git a/uploads/memoria/.gitkeep b/uploads/memoria/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/uploads/rasca/.gitkeep b/uploads/rasca/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/uploads/rasca_premios/.gitkeep b/uploads/rasca_premios/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/uploads/ruletas/.gitkeep b/uploads/ruletas/.gitkeep new file mode 100644 index 0000000..e69de29