Auto commit: 2026-02-17T19:12:57.521Z

This commit is contained in:
Flatlogic Bot 2026-02-17 19:12:57 +00:00
parent d10f6425ae
commit 9a708e280e
4 changed files with 362 additions and 0 deletions

33
api/get_active_perks.php Normal file
View File

@ -0,0 +1,33 @@
<?php
require_once __DIR__ . "/../db/config.php";
header("Content-Type: application/json");
try {
$pdo = db();
$now = date("Y-m-d H:i:s");
// Get latest active perk for each type
$stmt = $pdo->prepare("
SELECT perk_type, perk_value, user_name, expires_at
FROM shop_perks
WHERE expires_at > ?
ORDER BY created_at DESC
");
$stmt->execute([$now]);
$all_active = $stmt->fetchAll();
$perks = [
"pinned_message" => null,
"background" => null
];
foreach ($all_active as $p) {
if (!$perks[$p["perk_type"]]) {
$perks[$p["perk_type"]] = $p;
}
}
echo json_encode(["success" => true, "perks" => $perks]);
} catch (Exception $e) {
echo json_encode(["success" => false, "error" => $e->getMessage()]);
}

55
api/shop.php Normal file
View File

@ -0,0 +1,55 @@
<?php
require_once __DIR__ . "/../db/config.php";
header("Content-Type: application/json");
$pdo = db();
$action = $_POST["action"] ?? "";
$username = $_POST["username"] ?? "";
$value = $_POST["value"] ?? "";
if (!$username) {
echo json_encode(["success" => false, "error" => "Inicia sesión para comprar"]);
exit;
}
$perks = [
"pinned_message" => ["cost" => 500, "duration" => 3600], // 1 hour
"background" => ["cost" => 1000, "duration" => 1800], // 30 mins
];
if (!isset($perks[$action])) {
echo json_encode(["success" => false, "error" => "Producto no encontrado"]);
exit;
}
$perk = $perks[$action];
try {
$pdo->beginTransaction();
// Check points
$stmt = $pdo->prepare("SELECT points FROM fans WHERE name = ?");
$stmt->execute([$username]);
$user = $stmt->fetch();
if (!$user || $user["points"] < $perk["cost"]) {
echo json_encode(["success" => false, "error" => "No tienes suficientes puntos (" . $perk["cost"] . " requeridos)"]);
$pdo->rollBack();
exit;
}
// Deduct points
$stmt = $pdo->prepare("UPDATE fans SET points = points - ? WHERE name = ?");
$stmt->execute([$perk["cost"], $username]);
// Activate perk
$expires_at = date("Y-m-d H:i:s", time() + $perk["duration"]);
$stmt = $pdo->prepare("INSERT INTO shop_perks (perk_type, perk_value, user_name, expires_at) VALUES (?, ?, ?, ?)");
$stmt->execute([$action, $value, $username, $expires_at]);
$pdo->commit();
echo json_encode(["success" => true, "message" => "¡Compra exitosa! Perk activado."]);
} catch (Exception $e) {
$pdo->rollBack();
echo json_encode(["success" => false, "error" => $e->getMessage()]);
}

View File

@ -0,0 +1,9 @@
-- Migration: Create shop_perks table
CREATE TABLE IF NOT EXISTS shop_perks (
id INT AUTO_INCREMENT PRIMARY KEY,
perk_type VARCHAR(50) NOT NULL, -- "pinned_message", "background"
perk_value TEXT NOT NULL, -- The message text or the image URL
user_name VARCHAR(255) NOT NULL,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

265
index.php
View File

@ -1291,6 +1291,107 @@ $twitter_link = "https://twitter.com/";
gap: 5px;
font-weight: 700;
}
/* Shop Styles */
.shop-modal {
display: none;
position: fixed;
z-index: 4000;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.8);
backdrop-filter: blur(10px);
justify-content: center;
align-items: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.shop-modal.show {
display: flex;
opacity: 1;
}
.shop-modal-content {
background: rgba(20, 20, 20, 0.9);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 2.5rem;
border-radius: 32px;
width: 90%;
max-width: 500px;
transform: scale(0.8);
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
}
.shop-modal.show .shop-modal-content {
transform: scale(1);
}
.shop-item {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 1.2rem;
border-radius: 20px;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 15px;
transition: all 0.2s;
}
.shop-item:hover {
background: rgba(255, 255, 255, 0.1);
border-color: var(--primary-color);
}
.shop-item-icon {
width: 50px;
height: 50px;
background: rgba(56, 189, 248, 0.1);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
color: var(--primary-color);
}
.shop-item-info {
flex: 1;
}
.shop-item-name {
font-weight: 800;
font-size: 1rem;
color: white;
}
.shop-item-desc {
font-size: 0.75rem;
opacity: 0.6;
}
.shop-item-price {
font-weight: 800;
color: #facc15;
display: flex;
align-items: center;
gap: 4px;
}
.pinned-message-container {
background: linear-gradient(90deg, rgba(56, 189, 248, 0.2), rgba(0, 200, 83, 0.2));
border: 1px solid var(--primary-color);
border-radius: 12px;
padding: 0.8rem;
margin-bottom: 1rem;
display: none;
animation: fadeIn 0.5s ease;
position: relative;
}
.pinned-label {
font-size: 0.6rem;
font-weight: 800;
color: var(--primary-color);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 4px;
}
</style>
</head>
<body>
@ -1318,6 +1419,9 @@ $twitter_link = "https://twitter.com/";
<div class="glass-card">
<header class="brand">
<div style="position: absolute; top: 1rem; right: 1rem; display: flex; gap: 0.5rem; z-index: 100;">
<button onclick="openShopModal()" style="background: linear-gradient(135deg, #facc15, #fbbf24); border: none; color: #000; width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 0 15px rgba(250, 204, 21, 0.4); transition: all 0.3s; z-index: 101;" title="Tienda de la Radio">
<i class="bi bi-shop"></i>
</button>
<button onclick="toggleTheme()" id="theme-toggle" style="background: var(--glass-bg); border: 1px solid var(--glass-border); color: var(--text-color); width: 40px; height: 40px; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; backdrop-filter: blur(5px); transition: all 0.3s;" title="Cambiar tema">
<i class="bi bi-moon-fill"></i>
</button>
@ -1529,6 +1633,11 @@ $twitter_link = "https://twitter.com/";
<h3 style="font-size: 1.2rem; margin-bottom: 1rem; color: var(--primary-color);">
<i class="bi bi-chat-dots-fill"></i> CHAT EN VIVO
</h3>
<div id="pinned-message" class="pinned-message-container">
<div class="pinned-label"><i class="bi bi-pin-angle-fill"></i> MENSAJE FIJADO</div>
<div id="pinned-content" style="font-size: 0.85rem; font-weight: 600;"></div>
<div id="pinned-author" style="font-size: 0.65rem; opacity: 0.7; margin-top: 4px;"></div>
</div>
<div id="chat-messages" style="flex: 1; overflow-y: auto; margin-bottom: 1rem; padding-right: 5px; display: flex; flex-direction: column; gap: 0.8rem;">
<!-- Mensajes se cargarán aquí -->
<div style="opacity: 0.5; font-size: 0.9rem; text-align: center; margin-top: 2rem;">Cargando mensajes...</div>
@ -1736,6 +1845,40 @@ $twitter_link = "https://twitter.com/";
</div>
</div>
<!-- Shop Modal -->
<div id="shop-modal" class="shop-modal" onclick="closeShopModal()">
<div class="shop-modal-content" onclick="event.stopPropagation()">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
<h2 style="margin: 0; color: #facc15;"><i class="bi bi-shop"></i> Tienda Radio</h2>
<div id="shop-user-points" style="background: rgba(250, 204, 21, 0.2); color: #facc15; padding: 5px 15px; border-radius: 20px; font-weight: 800; font-size: 0.9rem;">
0 pts
</div>
</div>
<p style="opacity: 0.7; font-size: 0.9rem; margin-bottom: 2rem;">Gasta tus puntos en perks globales para todos los oyentes.</p>
<div class="shop-item" onclick="promptBuyPinnedMessage()">
<div class="shop-item-icon"><i class="bi bi-pin-angle-fill"></i></div>
<div class="shop-item-info">
<div class="shop-item-name">Mensaje Fijado</div>
<div class="shop-item-desc">Tu mensaje al inicio del chat por 1 hora.</div>
</div>
<div class="shop-item-price">500 <i class="bi bi-lightning-fill"></i></div>
</div>
<div class="shop-item" onclick="promptBuyBackground()">
<div class="shop-item-icon"><i class="bi bi-image-fill"></i></div>
<div class="shop-item-info">
<div class="shop-item-name">Cambiar Fondo</div>
<div class="shop-item-desc">Cambia el fondo de la radio por 30 min.</div>
</div>
<div class="shop-item-price">1000 <i class="bi bi-lightning-fill"></i></div>
</div>
<button onclick="closeShopModal()" style="width: 100%; margin-top: 1.5rem; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); color: white; padding: 0.8rem; border-radius: 12px; cursor: pointer; font-weight: 700;">Cerrar Tienda</button>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.6.0/dist/confetti.browser.min.js"></script>
<audio id="radio-audio" src="https://listen.radioking.com/radio/828046/stream/897251" preload="auto" crossorigin="anonymous"></audio>
<audio id="welcome-sound" src="https://assets.mixkit.co/active_storage/sfx/2013/2013-preview.mp3" preload="auto"></audio>
@ -2684,6 +2827,10 @@ $twitter_link = "https://twitter.com/";
const bar = document.getElementById('user-level-bar');
const rewards = document.getElementById('unlocked-rewards-msg');
// Update shop points display
const shopPoints = document.getElementById('shop-user-points');
if (shopPoints) shopPoints.innerText = `${parseInt(fan.total_likes).toLocaleString()} pts`;
container.style.display = 'block';
badge.innerText = `Nivel ${fan.level}`;
badge.style.background = fan.level_color;
@ -2999,6 +3146,124 @@ $twitter_link = "https://twitter.com/";
if (!audio.paused) audio.play();
}, 5000);
});
// --- Radio Shop Functionality ---
function openShopModal() {
const modal = document.getElementById('shop-modal');
const userName = document.getElementById('user-name').value.trim();
if (!userName) {
alert('Por favor, ingresa tu nombre arriba para acceder a la tienda.');
document.getElementById('user-name').focus();
document.getElementById('user-name').style.borderColor = '#facc15';
return;
}
modal.style.display = 'flex';
setTimeout(() => modal.classList.add('show'), 10);
fetchLeaderboard(); // This updates user points
}
function closeShopModal() {
const modal = document.getElementById('shop-modal');
modal.classList.remove('show');
setTimeout(() => modal.style.display = 'none', 300);
}
async function fetchActivePerks() {
try {
const response = await fetch('api/get_active_perks.php');
const data = await response.json();
if (data.success) {
// Pinned Message
const pinned = data.perks.pinned_message;
const pinnedEl = document.getElementById('pinned-message');
if (pinned) {
document.getElementById('pinned-content').innerText = pinned.perk_value;
document.getElementById('pinned-author').innerText = `Por: ${pinned.user_name}`;
pinnedEl.style.display = 'block';
} else {
pinnedEl.style.display = 'none';
}
// Background
const background = data.perks.background;
if (background && bg) {
bg.style.backgroundImage = `url('${background.perk_value}')`;
}
}
} catch (error) {
console.error('Error fetching perks:', error);
}
}
async function buyPerk(action, value) {
const userName = document.getElementById('user-name').value.trim();
try {
const formData = new FormData();
formData.append('action', action);
formData.append('username', userName);
formData.append('value', value);
const response = await fetch('api/shop.php', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
confetti({
particleCount: 150,
spread: 70,
origin: { y: 0.6 },
colors: ['#facc15', '#fbbf24', '#ffffff']
});
alert(result.message);
closeShopModal();
fetchActivePerks();
fetchLeaderboard();
} else {
alert('Error: ' + result.error);
}
} catch (error) {
console.error('Buy error:', error);
alert('Error de conexión al realizar la compra.');
}
}
function promptBuyPinnedMessage() {
const msg = prompt('Escribe el mensaje que quieres fijar (máx 100 carac.):');
if (msg && msg.trim()) {
if (msg.length > 100) {
alert('El mensaje es demasiado largo.');
return;
}
buyPerk('pinned_message', msg.trim());
}
}
function promptBuyBackground() {
const query = prompt('¿De qué quieres que sea el nuevo fondo? (ej: cyberpunk, ocean, tropical):');
if (query && query.trim()) {
// Fetch image from pexels first
fetch(`api/pexels.php?query=${encodeURIComponent(query.trim())}`)
.then(res => res.json())
.then(data => {
if (data.success && data.url) {
if (confirm('¿Te gusta este fondo? Lo activaremos para todos por 1000 pts.')) {
buyPerk('background', data.url);
}
} else {
alert('No encontramos una imagen para esa búsqueda.');
}
});
}
}
setInterval(fetchActivePerks, 10000);
fetchActivePerks();
// --- End Radio Shop Functionality ---
</script>
</body>
</html>