feat: Initial commit - Ruleta con PIN (credenciales sanitizadas)
@@ -0,0 +1,27 @@
|
||||
# ─── Credenciales y configuración local ───
|
||||
config.php
|
||||
.env
|
||||
|
||||
# ─── Sistema operativo ───
|
||||
Thumbs.db
|
||||
.DS_Store
|
||||
desktop.ini
|
||||
|
||||
# ─── IDEs y editores ───
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# ─── PHP ───
|
||||
vendor/
|
||||
composer.lock
|
||||
|
||||
# ─── Archivos temporales ───
|
||||
*.log
|
||||
*.tmp
|
||||
*.bak
|
||||
|
||||
# ─── ZIP original del proyecto ───
|
||||
*.zip
|
||||
@@ -0,0 +1,232 @@
|
||||
# 🎰 Ruleta con PIN
|
||||
|
||||
Aplicación web de **ruleta de premios** protegida por PIN, con registro de resultados vía backend PHP y base de datos MySQL/MariaDB.
|
||||
|
||||
Desarrollada para campañas promocionales de **Gomez Lee Marketing (GLM)**.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Tabla de Contenidos
|
||||
|
||||
- [Características](#-características)
|
||||
- [Requisitos](#-requisitos)
|
||||
- [Instalación](#-instalación)
|
||||
- [Configuración de Base de Datos](#-configuración-de-base-de-datos)
|
||||
- [Configuración de PINs](#-configuración-de-pins)
|
||||
- [Estructura del Proyecto](#-estructura-del-proyecto)
|
||||
- [Flujo del Sistema](#-flujo-del-sistema)
|
||||
- [Operación Diaria](#-operación-diaria)
|
||||
- [Solución de Problemas](#-solución-de-problemas)
|
||||
|
||||
---
|
||||
|
||||
## ✨ Características
|
||||
|
||||
- **Autenticación por PIN** antes de girar la ruleta
|
||||
- **Selección aleatoria ponderada** de premios según probabilidades configurables
|
||||
- **Control de stock** por premio y cupo global de giros
|
||||
- **Registro de auditoría** con IP, user agent y timestamp
|
||||
- **Animación fluida** de ruleta con CSS transitions
|
||||
- **Efecto confeti** al ganar (canvas-confetti)
|
||||
- **Diseño responsive** con soporte de pantalla completa
|
||||
- **Redirección a formulario JotForm** para reclamar el premio
|
||||
|
||||
---
|
||||
|
||||
## 📦 Requisitos
|
||||
|
||||
| Componente | Versión mínima |
|
||||
|---|---|
|
||||
| PHP | 8.0+ con extensión `mysqli` |
|
||||
| MySQL / MariaDB | 5.7+ / 10.3+ |
|
||||
| Servidor Web | Apache (XAMPP) o Nginx |
|
||||
| Navegador | Cualquier navegador moderno con JS habilitado |
|
||||
|
||||
> La aplicación requiere acceso HTTP saliente a `https://api.ipify.org` para obtener la IP pública del usuario.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Instalación
|
||||
|
||||
### 1. Clonar el repositorio
|
||||
|
||||
```bash
|
||||
git clone https://git.digitalcompass.agency/Isaac_Aracena/ruleta-con-pin.git
|
||||
```
|
||||
|
||||
### 2. Colocar en el servidor web
|
||||
|
||||
- **Windows (XAMPP):** Copiar a `C:\xampp\htdocs\ruleta\`
|
||||
- **Linux (Apache/Nginx):** Copiar a `/var/www/ruleta/`
|
||||
|
||||
### 3. Configurar credenciales de BD
|
||||
|
||||
```bash
|
||||
cp config.example.php config.php
|
||||
```
|
||||
|
||||
Editar `config.php` con los datos reales:
|
||||
|
||||
```php
|
||||
$servername = "localhost";
|
||||
$username = "tu_usuario";
|
||||
$password = "tu_contraseña";
|
||||
$dbname = "ruleta_db";
|
||||
```
|
||||
|
||||
> ⚠️ **`config.php` está en `.gitignore`** y nunca se versiona. Las credenciales quedan solo en tu servidor.
|
||||
|
||||
### 4. Crear la base de datos
|
||||
|
||||
Ejecutar el script SQL incluido:
|
||||
|
||||
```bash
|
||||
mysql -u root -p < database/schema.sql
|
||||
```
|
||||
|
||||
### 5. Cargar premios y PINs
|
||||
|
||||
Ver sección [Configuración de PINs](#-configuración-de-pins).
|
||||
|
||||
### 6. Probar
|
||||
|
||||
Abrir `http://localhost/ruleta/index.html` e ingresar un PIN válido.
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ Configuración de Base de Datos
|
||||
|
||||
### Tablas
|
||||
|
||||
| Tabla | Descripción |
|
||||
|---|---|
|
||||
| `girospermitidos` | Control global de cupo de giros (1 registro, id=1) |
|
||||
| `premiosnew` | Premios con stock, probabilidad y PIN asignado |
|
||||
| `premiosentregados` | Auditoría de premios entregados |
|
||||
|
||||
### Ajustar cupo de giros
|
||||
|
||||
```sql
|
||||
UPDATE girospermitidos SET cantidad = 50000 WHERE id = 1;
|
||||
```
|
||||
|
||||
### Cargar un premio
|
||||
|
||||
```sql
|
||||
INSERT INTO premiosnew (nombre, probabilidad, cantidad, inicio, final, nombre_usuario)
|
||||
VALUES ('Gorra', 1.0, 10, 0, 45, '1989');
|
||||
```
|
||||
|
||||
- `inicio` / `final` = ángulos en la imagen de la ruleta (0–360°)
|
||||
- `nombre_usuario` = el PIN que podrá ganar este premio
|
||||
- `cantidad` = stock disponible
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Configuración de PINs
|
||||
|
||||
Los PINs deben estar sincronizados en **dos lugares**:
|
||||
|
||||
1. **Base de datos:** columna `premiosnew.nombre_usuario`
|
||||
2. **Frontend:** array `validPins` en `script.js` (línea 35)
|
||||
|
||||
```javascript
|
||||
const validPins = ["1989", "4708", "6823", "7591", ...];
|
||||
```
|
||||
|
||||
> Si un PIN está en la BD pero no en `validPins`, el modal del frontend no dejará pasar.
|
||||
> Si un PIN está en `validPins` pero no en la BD, el backend responderá "No hay premios disponibles".
|
||||
|
||||
---
|
||||
|
||||
## 📁 Estructura del Proyecto
|
||||
|
||||
```
|
||||
ruleta-con-pin/
|
||||
├── index.html # Página principal (modal PIN + ruleta)
|
||||
├── script.js # Lógica del juego, validación, animación
|
||||
├── style.css # Estilos, responsive, modales
|
||||
├── seleccionar_premio.php # Backend: selección aleatoria + registro BD
|
||||
├── config.example.php # Plantilla de configuración de BD
|
||||
├── config.php # ⛔ Credenciales reales (no versionado)
|
||||
├── database/
|
||||
│ └── schema.sql # Script de creación de BD y tablas
|
||||
└── img/
|
||||
├── background.png # Fondo de la página
|
||||
├── ruleta.png # Imagen de la ruleta
|
||||
├── puntero.png # Flecha/puntero de la ruleta
|
||||
├── fullscreen-icon.png
|
||||
├── BANNER-1.png # Logo GLM
|
||||
├── BANNER-2.png # Banner lateral
|
||||
└── BANNER-4.png # Banner inferior
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Flujo del Sistema
|
||||
|
||||
```
|
||||
Usuario → Ingresa PIN → Validación frontend (validPins)
|
||||
→ Muestra ruleta → Click en puntero → Obtiene IP (ipify)
|
||||
→ POST a seleccionar_premio.php
|
||||
→ Verifica cupo global (girospermitidos)
|
||||
→ Selecciona premio aleatorio ponderado (premiosnew)
|
||||
→ Descuenta stock + registra en premiosentregados
|
||||
→ Retorna JSON {id, nombre, inicio, final}
|
||||
→ Anima ruleta al ángulo del premio
|
||||
→ Confeti 🎊 + Modal "¡Ganaste!"
|
||||
→ Botón "Ir al formulario" → JotForm
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Operación Diaria
|
||||
|
||||
### Agregar PINs/premios
|
||||
|
||||
1. Insertar en `premiosnew` con el PIN deseado
|
||||
2. Agregar el PIN al array `validPins` en `script.js`
|
||||
|
||||
### Consultar premios entregados
|
||||
|
||||
```sql
|
||||
SELECT * FROM premiosentregados ORDER BY created_at DESC;
|
||||
```
|
||||
|
||||
### Consultar stock restante
|
||||
|
||||
```sql
|
||||
SELECT nombre, nombre_usuario, cantidad FROM premiosnew;
|
||||
```
|
||||
|
||||
### Aumentar cupo de giros
|
||||
|
||||
```sql
|
||||
UPDATE girospermitidos SET cantidad = cantidad + 1000 WHERE id = 1;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Solución de Problemas
|
||||
|
||||
| Problema | Causa |
|
||||
|---|---|
|
||||
| "Conexión fallida…" | Credenciales en `config.php` incorrectas o BD inaccesible |
|
||||
| "No hay premios disponibles" | No hay fila en `premiosnew` con ese PIN y `cantidad > 0` |
|
||||
| No pasa del modal de PIN | El PIN no está en `validPins` en `script.js` |
|
||||
| No registra IP | Navegador no puede acceder a `https://api.ipify.org` |
|
||||
| "Se ha alcanzado el límite de giros" | `girospermitidos.cantidad` llegó a 0 |
|
||||
|
||||
---
|
||||
|
||||
## 🛡️ Notas de Seguridad
|
||||
|
||||
- Las credenciales de BD se cargan desde `config.php` (no versionado)
|
||||
- Los `INSERT` usan **prepared statements** para prevenir SQL injection
|
||||
- La validación de PINs en el frontend es solo UX; la validación real ocurre en el backend vía `nombre_usuario` en la consulta SQL
|
||||
|
||||
---
|
||||
|
||||
## 📄 Licencia
|
||||
|
||||
Proyecto interno de **Gomez Lee Marketing**. Uso privado.
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
// ─── Configuración de Base de Datos ───
|
||||
// Copia este archivo como "config.php" y completa los valores reales.
|
||||
// config.php está excluido de git (.gitignore) para proteger credenciales.
|
||||
|
||||
$servername = "localhost"; // Host de la BD (IP o dominio)
|
||||
$username = "tu_usuario"; // Usuario de MySQL
|
||||
$password = "tu_contraseña"; // Contraseña de MySQL
|
||||
$dbname = "ruleta_db"; // Nombre de la base de datos
|
||||
@@ -0,0 +1,54 @@
|
||||
-- =========================================================
|
||||
-- RuletaFullConPIN — Esquema de Base de Datos
|
||||
-- =========================================================
|
||||
-- Ejecutar este script en MySQL/MariaDB para crear la
|
||||
-- estructura necesaria antes de usar la aplicación.
|
||||
-- =========================================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS ruleta_db
|
||||
CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE ruleta_db;
|
||||
|
||||
-- ---------------------------------------------------------
|
||||
-- 1. Control de giros global (un único registro, id = 1)
|
||||
-- ---------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS girospermitidos (
|
||||
id INT PRIMARY KEY,
|
||||
cantidad INT NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO girospermitidos (id, cantidad)
|
||||
VALUES (1, 100000) -- Ajusta el cupo según la campaña
|
||||
ON DUPLICATE KEY UPDATE cantidad = VALUES(cantidad);
|
||||
|
||||
-- ---------------------------------------------------------
|
||||
-- 2. Premios disponibles (vinculados a un PIN / usuario)
|
||||
-- ---------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS premiosnew (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nombre VARCHAR(190) NOT NULL, -- Nombre del premio a mostrar
|
||||
probabilidad DECIMAL(10,6) NOT NULL, -- Peso relativo (0..1 o cualquier peso)
|
||||
cantidad INT NOT NULL, -- Stock disponible
|
||||
inicio INT NOT NULL, -- Ángulo inicial en la ruleta (0-360)
|
||||
final INT NOT NULL, -- Ángulo final en la ruleta (0-360)
|
||||
nombre_usuario VARCHAR(64) NOT NULL -- PIN que puede ganar este premio
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------
|
||||
-- 3. Registro de premios entregados (auditoría)
|
||||
-- ---------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS premiosentregados (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
nombre_usuario VARCHAR(64) NOT NULL, -- PIN usado
|
||||
premio VARCHAR(190) NOT NULL, -- Nombre del premio ganado
|
||||
ip_address VARCHAR(64) NULL,
|
||||
user_agent VARCHAR(255) NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- ---------------------------------------------------------
|
||||
-- Ejemplo: Cargar un premio para el PIN "1989"
|
||||
-- ---------------------------------------------------------
|
||||
-- INSERT INTO premiosnew (nombre, probabilidad, cantidad, inicio, final, nombre_usuario)
|
||||
-- VALUES ('Gorra', 1.0, 10, 0, 45, '1989');
|
||||
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 64 KiB |
@@ -0,0 +1,60 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>GLM - Ruleta</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Banners en las posiciones 1, 2, 3 y 4 -->
|
||||
<a href="https://promo.gomezleemarketing.com/" target="_blank" class="banner banner-1"></a>
|
||||
<div class="banner banner-2"></div>
|
||||
<div class="banner banner-3"></div>
|
||||
<div class="banner banner-4"></div>
|
||||
|
||||
<!-- Contenido principal -->
|
||||
<div class="content">
|
||||
<h1></h1>
|
||||
<div class="game">
|
||||
<img id="ruleta" src="img/ruleta.png"/>
|
||||
<img id="puntero" src="img/puntero.png"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Botón de pantalla completa -->
|
||||
<div class="fullscreen-btn" id="fullscreen-btn"></div>
|
||||
|
||||
<!-- Mensaje de orientaci¨®n vertical -->
|
||||
<div class="landscape-message">
|
||||
Por favor, gira tu dispositivo para ver el contenido en horizontal.
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Pop-up modal -->
|
||||
<div id="popup-modal" class="popup-modal">
|
||||
<div class="popup-content">
|
||||
<h2>Favor de ingresar su PIN</h2>
|
||||
<form id="user-form">
|
||||
<label for="user-name">PIN:</label>
|
||||
<input type="text" id="user-name" name="user-name" required>
|
||||
|
||||
<button type="submit">Continuar</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nuevo popup modal de agradecimiento -->
|
||||
<div id="thank-you-modal" class="popup-modal" style="display: none;">
|
||||
<div class="popup-content">
|
||||
<h2 id="thank-you-message"></h2>
|
||||
<p id="thank-you-details"></p>
|
||||
<button id="redirect-button">Ir al formulario</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Script -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.5.1/dist/confetti.browser.min.js"></script>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,216 @@
|
||||
var ruleta = document.getElementById("ruleta");
|
||||
var puntero = document.getElementById("puntero");
|
||||
let gira = 0;
|
||||
let giraDos = 0;
|
||||
|
||||
const rangos = [
|
||||
{ articulo: "Freidora de aire", inicio: 330, final: 16 },
|
||||
{ articulo: "Articulo plastico", inicio: 15, final: 16 },
|
||||
{ articulo: "Sandwichera", inicio: 150, final: 196 },
|
||||
{ articulo: "Kits Protex", inicio: 170, final: 16 },
|
||||
{ articulo: "Cubeta", inicio: 20, final: 52 },
|
||||
{ articulo: "Lonchera", inicio: 42, final: 88 },
|
||||
{ articulo: "Recipiente plastico", inicio: 130, final: 160 },
|
||||
{ articulo: "Kits Cuidado Oral", inicio: 220, final: 232 },
|
||||
{ articulo: "Plancha", inicio: 150, final: 232 },
|
||||
{ articulo: "Caldero", inicio: 30, final: 194 },
|
||||
{ articulo: "Abanico de piso", inicio: 296, final: 328 },
|
||||
];
|
||||
|
||||
let articuloGanador = "";
|
||||
|
||||
// Variables para el modal y formulario
|
||||
const modal = document.getElementById("popup-modal");
|
||||
const form = document.getElementById("user-form");
|
||||
const userNameInput = document.getElementById("user-name");
|
||||
|
||||
let nombreUsuario = "";
|
||||
|
||||
// Mostrar el modal al cargar la página
|
||||
window.onload = function () {
|
||||
modal.style.display = "flex";
|
||||
};
|
||||
|
||||
// Lista de PINs válidos
|
||||
const validPins = ["1989", "4708", "6823", "7591", "3664", "3520", "4528", "9033", "3688", "6159", "2258"];
|
||||
|
||||
// Manejar el envío del formulario
|
||||
form.addEventListener("submit", function (e) {
|
||||
e.preventDefault();
|
||||
nombreUsuario = userNameInput.value.trim(); // Eliminar espacios en blanco
|
||||
|
||||
if (!nombreUsuario) {
|
||||
alert("Por favor, completa todos los campos.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validar si el PIN es válido
|
||||
if (!validPins.includes(nombreUsuario)) {
|
||||
alert("PIN inválido. Por favor, inténtelo de nuevo.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Si el PIN es válido, ocultar el modal y continuar
|
||||
modal.style.display = "none";
|
||||
document.querySelector(".content").style.display = "block"; // Mostrar el contenido principal
|
||||
});
|
||||
|
||||
// Calcular el ángulo intermedio de un rango
|
||||
function calcularAngulo(rango) {
|
||||
if (rango.inicio > rango.final) {
|
||||
return (rango.inicio + (360 - rango.inicio + rango.final) / 2) % 360;
|
||||
}
|
||||
return rango.inicio + (rango.final - rango.inicio) / 2;
|
||||
}
|
||||
|
||||
// Girar la ruleta a un artículo específico
|
||||
function girarRuletaAArticulo(nombreArticulo) {
|
||||
let rangoSeleccionado = rangos.find(
|
||||
(rango) => rango.articulo === nombreArticulo
|
||||
);
|
||||
|
||||
if (rangoSeleccionado) {
|
||||
let anguloFinal = calcularAngulo(rangoSeleccionado);
|
||||
gira = 1440 + anguloFinal; // 1440 asegura varias vueltas antes de detenerse en el artículo ganador
|
||||
ruleta.style.transition = "all 5s ease-out";
|
||||
ruleta.style.transform = `rotate(${gira}deg)`;
|
||||
} else {
|
||||
console.log("Artículo no encontrado.");
|
||||
}
|
||||
}
|
||||
|
||||
// Fin de la rodada
|
||||
ruleta.addEventListener("transitionend", function () {
|
||||
puntero.style.pointerEvents = "auto"; // Reactivar clics
|
||||
ruleta.style.transition = "none";
|
||||
giraDos = gira % 360;
|
||||
ruleta.style.transform = `rotate(${giraDos}deg)`;
|
||||
|
||||
mostrarConfetiConTexto(articuloGanador);
|
||||
});
|
||||
|
||||
// Obtener dirección IP
|
||||
async function obtenerIP() {
|
||||
const response = await fetch("https://api.ipify.org?format=json");
|
||||
const data = await response.json();
|
||||
return data.ip;
|
||||
}
|
||||
|
||||
// Obtener artículo ganador
|
||||
async function obtenerArticuloGanador() {
|
||||
const userAgent = navigator.userAgent;
|
||||
const ipAddress = await obtenerIP();
|
||||
|
||||
fetch("seleccionar_premio.php", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user_agent: userAgent,
|
||||
ip_address: ipAddress,
|
||||
nombre_usuario: nombreUsuario,
|
||||
}),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
articuloGanador = data.nombre;
|
||||
girarRuletaAArticulo(articuloGanador);
|
||||
})
|
||||
.catch((error) =>
|
||||
console.error("Error al obtener el artículo ganador:", error)
|
||||
);
|
||||
}
|
||||
|
||||
// Bloquear interacción con la ruleta mientras gira
|
||||
puntero.addEventListener("mousedown", function () {
|
||||
if (!nombreUsuario) {
|
||||
alert("Por favor, completa el formulario antes de continuar.");
|
||||
return;
|
||||
}
|
||||
|
||||
puntero.style.pointerEvents = "none"; // Bloquear clics
|
||||
obtenerArticuloGanador();
|
||||
});
|
||||
|
||||
// Mostrar confeti con texto
|
||||
function mostrarConfetiConTexto(texto) {
|
||||
const duration = 5 * 1000;
|
||||
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);
|
||||
}
|
||||
})();
|
||||
|
||||
console.log("¡Felicidades! Ganaste: " + texto);
|
||||
}
|
||||
|
||||
const fullscreenBtn = document.getElementById("fullscreen-btn");
|
||||
|
||||
fullscreenBtn.addEventListener("click", () => {
|
||||
if (!document.fullscreenElement) {
|
||||
document.documentElement.requestFullscreen().catch((err) => {
|
||||
console.error(
|
||||
`Error al intentar entrar en pantalla completa: ${err.message}`
|
||||
);
|
||||
});
|
||||
} else {
|
||||
document.exitFullscreen().catch((err) => {
|
||||
console.error(
|
||||
`Error al intentar salir de pantalla completa: ${err.message}`
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Mostrar el popup de agradecimiento
|
||||
function mostrarMensajeAgradecimiento() {
|
||||
const modal = document.getElementById("thank-you-modal");
|
||||
const mensaje = document.getElementById("thank-you-message");
|
||||
const detalles = document.getElementById("thank-you-details");
|
||||
const botonRedirigir = document.getElementById("redirect-button");
|
||||
|
||||
// Personalizar contenido del mensaje
|
||||
mensaje.textContent = `🎊🎉¡Felicidades!🎉🎊`;
|
||||
detalles.textContent = `🎁 Has ganado "${articuloGanador}" 🎁`;
|
||||
|
||||
modal.style.display = "flex"; // Mostrar el modal
|
||||
|
||||
botonRedirigir.addEventListener("click", () => {
|
||||
// Abre el enlace en una nueva pestaña
|
||||
window.open("https://form.jotform.com/250134957677669", "_blank");
|
||||
|
||||
// Actualiza la pestaña actual
|
||||
window.location.href = window.location.href; // Redirige a la misma URL actual
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Fin de la rodada
|
||||
ruleta.addEventListener("transitionend", function () {
|
||||
puntero.style.pointerEvents = "auto"; // Reactivar clics
|
||||
ruleta.style.transition = "none";
|
||||
giraDos = gira % 360;
|
||||
ruleta.style.transform = `rotate(${giraDos}deg)`;
|
||||
|
||||
mostrarConfetiConTexto(articuloGanador); // Mostrar confeti
|
||||
mostrarMensajeAgradecimiento(); // Mostrar mensaje de agradecimiento
|
||||
});
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
// ─── Configuración de Base de Datos ───
|
||||
// Carga las credenciales desde config.php (no versionado en git).
|
||||
// Si no existe, usa valores por defecto para desarrollo local.
|
||||
if (file_exists(__DIR__ . '/config.php')) {
|
||||
require_once __DIR__ . '/config.php';
|
||||
} else {
|
||||
$servername = "localhost";
|
||||
$username = "root";
|
||||
$password = "";
|
||||
$dbname = "ruleta_db";
|
||||
}
|
||||
|
||||
// Crear la conexión
|
||||
$conn = new mysqli($servername, $username, $password, $dbname);
|
||||
|
||||
// Verificar la conexión
|
||||
if ($conn->connect_error) {
|
||||
die(json_encode(["error" => "Conexión fallida: " . $conn->connect_error]));
|
||||
}
|
||||
|
||||
// Obtener la IP del cliente en el servidor
|
||||
$ip_address = $_SERVER['REMOTE_ADDR'];
|
||||
|
||||
// Recibir los datos enviados desde el frontend
|
||||
$data = json_decode(file_get_contents("php://input"), true);
|
||||
$user_agent = $data['user_agent'] ?? null;
|
||||
$nombre_usuario = $data['nombre_usuario'] ?? null;
|
||||
|
||||
// Consultar la cantidad de giros disponibles en la tabla limite_giros
|
||||
$sqlLimite = "SELECT cantidad FROM girospermitidos WHERE id = 1"; // Se asume un único registro de control
|
||||
$resultLimite = $conn->query($sqlLimite);
|
||||
|
||||
if ($resultLimite->num_rows > 0) {
|
||||
$rowLimite = $resultLimite->fetch_assoc();
|
||||
$cantidadGirosDisponibles = $rowLimite['cantidad'];
|
||||
|
||||
if ($cantidadGirosDisponibles <= 0) {
|
||||
echo json_encode(["error" => "Se ha alcanzado el límite de giros"]);
|
||||
$conn->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
// Restar un giro al contador global
|
||||
$stmtGiros = $conn->prepare("UPDATE girospermitidos SET cantidad = cantidad - 1 WHERE id = 1");
|
||||
if ($stmtGiros) {
|
||||
$stmtGiros->execute();
|
||||
$stmtGiros->close();
|
||||
} else {
|
||||
echo json_encode(["error" => "Error al actualizar el límite de giros"]);
|
||||
$conn->close();
|
||||
exit();
|
||||
}
|
||||
} else {
|
||||
echo json_encode(["error" => "No se encontró el registro de límite de giros"]);
|
||||
$conn->close();
|
||||
exit();
|
||||
}
|
||||
|
||||
// Consultar los premios con cantidad mayor que 0
|
||||
$sql = "SELECT id, nombre, probabilidad, cantidad, inicio, final FROM premiosnew WHERE cantidad > 0 AND nombre_usuario = '$nombre_usuario'";
|
||||
$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) {
|
||||
// Actualizar la cantidad del premio seleccionado
|
||||
$stmt = $conn->prepare("UPDATE premiosnew SET cantidad = cantidad - 1 WHERE id = ?");
|
||||
if ($stmt) {
|
||||
$stmt->bind_param("i", $premio['id']);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
|
||||
// Agregar el premio y nombre a la tabla premios
|
||||
$stmtInsert = $conn->prepare(
|
||||
"INSERT INTO premiosentregados (nombre_usuario, premio, ip_address, user_agent)
|
||||
VALUES (?, ?, ?, ?)"
|
||||
);
|
||||
if ($stmtInsert) {
|
||||
$stmtInsert->bind_param(
|
||||
"ssss",
|
||||
$nombre_usuario,
|
||||
$premio['nombre'],
|
||||
$ip_address,
|
||||
$user_agent
|
||||
);
|
||||
$stmtInsert->execute();
|
||||
$stmtInsert->close();
|
||||
} else {
|
||||
echo json_encode(["error" => "Error al guardar el participante"]);
|
||||
exit();
|
||||
}
|
||||
|
||||
// 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();
|
||||
?>
|
||||
@@ -0,0 +1,282 @@
|
||||
/* Estilos generales existentes */
|
||||
body, html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #C10F1A url(img/background.png) repeat center center; /* Imagen centrada y no repetida */
|
||||
background-size: cover; /* Hace que la imagen se ajuste al contenedor sin recortarse */
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
overflow: hidden; /* Evitar desplazamiento */
|
||||
}
|
||||
|
||||
.content {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 48px;
|
||||
color: #ffffff;
|
||||
text-shadow:
|
||||
4px 4px 0 #C10F1A;
|
||||
}
|
||||
|
||||
.game {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-left: -400px;
|
||||
}
|
||||
|
||||
#ruleta {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
#puntero {
|
||||
position: absolute;
|
||||
top: 47%;
|
||||
left: 50%;
|
||||
width: 300px;
|
||||
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 !important;
|
||||
z-index: 100000;
|
||||
}
|
||||
|
||||
#ruleta, #puntero, .content, .game {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.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: 10%;
|
||||
right: 5%;
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
background-image: url('img/BANNER-2.png');
|
||||
}
|
||||
|
||||
.popup-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;
|
||||
}
|
||||
|
||||
.popup-content {
|
||||
background: #C10F1A;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
text-align: center;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.popup-content h2 {
|
||||
margin-bottom: 15px;
|
||||
font-size: 24px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
#user-form label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: bold;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
#user-form input, #user-form select, #user-form button {
|
||||
width: 50%;
|
||||
padding: 10px;
|
||||
margin-bottom: 15px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
}
|
||||
|
||||
#user-form button {
|
||||
background-color: #007BFF;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#user-form button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*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;
|
||||
}
|
||||