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);
}
};
This diff is collapsed.
This diff is collapsed.
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