Commit 868682de authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: bigger game menu, in-game emotes, full tournament system with Swiss pairings

Game Menu:
- Buttons: 72px min-height (was 52), 20px padding, 48px icons
- Feature chips: 40px min-height, 13px font, 10px gaps
- Menu takes up to 75vh for better visibility

In-Game Emotes:
- 💬 toggle button in bottom-right during chess game
- 8 preset emotes: GG, Good Move, Think, Hurry, Wow, Laugh, Angry, Hello
- 3-second cooldown between sends
- Floating emoji animation when received
- Sound feedback on send

Tournament System:
- New API: /api/swiss.php (tournament, standings, rounds, pairings, my-games)
- Integrates with Swiss API at swissapi.caprover.al-arcade.com
- Tournament Detail scene with 4 tabs:
  - Info: stats grid, prize pool, registration button
  - Standings: ranked player list with score + buchholz
  - Rounds: expandable rounds → click to load pairings
  - My Games: player's own tournament matches
- Syncs with el3ab-management SwissApiService endpoints
- Tournament cards in list now navigate to full detail view
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 00e476cc
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
require_once __DIR__ . '/../includes/auth.php';
require_once __DIR__ . '/../config/constants.php';
$action = $_GET['action'] ?? (getInput()['action'] ?? '');
switch ($action) {
case 'tournament':
getTournament();
break;
case 'standings':
getStandings();
break;
case 'rounds':
getRounds();
break;
case 'pairings':
getPairings();
break;
case 'my-games':
getMyGames();
break;
default:
jsonError('Invalid action');
}
function swissApi(string $method, string $path, ?array $body = null): array {
$url = SWISS_API . $path;
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
if ($method === 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
if ($body) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$data = json_decode($response, true);
if ($httpCode >= 400) {
return ['error' => $data['message'] ?? 'Swiss API error', 'code' => $httpCode];
}
return $data ?? [];
}
function getTournament(): void {
$id = $_GET['id'] ?? '';
if (!$id) jsonError('id required');
// Get from Supabase
require_once __DIR__ . '/../includes/supabase.php';
$db = supabaseService();
$tournaments = $db->get('el3ab_tournaments', ['id' => 'eq.' . $id, 'limit' => 1]);
$tournament = is_array($tournaments) && !empty($tournaments) && !isset($tournaments['error']) ? $tournaments[0] : null;
if (!$tournament) jsonError('Tournament not found', 404);
// Get Swiss API data if linked
if (!empty($tournament['swiss_api_tournament_id'])) {
$swiss = swissApi('GET', '/tournaments/' . $tournament['swiss_api_tournament_id']);
if (!isset($swiss['error'])) {
$tournament['swiss_data'] = $swiss;
}
}
// Get registrations count
$regs = $db->get('tournament_registrations', ['tournament_id' => 'eq.' . $id, 'status' => 'eq.registered', 'select' => 'id']);
$tournament['player_count'] = is_array($regs) && !isset($regs['error']) ? count($regs) : 0;
jsonResponse($tournament);
}
function getStandings(): void {
$tournamentId = $_GET['tournament_id'] ?? '';
if (!$tournamentId) jsonError('tournament_id required');
require_once __DIR__ . '/../includes/supabase.php';
$db = supabaseService();
$tournament = $db->get('el3ab_tournaments', ['id' => 'eq.' . $tournamentId, 'select' => 'swiss_api_tournament_id', 'limit' => 1]);
if (empty($tournament) || isset($tournament['error'])) jsonError('Not found', 404);
$swissId = $tournament[0]['swiss_api_tournament_id'] ?? null;
if ($swissId) {
$standings = swissApi('GET', '/tournaments/' . $swissId . '/standings');
if (!isset($standings['error'])) {
jsonResponse(['standings' => $standings]);
}
}
jsonResponse(['standings' => []]);
}
function getRounds(): void {
$tournamentId = $_GET['tournament_id'] ?? '';
if (!$tournamentId) jsonError('tournament_id required');
require_once __DIR__ . '/../includes/supabase.php';
$db = supabaseService();
$rounds = $db->get('el3ab_tournament_rounds', [
'tournament_id' => 'eq.' . $tournamentId,
'select' => 'id,round_number,status,started_at,completed_at,pairings',
'order' => 'round_number.asc'
]);
jsonResponse(['rounds' => is_array($rounds) && !isset($rounds['error']) ? $rounds : []]);
}
function getPairings(): void {
$roundId = $_GET['round_id'] ?? '';
if (!$roundId) jsonError('round_id required');
require_once __DIR__ . '/../includes/supabase.php';
$db = supabaseService();
$round = $db->get('el3ab_tournament_rounds', ['id' => 'eq.' . $roundId, 'select' => 'pairings,results', 'limit' => 1]);
if (empty($round) || isset($round['error'])) jsonResponse(['pairings' => []]);
$pairings = json_decode($round[0]['pairings'] ?? '[]', true);
$results = json_decode($round[0]['results'] ?? '[]', true);
jsonResponse(['pairings' => $pairings, 'results' => $results]);
}
function getMyGames(): void {
$token = requireAuth();
$userId = getUserId($token);
$tournamentId = $_GET['tournament_id'] ?? '';
if (!$tournamentId) jsonError('tournament_id required');
require_once __DIR__ . '/../includes/supabase.php';
$db = supabase($token);
$matches = $db->get('matches', [
'tournament_id' => 'eq.' . $tournamentId,
'or' => "(white_player_id.eq.{$userId},black_player_id.eq.{$userId})",
'select' => 'id,white_player_id,black_player_id,result,status,tournament_round,created_at',
'order' => 'tournament_round.asc'
]);
jsonResponse(['games' => is_array($matches) && !isset($matches['error']) ? $matches : []]);
}
// In-game emote system for chess
// Preset emotes that both players can send during a match
const EMOTES = [
{ key: 'gg', emoji: '🤝', label: 'GG' },
{ key: 'good_move', emoji: '👏', label: 'نقلة ممتازة' },
{ key: 'think', emoji: '🤔', label: 'يفكر...' },
{ key: 'hurry', emoji: '⏱️', label: 'أسرع' },
{ key: 'wow', emoji: '😮', label: 'واو!' },
{ key: 'laugh', emoji: '😂', label: 'هههه' },
{ key: 'angry', emoji: '😤', label: 'غاضب' },
{ key: 'hello', emoji: '👋', label: 'مرحبا' },
];
let emoteBar = null;
let emoteCallback = null;
let lastEmoteTime = 0;
const COOLDOWN = 3000; // 3 seconds between emotes
export function create(container, onSend) {
emoteCallback = 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}">
<span style="font-size:22px;">${e.emoji}</span>
</button>
`).join('')}
</div>
`;
const style = document.createElement('style');
style.textContent = `
.emote-bar { position:absolute;bottom:52px;right:8px;z-index:30;display:flex;flex-direction:column;align-items:flex-end;gap:6px; }
.emote-toggle { width:40px;height:40px;border-radius:50%;background:#1e1e3a;border:1px solid rgba(255,255,255,0.1);color:#f8fafc;font-size:18px;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;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); }
.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); }
.emote-btn:active { transform:scale(0.85); }
.emote-btn.cooldown { opacity:0.3;pointer-events:none; }
.emote-received { position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:48px;animation:emoteFloat 2s ease-out forwards;pointer-events:none;z-index:40; }
@keyframes emoteFloat { 0%{opacity:1;transform:translate(-50%,-50%) scale(0.5);} 30%{transform:translate(-50%,-50%) scale(1.2);} 60%{opacity:1;transform:translate(-50%,-80%) scale(1);} 100%{opacity:0;transform:translate(-50%,-120%) scale(0.8);} }
`;
container.appendChild(style);
container.appendChild(emoteBar);
// Toggle panel
emoteBar.querySelector('#emote-toggle').addEventListener('click', () => {
const panel = emoteBar.querySelector('#emote-panel');
panel.classList.toggle('hidden');
});
// Emote buttons
emoteBar.querySelectorAll('.emote-btn').forEach(btn => {
btn.addEventListener('click', () => {
const now = Date.now();
if (now - lastEmoteTime < COOLDOWN) return;
lastEmoteTime = now;
const key = btn.dataset.key;
const emote = EMOTES.find(e => e.key === key);
if (emote && emoteCallback) {
emoteCallback(emote);
showSentFeedback(btn);
}
// Close panel
emoteBar.querySelector('#emote-panel').classList.add('hidden');
});
});
}
function showSentFeedback(btn) {
btn.classList.add('cooldown');
setTimeout(() => btn.classList.remove('cooldown'), COOLDOWN);
}
export function showReceived(container, emote) {
const el = document.createElement('div');
el.className = 'emote-received';
el.textContent = emote.emoji || emote;
container.appendChild(el);
setTimeout(() => el.remove(), 2000);
}
export function destroy() {
if (emoteBar) {
emoteBar.remove();
emoteBar = null;
}
}
...@@ -10,6 +10,7 @@ import { ChessClock, parseTimeControl } from '../logic/clock.js'; ...@@ -10,6 +10,7 @@ import { ChessClock, parseTimeControl } from '../logic/clock.js';
import * as juice from '../../../core/juice.js'; import * as juice from '../../../core/juice.js';
import { getOpeningName } from '../logic/openings.js'; import { getOpeningName } from '../logic/openings.js';
import { getMaterialAdvantage, formatAdvantage } from '../logic/material.js'; import { getMaterialAdvantage, formatAdvantage } from '../logic/material.js';
import * as emoteSystem from '../components/emotes.js';
let board, clock, gameState; let board, clock, gameState;
...@@ -148,6 +149,13 @@ export function mountGame(el, params) { ...@@ -148,6 +149,13 @@ export function mountGame(el, params) {
requestBotMove(el); requestBotMove(el);
} }
// Emote system
const boardContainer = el.querySelector('#board-container');
emoteSystem.create(boardContainer, (emote) => {
audio.play('notification');
emoteSystem.showReceived(boardContainer, emote.emoji);
});
bus.emit('game:started', { gameKey: 'chess', matchId, opponent: botId, mode }); bus.emit('game:started', { gameKey: 'chess', matchId, opponent: botId, mode });
} }
......
...@@ -89,14 +89,15 @@ export function mountTable(el) { ...@@ -89,14 +89,15 @@ export function mountTable(el) {
left: 0; left: 0;
right: 0; right: 0;
background: #0f0f1e; background: #0f0f1e;
border-top-left-radius: 24px; border-top-left-radius: 28px;
border-top-right-radius: 24px; border-top-right-radius: 28px;
padding: 24px 20px; padding: 28px 20px;
padding-bottom: calc(24px + var(--tab-height, 60px) + var(--safe-bottom, 0px)); padding-bottom: calc(28px + var(--tab-height, 60px) + var(--safe-bottom, 0px));
z-index: 50; z-index: 50;
transform: translateY(0); transform: translateY(0);
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1); transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
box-shadow: 0 -8px 40px rgba(0,0,0,0.6); box-shadow: 0 -8px 40px rgba(0,0,0,0.6);
max-height: 75vh;
} }
.game-menu.hidden { .game-menu.hidden {
transform: translateY(100%); transform: translateY(100%);
...@@ -129,17 +130,18 @@ export function mountTable(el) { ...@@ -129,17 +130,18 @@ export function mountTable(el) {
.menu-btn { .menu-btn {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 14px; gap: 16px;
width: 100%; width: 100%;
padding: 16px; padding: 20px;
border-radius: 14px; border-radius: 16px;
border: none; border: none;
cursor: pointer; cursor: pointer;
margin-bottom: 10px; margin-bottom: 12px;
transition: transform 0.1s, background 0.15s; transition: transform 0.1s, background 0.15s;
text-align: right; text-align: right;
min-height: 72px;
} }
.menu-btn:active { transform: scale(0.97); } .menu-btn:active { transform: scale(0.96); }
.menu-btn-primary { .menu-btn-primary {
background: var(--game-gradient, linear-gradient(135deg, #2563eb, #3b82f6)); background: var(--game-gradient, linear-gradient(135deg, #2563eb, #3b82f6));
color: white; color: white;
...@@ -150,21 +152,21 @@ export function mountTable(el) { ...@@ -150,21 +152,21 @@ export function mountTable(el) {
color: #f8fafc; color: #f8fafc;
} }
.menu-btn-icon { .menu-btn-icon {
width: 40px; width: 48px;
height: 40px; height: 48px;
border-radius: 10px; border-radius: 12px;
background: rgba(255,255,255,0.15); background: rgba(255,255,255,0.15);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 20px; font-size: 24px;
flex-shrink: 0; flex-shrink: 0;
} }
.menu-btn-text { .menu-btn-text {
flex: 1; flex: 1;
} }
.menu-btn-label { .menu-btn-label {
font-size: 15px; font-size: 17px;
font-weight: 700; font-weight: 700;
} }
.menu-btn-desc { .menu-btn-desc {
...@@ -174,19 +176,23 @@ export function mountTable(el) { ...@@ -174,19 +176,23 @@ export function mountTable(el) {
} }
.menu-features { .menu-features {
display: flex; display: flex;
gap: 8px; gap: 10px;
margin-top: 6px; margin-top: 12px;
flex-wrap: wrap;
} }
.feature-chip { .feature-chip {
padding: 6px 12px; padding: 10px 16px;
border-radius: 8px; border-radius: 10px;
background: rgba(255,255,255,0.05); background: rgba(255,255,255,0.05);
border: 1px solid rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.08);
color: #94a3b8; color: #e2e8f0;
font-size: 11px; font-size: 13px;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: background 0.15s; transition: background 0.15s, transform 0.1s;
min-height: 40px;
display: flex;
align-items: center;
} }
.feature-chip:active { background: rgba(255,255,255,0.1); } .feature-chip:active { background: rgba(255,255,255,0.1); }
</style> </style>
......
import * as scene from '../../core/scene.js'; import * as scene from '../../core/scene.js';
import { mountLeaderboard } from './scenes/leaderboard.js'; import { mountLeaderboard } from './scenes/leaderboard.js';
import { mountTournaments } from './scenes/tournaments.js'; import { mountTournaments } from './scenes/tournaments.js';
import { mountTournamentDetail } from './scenes/tournament-detail.js';
scene.register('leaderboard', mountLeaderboard); scene.register('leaderboard', mountLeaderboard);
scene.register('tournaments', mountTournaments); scene.register('tournaments', mountTournaments);
scene.register('tournament-detail', mountTournamentDetail);
import * as scene from '../../../core/scene.js';
import * as net from '../../../core/net.js';
import * as audio from '../../../core/audio.js';
import { t } from '../../../core/i18n.js';
let activeTab = 'info';
export async function mountTournamentDetail(el, params) {
const { tournamentId } = params;
activeTab = 'info';
el.innerHTML = `
<div style="display:flex;flex-direction:column;height:100%;background:#0a0a1a;">
<div style="display:flex;align-items:center;gap:12px;padding:10px 14px;background:#0f0f1e;border-bottom:1px solid rgba(255,255,255,0.06);">
<button class="btn btn-secondary" id="back-btn" style="min-height:32px;padding:4px 12px;font-size:12px;">←</button>
<span id="tour-title" style="font-size:15px;font-weight:700;color:#f8fafc;flex:1;">بطولة</span>
</div>
<!-- Tabs -->
<div style="display:flex;background:#0f0f1e;border-bottom:1px solid rgba(255,255,255,0.04);padding:0 12px;">
<button class="tour-tab active" data-tab="info">معلومات</button>
<button class="tour-tab" data-tab="standings">الترتيب</button>
<button class="tour-tab" data-tab="rounds">الجولات</button>
<button class="tour-tab" data-tab="my-games">مبارياتي</button>
</div>
<!-- Content -->
<div id="tour-content" style="flex:1;overflow-y:auto;padding:14px;"></div>
</div>
<style>
.tour-tab { background:none;border:none;color:#64748b;font-size:13px;font-weight:600;padding:10px 14px;cursor:pointer;border-bottom:2px solid transparent;font-family:inherit; }
.tour-tab.active { color:#E4AC38;border-bottom-color:#E4AC38; }
.tour-tab:active { opacity:0.7; }
.standing-row { display:flex;align-items:center;padding:8px 0;border-bottom:1px solid rgba(255,255,255,0.04); }
.pairing-row { display:flex;align-items:center;justify-content:space-between;padding:10px;background:#1a1a2e;border-radius:8px;margin-bottom:6px; }
</style>
`;
el.querySelector('#back-btn').addEventListener('click', () => { audio.play('click'); scene.pop(); });
// Tab switching
el.querySelectorAll('.tour-tab').forEach(tab => {
tab.addEventListener('click', () => {
audio.play('click');
el.querySelectorAll('.tour-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
activeTab = tab.dataset.tab;
loadTab(el, tournamentId, activeTab);
});
});
// Load tournament info
loadTab(el, tournamentId, 'info');
}
async function loadTab(el, tournamentId, tab) {
const content = el.querySelector('#tour-content');
content.innerHTML = '<div style="text-align:center;color:#64748b;padding:24px;">جاري التحميل...</div>';
switch (tab) {
case 'info': await loadInfo(content, tournamentId, el); break;
case 'standings': await loadStandings(content, tournamentId); break;
case 'rounds': await loadRounds(content, tournamentId); break;
case 'my-games': await loadMyGames(content, tournamentId); break;
}
}
async function loadInfo(content, tournamentId, el) {
try {
const data = await net.get('swiss.php', { action: 'tournament', id: tournamentId });
if (data.error) throw new Error(data.error);
el.querySelector('#tour-title').textContent = data.name || 'بطولة';
const statusColors = { registration: '#34D399', in_progress: '#E4AC38', completed: '#64748b', draft: '#3B82F6' };
const statusLabels = { registration: 'تسجيل مفتوح', in_progress: 'جارية', completed: 'منتهية', draft: 'قريباً' };
content.innerHTML = `
<div style="text-align:center;margin-bottom:16px;">
<div style="font-size:20px;font-weight:800;color:#f8fafc;margin-bottom:4px;">${data.name || ''}</div>
<span style="display:inline-block;padding:4px 12px;border-radius:99px;background:${statusColors[data.status] || '#64748b'};color:#000;font-size:11px;font-weight:700;">${statusLabels[data.status] || data.status}</span>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:16px;">
<div style="background:#1a1a2e;border-radius:10px;padding:12px;text-align:center;">
<div style="font-size:22px;font-weight:800;color:#E4AC38;">${data.player_count || 0}</div>
<div style="font-size:11px;color:#64748b;">لاعبين</div>
</div>
<div style="background:#1a1a2e;border-radius:10px;padding:12px;text-align:center;">
<div style="font-size:22px;font-weight:800;color:#3B82F6;">${data.rounds_total || data.swiss_rounds || '?'}</div>
<div style="font-size:11px;color:#64748b;">جولات</div>
</div>
<div style="background:#1a1a2e;border-radius:10px;padding:12px;text-align:center;">
<div style="font-size:22px;font-weight:800;color:#f8fafc;">${data.format || 'swiss'}</div>
<div style="font-size:11px;color:#64748b;">النظام</div>
</div>
<div style="background:#1a1a2e;border-radius:10px;padding:12px;text-align:center;">
<div style="font-size:22px;font-weight:800;color:#10B981;">${data.time_control || '?'}</div>
<div style="font-size:11px;color:#64748b;">الوقت</div>
</div>
</div>
${data.prize_pool_coins ? `<div style="background:#1a1a2e;border-radius:10px;padding:14px;text-align:center;margin-bottom:12px;">
<div style="font-size:12px;color:#64748b;margin-bottom:4px;">الجوائز</div>
<div style="font-size:20px;font-weight:800;color:#E4AC38;">${data.prize_pool_coins} 🪙</div>
</div>` : ''}
${data.starts_at ? `<div style="text-align:center;color:#94a3b8;font-size:12px;margin-bottom:16px;">
📅 ${new Date(data.starts_at).toLocaleDateString('ar')}${new Date(data.starts_at).toLocaleTimeString('ar', { hour: '2-digit', minute: '2-digit' })}
</div>` : ''}
${data.status === 'registration' ? `
<button class="btn btn-primary w-full" id="register-btn" style="font-size:16px;padding:16px;">⚔️ سجّل الآن</button>
` : ''}
`;
content.querySelector('#register-btn')?.addEventListener('click', async () => {
const btn = content.querySelector('#register-btn');
btn.disabled = true;
btn.textContent = 'جاري التسجيل...';
try {
await net.post('tournaments.php', { action: 'register', tournament_id: tournamentId });
btn.textContent = '✅ تم التسجيل';
btn.style.background = '#34D399';
} catch (e) {
btn.textContent = e.message || 'فشل';
btn.disabled = false;
}
});
} catch (e) {
content.innerHTML = `<div style="text-align:center;color:#ef4444;padding:24px;">فشل تحميل البطولة</div>`;
}
}
async function loadStandings(content, tournamentId) {
try {
const data = await net.get('swiss.php', { action: 'standings', tournament_id: tournamentId });
const standings = data.standings || [];
if (standings.length === 0) {
content.innerHTML = '<div style="text-align:center;color:#64748b;padding:32px;">لا توجد نتائج بعد — ستظهر بعد بدء الجولات</div>';
return;
}
content.innerHTML = `
<div style="display:flex;padding:6px 0;border-bottom:1px solid rgba(255,255,255,0.06);color:#64748b;font-size:11px;font-weight:600;">
<span style="width:30px;">#</span>
<span style="flex:1;">اللاعب</span>
<span style="width:40px;text-align:center;">نقاط</span>
<span style="width:40px;text-align:center;">بوخ</span>
</div>
${standings.map((p, i) => `
<div class="standing-row">
<span style="width:30px;font-weight:700;color:${i < 3 ? '#E4AC38' : '#94a3b8'};font-size:13px;">${i + 1}</span>
<span style="flex:1;font-size:13px;color:#f8fafc;font-weight:500;">${p.name || p.player_name || 'Player'}</span>
<span style="width:40px;text-align:center;font-weight:700;color:#f8fafc;font-size:13px;">${p.score ?? p.points ?? '0'}</span>
<span style="width:40px;text-align:center;font-size:11px;color:#64748b;">${p.buchholz ?? ''}</span>
</div>
`).join('')}
`;
} catch (e) {
content.innerHTML = '<div style="text-align:center;color:#ef4444;">فشل تحميل الترتيب</div>';
}
}
async function loadRounds(content, tournamentId) {
try {
const data = await net.get('swiss.php', { action: 'rounds', tournament_id: tournamentId });
const rounds = data.rounds || [];
if (rounds.length === 0) {
content.innerHTML = '<div style="text-align:center;color:#64748b;padding:32px;">لم تبدأ الجولات بعد</div>';
return;
}
content.innerHTML = rounds.map(r => `
<div style="margin-bottom:16px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<span style="font-size:14px;font-weight:700;color:#f8fafc;">الجولة ${r.round_number}</span>
<span style="font-size:11px;padding:3px 8px;border-radius:99px;background:${r.status === 'completed' ? '#64748b' : r.status === 'in_progress' ? '#E4AC38' : '#3B82F6'};color:#000;font-weight:600;">${r.status === 'completed' ? 'منتهية' : r.status === 'in_progress' ? 'جارية' : 'قادمة'}</span>
</div>
<div id="round-pairings-${r.id}" style="font-size:12px;color:#64748b;">اضغط لعرض التقابلات</div>
</div>
`).join('');
// Make rounds expandable
rounds.forEach(r => {
const el = content.querySelector(`#round-pairings-${r.id}`);
if (el) {
el.style.cursor = 'pointer';
el.addEventListener('click', async () => {
el.textContent = 'جاري التحميل...';
try {
const pd = await net.get('swiss.php', { action: 'pairings', round_id: r.id });
const pairings = pd.pairings || [];
if (pairings.length === 0) {
el.textContent = 'لا توجد تقابلات';
return;
}
el.innerHTML = pairings.map(p => `
<div class="pairing-row">
<span style="flex:1;font-size:12px;color:#f8fafc;">${p.white_name || p.player_a || '?'}</span>
<span style="padding:2px 8px;background:#1e1e3a;border-radius:4px;font-size:11px;font-weight:700;color:#E4AC38;">${p.result || 'vs'}</span>
<span style="flex:1;text-align:left;font-size:12px;color:#f8fafc;">${p.black_name || p.player_b || '?'}</span>
</div>
`).join('');
} catch (e) {
el.textContent = 'فشل التحميل';
}
});
}
});
} catch (e) {
content.innerHTML = '<div style="text-align:center;color:#ef4444;">فشل تحميل الجولات</div>';
}
}
async function loadMyGames(content, tournamentId) {
try {
const data = await net.get('swiss.php', { action: 'my-games', tournament_id: tournamentId });
const games = data.games || [];
if (games.length === 0) {
content.innerHTML = '<div style="text-align:center;color:#64748b;padding:32px;">لم تلعب مباريات في هذه البطولة بعد</div>';
return;
}
content.innerHTML = games.map(g => {
const resultColors = { white_wins: '#34D399', black_wins: '#34D399', draw: '#E4AC38' };
return `
<div class="pairing-row">
<span style="font-size:12px;color:#64748b;">ج${g.tournament_round || '?'}</span>
<span style="font-size:12px;color:#f8fafc;flex:1;margin:0 8px;">${g.result || g.status}</span>
<span style="font-size:11px;color:${resultColors[g.result] || '#94a3b8'};">${g.result === 'white_wins' || g.result === 'black_wins' ? 'فوز' : g.result === 'draw' ? 'تعادل' : g.status}</span>
</div>
`;
}).join('');
} catch (e) {
content.innerHTML = '<div style="text-align:center;color:#ef4444;">فشل التحميل</div>';
}
}
...@@ -78,7 +78,7 @@ function renderTournaments(el, tournaments) { ...@@ -78,7 +78,7 @@ function renderTournaments(el, tournaments) {
list.querySelectorAll('.tournament-card').forEach(card => { list.querySelectorAll('.tournament-card').forEach(card => {
card.addEventListener('click', () => { card.addEventListener('click', () => {
audio.play('click'); audio.play('click');
showTournamentDetail(el, card.dataset.id, tournaments.find(t => t.id === card.dataset.id)); scene.push('tournament-detail', { tournamentId: card.dataset.id });
}); });
}); });
} }
......
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