Commit ebf076a5 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(chess): ad banner in top space, emote inline with player bar

- Add ad banner slot in the dead space above the board
- Move emote toggle button inline with player profile data
- Emote panel now opens below player bar (no more fixed overlay)
- Reduce top spacer to flex:0.4 so game sits near-center (slightly bottom-biased)
- Add api/ads.php endpoint to serve active campaigns from ad_campaigns table
- Ad system tracks impressions and filters by slot/game targeting
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 622d1454
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
require_once __DIR__ . '/../includes/supabase.php';
$input = json_decode(file_get_contents('php://input'), true) ?? [];
$action = $input['action'] ?? '';
$db = supabaseService();
switch ($action) {
case 'get':
$slot = $input['slot'] ?? 'banner_top';
$game = $input['game'] ?? '';
$params = [
'select' => 'id,name,image_url,click_url,target_slots',
'status' => 'eq.active',
'order' => 'priority.desc,created_at.desc',
'limit' => '5'
];
$campaigns = $db->get('ad_campaigns', $params);
if (empty($campaigns) || isset($campaigns['error'])) {
echo json_encode(['error' => 'no_ads']);
exit;
}
// Filter by slot and game targeting
$matched = null;
foreach ($campaigns as $c) {
$slots = $c['target_slots'] ?? [];
if (is_string($slots)) {
$slots = array_filter(explode(',', trim($slots, '{}')));
}
if (!empty($slots) && !in_array($slot, $slots)) continue;
$games = $c['target_games'] ?? [];
if (is_string($games)) {
$games = array_filter(explode(',', trim($games, '{}')));
}
if (!empty($games) && $game && !in_array($game, $games)) continue;
$matched = $c;
break;
}
if (!$matched) {
echo json_encode(['error' => 'no_ads']);
exit;
}
echo json_encode([
'id' => $matched['id'],
'image_url' => $matched['image_url'] ?? '',
'click_url' => $matched['click_url'] ?? '',
], JSON_UNESCAPED_UNICODE);
break;
case 'impression':
$campaignId = $input['campaign_id'] ?? null;
if ($campaignId) {
$campaign = $db->get('ad_campaigns', ['id' => "eq.{$campaignId}", 'select' => 'id,impressions']);
if (!empty($campaign) && !isset($campaign['error']) && isset($campaign[0])) {
$current = (int)($campaign[0]['impressions'] ?? 0);
$db->update('ad_campaigns', ['impressions' => $current + 1], ['id' => "eq.{$campaignId}"]);
}
}
echo json_encode(['ok' => true]);
break;
default:
echo json_encode(['error' => 'Invalid action']);
}
......@@ -23,7 +23,6 @@ export function create(container, onSend) {
emoteBar = document.createElement('div');
emoteBar.className = 'emote-bar';
emoteBar.innerHTML = `
<button class="emote-toggle" id="emote-toggle">💬</button>
<div class="emote-panel hidden" id="emote-panel">
${EMOTES.map(e => `
<button class="emote-btn" data-key="${e.key}" title="${e.label}">
......@@ -35,10 +34,8 @@ export function create(container, onSend) {
const style = document.createElement('style');
style.textContent = `
.emote-bar { position:fixed;bottom:110px;right:8px;z-index:30;display:flex;flex-direction:column;align-items:flex-end;gap:6px; }
.emote-toggle { width:38px;height:38px;border-radius:50%;background:#1e1e3a;border:1px solid rgba(255,255,255,0.1);color:#f8fafc;font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:transform 0.15s,background 0.15s;box-shadow:0 2px 8px rgba(0,0,0,0.3); }
.emote-toggle:active { transform:scale(0.9);background:#2a2a5a; }
.emote-panel { display:flex;flex-wrap:wrap;gap:6px;padding:8px;background:#1e1e3a;border-radius:12px;border:1px solid rgba(255,255,255,0.08);box-shadow:0 4px 20px rgba(0,0,0,0.5);animation:slideUpBounce 0.3s cubic-bezier(0.16,1,0.3,1);max-width:200px; }
.emote-bar { position:relative;z-index:30; }
.emote-panel { display:flex;flex-wrap:wrap;gap:6px;padding:8px 12px;background:#1e1e3a;border-radius:12px;border:1px solid rgba(255,255,255,0.08);box-shadow:0 4px 20px rgba(0,0,0,0.5);animation:slideUpBounce 0.3s cubic-bezier(0.16,1,0.3,1);max-width:100%; }
.emote-panel.hidden { display:none; }
.emote-btn { width:40px;height:40px;border-radius:8px;background:rgba(255,255,255,0.05);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:transform 0.1s,background 0.15s; }
.emote-btn:hover { background:rgba(255,255,255,0.1); }
......@@ -50,11 +47,14 @@ export function create(container, onSend) {
container.appendChild(style);
container.appendChild(emoteBar);
// Toggle panel
emoteBar.querySelector('#emote-toggle').addEventListener('click', () => {
const panel = emoteBar.querySelector('#emote-panel');
panel.classList.toggle('hidden');
});
// Use the inline toggle button already in the player bar
const inlineToggle = container.querySelector('#emote-inline-toggle');
if (inlineToggle) {
inlineToggle.addEventListener('click', () => {
const panel = emoteBar.querySelector('#emote-panel');
panel.classList.toggle('hidden');
});
}
// Emote buttons
emoteBar.querySelectorAll('.emote-btn').forEach(btn => {
......
......@@ -98,8 +98,14 @@ export function mountGame(el, params) {
el.innerHTML = `
<div class="chess-layout" style="display:flex;flex-direction:column;height:100%;background:#1a1a2e;">
<!-- Spacer pushes everything down on tall screens -->
<div style="flex:1;"></div>
<!-- Ad Banner (top dead space) -->
<div id="ad-banner-top" style="flex:0 0 auto;min-height:50px;display:flex;align-items:center;justify-content:center;padding:6px 12px;background:#0a0a1a;cursor:pointer;" onclick="window.__adClick && window.__adClick()">
<div id="ad-content" style="width:100%;max-width:400px;height:50px;border-radius:8px;background:linear-gradient(135deg,#1e1e3a,#2a2a5a);display:flex;align-items:center;justify-content:center;overflow:hidden;border:1px solid rgba(255,255,255,0.05);">
<span style="font-size:11px;color:#475569;">إعلان</span>
</div>
</div>
<!-- Spacer — near center, slightly bottom-biased -->
<div style="flex:0.4;"></div>
<!-- Opponent Bar (compact — keep minimal at top) -->
<div class="chess-bar" style="display:flex;align-items:center;justify-content:space-between;padding:4px 12px;background:#0f0f1e;">
<div style="display:flex;align-items:center;gap:8px;">
......@@ -125,7 +131,7 @@ export function mountGame(el, params) {
<div id="promo-dialog" style="display:none;position:absolute;z-index:20;background:#1e1e3a;border-radius:8px;padding:8px;box-shadow:0 8px 32px rgba(0,0,0,0.8);border:1px solid rgba(255,255,255,0.1);"></div>
</div>
<!-- Player Bar -->
<!-- Player Bar + Emote Toggle -->
<div class="chess-bar" style="display:flex;align-items:center;justify-content:space-between;padding:6px 12px;background:#0f0f1e;">
<div style="display:flex;align-items:center;gap:8px;">
<div style="width:34px;height:34px;border-radius:50%;background:#2a2a4a;display:flex;align-items:center;justify-content:center;overflow:hidden;border:2px solid var(--gold);">
......@@ -138,6 +144,7 @@ export function mountGame(el, params) {
<div id="player-captured" style="font-size:11px;color:#94a3b8;letter-spacing:1px;"></div>
</div>
</div>
<button id="emote-inline-toggle" style="width:32px;height:32px;border-radius:50%;background:#1e1e3a;border:1px solid rgba(255,255,255,0.1);color:#f8fafc;font-size:14px;cursor:pointer;display:flex;align-items:center;justify-content:center;margin-inline-start:4px;">💬</button>
</div>
<div id="clock-player" class="chess-clock" style="font-size:18px;font-weight:700;font-family:Inter,monospace;background:#1e1e3a;padding:4px 12px;border-radius:6px;color:#f8fafc;min-width:60px;text-align:center;">${clock.format(tc.time)}</div>
</div>
......@@ -303,16 +310,24 @@ export function mountGame(el, params) {
});
}
// Mount emote panel right after the player bar (second .chess-bar)
const playerBar = el.querySelectorAll('.chess-bar')[1];
const emoteContainer = el.querySelector('.chess-layout');
emoteSystem.create(emoteContainer, (emote) => {
audio.play('notification');
const myBar = el.querySelectorAll('.chess-bar')[1];
emoteSystem.showReceived(emoteContainer, emote.emoji, myBar);
// Sync to opponent in live mode
emoteSystem.showReceived(emoteContainer, emote.emoji, playerBar);
if (gameState.mode === 'live' && matchId) {
mp.sendEmote(matchId, 'chess', emote.key);
}
});
// Move emote panel to sit right after player bar
const emotePanel = emoteContainer.querySelector('.emote-bar');
if (emotePanel && playerBar && playerBar.nextSibling) {
emoteContainer.insertBefore(emotePanel, playerBar.nextSibling);
}
// Load ad banner
loadAdBanner(el);
bus.emit('game:started', { gameKey: 'chess', matchId, opponent: botId, mode });
}
......@@ -843,3 +858,17 @@ function endGame(result, reason) {
}, 1000);
});
}
async function loadAdBanner(el) {
try {
const res = await net.post('ads.php', { action: 'get', slot: 'banner_top', game: 'chess' });
if (!res || res.error || !res.image_url) return;
const banner = el.querySelector('#ad-content');
if (!banner) return;
banner.innerHTML = `<img src="${res.image_url}" style="width:100%;height:100%;object-fit:cover;border-radius:8px;">`;
window.__adClick = () => {
if (res.click_url) window.open(res.click_url, '_blank');
net.post('ads.php', { action: 'impression', campaign_id: res.id }).catch(() => {});
};
} catch (e) { /* ad load failed silently */ }
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment