Commit cabcceab authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: build all game phases (4-10) - chess engine, social, competitive, economy, orgs, settings

Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 233f0436
<?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json');
$token = null;
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (str_starts_with($authHeader, 'Bearer ')) {
$token = substr($authHeader, 7);
}
if (!$token) {
http_response_code(401);
echo json_encode(['error' => 'unauthorized']);
exit;
}
$category = $_GET['category'] ?? null;
$endpoint = 'achievements?select=*&order=sort_order.asc';
if ($category) {
$endpoint .= '&category=eq.' . $category;
}
$achRes = supabase_rest('GET', $endpoint, [], $token);
$achievements = $achRes['data'] ?? [];
$unlockedRes = supabase_rest('GET', 'user_achievements?select=achievement_id,unlocked_at', [], $token);
$unlocked = [];
if ($unlockedRes['status'] === 200 && is_array($unlockedRes['data'])) {
foreach ($unlockedRes['data'] as $row) {
$unlocked[$row['achievement_id']] = $row['unlocked_at'];
}
}
$progressRes = supabase_rest('GET', 'user_achievement_progress?select=achievement_id,progress', [], $token);
$progressMap = [];
if ($progressRes['status'] === 200 && is_array($progressRes['data'])) {
foreach ($progressRes['data'] as $row) {
$progressMap[$row['achievement_id']] = $row['progress'];
}
}
foreach ($achievements as &$ach) {
$ach['unlocked'] = isset($unlocked[$ach['id']]);
$ach['unlocked_at'] = $unlocked[$ach['id']] ?? null;
$ach['progress'] = $progressMap[$ach['id']] ?? 0;
}
echo json_encode(['achievements' => $achievements]);
<?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json');
$token = null;
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (str_starts_with($authHeader, 'Bearer ')) {
$token = substr($authHeader, 7);
}
if (!$token) {
http_response_code(401);
echo json_encode(['error' => 'unauthorized']);
exit;
}
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'POST') {
$profileRes = supabase_rest('GET', 'profiles?select=id,coins,daily_streak,last_daily_claim', [], $token);
if (empty($profileRes['data'])) {
http_response_code(400);
echo json_encode(['error' => 'profile not found']);
exit;
}
$profile = $profileRes['data'][0];
$lastClaim = $profile['last_daily_claim'] ?? null;
$today = date('Y-m-d');
if ($lastClaim === $today) {
echo json_encode(['error' => 'already_claimed', 'message' => 'لقد جمعت المكافأة اليوم']);
exit;
}
$yesterday = date('Y-m-d', strtotime('-1 day'));
$streak = ($lastClaim === $yesterday) ? ($profile['daily_streak'] ?? 0) + 1 : 1;
$reward = 50 + ($streak - 1) * 10;
$newCoins = ($profile['coins'] ?? 0) + $reward;
supabase_rest('PATCH', "profiles?id=eq.{$profile['id']}", [
'coins' => $newCoins,
'daily_streak' => $streak,
'last_daily_claim' => $today
], $token);
echo json_encode([
'ok' => true,
'reward' => $reward,
'streak' => $streak,
'coins' => $newCoins
]);
} else {
http_response_code(405);
echo json_encode(['error' => 'method not allowed']);
}
<?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json');
$token = null;
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (str_starts_with($authHeader, 'Bearer ')) {
$token = substr($authHeader, 7);
}
if (!$token) {
http_response_code(401);
echo json_encode(['error' => 'unauthorized']);
exit;
}
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET') {
$action = $_GET['action'] ?? 'list';
switch ($action) {
case 'list':
$res = supabase_rest('GET', 'friends?select=*,friend:profiles!friend_id(id,username,display_name,elo_blitz,is_online)&status=eq.accepted', [], $token);
$friends = [];
if ($res['status'] === 200 && is_array($res['data'])) {
foreach ($res['data'] as $row) {
if (isset($row['friend'])) {
$friends[] = $row['friend'];
}
}
}
echo json_encode(['friends' => $friends]);
break;
case 'requests':
$res = supabase_rest('GET', 'friends?select=*,sender:profiles!user_id(id,username,display_name)&status=eq.pending', [], $token);
$requests = [];
if ($res['status'] === 200 && is_array($res['data'])) {
foreach ($res['data'] as $row) {
if (isset($row['sender'])) {
$requests[] = array_merge($row['sender'], ['id' => $row['id']]);
}
}
}
echo json_encode(['requests' => $requests]);
break;
case 'search':
$q = $_GET['q'] ?? '';
if (strlen($q) < 2) {
echo json_encode(['players' => []]);
break;
}
$res = supabase_rest('GET', 'profiles?select=id,username,display_name,elo_blitz&or=(username.ilike.*' . urlencode($q) . '*,display_name.ilike.*' . urlencode($q) . '*)&limit=10', [], $token);
echo json_encode(['players' => $res['data'] ?? []]);
break;
}
} elseif ($method === 'POST') {
$input = json_decode(file_get_contents('php://input'), true);
$action = $input['action'] ?? '';
switch ($action) {
case 'add':
$userId = $input['user_id'] ?? '';
$res = supabase_rest('POST', 'friends', [
'friend_id' => $userId,
'status' => 'pending'
], $token);
echo json_encode(['ok' => true]);
break;
case 'accept':
$requestId = $input['request_id'] ?? '';
supabase_rest('PATCH', "friends?id=eq.{$requestId}", [
'status' => 'accepted',
'accepted_at' => date('c')
], $token);
echo json_encode(['ok' => true]);
break;
case 'reject':
$requestId = $input['request_id'] ?? '';
supabase_rest('DELETE', "friends?id=eq.{$requestId}", [], $token);
echo json_encode(['ok' => true]);
break;
case 'remove':
$friendId = $input['friend_id'] ?? '';
supabase_rest('DELETE', "friends?friend_id=eq.{$friendId}", [], $token);
echo json_encode(['ok' => true]);
break;
default:
echo json_encode(['error' => 'invalid action']);
}
}
<?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json');
$token = null;
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (str_starts_with($authHeader, 'Bearer ')) {
$token = substr($authHeader, 7);
}
if (!$token) {
http_response_code(401);
echo json_encode(['error' => 'unauthorized']);
exit;
}
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET') {
$action = $_GET['action'] ?? '';
if ($action === 'recent') {
$res = supabase_rest('GET', 'games?select=id,bot_id,result,reason,time_control,created_at&status=eq.completed&order=created_at.desc&limit=10', [], $token);
echo json_encode(['games' => $res['data'] ?? []]);
} else {
echo json_encode(['error' => 'invalid action']);
}
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
$action = $input['action'] ?? '';
switch ($action) {
case 'start':
$botId = $input['bot_id'] ?? 'nour';
$color = $input['color'] ?? 'w';
$timeControl = $input['time_control'] ?? 600;
$increment = $input['increment'] ?? 0;
$rated = $input['rated'] ?? true;
$gameData = [
'player_color' => $color,
'bot_id' => $botId,
'time_control' => $timeControl,
'increment' => $increment,
'rated' => $rated,
'status' => 'active',
'started_at' => date('c'),
'fen' => 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'
];
$res = supabase_rest('POST', 'games', $gameData, $token);
if ($res['status'] >= 200 && $res['status'] < 300 && !empty($res['data'])) {
echo json_encode(['game_id' => $res['data'][0]['id'] ?? null, 'status' => 'started']);
} else {
echo json_encode(['game_id' => uniqid('game_'), 'status' => 'started']);
}
break;
case 'bot_move':
$botId = $input['bot_id'] ?? 'nour';
$fen = $input['fen'] ?? '';
$wtime = $input['wtime'] ?? 60000;
$btime = $input['btime'] ?? 60000;
$apiUrl = STOCKFISH_API . '/play';
$payload = json_encode([
'bot_id' => $botId,
'fen' => $fen,
'wtime' => $wtime,
'btime' => $btime
]);
$ch = curl_init($apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'X-API-Key: ' . STOCKFISH_MGMT_KEY
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
unset($ch);
if ($httpCode === 200) {
$data = json_decode($response, true);
echo json_encode(['move' => $data['move'] ?? null]);
} else {
http_response_code(502);
echo json_encode(['error' => 'bot_unavailable', 'status' => $httpCode]);
}
break;
case 'move':
$gameId = $input['game_id'] ?? '';
$move = $input['move'] ?? '';
$fen = $input['fen'] ?? '';
if ($gameId && !str_starts_with($gameId, 'game_')) {
supabase_rest('PATCH', "games?id=eq.{$gameId}", [
'fen' => $fen,
'last_move' => $move,
'clock_white' => $input['clock_white'] ?? null,
'clock_black' => $input['clock_black'] ?? null
], $token);
}
echo json_encode(['ok' => true]);
break;
case 'end':
$gameId = $input['game_id'] ?? '';
$result = $input['result'] ?? '';
$reason = $input['reason'] ?? '';
$pgn = $input['pgn'] ?? '';
$finalFen = $input['final_fen'] ?? '';
if ($gameId && !str_starts_with($gameId, 'game_')) {
supabase_rest('PATCH', "games?id=eq.{$gameId}", [
'status' => 'completed',
'result' => $result,
'reason' => $reason,
'pgn' => $pgn,
'final_fen' => $finalFen,
'ended_at' => date('c')
], $token);
}
if ($result === 'win') {
supabase_rest('POST', 'rpc/add_coins', ['amount' => 50], $token);
}
echo json_encode(['ok' => true, 'result' => $result]);
break;
default:
http_response_code(400);
echo json_encode(['error' => 'invalid action']);
}
<?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json');
$token = null;
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (str_starts_with($authHeader, 'Bearer ')) {
$token = substr($authHeader, 7);
}
if (!$token) {
http_response_code(401);
echo json_encode(['error' => 'unauthorized']);
exit;
}
$mode = $_GET['mode'] ?? 'blitz';
$validModes = ['blitz', 'rapid', 'bullet'];
if (!in_array($mode, $validModes)) {
$mode = 'blitz';
}
$orderField = 'elo_' . $mode;
$res = supabase_rest('GET', "profiles?select=id,username,display_name,elo_blitz,elo_rapid,elo_bullet&order={$orderField}.desc&limit=50", [], $token);
echo json_encode(['players' => $res['data'] ?? []]);
<?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json');
$token = null;
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (str_starts_with($authHeader, 'Bearer ')) {
$token = substr($authHeader, 7);
}
if (!$token) {
http_response_code(401);
echo json_encode(['error' => 'unauthorized']);
exit;
}
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET') {
$res = supabase_rest('GET', 'notifications?select=*&order=created_at.desc&limit=50', [], $token);
echo json_encode(['notifications' => $res['data'] ?? []]);
} elseif ($method === 'POST') {
$input = json_decode(file_get_contents('php://input'), true);
$action = $input['action'] ?? '';
if ($action === 'read_all') {
supabase_rest('PATCH', 'notifications?read_at=is.null', [
'read_at' => date('c')
], $token);
echo json_encode(['ok' => true]);
} elseif ($action === 'read') {
$id = $input['id'] ?? '';
supabase_rest('PATCH', "notifications?id=eq.{$id}", [
'read_at' => date('c')
], $token);
echo json_encode(['ok' => true]);
} else {
echo json_encode(['error' => 'invalid action']);
}
}
<?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json');
$token = null;
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (str_starts_with($authHeader, 'Bearer ')) {
$token = substr($authHeader, 7);
}
if (!$token) {
http_response_code(401);
echo json_encode(['error' => 'unauthorized']);
exit;
}
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET') {
$action = $_GET['action'] ?? 'my';
switch ($action) {
case 'my':
$res = supabase_rest('GET', 'org_members?select=*,org:organizations(*)&order=joined_at.desc', [], $token);
$orgs = [];
if ($res['status'] === 200 && is_array($res['data'])) {
foreach ($res['data'] as $row) {
if (isset($row['org'])) {
$orgs[] = $row['org'];
}
}
}
echo json_encode(['orgs' => $orgs]);
break;
case 'browse':
$res = supabase_rest('GET', 'organizations?select=*&order=members_count.desc&limit=20', [], $token);
echo json_encode(['orgs' => $res['data'] ?? []]);
break;
case 'search':
$q = $_GET['q'] ?? '';
if (strlen($q) < 2) {
echo json_encode(['orgs' => []]);
break;
}
$res = supabase_rest('GET', 'organizations?select=*&name=ilike.*' . urlencode($q) . '*&limit=10', [], $token);
echo json_encode(['orgs' => $res['data'] ?? []]);
break;
case 'detail':
$id = $_GET['id'] ?? '';
$orgRes = supabase_rest('GET', "organizations?id=eq.{$id}&select=*", [], $token);
$org = (!empty($orgRes['data'])) ? $orgRes['data'][0] : null;
$membersRes = supabase_rest('GET', "org_members?org_id=eq.{$id}&select=*,profile:profiles(username,display_name,elo_blitz)&order=joined_at.asc", [], $token);
$members = [];
$isMember = false;
if ($membersRes['status'] === 200 && is_array($membersRes['data'])) {
foreach ($membersRes['data'] as $row) {
$member = $row['profile'] ?? [];
$member['role'] = $row['role'] ?? 'member';
$members[] = $member;
// is_member check handled client-side
}
}
echo json_encode(['org' => $org, 'members' => $members, 'is_member' => $isMember]);
break;
}
} elseif ($method === 'POST') {
$input = json_decode(file_get_contents('php://input'), true);
$action = $input['action'] ?? '';
switch ($action) {
case 'create':
$name = $input['name'] ?? '';
$description = $input['description'] ?? '';
$res = supabase_rest('POST', 'organizations', [
'name' => $name,
'description' => $description,
'members_count' => 1
], $token);
if ($res['status'] >= 200 && $res['status'] < 300 && !empty($res['data'])) {
$orgId = $res['data'][0]['id'];
supabase_rest('POST', 'org_members', [
'org_id' => $orgId,
'role' => 'owner'
], $token);
echo json_encode(['ok' => true, 'org_id' => $orgId]);
} else {
http_response_code(400);
echo json_encode(['error' => 'could not create org']);
}
break;
case 'join':
$orgId = $input['org_id'] ?? '';
supabase_rest('POST', 'org_members', [
'org_id' => $orgId,
'role' => 'member'
], $token);
echo json_encode(['ok' => true]);
break;
case 'leave':
$orgId = $input['org_id'] ?? '';
supabase_rest('DELETE', "org_members?org_id=eq.{$orgId}", [], $token);
echo json_encode(['ok' => true]);
break;
default:
echo json_encode(['error' => 'invalid action']);
}
}
<?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json');
$token = null;
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (str_starts_with($authHeader, 'Bearer ')) {
$token = substr($authHeader, 7);
}
if (!$token) {
http_response_code(401);
echo json_encode(['error' => 'unauthorized']);
exit;
}
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET') {
$category = $_GET['category'] ?? 'boards';
$res = supabase_rest('GET', "shop_items?category=eq.{$category}&select=*&order=price.asc", [], $token);
$items = $res['data'] ?? [];
$ownedRes = supabase_rest('GET', 'user_items?select=item_id', [], $token);
$ownedIds = [];
if ($ownedRes['status'] === 200 && is_array($ownedRes['data'])) {
$ownedIds = array_column($ownedRes['data'], 'item_id');
}
foreach ($items as &$item) {
$item['owned'] = in_array($item['id'], $ownedIds);
}
echo json_encode(['items' => $items]);
} elseif ($method === 'POST') {
$input = json_decode(file_get_contents('php://input'), true);
$action = $input['action'] ?? '';
if ($action === 'buy') {
$itemId = $input['item_id'] ?? '';
$itemRes = supabase_rest('GET', "shop_items?id=eq.{$itemId}&select=*", [], $token);
if (empty($itemRes['data'])) {
http_response_code(404);
echo json_encode(['error' => 'item not found']);
exit;
}
$item = $itemRes['data'][0];
$profileRes = supabase_rest('GET', 'profiles?select=coins,gems', [], $token);
if (empty($profileRes['data'])) {
http_response_code(400);
echo json_encode(['error' => 'profile not found']);
exit;
}
$profile = $profileRes['data'][0];
$currency = $item['currency'] ?? 'coins';
$price = $item['price'] ?? 0;
$balance = $profile[$currency] ?? 0;
if ($balance < $price) {
http_response_code(400);
echo json_encode(['error' => 'رصيد غير كافي']);
exit;
}
supabase_rest('POST', 'user_items', [
'item_id' => $itemId,
], $token);
supabase_rest('PATCH', 'profiles?id=eq.' . ($profile['id'] ?? ''), [
$currency => $balance - $price
], $token);
echo json_encode(['ok' => true]);
} elseif ($action === 'equip') {
$itemId = $input['item_id'] ?? '';
$slot = $input['slot'] ?? '';
supabase_rest('PATCH', 'profiles', [
'equipped_' . $slot => $itemId
], $token);
echo json_encode(['ok' => true]);
} else {
echo json_encode(['error' => 'invalid action']);
}
}
<?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json');
$token = null;
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (str_starts_with($authHeader, 'Bearer ')) {
$token = substr($authHeader, 7);
}
if (!$token) {
http_response_code(401);
echo json_encode(['error' => 'unauthorized']);
exit;
}
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET') {
$id = $_GET['id'] ?? null;
if ($id) {
$res = supabase_rest('GET', "tournaments?id=eq.{$id}&select=*", [], $token);
$tournament = ($res['status'] === 200 && !empty($res['data'])) ? $res['data'][0] : null;
$standings = [];
if ($tournament) {
$standingsRes = supabase_rest('GET', "tournament_participants?tournament_id=eq.{$id}&select=*,player:profiles(username,display_name)&order=points.desc", [], $token);
if ($standingsRes['status'] === 200 && is_array($standingsRes['data'])) {
foreach ($standingsRes['data'] as $row) {
$standings[] = array_merge($row['player'] ?? [], ['points' => $row['points'] ?? 0]);
}
}
}
echo json_encode(['tournament' => $tournament, 'standings' => $standings]);
} else {
$res = supabase_rest('GET', 'tournaments?select=*&order=start_time.desc&limit=30', [], $token);
echo json_encode(['tournaments' => $res['data'] ?? []]);
}
} elseif ($method === 'POST') {
$input = json_decode(file_get_contents('php://input'), true);
$action = $input['action'] ?? '';
if ($action === 'join') {
$tournamentId = $input['tournament_id'] ?? '';
$res = supabase_rest('POST', 'tournament_participants', [
'tournament_id' => $tournamentId,
'points' => 0,
'games_played' => 0
], $token);
if ($res['status'] >= 200 && $res['status'] < 300) {
echo json_encode(['ok' => true]);
} else {
http_response_code(400);
echo json_encode(['error' => 'could not join', 'details' => $res['data']]);
}
} else {
echo json_encode(['error' => 'invalid action']);
}
}
<?php $pageTitle = 'EL3AB'; ?> <?php $pageTitle = 'EL3AB - الانجازات'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?> <?php require __DIR__ . '/../includes/header.php'; ?>
<div class="text-center" style="padding:48px 20px;">
<p class="page-title">قريبا</p> <div class="space-y-6">
<p class="text-muted">هذه الصفحة قيد البناء</p>
<div class="text-center">
<h2 style="font-size:22px;font-weight:700;">الانجازات</h2>
<p class="text-muted text-sm" id="ach-progress">0 / 0 مكتمل</p>
</div>
<!-- Progress Bar -->
<div class="stat-bar" style="height:8px;">
<div class="stat-bar-fill" id="ach-bar" style="width:0%;background:var(--gold);"></div>
</div>
<!-- Categories -->
<div class="tab-group" id="ach-tabs">
<button class="tab active" data-cat="all">الكل</button>
<button class="tab" data-cat="games">مباريات</button>
<button class="tab" data-cat="social">اجتماعي</button>
<button class="tab" data-cat="streak">متتالية</button>
</div>
<!-- Achievement List -->
<div class="space-y-2" id="achievements-list">
<div class="card"><div class="empty-state">جاري التحميل...</div></div>
</div>
</div> </div>
<script>
document.addEventListener('DOMContentLoaded', () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
let currentCat = 'all';
document.querySelectorAll('#ach-tabs .tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('#ach-tabs .tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
currentCat = tab.dataset.cat;
loadAchievements(currentCat);
});
});
loadAchievements(currentCat);
});
async function loadAchievements(category) {
const url = '/api/achievements' + (category !== 'all' ? '?category=' + category : '');
const data = await App.fetch(url);
const container = document.getElementById('achievements-list');
if (!data || !data.achievements) {
container.innerHTML = '<div class="card"><div class="empty-state">لا يوجد انجازات</div></div>';
return;
}
const achievements = data.achievements;
const completed = achievements.filter(a => a.unlocked).length;
document.getElementById('ach-progress').textContent = completed + ' / ' + achievements.length + ' مكتمل';
const pct = achievements.length > 0 ? (completed / achievements.length * 100) : 0;
document.getElementById('ach-bar').style.width = pct + '%';
container.innerHTML = achievements.map(a => {
const unlocked = a.unlocked;
const opacity = unlocked ? '' : 'opacity:0.5;';
return `
<div class="card" style="${opacity}">
<div class="card-body" style="display:flex;align-items:center;gap:16px;">
<div class="avatar" style="background:${unlocked ? 'var(--gold-dim)' : 'var(--bg-3)'};">
<svg class="icon-lg" style="color:${unlocked ? 'var(--gold)' : 'var(--text-3)'}"><use href="/public/icons/sprite.svg#icon-star"></use></svg>
</div>
<div style="flex:1;">
<p style="font-size:14px;font-weight:600;">${a.name}</p>
<p class="text-muted text-xs">${a.description}</p>
${a.progress !== undefined && !unlocked ? `
<div class="stat-bar" style="margin-top:6px;height:4px;">
<div class="stat-bar-fill" style="width:${Math.min((a.progress / a.target) * 100, 100)}%;background:var(--cyan);"></div>
</div>
<p class="text-muted text-xs" style="margin-top:2px;">${a.progress} / ${a.target}</p>
` : ''}
</div>
${unlocked ? '<svg class="icon" style="color:var(--success);"><use href="/public/icons/sprite.svg#icon-check"></use></svg>' : ''}
</div>
</div>
`;
}).join('');
}
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?> <?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB'; ?> <?php $pageTitle = 'EL3AB - البوتات'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?> <?php require __DIR__ . '/../includes/header.php'; ?>
<div class="text-center" style="padding:48px 20px;">
<p class="page-title">قريبا</p> <div class="space-y-6">
<p class="text-muted">هذه الصفحة قيد البناء</p>
<div class="text-center">
<h2 style="font-size:22px;font-weight:700;">اختر خصمك</h2>
<p class="text-muted text-sm">7 بوتات بمستويات مختلفة</p>
</div>
<div class="space-y-3" id="bots-grid">
<div class="card card-hover bot-card" data-bot="amina" data-elo="800">
<div class="card-body" style="display:flex;align-items:center;gap:16px;">
<img src="https://stockfishapi.caprover.al-arcade.com/portraits/amina.jpg" class="avatar" alt="amina" style="object-fit:cover;">
<div style="flex:1;">
<p style="font-size:16px;font-weight:600;">امينة</p>
<p class="text-muted text-xs">مبتدئة - ELO 800</p>
<div class="stat-bar" style="margin-top:6px;"><div class="stat-bar-fill" style="width:11%;background:var(--success);"></div></div>
</div>
<svg class="icon" style="color:var(--text-3);transform:scaleX(-1)"><use href="/public/icons/sprite.svg#icon-arrow-right"></use></svg>
</div>
</div>
<div class="card card-hover bot-card" data-bot="tarek" data-elo="1000">
<div class="card-body" style="display:flex;align-items:center;gap:16px;">
<img src="https://stockfishapi.caprover.al-arcade.com/portraits/tarek.jpg" class="avatar" alt="tarek" style="object-fit:cover;">
<div style="flex:1;">
<p style="font-size:16px;font-weight:600;">طارق</p>
<p class="text-muted text-xs">هاوي - ELO 1000</p>
<div class="stat-bar" style="margin-top:6px;"><div class="stat-bar-fill" style="width:25%;background:var(--success);"></div></div>
</div>
<svg class="icon" style="color:var(--text-3);transform:scaleX(-1)"><use href="/public/icons/sprite.svg#icon-arrow-right"></use></svg>
</div>
</div>
<div class="card card-hover bot-card" data-bot="nour" data-elo="1200">
<div class="card-body" style="display:flex;align-items:center;gap:16px;">
<img src="https://stockfishapi.caprover.al-arcade.com/portraits/nour.jpg" class="avatar" alt="nour" style="object-fit:cover;">
<div style="flex:1;">
<p style="font-size:16px;font-weight:600;">نور</p>
<p class="text-muted text-xs">متوسطة - ELO 1200</p>
<div class="stat-bar" style="margin-top:6px;"><div class="stat-bar-fill" style="width:40%;background:var(--warning);"></div></div>
</div>
<svg class="icon" style="color:var(--text-3);transform:scaleX(-1)"><use href="/public/icons/sprite.svg#icon-arrow-right"></use></svg>
</div>
</div>
<div class="card card-hover bot-card" data-bot="omar" data-elo="1400">
<div class="card-body" style="display:flex;align-items:center;gap:16px;">
<img src="https://stockfishapi.caprover.al-arcade.com/portraits/omar.jpg" class="avatar" alt="omar" style="object-fit:cover;">
<div style="flex:1;">
<p style="font-size:16px;font-weight:600;">عمر</p>
<p class="text-muted text-xs">جيد - ELO 1400</p>
<div class="stat-bar" style="margin-top:6px;"><div class="stat-bar-fill" style="width:55%;background:var(--warning);"></div></div>
</div>
<svg class="icon" style="color:var(--text-3);transform:scaleX(-1)"><use href="/public/icons/sprite.svg#icon-arrow-right"></use></svg>
</div>
</div>
<div class="card card-hover bot-card" data-bot="layla" data-elo="1600">
<div class="card-body" style="display:flex;align-items:center;gap:16px;">
<img src="https://stockfishapi.caprover.al-arcade.com/portraits/layla.jpg" class="avatar" alt="layla" style="object-fit:cover;">
<div style="flex:1;">
<p style="font-size:16px;font-weight:600;">ليلى</p>
<p class="text-muted text-xs">قوية - ELO 1600</p>
<div class="stat-bar" style="margin-top:6px;"><div class="stat-bar-fill" style="width:70%;background:var(--error);"></div></div>
</div>
<svg class="icon" style="color:var(--text-3);transform:scaleX(-1)"><use href="/public/icons/sprite.svg#icon-arrow-right"></use></svg>
</div>
</div>
<div class="card card-hover bot-card" data-bot="ziad" data-elo="1800">
<div class="card-body" style="display:flex;align-items:center;gap:16px;">
<img src="https://stockfishapi.caprover.al-arcade.com/portraits/ziad.jpg" class="avatar" alt="ziad" style="object-fit:cover;">
<div style="flex:1;">
<p style="font-size:16px;font-weight:600;">زياد</p>
<p class="text-muted text-xs">خبير - ELO 1800</p>
<div class="stat-bar" style="margin-top:6px;"><div class="stat-bar-fill" style="width:85%;background:var(--error);"></div></div>
</div>
<svg class="icon" style="color:var(--text-3);transform:scaleX(-1)"><use href="/public/icons/sprite.svg#icon-arrow-right"></use></svg>
</div>
</div>
<div class="card card-hover bot-card" data-bot="grandmaster" data-elo="2200">
<div class="card-body" style="display:flex;align-items:center;gap:16px;">
<img src="https://stockfishapi.caprover.al-arcade.com/portraits/grandmaster.jpg" class="avatar" alt="grandmaster" style="object-fit:cover;">
<div style="flex:1;">
<p style="font-size:16px;font-weight:600;">الاستاذ الكبير</p>
<p class="text-muted text-xs">جراند ماستر - ELO 2200</p>
<div class="stat-bar" style="margin-top:6px;"><div class="stat-bar-fill" style="width:100%;background:var(--purple);"></div></div>
</div>
<svg class="icon" style="color:var(--text-3);transform:scaleX(-1)"><use href="/public/icons/sprite.svg#icon-arrow-right"></use></svg>
</div>
</div>
</div>
</div> </div>
<script>
document.addEventListener('DOMContentLoaded', () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
document.querySelectorAll('.bot-card').forEach(card => {
card.style.cursor = 'pointer';
card.addEventListener('click', () => {
const bot = card.dataset.bot;
const color = Math.random() < 0.5 ? 'w' : 'b';
window.location.href = '/game?bot=' + bot + '&color=' + color + '&time=600&inc=0&rated=true';
});
});
});
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?> <?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB'; ?> <?php $pageTitle = 'EL3AB - الاصدقاء'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?> <?php require __DIR__ . '/../includes/header.php'; ?>
<div class="text-center" style="padding:48px 20px;">
<p class="page-title">قريبا</p> <div class="space-y-6">
<p class="text-muted">هذه الصفحة قيد البناء</p>
<div class="text-center">
<h2 style="font-size:22px;font-weight:700;">الاصدقاء</h2>
</div>
<!-- Search -->
<div class="input-group">
<svg class="icon input-icon"><use href="/public/icons/sprite.svg#icon-search"></use></svg>
<input type="text" class="input" id="friend-search" placeholder="ابحث عن لاعب...">
</div>
<!-- Tabs -->
<div class="tab-group" id="friends-tabs">
<button class="tab active" data-tab="friends">اصدقائي</button>
<button class="tab" data-tab="requests">طلبات</button>
<button class="tab" data-tab="search">بحث</button>
</div>
<!-- Friends List -->
<div id="tab-friends" class="space-y-2">
<div class="card" id="friends-list">
<div class="empty-state">
<svg class="icon-lg" style="color:var(--text-3);margin-bottom:8px;"><use href="/public/icons/sprite.svg#icon-friends"></use></svg>
<p>لا يوجد اصدقاء بعد</p>
<p class="text-muted text-xs">ابحث عن لاعبين واضفهم</p>
</div>
</div>
</div>
<!-- Requests -->
<div id="tab-requests" class="space-y-2" style="display:none;">
<div class="card" id="requests-list">
<div class="empty-state">
<p>لا يوجد طلبات صداقة</p>
</div>
</div>
</div>
<!-- Search Results -->
<div id="tab-search" class="space-y-2" style="display:none;">
<div class="card" id="search-results">
<div class="empty-state">
<p>اكتب اسم اللاعب للبحث</p>
</div>
</div>
</div>
</div> </div>
<script>
document.addEventListener('DOMContentLoaded', () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
const tabs = document.querySelectorAll('#friends-tabs .tab');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
tabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
document.querySelectorAll('[id^="tab-"]').forEach(p => p.style.display = 'none');
document.getElementById('tab-' + tab.dataset.tab).style.display = 'block';
});
});
loadFriends();
let searchTimeout;
document.getElementById('friend-search').addEventListener('input', (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => searchPlayers(e.target.value), 500);
});
});
async function loadFriends() {
const data = await App.fetch('/api/friends?action=list');
if (data && data.friends && data.friends.length > 0) {
const list = document.getElementById('friends-list');
list.innerHTML = data.friends.map(f => `
<div class="card-body" style="display:flex;align-items:center;gap:12px;padding:12px;border-bottom:1px solid var(--border);">
<div class="avatar avatar-sm">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-profile"></use></svg>
</div>
<div style="flex:1;">
<p style="font-size:14px;font-weight:600;">${f.display_name || f.username}</p>
<p class="text-muted text-xs">${f.elo_blitz || 1200} ELO</p>
</div>
<span class="badge ${f.online ? 'badge-success' : ''}">${f.online ? 'متصل' : 'غير متصل'}</span>
</div>
`).join('');
}
const reqData = await App.fetch('/api/friends?action=requests');
if (reqData && reqData.requests && reqData.requests.length > 0) {
const reqList = document.getElementById('requests-list');
reqList.innerHTML = reqData.requests.map(r => `
<div class="card-body" style="display:flex;align-items:center;gap:12px;padding:12px;border-bottom:1px solid var(--border);">
<div class="avatar avatar-sm">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-profile"></use></svg>
</div>
<div style="flex:1;">
<p style="font-size:14px;font-weight:600;">${r.display_name || r.username}</p>
</div>
<button class="btn btn-cyan btn-xs" onclick="acceptFriend('${r.id}')">قبول</button>
<button class="btn btn-ghost btn-xs" onclick="rejectFriend('${r.id}')">رفض</button>
</div>
`).join('');
}
}
async function searchPlayers(query) {
if (!query || query.length < 2) return;
const data = await App.fetch('/api/friends?action=search&q=' + encodeURIComponent(query));
const results = document.getElementById('search-results');
if (data && data.players && data.players.length > 0) {
results.innerHTML = data.players.map(p => `
<div class="card-body" style="display:flex;align-items:center;gap:12px;padding:12px;border-bottom:1px solid var(--border);">
<div class="avatar avatar-sm">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-profile"></use></svg>
</div>
<div style="flex:1;">
<p style="font-size:14px;font-weight:600;">${p.display_name || p.username}</p>
<p class="text-muted text-xs">${p.elo_blitz || 1200} ELO</p>
</div>
<button class="btn btn-cyan btn-xs" onclick="addFriend('${p.id}')">اضافة</button>
</div>
`).join('');
} else {
results.innerHTML = '<div class="empty-state"><p>لا توجد نتائج</p></div>';
}
}
async function addFriend(userId) {
await App.fetch('/api/friends', {
method: 'POST',
body: JSON.stringify({ action: 'add', user_id: userId })
});
App.toast('تم ارسال طلب الصداقة', 'success');
}
async function acceptFriend(requestId) {
await App.fetch('/api/friends', {
method: 'POST',
body: JSON.stringify({ action: 'accept', request_id: requestId })
});
App.toast('تم قبول الصداقة', 'success');
loadFriends();
}
async function rejectFriend(requestId) {
await App.fetch('/api/friends', {
method: 'POST',
body: JSON.stringify({ action: 'reject', request_id: requestId })
});
loadFriends();
}
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?> <?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB'; ?> <?php
$pageTitle = 'EL3AB - المباراة';
$extraCss = '/public/css/chessboard.css';
?>
<?php require __DIR__ . '/../includes/header.php'; ?> <?php require __DIR__ . '/../includes/header.php'; ?>
<div class="text-center" style="padding:48px 20px;">
<p class="page-title">قريبا</p> <div class="game-container" id="game-container">
<p class="text-muted">هذه الصفحة قيد البناء</p>
<!-- Opponent info -->
<div class="game-header">
<div class="game-player">
<div class="avatar avatar-sm" id="opponent-avatar">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-bot"></use></svg>
</div>
<div>
<div class="game-player-name" id="opponent-name">Bot</div>
<div class="game-player-rating" id="opponent-rating">1200</div>
</div>
</div>
<div class="game-clock" id="clock-top">10:00</div>
</div>
<!-- Board -->
<div class="board-wrapper">
<div class="board" id="board"></div>
</div>
<!-- Thinking indicator -->
<div class="thinking" id="thinking-indicator" style="display:none;">
<div class="thinking-dots">
<span></span><span></span><span></span>
</div>
<span>يفكر...</span>
</div>
<!-- Player info -->
<div class="game-header">
<div class="game-player">
<div class="avatar avatar-sm">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-profile"></use></svg>
</div>
<div>
<div class="game-player-name" id="player-name">انت</div>
<div class="game-player-rating" id="player-rating">1200</div>
</div>
</div>
<div class="game-clock" id="clock-bottom">10:00</div>
</div>
<!-- Status -->
<div class="text-center text-sm text-muted" id="game-status">دورك</div>
<!-- Move list -->
<div class="move-list" id="move-list"></div>
<!-- Controls -->
<div class="game-controls">
<button class="btn btn-ghost btn-sm" onclick="Game.resign()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-flag"></use></svg>
استسلام
</button>
<button class="btn btn-ghost btn-sm" onclick="Board.flip()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-arrows"></use></svg>
اقلب
</button>
</div>
</div> </div>
<script src="/public/js/chess.min.js"></script>
<script src="/public/js/board.js"></script>
<script src="/public/js/game.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
const params = new URLSearchParams(window.location.search);
const botId = params.get('bot') || 'amina';
const color = params.get('color') || 'w';
const time = parseInt(params.get('time') || '600');
const inc = parseInt(params.get('inc') || '0');
const rated = params.get('rated') !== 'false';
const botNames = {
amina: 'امينة', tarek: 'طارق', nour: 'نور',
omar: 'عمر', layla: 'ليلى', ziad: 'زياد', grandmaster: 'الاستاذ'
};
document.getElementById('opponent-name').textContent = botNames[botId] || botId;
const avatarEl = document.getElementById('opponent-avatar');
avatarEl.innerHTML = '<img src="https://stockfishapi.caprover.al-arcade.com/portraits/' + botId + '.jpg" alt="' + botId + '" style="width:100%;height:100%;border-radius:50%;object-fit:cover;">';
Game.start({ botId, color, time, increment: inc, rated });
});
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?> <?php require __DIR__ . '/../includes/footer.php'; ?>
...@@ -53,7 +53,41 @@ document.addEventListener('DOMContentLoaded', async () => { ...@@ -53,7 +53,41 @@ document.addEventListener('DOMContentLoaded', async () => {
document.getElementById('home-subtitle').textContent = 'المستوى ' + (p.level || 1) + ' • ' + (p.elo_blitz || 1200) + ' بليتز'; document.getElementById('home-subtitle').textContent = 'المستوى ' + (p.level || 1) + ' • ' + (p.elo_blitz || 1200) + ' بليتز';
document.getElementById('home-streak').textContent = 'اليوم ' + (p.daily_streak || 0); document.getElementById('home-streak').textContent = 'اليوم ' + (p.daily_streak || 0);
document.getElementById('home-streak-reward').textContent = '+' + (50 + (p.daily_streak || 0) * 10) + ' عملة'; document.getElementById('home-streak-reward').textContent = '+' + (50 + (p.daily_streak || 0) * 10) + ' عملة';
if (p.last_daily_claim === new Date().toISOString().slice(0, 10)) {
document.getElementById('home-claim-btn').textContent = 'تم';
document.getElementById('home-claim-btn').disabled = true;
document.getElementById('home-claim-btn').classList.add('btn-ghost');
document.getElementById('home-claim-btn').classList.remove('btn-cyan');
}
}
const gamesData = await App.fetch('/api/game?action=recent');
if (gamesData && gamesData.games && gamesData.games.length > 0) {
const container = document.getElementById('home-recent-games');
container.innerHTML = gamesData.games.slice(0, 5).map(g => {
const resultClass = g.result === 'win' ? 'color:var(--success)' : g.result === 'loss' ? 'color:var(--error)' : 'color:var(--text-3)';
const resultText = g.result === 'win' ? 'فوز' : g.result === 'loss' ? 'خسارة' : 'تعادل';
return '<div style="display:flex;align-items:center;gap:12px;padding:10px 16px;border-bottom:1px solid var(--border);">' +
'<svg class="icon" style="' + resultClass + '"><use href="/public/icons/sprite.svg#icon-' + (g.result === 'win' ? 'check' : 'x') + '"></use></svg>' +
'<div style="flex:1;"><p style="font-size:13px;font-weight:600;">ضد ' + (g.bot_id || '?') + '</p></div>' +
'<span class="badge" style="' + resultClass + '">' + resultText + '</span>' +
'</div>';
}).join('');
} }
document.getElementById('home-claim-btn').addEventListener('click', async () => {
const res = await App.fetch('/api/daily-reward', { method: 'POST' });
if (res && res.ok) {
App.toast('+' + res.reward + ' عملة!', 'success');
document.getElementById('home-claim-btn').textContent = 'تم';
document.getElementById('home-claim-btn').disabled = true;
document.getElementById('home-streak').textContent = 'اليوم ' + res.streak;
App.loadProfile();
} else if (res && res.error === 'already_claimed') {
App.toast('لقد جمعت المكافأة اليوم', 'error');
}
});
}); });
</script> </script>
......
<?php $pageTitle = 'EL3AB'; ?> <?php $pageTitle = 'EL3AB - المتصدرين'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?> <?php require __DIR__ . '/../includes/header.php'; ?>
<div class="text-center" style="padding:48px 20px;">
<p class="page-title">قريبا</p> <div class="space-y-6">
<p class="text-muted">هذه الصفحة قيد البناء</p>
<div class="text-center">
<h2 style="font-size:22px;font-weight:700;">المتصدرين</h2>
</div>
<!-- Category Tabs -->
<div class="tab-group" id="lb-tabs">
<button class="tab active" data-mode="blitz">بليتز</button>
<button class="tab" data-mode="rapid">رابيد</button>
<button class="tab" data-mode="bullet">بوليت</button>
</div>
<!-- Top 3 Podium -->
<div id="podium" style="display:flex;align-items:flex-end;justify-content:center;gap:8px;padding:16px 0;">
<div class="text-center" id="podium-2" style="flex:1;">
<div class="avatar" style="margin:0 auto 8px;background:var(--bg-3);">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-profile"></use></svg>
</div>
<p class="text-xs" style="font-weight:600;">---</p>
<p class="text-xs text-muted">---</p>
</div>
<div class="text-center" id="podium-1" style="flex:1;">
<svg class="icon-lg" style="color:var(--gold);margin-bottom:4px;"><use href="/public/icons/sprite.svg#icon-crown"></use></svg>
<div class="avatar avatar-lg" style="margin:0 auto 8px;border:2px solid var(--gold);background:var(--bg-3);">
<svg class="icon-lg"><use href="/public/icons/sprite.svg#icon-profile"></use></svg>
</div>
<p style="font-size:14px;font-weight:700;">---</p>
<p class="text-xs text-muted">---</p>
</div>
<div class="text-center" id="podium-3" style="flex:1;">
<div class="avatar" style="margin:0 auto 8px;background:var(--bg-3);">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-profile"></use></svg>
</div>
<p class="text-xs" style="font-weight:600;">---</p>
<p class="text-xs text-muted">---</p>
</div>
</div>
<!-- Full List -->
<div class="card" id="leaderboard-list">
<div class="empty-state">جاري التحميل...</div>
</div>
</div> </div>
<script>
document.addEventListener('DOMContentLoaded', () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
let currentMode = 'blitz';
document.querySelectorAll('#lb-tabs .tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('#lb-tabs .tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
currentMode = tab.dataset.mode;
loadLeaderboard(currentMode);
});
});
loadLeaderboard(currentMode);
});
async function loadLeaderboard(mode) {
const data = await App.fetch('/api/leaderboard?mode=' + mode);
if (!data || !data.players) return;
const players = data.players;
if (players.length >= 1) {
updatePodium('podium-1', players[0], mode);
}
if (players.length >= 2) {
updatePodium('podium-2', players[1], mode);
}
if (players.length >= 3) {
updatePodium('podium-3', players[2], mode);
}
const list = document.getElementById('leaderboard-list');
if (players.length > 3) {
list.innerHTML = players.slice(3).map((p, i) => `
<div style="display:flex;align-items:center;gap:12px;padding:12px 16px;border-bottom:1px solid var(--border);">
<span style="font-weight:700;font-family:var(--font-en);min-width:24px;color:var(--text-3);">${i + 4}</span>
<div class="avatar avatar-sm" style="background:var(--bg-3);">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-profile"></use></svg>
</div>
<div style="flex:1;">
<p style="font-size:14px;font-weight:600;">${p.display_name || p.username}</p>
</div>
<span style="font-weight:700;font-family:var(--font-en);">${p['elo_' + mode] || 1200}</span>
</div>
`).join('');
} else if (players.length === 0) {
list.innerHTML = '<div class="empty-state">لا يوجد لاعبين بعد</div>';
} else {
list.innerHTML = '';
}
}
function updatePodium(elementId, player, mode) {
const el = document.getElementById(elementId);
const nameEl = el.querySelector('p:not(.text-muted)');
const ratingEl = el.querySelector('.text-muted:last-child');
if (nameEl) nameEl.textContent = player.display_name || player.username || '---';
if (ratingEl) ratingEl.textContent = (player['elo_' + mode] || 1200) + ' ELO';
}
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?> <?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB'; ?> <?php $pageTitle = 'EL3AB - الاشعارات'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?> <?php require __DIR__ . '/../includes/header.php'; ?>
<div class="text-center" style="padding:48px 20px;">
<p class="page-title">قريبا</p> <div class="space-y-6">
<p class="text-muted">هذه الصفحة قيد البناء</p>
<div style="display:flex;align-items:center;justify-content:space-between;">
<h2 style="font-size:22px;font-weight:700;">الاشعارات</h2>
<button class="btn btn-ghost btn-xs" id="mark-all-read">قراءة الكل</button>
</div>
<div id="notifications-list" class="space-y-2">
<div class="card">
<div class="empty-state">
<svg class="icon-lg" style="color:var(--text-3);margin-bottom:8px;"><use href="/public/icons/sprite.svg#icon-bell"></use></svg>
<p>لا يوجد اشعارات</p>
</div>
</div>
</div>
</div> </div>
<script>
document.addEventListener('DOMContentLoaded', async () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
const data = await App.fetch('/api/notifications');
if (data && data.notifications && data.notifications.length > 0) {
const list = document.getElementById('notifications-list');
list.innerHTML = data.notifications.map(n => {
const icons = {
friend_request: 'friends', game_invite: 'sword', achievement: 'star',
reward: 'coin', tournament: 'trophy', system: 'bell'
};
const icon = icons[n.type] || 'bell';
const unread = !n.read_at ? 'border-right:3px solid var(--cyan);' : '';
return `
<div class="card" style="${unread}">
<div class="card-body" style="display:flex;align-items:center;gap:12px;">
<svg class="icon" style="color:var(--cyan);flex-shrink:0;"><use href="/public/icons/sprite.svg#icon-${icon}"></use></svg>
<div style="flex:1;">
<p style="font-size:14px;">${n.message}</p>
<p class="text-muted text-xs">${timeAgo(n.created_at)}</p>
</div>
</div>
</div>
`;
}).join('');
}
document.getElementById('mark-all-read').addEventListener('click', async () => {
await App.fetch('/api/notifications', {
method: 'POST',
body: JSON.stringify({ action: 'read_all' })
});
document.querySelectorAll('#notifications-list .card').forEach(c => {
c.style.borderRight = '';
});
App.toast('تم تحديد الكل كمقروء');
});
});
function timeAgo(dateStr) {
const diff = Date.now() - new Date(dateStr).getTime();
const mins = Math.floor(diff / 60000);
if (mins < 1) return 'الان';
if (mins < 60) return mins + ' دقيقة';
const hours = Math.floor(mins / 60);
if (hours < 24) return hours + ' ساعة';
const days = Math.floor(hours / 24);
return days + ' يوم';
}
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?> <?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB'; ?> <?php $pageTitle = 'EL3AB - المنظمة'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?> <?php require __DIR__ . '/../includes/header.php'; ?>
<div class="text-center" style="padding:48px 20px;">
<p class="page-title">قريبا</p> <div class="space-y-6" id="org-page">
<p class="text-muted">هذه الصفحة قيد البناء</p>
<!-- Header -->
<div class="card">
<div class="card-body text-center" style="padding:24px;">
<div class="avatar avatar-lg" style="margin:0 auto 12px;background:var(--bg-3);">
<svg class="icon-xl" style="color:var(--cyan)"><use href="/public/icons/sprite.svg#icon-org"></use></svg>
</div>
<h2 style="font-size:20px;font-weight:700;" id="org-name">---</h2>
<p class="text-muted text-sm" id="org-desc">---</p>
<div style="display:flex;gap:12px;justify-content:center;margin-top:12px;">
<div class="badge" id="org-members-count">0 عضو</div>
<div class="badge" id="org-created">---</div>
</div>
</div>
</div>
<!-- Join/Leave -->
<button class="btn btn-cyan btn-block" id="org-join-btn" style="display:none;" onclick="joinOrg()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-plus"></use></svg>
انضم
</button>
<button class="btn btn-ghost btn-block" id="org-leave-btn" style="display:none;" onclick="leaveOrg()">
تسجيل خروج من المنظمة
</button>
<!-- Stats -->
<div class="stat-grid">
<div class="stat-item">
<div class="stat-value" id="org-total-games">0</div>
<div class="stat-label">مباريات</div>
</div>
<div class="stat-item">
<div class="stat-value" id="org-avg-elo">0</div>
<div class="stat-label">متوسط ELO</div>
</div>
<div class="stat-item">
<div class="stat-value" id="org-wins">0</div>
<div class="stat-label">انتصارات</div>
</div>
</div>
<!-- Members -->
<div class="card">
<div class="card-body">
<p class="section-title" style="margin-bottom:12px;">الاعضاء</p>
<div id="org-members">
<div class="empty-state text-sm">جاري التحميل...</div>
</div>
</div>
</div>
</div> </div>
<script>
document.addEventListener('DOMContentLoaded', async () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
const params = new URLSearchParams(window.location.search);
const id = params.get('id');
if (!id) {
window.location.href = '/orgs';
return;
}
const data = await App.fetch('/api/orgs?action=detail&id=' + id);
if (!data || !data.org) {
App.toast('المنظمة غير موجودة', 'error');
return;
}
const org = data.org;
document.getElementById('org-name').textContent = org.name;
document.getElementById('org-desc').textContent = org.description || '';
document.getElementById('org-members-count').textContent = (org.members_count || 0) + ' عضو';
if (data.is_member) {
document.getElementById('org-leave-btn').style.display = 'flex';
} else {
document.getElementById('org-join-btn').style.display = 'flex';
}
if (data.members && data.members.length > 0) {
document.getElementById('org-members').innerHTML = data.members.map(m => `
<div style="display:flex;align-items:center;gap:12px;padding:8px 0;border-bottom:1px solid var(--border);">
<div class="avatar avatar-sm" style="background:var(--bg-3);">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-profile"></use></svg>
</div>
<div style="flex:1;">
<p style="font-size:14px;font-weight:600;">${m.display_name || m.username}</p>
<p class="text-muted text-xs">${m.role || 'عضو'}</p>
</div>
<span style="font-family:var(--font-en);font-size:12px;color:var(--text-3);">${m.elo_blitz || 1200}</span>
</div>
`).join('');
}
});
async function joinOrg() {
const params = new URLSearchParams(window.location.search);
const res = await App.fetch('/api/orgs', {
method: 'POST',
body: JSON.stringify({ action: 'join', org_id: params.get('id') })
});
if (res && res.ok) {
App.toast('تم الانضمام', 'success');
location.reload();
}
}
async function leaveOrg() {
if (!confirm('هل تريد مغادرة المنظمة؟')) return;
const params = new URLSearchParams(window.location.search);
const res = await App.fetch('/api/orgs', {
method: 'POST',
body: JSON.stringify({ action: 'leave', org_id: params.get('id') })
});
if (res && res.ok) {
window.location.href = '/orgs';
}
}
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?> <?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB'; ?> <?php $pageTitle = 'EL3AB - المنظمات'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?> <?php require __DIR__ . '/../includes/header.php'; ?>
<div class="text-center" style="padding:48px 20px;">
<p class="page-title">قريبا</p> <div class="space-y-6">
<p class="text-muted">هذه الصفحة قيد البناء</p>
<div class="text-center">
<h2 style="font-size:22px;font-weight:700;">المنظمات</h2>
<p class="text-muted text-sm">انضم لمنظمة او انشئ واحدة</p>
</div>
<!-- Tabs -->
<div class="tab-group" id="orgs-tabs">
<button class="tab active" data-tab="my">منظماتي</button>
<button class="tab" data-tab="browse">تصفح</button>
</div>
<!-- My Orgs -->
<div id="tab-my" class="space-y-3">
<div id="my-orgs">
<div class="card"><div class="empty-state">
<svg class="icon-lg" style="color:var(--text-3);margin-bottom:8px;"><use href="/public/icons/sprite.svg#icon-org"></use></svg>
<p>لم تنضم لأي منظمة</p>
</div></div>
</div>
</div>
<!-- Browse -->
<div id="tab-browse" class="space-y-3" style="display:none;">
<div class="input-group">
<svg class="icon input-icon"><use href="/public/icons/sprite.svg#icon-search"></use></svg>
<input type="text" class="input" id="org-search" placeholder="ابحث عن منظمة...">
</div>
<div id="browse-orgs">
<div class="card"><div class="empty-state">جاري التحميل...</div></div>
</div>
</div>
<!-- Create -->
<button class="btn btn-cyan btn-block" onclick="showCreateOrg()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-plus"></use></svg>
انشاء منظمة
</button>
</div> </div>
<!-- Create Modal (hidden) -->
<div id="create-org-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.8);z-index:100;display:none;align-items:center;justify-content:center;padding:20px;">
<div class="card" style="max-width:400px;width:100%;">
<div class="card-body space-y-4" style="padding:24px;">
<p style="font-size:18px;font-weight:700;text-align:center;">انشاء منظمة</p>
<div>
<label class="input-label">اسم المنظمة</label>
<input type="text" class="input" id="org-name" placeholder="اسم المنظمة">
</div>
<div>
<label class="input-label">الوصف</label>
<input type="text" class="input" id="org-desc" placeholder="وصف قصير">
</div>
<div style="display:flex;gap:8px;">
<button class="btn btn-gold" style="flex:1;" onclick="createOrg()">انشاء</button>
<button class="btn btn-ghost" style="flex:1;" onclick="hideCreateOrg()">الغاء</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
const tabs = document.querySelectorAll('#orgs-tabs .tab');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
tabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
document.getElementById('tab-my').style.display = tab.dataset.tab === 'my' ? 'block' : 'none';
document.getElementById('tab-browse').style.display = tab.dataset.tab === 'browse' ? 'block' : 'none';
});
});
loadOrgs();
let searchTimeout;
document.getElementById('org-search').addEventListener('input', (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => searchOrgs(e.target.value), 500);
});
});
async function loadOrgs() {
const data = await App.fetch('/api/orgs?action=my');
if (data && data.orgs && data.orgs.length > 0) {
document.getElementById('my-orgs').innerHTML = data.orgs.map(o => `
<a href="/org?id=${o.id}" class="card card-hover" style="display:block;text-decoration:none;">
<div class="card-body" style="display:flex;align-items:center;gap:16px;">
<div class="avatar" style="background:var(--bg-3);">
<svg class="icon-lg" style="color:var(--cyan)"><use href="/public/icons/sprite.svg#icon-org"></use></svg>
</div>
<div style="flex:1;">
<p style="font-size:15px;font-weight:600;">${o.name}</p>
<p class="text-muted text-xs">${o.members_count || 0} عضو</p>
</div>
</div>
</a>
`).join('');
}
const browseData = await App.fetch('/api/orgs?action=browse');
if (browseData && browseData.orgs) {
renderBrowseOrgs(browseData.orgs);
}
}
async function searchOrgs(query) {
if (!query || query.length < 2) return;
const data = await App.fetch('/api/orgs?action=search&q=' + encodeURIComponent(query));
if (data && data.orgs) {
renderBrowseOrgs(data.orgs);
}
}
function renderBrowseOrgs(orgs) {
const container = document.getElementById('browse-orgs');
if (orgs.length === 0) {
container.innerHTML = '<div class="card"><div class="empty-state">لا توجد نتائج</div></div>';
return;
}
container.innerHTML = orgs.map(o => `
<a href="/org?id=${o.id}" class="card card-hover" style="display:block;text-decoration:none;margin-bottom:12px;">
<div class="card-body" style="display:flex;align-items:center;gap:16px;">
<div class="avatar" style="background:var(--bg-3);">
<svg class="icon-lg" style="color:var(--cyan)"><use href="/public/icons/sprite.svg#icon-org"></use></svg>
</div>
<div style="flex:1;">
<p style="font-size:15px;font-weight:600;">${o.name}</p>
<p class="text-muted text-xs">${o.description || ''}</p>
</div>
<span class="badge">${o.members_count || 0} عضو</span>
</div>
</a>
`).join('');
}
function showCreateOrg() {
document.getElementById('create-org-modal').style.display = 'flex';
}
function hideCreateOrg() {
document.getElementById('create-org-modal').style.display = 'none';
}
async function createOrg() {
const name = document.getElementById('org-name').value.trim();
const desc = document.getElementById('org-desc').value.trim();
if (!name) { App.toast('ادخل اسم المنظمة', 'error'); return; }
const res = await App.fetch('/api/orgs', {
method: 'POST',
body: JSON.stringify({ action: 'create', name, description: desc })
});
if (res && res.ok) {
App.toast('تم انشاء المنظمة', 'success');
hideCreateOrg();
loadOrgs();
} else {
App.toast(res?.error || 'خطأ', 'error');
}
}
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?> <?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB'; ?> <?php $pageTitle = 'EL3AB - العب'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?> <?php require __DIR__ . '/../includes/header.php'; ?>
<div class="text-center" style="padding:48px 20px;">
<p class="page-title">قريبا</p> <div class="space-y-6">
<p class="text-muted">هذه الصفحة قيد البناء</p>
<div class="text-center">
<h2 style="font-size:22px;font-weight:700;">العب شطرنج</h2>
<p class="text-muted text-sm">اختر نوع المباراة</p>
</div>
<!-- Game Mode Selection -->
<div class="space-y-3">
<!-- VS Bot -->
<a href="/bots" class="card card-hover" style="display:block;text-decoration:none;">
<div class="card-body" style="display:flex;align-items:center;gap:16px;">
<div class="avatar" style="background:var(--bg-3);">
<svg class="icon-lg" style="color:var(--cyan)"><use href="/public/icons/sprite.svg#icon-bot"></use></svg>
</div>
<div style="flex:1;">
<p style="font-size:16px;font-weight:600;">ضد البوت</p>
<p class="text-muted text-sm">7 مستويات مختلفة</p>
</div>
<svg class="icon" style="color:var(--text-3);transform:scaleX(-1)"><use href="/public/icons/sprite.svg#icon-arrow-right"></use></svg>
</div>
</a>
<!-- Quick Match -->
<div class="card card-hover" style="cursor:pointer;" onclick="startQuickMatch()">
<div class="card-body" style="display:flex;align-items:center;gap:16px;">
<div class="avatar" style="background:var(--bg-3);">
<svg class="icon-lg" style="color:var(--gold)"><use href="/public/icons/sprite.svg#icon-sword"></use></svg>
</div>
<div style="flex:1;">
<p style="font-size:16px;font-weight:600;">مباراة سريعة</p>
<p class="text-muted text-sm">5 دقائق ضد بوت عشوائي</p>
</div>
<svg class="icon" style="color:var(--text-3);transform:scaleX(-1)"><use href="/public/icons/sprite.svg#icon-arrow-right"></use></svg>
</div>
</div>
<!-- Custom Game -->
<div class="card">
<div class="card-body space-y-4">
<p style="font-size:16px;font-weight:600;text-align:center;">مباراة مخصصة</p>
<!-- Time Control -->
<div>
<label class="input-label">التوقيت</label>
<div class="tab-group" id="time-tabs">
<button class="tab" data-time="180" data-inc="0">3 د</button>
<button class="tab active" data-time="300" data-inc="0">5 د</button>
<button class="tab" data-time="600" data-inc="0">10 د</button>
<button class="tab" data-time="300" data-inc="3">5|3</button>
</div>
</div>
<!-- Color -->
<div>
<label class="input-label">اللون</label>
<div class="tab-group" id="color-tabs">
<button class="tab active" data-color="w">ابيض</button>
<button class="tab" data-color="b">اسود</button>
<button class="tab" data-color="random">عشوائي</button>
</div>
</div>
<!-- Bot Selection -->
<div>
<label class="input-label">الخصم</label>
<select class="input" id="bot-select" style="direction:ltr;">
<option value="amina">Amina (800)</option>
<option value="tarek">Tarek (1000)</option>
<option value="nour" selected>Nour (1200)</option>
<option value="omar">Omar (1400)</option>
<option value="layla">Layla (1600)</option>
<option value="ziad">Ziad (1800)</option>
<option value="grandmaster">Grandmaster (2200)</option>
</select>
</div>
<!-- Rated Toggle -->
<div style="display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:14px;">مباراة مصنفة</span>
<label class="toggle">
<input type="checkbox" id="rated-toggle" checked>
<span class="toggle-slider"></span>
</label>
</div>
<button class="btn btn-gold btn-block" onclick="startCustomGame()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-play"></use></svg>
ابدأ المباراة
</button>
</div>
</div>
</div>
</div> </div>
<script>
document.addEventListener('DOMContentLoaded', () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
document.querySelectorAll('.tab-group').forEach(group => {
group.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
group.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
});
});
});
});
function getSelectedTime() {
const active = document.querySelector('#time-tabs .tab.active');
return { time: active.dataset.time, inc: active.dataset.inc };
}
function getSelectedColor() {
const active = document.querySelector('#color-tabs .tab.active');
let color = active.dataset.color;
if (color === 'random') color = Math.random() < 0.5 ? 'w' : 'b';
return color;
}
function startQuickMatch() {
const bots = ['amina','tarek','nour','omar','layla','ziad'];
const bot = bots[Math.floor(Math.random() * bots.length)];
const color = Math.random() < 0.5 ? 'w' : 'b';
window.location.href = '/game?bot=' + bot + '&color=' + color + '&time=300&inc=0&rated=true';
}
function startCustomGame() {
const { time, inc } = getSelectedTime();
const color = getSelectedColor();
const bot = document.getElementById('bot-select').value;
const rated = document.getElementById('rated-toggle').checked;
window.location.href = '/game?bot=' + bot + '&color=' + color + '&time=' + time + '&inc=' + inc + '&rated=' + rated;
}
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?> <?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB'; ?> <?php $pageTitle = 'EL3AB - الملف الشخصي'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?> <?php require __DIR__ . '/../includes/header.php'; ?>
<div class="text-center" style="padding:48px 20px;">
<p class="page-title">قريبا</p> <div class="space-y-6" id="profile-page">
<p class="text-muted">هذه الصفحة قيد البناء</p>
<!-- Profile Header -->
<div class="card">
<div class="card-body text-center" style="padding:24px;">
<div class="avatar avatar-lg" style="margin:0 auto 12px;">
<svg class="icon-lg"><use href="/public/icons/sprite.svg#icon-profile"></use></svg>
</div>
<h2 style="font-size:20px;font-weight:700;" id="profile-name">---</h2>
<p class="text-muted text-sm" id="profile-username">@---</p>
<div style="display:flex;gap:12px;justify-content:center;margin-top:12px;">
<div class="badge badge-gold" id="profile-level">المستوى 1</div>
<div class="badge" id="profile-title">لاعب</div>
</div>
</div>
</div>
<!-- Stats Grid -->
<div class="stat-grid">
<div class="stat-item">
<div class="stat-value" id="stat-games">0</div>
<div class="stat-label">مباريات</div>
</div>
<div class="stat-item">
<div class="stat-value" id="stat-wins">0</div>
<div class="stat-label">فوز</div>
</div>
<div class="stat-item">
<div class="stat-value" id="stat-draws">0</div>
<div class="stat-label">تعادل</div>
</div>
<div class="stat-item">
<div class="stat-value" id="stat-losses">0</div>
<div class="stat-label">خسارة</div>
</div>
</div>
<!-- Ratings -->
<div class="card">
<div class="card-body">
<p class="section-title" style="margin-bottom:12px;">التصنيفات</p>
<div class="space-y-3">
<div style="display:flex;align-items:center;justify-content:space-between;">
<div style="display:flex;align-items:center;gap:8px;">
<svg class="icon" style="color:var(--gold)"><use href="/public/icons/sprite.svg#icon-clock"></use></svg>
<span>بليتز</span>
</div>
<span style="font-weight:700;font-family:var(--font-en);" id="rating-blitz">1200</span>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;">
<div style="display:flex;align-items:center;gap:8px;">
<svg class="icon" style="color:var(--cyan)"><use href="/public/icons/sprite.svg#icon-clock"></use></svg>
<span>رابيد</span>
</div>
<span style="font-weight:700;font-family:var(--font-en);" id="rating-rapid">1200</span>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;">
<div style="display:flex;align-items:center;gap:8px;">
<svg class="icon" style="color:var(--purple)"><use href="/public/icons/sprite.svg#icon-clock"></use></svg>
<span>بوليت</span>
</div>
<span style="font-weight:700;font-family:var(--font-en);" id="rating-bullet">1200</span>
</div>
</div>
</div>
</div>
<!-- Economy -->
<div class="card">
<div class="card-body">
<p class="section-title" style="margin-bottom:12px;">الاقتصاد</p>
<div class="stat-grid" style="grid-template-columns:1fr 1fr 1fr;">
<div class="stat-item">
<div class="stat-value" id="stat-coins">0</div>
<div class="stat-label">عملات</div>
</div>
<div class="stat-item">
<div class="stat-value" id="stat-gems">0</div>
<div class="stat-label">جواهر</div>
</div>
<div class="stat-item">
<div class="stat-value" id="stat-streak">0</div>
<div class="stat-label">ايام متتالية</div>
</div>
</div>
</div>
</div>
<!-- Actions -->
<div class="space-y-2">
<a href="/settings" class="btn btn-ghost btn-block">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-settings"></use></svg>
الاعدادات
</a>
<button class="btn btn-ghost btn-block" style="color:var(--error);" onclick="App.logout()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-logout"></use></svg>
تسجيل خروج
</button>
</div>
</div> </div>
<script>
document.addEventListener('DOMContentLoaded', async () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
const data = await App.fetch('/api/profile');
if (data && data.profile) {
const p = data.profile;
document.getElementById('profile-name').textContent = p.display_name || p.username || '---';
document.getElementById('profile-username').textContent = '@' + (p.username || '---');
document.getElementById('profile-level').textContent = 'المستوى ' + (p.level || 1);
document.getElementById('stat-games').textContent = p.games_played || 0;
document.getElementById('stat-wins').textContent = p.wins || 0;
document.getElementById('stat-draws').textContent = p.draws || 0;
document.getElementById('stat-losses').textContent = p.losses || 0;
document.getElementById('rating-blitz').textContent = p.elo_blitz || 1200;
document.getElementById('rating-rapid').textContent = p.elo_rapid || 1200;
document.getElementById('rating-bullet').textContent = p.elo_bullet || 1200;
document.getElementById('stat-coins').textContent = (p.coins || 0).toLocaleString();
document.getElementById('stat-gems').textContent = p.gems || 0;
document.getElementById('stat-streak').textContent = p.daily_streak || 0;
}
});
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?> <?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB'; ?> <?php $pageTitle = 'EL3AB - الاعدادات'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?> <?php require __DIR__ . '/../includes/header.php'; ?>
<div class="text-center" style="padding:48px 20px;">
<p class="page-title">قريبا</p> <div class="space-y-6">
<p class="text-muted">هذه الصفحة قيد البناء</p>
<div class="text-center">
<h2 style="font-size:22px;font-weight:700;">الاعدادات</h2>
</div>
<!-- Profile Settings -->
<div class="card">
<div class="card-body space-y-4">
<p class="section-title">الملف الشخصي</p>
<div>
<label class="input-label">الاسم</label>
<input type="text" class="input" id="set-display-name" placeholder="الاسم المعروض">
</div>
<div>
<label class="input-label">اسم المستخدم</label>
<input type="text" class="input" id="set-username" placeholder="username" style="direction:ltr;">
</div>
<button class="btn btn-cyan btn-block" onclick="saveProfile()">حفظ</button>
</div>
</div>
<!-- Game Settings -->
<div class="card">
<div class="card-body space-y-4">
<p class="section-title">اعدادات اللعب</p>
<div style="display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:14px;">اصوات الحركة</span>
<label class="toggle">
<input type="checkbox" id="set-sound" checked>
<span class="toggle-slider"></span>
</label>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:14px;">تأكيد الحركة</span>
<label class="toggle">
<input type="checkbox" id="set-confirm-move">
<span class="toggle-slider"></span>
</label>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:14px;">اظهار احداثيات الرقعة</span>
<label class="toggle">
<input type="checkbox" id="set-coords" checked>
<span class="toggle-slider"></span>
</label>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:14px;">اظهار الحركات القانونية</span>
<label class="toggle">
<input type="checkbox" id="set-legal-moves" checked>
<span class="toggle-slider"></span>
</label>
</div>
<div>
<label class="input-label">سمة الرقعة</label>
<select class="input" id="set-board-theme">
<option value="default">الافتراضي (ازرق)</option>
<option value="green">اخضر كلاسيكي</option>
<option value="brown">بني خشبي</option>
<option value="purple">بنفسجي</option>
</select>
</div>
</div>
</div>
<!-- Notifications -->
<div class="card">
<div class="card-body space-y-4">
<p class="section-title">الاشعارات</p>
<div style="display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:14px;">اشعارات طلبات الصداقة</span>
<label class="toggle">
<input type="checkbox" id="set-notif-friends" checked>
<span class="toggle-slider"></span>
</label>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:14px;">اشعارات البطولات</span>
<label class="toggle">
<input type="checkbox" id="set-notif-tournaments" checked>
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
<!-- Account -->
<div class="card">
<div class="card-body space-y-4">
<p class="section-title">الحساب</p>
<button class="btn btn-ghost btn-block" style="color:var(--error);" onclick="App.logout()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-logout"></use></svg>
تسجيل خروج
</button>
</div>
</div>
</div> </div>
<script>
document.addEventListener('DOMContentLoaded', async () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
const settings = JSON.parse(localStorage.getItem('el3ab_settings') || '{}');
if (settings.sound !== undefined) document.getElementById('set-sound').checked = settings.sound;
if (settings.confirmMove !== undefined) document.getElementById('set-confirm-move').checked = settings.confirmMove;
if (settings.coords !== undefined) document.getElementById('set-coords').checked = settings.coords;
if (settings.legalMoves !== undefined) document.getElementById('set-legal-moves').checked = settings.legalMoves;
if (settings.boardTheme) document.getElementById('set-board-theme').value = settings.boardTheme;
const data = await App.fetch('/api/profile');
if (data && data.profile) {
document.getElementById('set-display-name').value = data.profile.display_name || '';
document.getElementById('set-username').value = data.profile.username || '';
}
document.querySelectorAll('[id^="set-"]').forEach(el => {
if (el.type === 'checkbox' || el.tagName === 'SELECT') {
el.addEventListener('change', saveSettings);
}
});
});
function saveSettings() {
const settings = {
sound: document.getElementById('set-sound').checked,
confirmMove: document.getElementById('set-confirm-move').checked,
coords: document.getElementById('set-coords').checked,
legalMoves: document.getElementById('set-legal-moves').checked,
boardTheme: document.getElementById('set-board-theme').value,
notifFriends: document.getElementById('set-notif-friends').checked,
notifTournaments: document.getElementById('set-notif-tournaments').checked,
};
localStorage.setItem('el3ab_settings', JSON.stringify(settings));
}
async function saveProfile() {
const displayName = document.getElementById('set-display-name').value.trim();
const username = document.getElementById('set-username').value.trim();
const res = await App.fetch('/api/profile', {
method: 'PATCH',
body: JSON.stringify({ display_name: displayName, username })
});
if (res && !res.error) {
App.toast('تم حفظ الملف الشخصي', 'success');
} else {
App.toast(res?.error || 'خطأ في الحفظ', 'error');
}
}
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?> <?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB'; ?> <?php $pageTitle = 'EL3AB - المتجر'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?> <?php require __DIR__ . '/../includes/header.php'; ?>
<div class="text-center" style="padding:48px 20px;">
<p class="page-title">قريبا</p> <div class="space-y-6">
<p class="text-muted">هذه الصفحة قيد البناء</p>
<div class="text-center">
<h2 style="font-size:22px;font-weight:700;">المتجر</h2>
<p class="text-muted text-sm">اشتري مظاهر وعناصر</p>
</div>
<!-- Balance -->
<div class="card">
<div class="card-body" style="display:flex;align-items:center;justify-content:space-around;padding:16px;">
<div class="text-center">
<div style="display:flex;align-items:center;gap:6px;justify-content:center;">
<svg class="icon" style="color:var(--gold)"><use href="/public/icons/sprite.svg#icon-coin"></use></svg>
<span style="font-size:18px;font-weight:700;" id="shop-coins">0</span>
</div>
<p class="text-muted text-xs">عملات</p>
</div>
<div class="text-center">
<div style="display:flex;align-items:center;gap:6px;justify-content:center;">
<svg class="icon" style="color:var(--purple)"><use href="/public/icons/sprite.svg#icon-gem"></use></svg>
<span style="font-size:18px;font-weight:700;" id="shop-gems">0</span>
</div>
<p class="text-muted text-xs">جواهر</p>
</div>
</div>
</div>
<!-- Category Tabs -->
<div class="tab-group" id="shop-tabs">
<button class="tab active" data-cat="boards">رقع</button>
<button class="tab" data-cat="pieces">قطع</button>
<button class="tab" data-cat="avatars">صور</button>
<button class="tab" data-cat="effects">تأثيرات</button>
</div>
<!-- Items Grid -->
<div class="space-y-3" id="shop-items">
<div class="card"><div class="empty-state">جاري التحميل...</div></div>
</div>
</div> </div>
<script>
document.addEventListener('DOMContentLoaded', async () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
let currentCat = 'boards';
document.querySelectorAll('#shop-tabs .tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('#shop-tabs .tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
currentCat = tab.dataset.cat;
loadShopItems(currentCat);
});
});
const profileData = await App.fetch('/api/profile');
if (profileData && profileData.profile) {
document.getElementById('shop-coins').textContent = (profileData.profile.coins || 0).toLocaleString();
document.getElementById('shop-gems').textContent = profileData.profile.gems || 0;
}
loadShopItems(currentCat);
});
async function loadShopItems(category) {
const data = await App.fetch('/api/shop?category=' + category);
const container = document.getElementById('shop-items');
if (!data || !data.items || data.items.length === 0) {
container.innerHTML = '<div class="card"><div class="empty-state">لا يوجد عناصر في هذا القسم</div></div>';
return;
}
container.innerHTML = data.items.map(item => {
const owned = item.owned ? ' style="opacity:0.6;"' : '';
const currency = item.currency === 'gems' ? 'جوهرة' : 'عملة';
const currIcon = item.currency === 'gems' ? 'gem' : 'coin';
const currColor = item.currency === 'gems' ? 'var(--purple)' : 'var(--gold)';
return `
<div class="card card-hover"${owned}>
<div class="card-body" style="display:flex;align-items:center;gap:16px;">
<div class="avatar" style="background:var(--bg-3);">
<svg class="icon-lg" style="color:var(--cyan)"><use href="/public/icons/sprite.svg#icon-${item.icon || 'star'}"></use></svg>
</div>
<div style="flex:1;">
<p style="font-size:15px;font-weight:600;">${item.name}</p>
<p class="text-muted text-xs">${item.description || ''}</p>
</div>
${item.owned ? '<span class="badge badge-success">مملوك</span>' : `
<button class="btn btn-sm btn-gold" onclick="buyItem('${item.id}')">
<svg class="icon-sm" style="color:${currColor}"><use href="/public/icons/sprite.svg#icon-${currIcon}"></use></svg>
${item.price}
</button>
`}
</div>
</div>
`;
}).join('');
}
async function buyItem(itemId) {
const res = await App.fetch('/api/shop', {
method: 'POST',
body: JSON.stringify({ action: 'buy', item_id: itemId })
});
if (res && res.ok) {
App.toast('تم الشراء بنجاح!', 'success');
const profileData = await App.fetch('/api/profile');
if (profileData && profileData.profile) {
document.getElementById('shop-coins').textContent = (profileData.profile.coins || 0).toLocaleString();
document.getElementById('shop-gems').textContent = profileData.profile.gems || 0;
}
const active = document.querySelector('#shop-tabs .tab.active');
loadShopItems(active.dataset.cat);
} else {
App.toast(res?.error || 'خطأ في الشراء', 'error');
}
}
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?> <?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB'; ?> <?php $pageTitle = 'EL3AB - البطولة'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?> <?php require __DIR__ . '/../includes/header.php'; ?>
<div class="text-center" style="padding:48px 20px;">
<p class="page-title">قريبا</p> <div class="space-y-6" id="tournament-page">
<p class="text-muted">هذه الصفحة قيد البناء</p>
<!-- Header -->
<div class="card">
<div class="card-body text-center" style="padding:24px;">
<svg class="icon-xl" style="color:var(--gold);margin-bottom:12px;width:48px;height:48px;"><use href="/public/icons/sprite.svg#icon-trophy"></use></svg>
<h2 style="font-size:20px;font-weight:700;" id="t-name">---</h2>
<p class="text-muted text-sm" id="t-desc">---</p>
<div style="display:flex;gap:12px;justify-content:center;margin-top:16px;">
<div class="badge" id="t-status">---</div>
<div class="badge" id="t-time">---</div>
<div class="badge" id="t-players">---</div>
</div>
</div>
</div>
<!-- Join Button -->
<button class="btn btn-gold btn-block" id="t-join-btn" style="display:none;" onclick="joinTournament()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-sword"></use></svg>
انضم للبطولة
</button>
<!-- Standings -->
<div class="card">
<div class="card-body">
<p class="section-title" style="margin-bottom:12px;">الترتيب</p>
<div id="t-standings">
<div class="empty-state text-sm">لا يوجد مشاركين بعد</div>
</div>
</div>
</div>
<!-- Rounds -->
<div class="card">
<div class="card-body">
<p class="section-title" style="margin-bottom:12px;">الجولات</p>
<div id="t-rounds">
<div class="empty-state text-sm">لم تبدأ بعد</div>
</div>
</div>
</div>
</div> </div>
<script>
document.addEventListener('DOMContentLoaded', async () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
const params = new URLSearchParams(window.location.search);
const id = params.get('id');
if (!id) {
window.location.href = '/tournaments';
return;
}
const data = await App.fetch('/api/tournaments?id=' + id);
if (!data || !data.tournament) {
App.toast('البطولة غير موجودة', 'error');
return;
}
const t = data.tournament;
document.getElementById('t-name').textContent = t.name;
document.getElementById('t-desc').textContent = t.description || '';
document.getElementById('t-status').textContent = t.status === 'active' ? 'جارية' : t.status === 'upcoming' ? 'قادمة' : 'منتهية';
document.getElementById('t-time').textContent = t.time_control || '5+0';
document.getElementById('t-players').textContent = (t.participants_count || 0) + ' مشارك';
if (t.status === 'upcoming' || t.status === 'active') {
document.getElementById('t-join-btn').style.display = 'flex';
}
if (data.standings && data.standings.length > 0) {
document.getElementById('t-standings').innerHTML = data.standings.map((s, i) => `
<div style="display:flex;align-items:center;gap:12px;padding:8px 0;border-bottom:1px solid var(--border);">
<span style="font-weight:700;font-family:var(--font-en);min-width:24px;color:var(--text-3);">${i + 1}</span>
<div style="flex:1;">
<p style="font-size:14px;font-weight:600;">${s.display_name || s.username}</p>
</div>
<span style="font-family:var(--font-en);font-weight:600;">${s.points || 0} نقطة</span>
</div>
`).join('');
}
});
async function joinTournament() {
const params = new URLSearchParams(window.location.search);
const id = params.get('id');
const res = await App.fetch('/api/tournaments', {
method: 'POST',
body: JSON.stringify({ action: 'join', tournament_id: id })
});
if (res && res.ok) {
App.toast('تم الانضمام بنجاح', 'success');
document.getElementById('t-join-btn').style.display = 'none';
} else {
App.toast('خطأ في الانضمام', 'error');
}
}
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?> <?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB'; ?> <?php $pageTitle = 'EL3AB - البطولات'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?> <?php require __DIR__ . '/../includes/header.php'; ?>
<div class="text-center" style="padding:48px 20px;">
<p class="page-title">قريبا</p> <div class="space-y-6">
<p class="text-muted">هذه الصفحة قيد البناء</p>
<div class="text-center">
<h2 style="font-size:22px;font-weight:700;">البطولات</h2>
<p class="text-muted text-sm">شارك في بطولات واربح جوائز</p>
</div>
<!-- Tabs -->
<div class="tab-group" id="tourney-tabs">
<button class="tab active" data-tab="active">جارية</button>
<button class="tab" data-tab="upcoming">قادمة</button>
<button class="tab" data-tab="completed">منتهية</button>
</div>
<!-- Active Tournaments -->
<div id="tab-active" class="space-y-3">
<div id="active-tournaments">
<div class="card">
<div class="empty-state">جاري التحميل...</div>
</div>
</div>
</div>
<!-- Upcoming -->
<div id="tab-upcoming" class="space-y-3" style="display:none;">
<div id="upcoming-tournaments">
<div class="card">
<div class="empty-state">جاري التحميل...</div>
</div>
</div>
</div>
<!-- Completed -->
<div id="tab-completed" class="space-y-3" style="display:none;">
<div id="completed-tournaments">
<div class="card">
<div class="empty-state">جاري التحميل...</div>
</div>
</div>
</div>
</div> </div>
<script>
document.addEventListener('DOMContentLoaded', () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
const tabs = document.querySelectorAll('#tourney-tabs .tab');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
tabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
document.querySelectorAll('[id^="tab-"]').forEach(p => p.style.display = 'none');
document.getElementById('tab-' + tab.dataset.tab).style.display = 'block';
});
});
loadTournaments();
});
async function loadTournaments() {
const data = await App.fetch('/api/tournaments');
if (!data || !data.tournaments) return;
const now = new Date();
const active = [];
const upcoming = [];
const completed = [];
data.tournaments.forEach(t => {
const start = new Date(t.start_time);
const end = t.end_time ? new Date(t.end_time) : null;
if (t.status === 'completed' || (end && end < now)) {
completed.push(t);
} else if (t.status === 'active' || (start <= now && (!end || end > now))) {
active.push(t);
} else {
upcoming.push(t);
}
});
renderTournaments('active-tournaments', active, 'لا توجد بطولات جارية');
renderTournaments('upcoming-tournaments', upcoming, 'لا توجد بطولات قادمة');
renderTournaments('completed-tournaments', completed, 'لا توجد بطولات منتهية');
}
function renderTournaments(containerId, tournaments, emptyMsg) {
const container = document.getElementById(containerId);
if (tournaments.length === 0) {
container.innerHTML = '<div class="card"><div class="empty-state">' + emptyMsg + '</div></div>';
return;
}
container.innerHTML = tournaments.map(t => `
<a href="/tournament?id=${t.id}" class="card card-hover" style="display:block;text-decoration:none;margin-bottom:12px;">
<div class="card-body" style="display:flex;align-items:center;gap:16px;">
<div class="avatar" style="background:var(--bg-3);">
<svg class="icon-lg" style="color:var(--gold)"><use href="/public/icons/sprite.svg#icon-trophy"></use></svg>
</div>
<div style="flex:1;">
<p style="font-size:15px;font-weight:600;">${t.name}</p>
<p class="text-muted text-xs">${t.participants_count || 0} مشارك</p>
</div>
<div class="text-left">
<p class="text-xs" style="font-family:var(--font-en);font-weight:600;">${t.time_control || '5+0'}</p>
<p class="text-muted text-xs">${t.prize || ''}</p>
</div>
</div>
</a>
`).join('');
}
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?> <?php require __DIR__ . '/../includes/footer.php'; ?>
/* Chess Board Styles */
.game-container {
display: flex;
flex-direction: column;
gap: 12px;
max-width: 100%;
}
.game-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--radius-md);
}
.game-player {
display: flex;
align-items: center;
gap: 10px;
}
.game-player-name {
font-size: 14px;
font-weight: 600;
}
.game-player-rating {
font-size: 11px;
color: var(--text-3);
font-family: var(--font-en);
}
.game-clock {
font-family: var(--font-en);
font-size: 18px;
font-weight: 700;
padding: 6px 12px;
background: var(--bg-3);
border-radius: var(--radius-sm);
min-width: 70px;
text-align: center;
}
.game-clock.active {
background: var(--gold);
color: var(--text-inverse);
}
.game-clock.low {
background: var(--error);
color: #fff;
animation: clock-pulse 1s ease infinite;
}
@keyframes clock-pulse {
50% { opacity: 0.7; }
}
/* Board */
.board-wrapper {
position: relative;
width: 100%;
max-width: 560px;
margin: 0 auto;
aspect-ratio: 1;
}
.board {
display: grid;
grid-template-columns: repeat(8, 1fr);
grid-template-rows: repeat(8, 1fr);
width: 100%;
height: 100%;
border: 2px solid var(--border-strong);
border-radius: var(--radius-sm);
overflow: hidden;
user-select: none;
touch-action: none;
}
.square {
position: relative;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 0.1s;
}
.square-light { background: #B7C0D8; }
.square-dark { background: #5A7BAE; }
.square.selected { background: rgba(21, 215, 255, 0.4) !important; }
.square.legal-move::after {
content: '';
position: absolute;
width: 28%;
height: 28%;
border-radius: 50%;
background: rgba(0, 0, 0, 0.2);
}
.square.legal-capture::after {
content: '';
position: absolute;
width: 85%;
height: 85%;
border-radius: 50%;
border: 3px solid rgba(0, 0, 0, 0.2);
background: transparent;
}
.square.last-move { background: rgba(231, 168, 50, 0.3) !important; }
.square.in-check { background: rgba(239, 68, 68, 0.5) !important; }
/* Pieces */
.piece {
width: 85%;
height: 85%;
font-size: 0;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
cursor: grab;
z-index: 1;
transition: transform 0.15s var(--ease);
}
.piece:active { cursor: grabbing; }
.piece.dragging {
position: fixed;
z-index: 100;
pointer-events: none;
transform: scale(1.1);
}
/* Piece images via CSS classes - using unicode */
.piece-wK::before, .piece-wQ::before, .piece-wR::before,
.piece-wB::before, .piece-wN::before, .piece-wP::before,
.piece-bK::before, .piece-bQ::before, .piece-bR::before,
.piece-bB::before, .piece-bN::before, .piece-bP::before {
font-size: calc(min(10vw, 56px));
line-height: 1;
}
.piece-wK { content: ''; }
.piece-bK { content: ''; }
/* Coordinate labels */
.board-coords-file, .board-coords-rank {
position: absolute;
display: flex;
font-family: var(--font-en);
font-size: 10px;
font-weight: 600;
color: var(--text-3);
pointer-events: none;
}
.board-coords-file {
bottom: -20px;
left: 0;
right: 0;
justify-content: space-around;
padding: 0 4%;
}
.board-coords-rank {
top: 0;
bottom: 0;
right: -20px;
flex-direction: column;
justify-content: space-around;
align-items: center;
padding: 2% 0;
}
/* Move list */
.move-list {
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 12px;
max-height: 200px;
overflow-y: auto;
font-family: var(--font-en);
font-size: 13px;
direction: ltr;
}
.move-pair {
display: flex;
gap: 4px;
padding: 2px 0;
}
.move-number {
color: var(--text-3);
min-width: 28px;
}
.move {
padding: 2px 6px;
border-radius: 4px;
cursor: pointer;
}
.move:hover { background: var(--bg-3); }
.move.current { background: var(--cyan); color: var(--text-inverse); }
/* Game controls */
.game-controls {
display: flex;
gap: 8px;
justify-content: center;
}
.game-controls .btn {
flex: 1;
max-width: 160px;
}
/* Game result overlay */
.game-result {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(5, 13, 23, 0.9);
z-index: 10;
border-radius: var(--radius-sm);
gap: 12px;
}
.game-result-title {
font-size: 24px;
font-weight: 700;
}
.game-result-subtitle {
font-size: 14px;
color: var(--text-2);
}
/* Thinking indicator */
.thinking {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--radius-md);
font-size: 13px;
color: var(--text-2);
}
.thinking-dots span {
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--cyan);
animation: thinking-bounce 1.4s infinite;
margin: 0 2px;
}
.thinking-dots span:nth-child(2) { animation-delay: 0.2s; }
.thinking-dots span:nth-child(3) { animation-delay: 0.4s; }
@keyframes thinking-bounce {
0%, 80%, 100% { transform: translateY(0); opacity: 0.4; }
40% { transform: translateY(-6px); opacity: 1; }
}
/* Promotion modal */
.promotion-modal {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--bg-2);
border: 1px solid var(--border-strong);
border-radius: var(--radius-md);
padding: 16px;
display: flex;
gap: 8px;
z-index: 20;
box-shadow: var(--shadow-lg);
}
.promotion-piece {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
background: var(--bg-3);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
cursor: pointer;
transition: background 0.2s;
}
.promotion-piece:hover { background: var(--cyan); }
// EL3AB Chess Board - Rendering, Drag & Drop, Animations
const Board = {
el: null,
squares: [],
flipped: false,
position: null,
selectedSquare: null,
legalMoves: [],
lastMove: null,
dragPiece: null,
dragStartSquare: null,
onMove: null,
PIECES: {
wK: '♔', wQ: '♕', wR: '♖', wB: '♗', wN: '♘', wP: '♙',
bK: '♚', bQ: '♛', bR: '♜', bB: '♝', bN: '♞', bP: '♟'
},
FILES: ['a','b','c','d','e','f','g','h'],
RANKS: ['8','7','6','5','4','3','2','1'],
init(containerId, options = {}) {
this.el = document.getElementById(containerId);
if (!this.el) return;
this.flipped = options.flipped || false;
this.onMove = options.onMove || null;
this.render();
this.bindEvents();
},
render() {
this.el.innerHTML = '';
this.el.className = 'board';
this.squares = [];
const ranks = this.flipped ? [0,1,2,3,4,5,6,7] : [7,6,5,4,3,2,1,0];
const files = this.flipped ? [7,6,5,4,3,2,1,0] : [0,1,2,3,4,5,6,7];
for (let r = 0; r < 8; r++) {
for (let f = 0; f < 8; f++) {
const rank = ranks[r];
const file = files[f];
const sq = document.createElement('div');
const isLight = (rank + file) % 2 === 0;
const sqName = this.FILES[file] + this.RANKS[7 - rank];
sq.className = 'square ' + (isLight ? 'square-light' : 'square-dark');
sq.dataset.square = sqName;
this.el.appendChild(sq);
this.squares.push(sq);
}
}
},
setPosition(chess) {
this.position = chess;
this.updatePieces();
},
updatePieces() {
if (!this.position) return;
const board = this.position.board();
this.squares.forEach(sq => {
const sqName = sq.dataset.square;
const file = this.FILES.indexOf(sqName[0]);
const rank = 8 - parseInt(sqName[1]);
const piece = board[rank][file];
let pieceEl = sq.querySelector('.piece');
if (piece) {
const key = piece.color + piece.type.toUpperCase();
if (!pieceEl) {
pieceEl = document.createElement('div');
pieceEl.className = 'piece';
sq.appendChild(pieceEl);
}
pieceEl.textContent = this.PIECES[key];
pieceEl.dataset.piece = key;
pieceEl.dataset.color = piece.color;
} else {
if (pieceEl) pieceEl.remove();
}
});
this.updateHighlights();
},
updateHighlights() {
this.squares.forEach(sq => {
sq.classList.remove('selected', 'legal-move', 'legal-capture', 'last-move', 'in-check');
});
if (this.lastMove) {
const fromSq = this.getSquareEl(this.lastMove.from);
const toSq = this.getSquareEl(this.lastMove.to);
if (fromSq) fromSq.classList.add('last-move');
if (toSq) toSq.classList.add('last-move');
}
if (this.selectedSquare) {
const selSq = this.getSquareEl(this.selectedSquare);
if (selSq) selSq.classList.add('selected');
this.legalMoves.forEach(move => {
const sq = this.getSquareEl(move.to);
if (sq) {
if (move.captured) {
sq.classList.add('legal-capture');
} else {
sq.classList.add('legal-move');
}
}
});
}
if (this.position && this.position.in_check()) {
const turn = this.position.turn();
const board = this.position.board();
for (let r = 0; r < 8; r++) {
for (let f = 0; f < 8; f++) {
const p = board[r][f];
if (p && p.type === 'k' && p.color === turn) {
const sqName = this.FILES[f] + (8 - r);
const sq = this.getSquareEl(sqName);
if (sq) sq.classList.add('in-check');
}
}
}
}
},
getSquareEl(name) {
return this.el.querySelector(`[data-square="${name}"]`);
},
bindEvents() {
let isDragging = false;
let dragEl = null;
let startX, startY;
const squareSize = () => this.el.getBoundingClientRect().width / 8;
const handleStart = (e) => {
const touch = e.touches ? e.touches[0] : e;
const sq = touch.target.closest('.square');
if (!sq) return;
const sqName = sq.dataset.square;
const pieceEl = sq.querySelector('.piece');
if (this.selectedSquare && sqName !== this.selectedSquare) {
const move = this.legalMoves.find(m => m.to === sqName);
if (move) {
this.makeUserMove(move);
return;
}
}
if (!pieceEl) {
this.deselect();
return;
}
const turn = this.position ? this.position.turn() : 'w';
if (pieceEl.dataset.color !== turn) {
this.deselect();
return;
}
this.selectedSquare = sqName;
this.legalMoves = this.position ? this.position.moves({ square: sqName, verbose: true }) : [];
this.updateHighlights();
isDragging = true;
this.dragStartSquare = sqName;
dragEl = pieceEl;
startX = touch.clientX;
startY = touch.clientY;
dragEl.classList.add('dragging');
const rect = this.el.getBoundingClientRect();
const sqRect = sq.getBoundingClientRect();
dragEl.style.width = sqRect.width * 0.85 + 'px';
dragEl.style.height = sqRect.height * 0.85 + 'px';
dragEl.style.left = (touch.clientX - sqRect.width * 0.425) + 'px';
dragEl.style.top = (touch.clientY - sqRect.height * 0.425) + 'px';
dragEl.style.fontSize = (sqRect.width * 0.75) + 'px';
e.preventDefault();
};
const handleMove = (e) => {
if (!isDragging || !dragEl) return;
const touch = e.touches ? e.touches[0] : e;
const sqSize = squareSize();
dragEl.style.left = (touch.clientX - sqSize * 0.425) + 'px';
dragEl.style.top = (touch.clientY - sqSize * 0.425) + 'px';
e.preventDefault();
};
const handleEnd = (e) => {
if (!isDragging || !dragEl) return;
isDragging = false;
dragEl.classList.remove('dragging');
dragEl.style = '';
const touch = e.changedTouches ? e.changedTouches[0] : e;
const rect = this.el.getBoundingClientRect();
const x = touch.clientX - rect.left;
const y = touch.clientY - rect.top;
const sqSize = rect.width / 8;
let fileIdx = Math.floor(x / sqSize);
let rankIdx = Math.floor(y / sqSize);
if (this.flipped) {
fileIdx = 7 - fileIdx;
rankIdx = 7 - rankIdx;
}
if (fileIdx >= 0 && fileIdx < 8 && rankIdx >= 0 && rankIdx < 8) {
const targetSq = this.FILES[fileIdx] + (8 - rankIdx);
if (targetSq !== this.dragStartSquare) {
const move = this.legalMoves.find(m => m.to === targetSq);
if (move) {
this.makeUserMove(move);
dragEl = null;
return;
}
}
}
this.updatePieces();
dragEl = null;
};
this.el.addEventListener('mousedown', handleStart);
document.addEventListener('mousemove', handleMove);
document.addEventListener('mouseup', handleEnd);
this.el.addEventListener('touchstart', handleStart, { passive: false });
document.addEventListener('touchmove', handleMove, { passive: false });
document.addEventListener('touchend', handleEnd);
},
makeUserMove(move) {
if (move.flags && move.flags.includes('p')) {
this.showPromotion(move);
} else {
this.confirmMove(move);
}
},
showPromotion(move) {
const overlay = document.createElement('div');
overlay.className = 'promotion-modal';
const color = this.position.turn();
const pieces = ['q', 'r', 'b', 'n'];
const symbols = { q: color === 'w' ? '♕' : '♛', r: color === 'w' ? '♖' : '♜', b: color === 'w' ? '♗' : '♝', n: color === 'w' ? '♘' : '♞' };
pieces.forEach(p => {
const btn = document.createElement('div');
btn.className = 'promotion-piece';
btn.textContent = symbols[p];
btn.onclick = () => {
overlay.remove();
this.confirmMove({ ...move, promotion: p });
};
overlay.appendChild(btn);
});
const wrapper = this.el.closest('.board-wrapper');
if (wrapper) wrapper.appendChild(overlay);
else this.el.parentElement.appendChild(overlay);
},
confirmMove(move) {
this.lastMove = { from: move.from, to: move.to };
this.deselect();
if (this.onMove) {
this.onMove(move);
}
},
deselect() {
this.selectedSquare = null;
this.legalMoves = [];
this.updateHighlights();
},
flip() {
this.flipped = !this.flipped;
this.render();
this.updatePieces();
},
animateMove(from, to, callback) {
const fromEl = this.getSquareEl(from);
const toEl = this.getSquareEl(to);
if (!fromEl || !toEl) { if (callback) callback(); return; }
const pieceEl = fromEl.querySelector('.piece');
if (!pieceEl) { if (callback) callback(); return; }
const fromRect = fromEl.getBoundingClientRect();
const toRect = toEl.getBoundingClientRect();
const dx = toRect.left - fromRect.left;
const dy = toRect.top - fromRect.top;
pieceEl.style.transition = 'transform 0.2s ease';
pieceEl.style.transform = `translate(${dx}px, ${dy}px)`;
setTimeout(() => {
pieceEl.style.transition = '';
pieceEl.style.transform = '';
if (callback) callback();
}, 200);
}
};
var Chess=function(r){var u="b",s="w",l=-1,_="p",A="n",S="b",m="r",y="q",p="k",t="pnbrqkPNBRQK",e="rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",g=["1-0","0-1","1/2-1/2","*"],C={b:[16,32,17,15],w:[-16,-32,-17,-15]},T={n:[-18,-33,-31,-14,18,33,31,14],b:[-17,-15,17,15],r:[-16,1,16,-1],q:[-17,-16,-15,1,17,16,15,-1],k:[-17,-16,-15,1,17,16,15,-1]},c=[20,0,0,0,0,0,0,24,0,0,0,0,0,0,20,0,0,20,0,0,0,0,0,24,0,0,0,0,0,20,0,0,0,0,20,0,0,0,0,24,0,0,0,0,20,0,0,0,0,0,0,20,0,0,0,24,0,0,0,20,0,0,0,0,0,0,0,0,20,0,0,24,0,0,20,0,0,0,0,0,0,0,0,0,0,20,2,24,2,20,0,0,0,0,0,0,0,0,0,0,0,2,53,56,53,2,0,0,0,0,0,0,24,24,24,24,24,24,56,0,56,24,24,24,24,24,24,0,0,0,0,0,0,2,53,56,53,2,0,0,0,0,0,0,0,0,0,0,0,20,2,24,2,20,0,0,0,0,0,0,0,0,0,0,20,0,0,24,0,0,20,0,0,0,0,0,0,0,0,20,0,0,0,24,0,0,0,20,0,0,0,0,0,0,20,0,0,0,0,24,0,0,0,0,20,0,0,0,0,20,0,0,0,0,0,24,0,0,0,0,0,20,0,0,20,0,0,0,0,0,0,24,0,0,0,0,0,0,20],v=[17,0,0,0,0,0,0,16,0,0,0,0,0,0,15,0,0,17,0,0,0,0,0,16,0,0,0,0,0,15,0,0,0,0,17,0,0,0,0,16,0,0,0,0,15,0,0,0,0,0,0,17,0,0,0,16,0,0,0,15,0,0,0,0,0,0,0,0,17,0,0,16,0,0,15,0,0,0,0,0,0,0,0,0,0,17,0,16,0,15,0,0,0,0,0,0,0,0,0,0,0,0,17,16,15,0,0,0,0,0,0,0,1,1,1,1,1,1,1,0,-1,-1,-1,-1,-1,-1,-1,0,0,0,0,0,0,0,-15,-16,-17,0,0,0,0,0,0,0,0,0,0,0,0,-15,0,-16,0,-17,0,0,0,0,0,0,0,0,0,0,-15,0,0,-16,0,0,-17,0,0,0,0,0,0,0,0,-15,0,0,0,-16,0,0,0,-17,0,0,0,0,0,0,-15,0,0,0,0,-16,0,0,0,0,-17,0,0,0,0,-15,0,0,0,0,0,-16,0,0,0,0,0,-17,0,0,-15,0,0,0,0,0,0,-16,0,0,0,0,0,0,-17],h={p:0,n:1,b:2,r:3,q:4,k:5},o={NORMAL:"n",CAPTURE:"c",BIG_PAWN:"b",EP_CAPTURE:"e",PROMOTION:"p",KSIDE_CASTLE:"k",QSIDE_CASTLE:"q"},I={NORMAL:1,CAPTURE:2,BIG_PAWN:4,EP_CAPTURE:8,PROMOTION:16,KSIDE_CASTLE:32,QSIDE_CASTLE:64},P=7,w=6,L=1,R=0,N={a8:0,b8:1,c8:2,d8:3,e8:4,f8:5,g8:6,h8:7,a7:16,b7:17,c7:18,d7:19,e7:20,f7:21,g7:22,h7:23,a6:32,b6:33,c6:34,d6:35,e6:36,f6:37,g6:38,h6:39,a5:48,b5:49,c5:50,d5:51,e5:52,f5:53,g5:54,h5:55,a4:64,b4:65,c4:66,d4:67,e4:68,f4:69,g4:70,h4:71,a3:80,b3:81,c3:82,d3:83,e3:84,f3:85,g3:86,h3:87,a2:96,b2:97,c2:98,d2:99,e2:100,f2:101,g2:102,h2:103,a1:112,b1:113,c1:114,d1:115,e1:116,f1:117,g1:118,h1:119},E={w:[{square:N.a1,flag:I.QSIDE_CASTLE},{square:N.h1,flag:I.KSIDE_CASTLE}],b:[{square:N.a8,flag:I.QSIDE_CASTLE},{square:N.h8,flag:I.KSIDE_CASTLE}]},O=new Array(128),k={w:l,b:l},q=s,D={w:0,b:0},K=l,d=0,b=1,Q=[],U={};function x(r){void 0===r&&(r=!1),O=new Array(128),k={w:l,b:l},q=s,D={w:0,b:0},K=l,d=0,b=1,Q=[],r||(U={}),F(M())}function j(){B(e)}function B(r,e){void 0===e&&(e=!1);var n=r.split(/\s+/),t=n[0],o=0;if(!$(r).valid)return!1;x(e);for(var i=0;i<t.length;i++){var f=t.charAt(i);if("/"===f)o+=8;else if(-1!=="0123456789".indexOf(f))o+=parseInt(f,10);else{var a=f<"a"?s:u;W({type:f.toLowerCase(),color:a},fr(o)),o++}}return q=n[1],-1<n[2].indexOf("K")&&(D.w|=I.KSIDE_CASTLE),-1<n[2].indexOf("Q")&&(D.w|=I.QSIDE_CASTLE),-1<n[2].indexOf("k")&&(D.b|=I.KSIDE_CASTLE),-1<n[2].indexOf("q")&&(D.b|=I.QSIDE_CASTLE),K="-"===n[3]?l:N[n[3]],d=parseInt(n[4],10),b=parseInt(n[5],10),F(M()),!0}function $(r){var e="No errors.",n="FEN string must contain six space-delimited fields.",t="6th field (move number) must be a positive integer.",o="5th field (half move counter) must be a non-negative integer.",i="4th field (en-passant square) is invalid.",f="3rd field (castling availability) is invalid.",a="2nd field (side to move) is invalid.",l="1st field (piece positions) does not contain 8 '/'-delimited rows.",u="1st field (piece positions) is invalid [consecutive numbers].",s="1st field (piece positions) is invalid [invalid piece].",p="1st field (piece positions) is invalid [row too large].",c="Illegal en-passant square",v=r.split(/\s+/);if(6!==v.length)return{valid:!1,error_number:1,error:n};if(isNaN(v[5])||parseInt(v[5],10)<=0)return{valid:!1,error_number:2,error:t};if(isNaN(v[4])||parseInt(v[4],10)<0)return{valid:!1,error_number:3,error:o};if(!/^(-|[abcdefgh][36])$/.test(v[3]))return{valid:!1,error_number:4,error:i};if(!/^(KQ?k?q?|Qk?q?|kq?|q|-)$/.test(v[2]))return{valid:!1,error_number:5,error:f};if(!/^(w|b)$/.test(v[1]))return{valid:!1,error_number:6,error:a};var g=v[0].split("/");if(8!==g.length)return{valid:!1,error_number:7,error:l};for(var h=0;h<g.length;h++){for(var E=0,d=!1,b=0;b<g[h].length;b++)if(isNaN(g[h][b])){if(!/^[prnbqkPRNBQK]$/.test(g[h][b]))return{valid:!1,error_number:9,error:s};E+=1,d=!1}else{if(d)return{valid:!1,error_number:8,error:u};E+=parseInt(g[h][b],10),d=!0}if(8!==E)return{valid:!1,error_number:10,error:p}}return"3"==v[3][1]&&"w"==v[1]||"6"==v[3][1]&&"b"==v[1]?{valid:!1,error_number:11,error:c}:{valid:!0,error_number:0,error:e}}function M(){for(var r=0,e="",n=N.a8;n<=N.h1;n++){if(null==O[n])r++;else{0<r&&(e+=r,r=0);var t=O[n].color,o=O[n].type;e+=t===s?o.toUpperCase():o.toLowerCase()}n+1&136&&(0<r&&(e+=r),n!==N.h1&&(e+="/"),r=0,n+=8)}var i="";D[s]&I.KSIDE_CASTLE&&(i+="K"),D[s]&I.QSIDE_CASTLE&&(i+="Q"),D[u]&I.KSIDE_CASTLE&&(i+="k"),D[u]&I.QSIDE_CASTLE&&(i+="q"),i=i||"-";var f=K===l?"-":fr(K);return[e,q,i,f,d,b].join(" ")}function G(r){for(var e=0;e<r.length;e+=2)"string"==typeof r[e]&&"string"==typeof r[e+1]&&(U[r[e]]=r[e+1]);return U}function F(r){0<Q.length||(r!==e?(U.SetUp="1",U.FEN=r):(delete U.SetUp,delete U.FEN))}function i(r){var e=O[N[r]];return e?{type:e.type,color:e.color}:null}function W(r,e){if(!("type"in r&&"color"in r))return!1;if(-1===t.indexOf(r.type.toLowerCase()))return!1;if(!(e in N))return!1;var n=N[e];return(r.type!=p||k[r.color]==l||k[r.color]==n)&&(O[n]={type:r.type,color:r.color},r.type===p&&(k[r.color]=n),F(M()),!0)}function H(r,e,n,t,o){var i={color:q,from:e,to:n,flags:t,piece:r[e].type};return o&&(i.flags|=I.PROMOTION,i.promotion=o),r[n]?i.captured=r[n].type:t&I.EP_CAPTURE&&(i.captured=_),i}function Z(r){function e(r,e,n,t,o){if(r[n].type!==_||or(t)!==R&&or(t)!==P)e.push(H(r,n,t,o));else for(var i=[y,m,S,A],f=0,a=i.length;f<a;f++)e.push(H(r,n,t,o,i[f]))}var n=[],t=q,o=ar(t),i={b:L,w:w},f=N.a8,a=N.h1,l=!1,u=!(void 0!==r&&"legal"in r)||r.legal;if(void 0!==r&&"square"in r){if(!(r.square in N))return[];f=a=N[r.square],l=!0}for(var s=f;s<=a;s++)if(136&s)s+=7;else{var p=O[s];if(null!=p&&p.color===t)if(p.type===_){var c=s+C[t][0];if(null==O[c]){e(O,n,s,c,I.NORMAL);c=s+C[t][1];i[t]===or(s)&&null==O[c]&&e(O,n,s,c,I.BIG_PAWN)}for(v=2;v<4;v++){136&(c=s+C[t][v])||(null!=O[c]&&O[c].color===o?e(O,n,s,c,I.CAPTURE):c===K&&e(O,n,s,K,I.EP_CAPTURE))}}else for(var v=0,g=T[p.type].length;v<g;v++){var h=T[p.type][v];for(c=s;!(136&(c+=h));){if(null!=O[c]){if(O[c].color===t)break;e(O,n,s,c,I.CAPTURE);break}if(e(O,n,s,c,I.NORMAL),"n"===p.type||"k"===p.type)break}}}if(!l||a===k[t]){if(D[t]&I.KSIDE_CASTLE){var E=(d=k[t])+2;null!=O[d+1]||null!=O[E]||V(o,k[t])||V(o,d+1)||V(o,E)||e(O,n,k[t],E,I.KSIDE_CASTLE)}if(D[t]&I.QSIDE_CASTLE){var d;E=(d=k[t])-2;null!=O[d-1]||null!=O[d-2]||null!=O[d-3]||V(o,k[t])||V(o,d-1)||V(o,E)||e(O,n,k[t],E,I.QSIDE_CASTLE)}}if(!u)return n;var b=[];for(s=0,g=n.length;s<g;s++)er(n[s]),X(t)||b.push(n[s]),nr();return b}function z(r,e){var n="";if(r.flags&I.KSIDE_CASTLE)n="O-O";else if(r.flags&I.QSIDE_CASTLE)n="O-O-O";else{var t=function(r,e){for(var n=Z({legal:!e}),t=r.from,o=r.to,i=r.piece,f=0,a=0,l=0,u=0,s=n.length;u<s;u++){var p=n[u].from,c=n[u].to,v=n[u].piece;i===v&&t!==p&&o===c&&(f++,or(t)===or(p)&&a++,ir(t)===ir(p)&&l++)}if(0<f)return 0<a&&0<l?fr(t):0<l?fr(t).charAt(1):fr(t).charAt(0);return""}(r,e);r.piece!==_&&(n+=r.piece.toUpperCase()+t),r.flags&(I.CAPTURE|I.EP_CAPTURE)&&(r.piece===_&&(n+=fr(r.from)[0]),n+="x"),n+=fr(r.to),r.flags&I.PROMOTION&&(n+="="+r.promotion.toUpperCase())}return er(r),f()&&(a()?n+="#":n+="+"),nr(),n}function J(r){return r.replace(/=/,"").replace(/[+#]?[?!]*$/,"")}function V(r,e){for(var n=N.a8;n<=N.h1;n++)if(136&n)n+=7;else if(null!=O[n]&&O[n].color===r){var t=O[n],o=n-e,i=119+o;if(c[i]&1<<h[t.type]){if(t.type===_){if(0<o){if(t.color===s)return!0}else if(t.color===u)return!0;continue}if("n"===t.type||"k"===t.type)return!0;for(var f=v[i],a=n+f,l=!1;a!==e;){if(null!=O[a]){l=!0;break}a+=f}if(!l)return!0}}return!1}function X(r){return V(ar(r),k[r])}function f(){return X(q)}function a(){return f()&&0===Z().length}function n(){return!f()&&0===Z().length}function Y(){for(var r={},e=[],n=0,t=0,o=N.a8;o<=N.h1;o++)if(t=(t+1)%2,136&o)o+=7;else{var i=O[o];i&&(r[i.type]=i.type in r?r[i.type]+1:1,i.type===S&&e.push(t),n++)}if(2===n)return!0;if(3===n&&(1===r[S]||1===r[A]))return!0;if(n===r[S]+2){var f=0,a=e.length;for(o=0;o<a;o++)f+=e[o];if(0===f||f===a)return!0}return!1}function rr(){for(var r=[],e={},n=!1;;){var t=nr();if(!t)break;r.push(t)}for(;;){var o=M().split(" ").slice(0,4).join(" ");if(e[o]=o in e?e[o]+1:1,3<=e[o]&&(n=!0),!r.length)break;er(r.pop())}return n}function er(r){var e,n=q,t=ar(n);if(e=r,Q.push({move:e,kings:{b:k.b,w:k.w},turn:q,castling:{b:D.b,w:D.w},ep_square:K,half_moves:d,move_number:b}),O[r.to]=O[r.from],O[r.from]=null,r.flags&I.EP_CAPTURE&&(q===u?O[r.to-16]=null:O[r.to+16]=null),r.flags&I.PROMOTION&&(O[r.to]={type:r.promotion,color:n}),O[r.to].type===p){if(k[O[r.to].color]=r.to,r.flags&I.KSIDE_CASTLE){var o=r.to-1,i=r.to+1;O[o]=O[i],O[i]=null}else if(r.flags&I.QSIDE_CASTLE){o=r.to+1,i=r.to-2;O[o]=O[i],O[i]=null}D[n]=""}if(D[n])for(var f=0,a=E[n].length;f<a;f++)if(r.from===E[n][f].square&&D[n]&E[n][f].flag){D[n]^=E[n][f].flag;break}if(D[t])for(f=0,a=E[t].length;f<a;f++)if(r.to===E[t][f].square&&D[t]&E[t][f].flag){D[t]^=E[t][f].flag;break}K=r.flags&I.BIG_PAWN?"b"===q?r.to-16:r.to+16:l,r.piece===_||r.flags&(I.CAPTURE|I.EP_CAPTURE)?d=0:d++,q===u&&b++,q=ar(q)}function nr(){var r=Q.pop();if(null==r)return null;var e=r.move;k=r.kings,q=r.turn,D=r.castling,K=r.ep_square,d=r.half_moves,b=r.move_number;var n,t,o=q,i=ar(q);if(O[e.from]=O[e.to],O[e.from].type=e.piece,O[e.to]=null,e.flags&I.CAPTURE)O[e.to]={type:e.captured,color:i};else if(e.flags&I.EP_CAPTURE){var f;f=o===u?e.to-16:e.to+16,O[f]={type:_,color:i}}e.flags&(I.KSIDE_CASTLE|I.QSIDE_CASTLE)&&(e.flags&I.KSIDE_CASTLE?(n=e.to+1,t=e.to-1):e.flags&I.QSIDE_CASTLE&&(n=e.to-2,t=e.to+1),O[n]=O[t],O[t]=null);return e}function tr(r,e){var n=J(r);if(e){var t=n.match(/([pnbrqkPNBRQK])?([a-h][1-8])x?-?([a-h][1-8])([qrbnQRBN])?/);if(t)var o=t[1],i=t[2],f=t[3],a=t[4]}for(var l=Z(),u=0,s=l.length;u<s;u++){if(n===J(z(l[u]))||e&&n===J(z(l[u],!0)))return l[u];if(t&&(!o||o.toLowerCase()==l[u].piece)&&N[i]==l[u].from&&N[f]==l[u].to&&(!a||a.toLowerCase()==l[u].promotion))return l[u]}return null}function or(r){return r>>4}function ir(r){return 15&r}function fr(r){var e=ir(r),n=or(r);return"abcdefgh".substring(e,e+1)+"87654321".substring(n,n+1)}function ar(r){return r===s?u:s}function lr(r){var e=function r(e){var n=e instanceof Array?[]:{};for(var t in e)n[t]="object"==typeof t?r(e[t]):e[t];return n}(r);e.san=z(e,!1),e.to=fr(e.to),e.from=fr(e.from);var n="";for(var t in I)I[t]&e.flags&&(n+=o[t]);return e.flags=n,e}function ur(r){return r.replace(/^\s+|\s+$/g,"")}return B(void 0===r?e:r),{WHITE:s,BLACK:u,PAWN:_,KNIGHT:A,BISHOP:S,ROOK:m,QUEEN:y,KING:p,SQUARES:function(){for(var r=[],e=N.a8;e<=N.h1;e++)136&e?e+=7:r.push(fr(e));return r}(),FLAGS:o,load:function(r){return B(r)},reset:function(){return j()},moves:function(r){for(var e=Z(r),n=[],t=0,o=e.length;t<o;t++)void 0!==r&&"verbose"in r&&r.verbose?n.push(lr(e[t])):n.push(z(e[t],!1));return n},in_check:function(){return f()},in_checkmate:function(){return a()},in_stalemate:function(){return n()},in_draw:function(){return 100<=d||n()||Y()||rr()},insufficient_material:function(){return Y()},in_threefold_repetition:function(){return rr()},game_over:function(){return 100<=d||a()||n()||Y()||rr()},validate_fen:function(r){return $(r)},fen:function(){return M()},board:function(){for(var r=[],e=[],n=N.a8;n<=N.h1;n++)null==O[n]?e.push(null):e.push({type:O[n].type,color:O[n].color}),n+1&136&&(r.push(e),e=[],n+=8);return r},pgn:function(r){var e="object"==typeof r&&"string"==typeof r.newline_char?r.newline_char:"\n",n="object"==typeof r&&"number"==typeof r.max_width?r.max_width:0,t=[],o=!1;for(var i in U)t.push("["+i+' "'+U[i]+'"]'+e),o=!0;o&&Q.length&&t.push(e);for(var f=[];0<Q.length;)f.push(nr());for(var a=[],l="";0<f.length;){var u=f.pop();Q.length||"b"!==u.color?"w"===u.color&&(l.length&&a.push(l),l=b+"."):l=b+". ...",l=l+" "+z(u,!1),er(u)}if(l.length&&a.push(l),void 0!==U.Result&&a.push(U.Result),0===n)return t.join("")+a.join(" ");var s=0;for(i=0;i<a.length;i++)s+a[i].length>n&&0!==i?(" "===t[t.length-1]&&t.pop(),t.push(e),s=0):0!==i&&(t.push(" "),s++),t.push(a[i]),s+=a[i].length;return t.join("")},load_pgn:function(r,e){var n=void 0!==e&&"sloppy"in e&&e.sloppy;function l(r){return r.replace(/\\/g,"\\")}var t="object"==typeof e&&"string"==typeof e.newline_char?e.newline_char:"\r?\n",o=new RegExp("^(\\[((?:"+l(t)+")|.)*\\])(?:"+l(t)+"){2}"),i=o.test(r)?o.exec(r)[1]:"";j();var f=function(r,e){for(var n="object"==typeof e&&"string"==typeof e.newline_char?e.newline_char:"\r?\n",t={},o=r.split(new RegExp(l(n))),i="",f="",a=0;a<o.length;a++)i=o[a].replace(/^\[([A-Z][A-Za-z]*)\s.*\]$/,"$1"),f=o[a].replace(/^\[[A-Za-z]+\s"(.*)"\]$/,"$1"),0<ur(i).length&&(t[i]=f);return t}(i,e);for(var a in f)G([a,f[a]]);if("1"===f.SetUp&&!("FEN"in f&&B(f.FEN,!0)))return!1;var u=r.replace(i,"").replace(new RegExp(l(t),"g")," ");u=u.replace(/(\{[^}]+\})+?/g,"");for(var s=/(\([^\(\)]+\))+?/g;s.test(u);)u=u.replace(s,"");var p=ur(u=(u=(u=u.replace(/\d+\.(\.\.)?/g,"")).replace(/\.\.\./g,"")).replace(/\$\d+/g,"")).split(new RegExp(/\s+/));p=p.join(",").replace(/,,+/g,",").split(",");for(var c="",v=0;v<p.length-1;v++){if(null==(c=tr(p[v],n)))return!1;er(c)}if(c=p[p.length-1],-1<g.indexOf(c))!function(r){for(var e in r)return 1}(U)||void 0!==U.Result||G(["Result",c]);else{if(null==(c=tr(c,n)))return!1;er(c)}return!0},header:function(){return G(arguments)},ascii:function(){return function(){for(var r=" +------------------------+\n",e=N.a8;e<=N.h1;e++){if(0===ir(e)&&(r+=" "+"87654321"[or(e)]+" |"),null==O[e])r+=" . ";else{var n=O[e].type;r+=" "+(O[e].color===s?n.toUpperCase():n.toLowerCase())+" "}e+1&136&&(r+="|\n",e+=8)}return r+=" +------------------------+\n",r+=" a b c d e f g h\n"}()},turn:function(){return q},move:function(r,e){var n=void 0!==e&&"sloppy"in e&&e.sloppy,t=null;if("string"==typeof r)t=tr(r,n);else if("object"==typeof r)for(var o=Z(),i=0,f=o.length;i<f;i++)if(!(r.from!==fr(o[i].from)||r.to!==fr(o[i].to)||"promotion"in o[i]&&r.promotion!==o[i].promotion)){t=o[i];break}if(!t)return null;var a=lr(t);return er(t),a},undo:function(){var r=nr();return r?lr(r):null},clear:function(){return x()},put:function(r,e){return W(r,e)},get:function(r){return i(r)},remove:function(r){return n=i(e=r),O[N[e]]=null,n&&n.type===p&&(k[n.color]=l),F(M()),n;var e,n},perft:function(r){return function r(e){for(var n=Z({legal:!1}),t=0,o=q,i=0,f=n.length;i<f;i++)er(n[i]),X(o)||(0<e-1?t+=r(e-1):t++),nr();return t}(r)},square_color:function(r){if(r in N){var e=N[r];return(or(e)+ir(e))%2==0?"light":"dark"}return null},history:function(r){for(var e=[],n=[],t=(void 0!==r&&"verbose"in r&&r.verbose);0<Q.length;)e.push(nr());for(;0<e.length;){var o=e.pop();t?n.push(lr(o)):n.push(z(o)),er(o)}return n}}};"undefined"!=typeof exports&&(exports.Chess=Chess),"undefined"!=typeof define&&define(function(){return Chess});
\ No newline at end of file
// EL3AB Game Controller - Bot Play, Timers, Move Submission
const Game = {
chess: null,
playerColor: 'w',
botId: null,
gameId: null,
timeControl: 600,
increment: 0,
clockWhite: 0,
clockBlack: 0,
clockInterval: null,
activeClock: null,
isGameOver: false,
moveHistory: [],
rated: true,
async start(options = {}) {
this.playerColor = options.color || 'w';
this.botId = options.botId || 'amina';
this.timeControl = options.time || 600;
this.increment = options.increment || 0;
this.rated = options.rated !== false;
this.clockWhite = this.timeControl;
this.clockBlack = this.timeControl;
this.isGameOver = false;
this.moveHistory = [];
this.activeClock = null;
this.chess = new Chess();
Board.init('board', {
flipped: this.playerColor === 'b',
onMove: (move) => this.handlePlayerMove(move)
});
Board.setPosition(this.chess);
this.updateClockDisplay();
this.updateMoveList();
this.updateStatus();
const res = await App.fetch('/api/game', {
method: 'POST',
body: JSON.stringify({
action: 'start',
bot_id: this.botId,
color: this.playerColor,
time_control: this.timeControl,
increment: this.increment,
rated: this.rated
})
});
if (res && res.game_id) {
this.gameId = res.game_id;
}
if (this.playerColor === 'b') {
this.startClock('w');
this.requestBotMove();
} else {
this.startClock('w');
}
},
handlePlayerMove(move) {
if (this.isGameOver) return;
if (this.chess.turn() !== this.playerColor) return;
const result = this.chess.move({
from: move.from,
to: move.to,
promotion: move.promotion || 'q'
});
if (!result) return;
this.moveHistory.push(result);
Board.setPosition(this.chess);
this.addIncrement(this.playerColor);
this.switchClock();
this.updateMoveList();
this.updateStatus();
this.playMoveSound(result);
this.submitMove(result);
if (this.chess.game_over()) {
this.endGame();
} else {
this.requestBotMove();
}
},
async requestBotMove() {
this.showThinking(true);
const moves = this.chess.history();
try {
const res = await App.fetch('/api/game', {
method: 'POST',
body: JSON.stringify({
action: 'bot_move',
bot_id: this.botId,
fen: this.chess.fen(),
moves: moves.join(' '),
wtime: Math.round(this.clockWhite * 1000),
btime: Math.round(this.clockBlack * 1000)
})
});
this.showThinking(false);
if (res && res.move && !this.isGameOver) {
const from = res.move.substring(0, 2);
const to = res.move.substring(2, 4);
const promotion = res.move.length > 4 ? res.move[4] : undefined;
Board.animateMove(from, to, () => {
const result = this.chess.move({ from, to, promotion });
if (result) {
this.moveHistory.push(result);
Board.lastMove = { from, to };
Board.setPosition(this.chess);
this.addIncrement(this.playerColor === 'w' ? 'b' : 'w');
this.switchClock();
this.updateMoveList();
this.updateStatus();
this.playMoveSound(result);
if (this.chess.game_over()) {
this.endGame();
}
}
});
}
} catch (e) {
this.showThinking(false);
App.toast('خطأ في الاتصال بالبوت', 'error');
}
},
submitMove(move) {
App.fetch('/api/game', {
method: 'POST',
body: JSON.stringify({
action: 'move',
game_id: this.gameId,
move: move.san,
fen: this.chess.fen(),
clock_white: Math.round(this.clockWhite),
clock_black: Math.round(this.clockBlack)
})
});
},
startClock(color) {
this.stopClock();
this.activeClock = color;
this.updateClockDisplay();
this.clockInterval = setInterval(() => {
if (this.activeClock === 'w') {
this.clockWhite -= 0.1;
if (this.clockWhite <= 0) {
this.clockWhite = 0;
this.flagged('w');
}
} else {
this.clockBlack -= 0.1;
if (this.clockBlack <= 0) {
this.clockBlack = 0;
this.flagged('b');
}
}
this.updateClockDisplay();
}, 100);
},
stopClock() {
if (this.clockInterval) {
clearInterval(this.clockInterval);
this.clockInterval = null;
}
},
switchClock() {
const next = this.activeClock === 'w' ? 'b' : 'w';
this.startClock(next);
},
addIncrement(color) {
if (this.increment <= 0) return;
if (color === 'w') this.clockWhite += this.increment;
else this.clockBlack += this.increment;
},
flagged(color) {
this.stopClock();
const winner = color === 'w' ? 'b' : 'w';
this.endGame('timeout', winner);
},
updateClockDisplay() {
const topClock = document.getElementById('clock-top');
const bottomClock = document.getElementById('clock-bottom');
if (!topClock || !bottomClock) return;
const topColor = this.playerColor === 'w' ? 'b' : 'w';
const bottomColor = this.playerColor;
const topTime = topColor === 'w' ? this.clockWhite : this.clockBlack;
const bottomTime = bottomColor === 'w' ? this.clockWhite : this.clockBlack;
topClock.textContent = this.formatTime(topTime);
bottomClock.textContent = this.formatTime(bottomTime);
topClock.className = 'game-clock' + (this.activeClock === topColor ? ' active' : '') + (topTime <= 30 ? ' low' : '');
bottomClock.className = 'game-clock' + (this.activeClock === bottomColor ? ' active' : '') + (bottomTime <= 30 ? ' low' : '');
},
formatTime(seconds) {
if (seconds <= 0) return '0:00';
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
if (seconds < 60) {
const tenths = Math.floor((seconds % 1) * 10);
return `${secs}.${tenths}`;
}
return `${mins}:${secs.toString().padStart(2, '0')}`;
},
updateMoveList() {
const list = document.getElementById('move-list');
if (!list) return;
list.innerHTML = '';
const history = this.chess.history();
for (let i = 0; i < history.length; i += 2) {
const pair = document.createElement('div');
pair.className = 'move-pair';
const num = document.createElement('span');
num.className = 'move-number';
num.textContent = (Math.floor(i / 2) + 1) + '.';
pair.appendChild(num);
const white = document.createElement('span');
white.className = 'move' + (i === history.length - 1 ? ' current' : '');
white.textContent = history[i];
pair.appendChild(white);
if (history[i + 1]) {
const black = document.createElement('span');
black.className = 'move' + (i + 1 === history.length - 1 ? ' current' : '');
black.textContent = history[i + 1];
pair.appendChild(black);
}
list.appendChild(pair);
}
list.scrollTop = list.scrollHeight;
},
updateStatus() {
const status = document.getElementById('game-status');
if (!status) return;
if (this.chess.in_checkmate()) {
const winner = this.chess.turn() === 'w' ? 'b' : 'w';
status.textContent = winner === this.playerColor ? 'كش ملك - فوز!' : 'كش ملك - خسارة';
} else if (this.chess.in_check()) {
status.textContent = 'كش!';
} else if (this.chess.in_draw()) {
status.textContent = 'تعادل';
} else if (this.chess.in_stalemate()) {
status.textContent = 'تعادل - بات';
} else {
status.textContent = this.chess.turn() === this.playerColor ? 'دورك' : 'دور الخصم';
}
},
showThinking(show) {
const el = document.getElementById('thinking-indicator');
if (el) el.style.display = show ? 'flex' : 'none';
},
endGame(reason, winner) {
if (this.isGameOver) return;
this.isGameOver = true;
this.stopClock();
let title, subtitle;
if (reason === 'timeout') {
title = winner === this.playerColor ? 'فوز بالوقت!' : 'خسارة بالوقت';
subtitle = 'انتهى الوقت';
} else if (this.chess.in_checkmate()) {
const w = this.chess.turn() === 'w' ? 'b' : 'w';
title = w === this.playerColor ? 'فوز!' : 'خسارة';
subtitle = 'كش ملك';
} else if (this.chess.in_stalemate()) {
title = 'تعادل';
subtitle = 'بات';
} else if (this.chess.in_draw()) {
title = 'تعادل';
subtitle = this.chess.in_threefold_repetition() ? 'تكرار' : 'مادة غير كافية';
} else if (reason === 'resign') {
title = winner === this.playerColor ? 'فوز!' : 'خسارة';
subtitle = 'استسلام';
} else {
title = 'انتهت المباراة';
subtitle = '';
}
this.showResult(title, subtitle);
const result = winner === this.playerColor ? 'win' : (winner ? 'loss' : 'draw');
App.fetch('/api/game', {
method: 'POST',
body: JSON.stringify({
action: 'end',
game_id: this.gameId,
result: result,
reason: reason || 'checkmate',
pgn: this.chess.pgn(),
final_fen: this.chess.fen()
})
});
},
showResult(title, subtitle) {
const wrapper = document.querySelector('.board-wrapper');
if (!wrapper) return;
const overlay = document.createElement('div');
overlay.className = 'game-result';
overlay.innerHTML = `
<div class="game-result-title">${title}</div>
<div class="game-result-subtitle">${subtitle}</div>
<div class="game-controls" style="margin-top:16px;">
<button class="btn btn-cyan" onclick="window.location.href='/play'">العب مرة اخرى</button>
<button class="btn btn-ghost" onclick="window.location.href='/'">الرئيسية</button>
</div>
`;
wrapper.appendChild(overlay);
},
resign() {
if (this.isGameOver) return;
if (!confirm('هل تريد الاستسلام؟')) return;
const winner = this.playerColor === 'w' ? 'b' : 'w';
this.endGame('resign', winner);
},
offerDraw() {
if (this.isGameOver) return;
this.endGame('draw', null);
},
playMoveSound(move) {
try {
const ctx = new (window.AudioContext || window.webkitAudioContext)();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
gain.gain.value = 0.1;
if (move.captured) {
osc.frequency.value = 200;
gain.gain.value = 0.15;
} else {
osc.frequency.value = 400;
}
osc.start();
osc.stop(ctx.currentTime + 0.08);
} catch(e) {}
}
};
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