Commit bd053eda authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: multi-game platform overhaul — per-game ratings, redesigned UI, kill chess-legacy

- New player_game_ratings table (any game, any mode, ELO + W/L/D + streaks)
- New /api/ratings.php — unified ratings API (player, leaderboard, history, modes)
- game.php dual-writes to new table on chess game end
- ludo.php now tracks ELO + XP + coins on game completion
- Full v2 design system (app-v2.css) — WCAG AA, component-driven
- Redesigned: home, profile, leaderboard, play, login pages
- New nav: 4-tab mobile + floating play FAB + grouped desktop sidebar
- XP level titles now game-neutral (مبتدئ → استاذ → اسطورة)
- Leaderboard supports all games with mode selector
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent ffa99eb9
......@@ -268,6 +268,44 @@ switch ($action) {
'result' => $result,
'k_factor' => $kFactor,
], SUPABASE_SERVICE_KEY);
// DUAL-WRITE: Update player_game_ratings table
$winInc = ($result === 'win') ? 1 : 0;
$lossInc = ($result === 'loss') ? 1 : 0;
$drawInc = ($result === 'draw') ? 1 : 0;
$existingRating = supabase_rest('GET', "player_game_ratings?player_id=eq.{$userId}&game_key=eq.chess&mode=eq.{$tcType}&select=id,games_played,wins,losses,draws,win_streak,best_win_streak", [], SUPABASE_SERVICE_KEY);
if (!empty($existingRating['data'])) {
$pgr = $existingRating['data'][0];
$newWinStreak = ($result === 'win') ? ($pgr['win_streak'] + 1) : 0;
$bestStreak = max($pgr['best_win_streak'], $newWinStreak);
supabase_rest('PATCH', "player_game_ratings?id=eq.{$pgr['id']}", [
'rating' => $newElo,
'games_played' => $pgr['games_played'] + 1,
'wins' => $pgr['wins'] + $winInc,
'losses' => $pgr['losses'] + $lossInc,
'draws' => $pgr['draws'] + $drawInc,
'win_streak' => $newWinStreak,
'best_win_streak' => $bestStreak,
'last_played_at' => date('c'),
'updated_at' => date('c'),
], SUPABASE_SERVICE_KEY);
} else {
supabase_rest('POST', 'player_game_ratings', [
'player_id' => $userId,
'game_key' => 'chess',
'mode' => $tcType,
'rating' => $newElo,
'games_played' => 1,
'wins' => $winInc,
'losses' => $lossInc,
'draws' => $drawInc,
'win_streak' => $winInc,
'best_win_streak' => $winInc,
'last_played_at' => date('c'),
], SUPABASE_SERVICE_KEY);
}
}
// --- XP Awards ---
......
......@@ -480,6 +480,11 @@ function movePiece($input, $userId, $token) {
if ($killed) $response['killed'] = $killed;
if ($gameOver) $response['game_over'] = true;
// Update ludo ratings for all human players when game ends
if ($gameOver) {
updateLudoRatings($players, $winners, $userId);
}
echo json_encode($response);
// Execute bot turns if next player is a bot
......@@ -488,6 +493,77 @@ function movePiece($input, $userId, $token) {
}
}
function updateLudoRatings($players, $winners, $currentUserId) {
foreach ($players as $p) {
if ($p['type'] !== 'human') continue;
$pid = $p['id'];
$color = $p['color'];
$isWinner = isset($winners[0]) && $winners[0] === $color;
$result = $isWinner ? 'win' : 'loss';
$existing = supabase_rest('GET', "player_game_ratings?player_id=eq.{$pid}&game_key=eq.ludo&mode=eq.default&select=id,rating,games_played,wins,losses,draws,win_streak,best_win_streak", [], SUPABASE_SERVICE_KEY);
if (!empty($existing['data'])) {
$pgr = $existing['data'][0];
$currentRating = $pgr['rating'];
$gp = $pgr['games_played'];
$kFactor = ($gp < 30) ? 40 : 20;
$opponentRating = 1200;
$expected = 1.0 / (1.0 + pow(10, ($opponentRating - $currentRating) / 400.0));
$actual = $isWinner ? 1.0 : 0.0;
$change = (int)round($kFactor * ($actual - $expected));
$newRating = max(0, $currentRating + $change);
$newWinStreak = $isWinner ? ($pgr['win_streak'] + 1) : 0;
$bestStreak = max($pgr['best_win_streak'], $newWinStreak);
supabase_rest('PATCH', "player_game_ratings?id=eq.{$pgr['id']}", [
'rating' => $newRating,
'games_played' => $gp + 1,
'wins' => $pgr['wins'] + ($isWinner ? 1 : 0),
'losses' => $pgr['losses'] + ($isWinner ? 0 : 1),
'win_streak' => $newWinStreak,
'best_win_streak' => $bestStreak,
'last_played_at' => date('c'),
'updated_at' => date('c'),
], SUPABASE_SERVICE_KEY);
} else {
$newRating = $isWinner ? 1220 : 1180;
supabase_rest('POST', 'player_game_ratings', [
'player_id' => $pid,
'game_key' => 'ludo',
'mode' => 'default',
'rating' => $newRating,
'games_played' => 1,
'wins' => $isWinner ? 1 : 0,
'losses' => $isWinner ? 0 : 1,
'draws' => 0,
'win_streak' => $isWinner ? 1 : 0,
'best_win_streak' => $isWinner ? 1 : 0,
'last_played_at' => date('c'),
], SUPABASE_SERVICE_KEY);
}
// XP + coins for ludo
$profileRes = supabase_rest('GET', "profiles?id=eq.{$pid}&select=id,xp,level,coins", [], SUPABASE_SERVICE_KEY);
if (!empty($profileRes['data'])) {
$profile = $profileRes['data'][0];
$xpAward = 50 + ($isWinner ? 30 : 0);
$coinsAward = $isWinner ? 15 : 0;
$pUpdate = [
'xp' => ($profile['xp'] ?? 0) + $xpAward,
'total_games_played' => ($profile['total_games_played'] ?? 0) + 1,
];
if ($isWinner) {
$pUpdate['total_wins'] = ($profile['total_wins'] ?? 0) + 1;
$pUpdate['coins'] = ($profile['coins'] ?? 0) + $coinsAward;
} else {
$pUpdate['total_losses'] = ($profile['total_losses'] ?? 0) + 1;
}
supabase_rest('PATCH', "profiles?id=eq.{$pid}", $pUpdate, SUPABASE_SERVICE_KEY);
}
}
}
function executeBotTurns($matchId, $match, $token) {
$maxIterations = 20;
$iteration = 0;
......
<?php
require_once __DIR__ . '/../config/database.php';
header('Content-Type: application/json');
$token = get_auth_token();
if (!$token) {
http_response_code(401);
echo json_encode(['error' => 'unauthorized']);
exit;
}
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET') {
$action = $_GET['action'] ?? 'player';
switch ($action) {
case 'player':
$playerId = $_GET['player_id'] ?? null;
$gameKey = $_GET['game'] ?? null;
if (!$playerId) {
$userRes = supabase_auth('user', [], $token, 'GET');
$playerId = $userRes['data']['id'] ?? null;
}
if (!$playerId) {
http_response_code(400);
echo json_encode(['error' => 'player_id required']);
exit;
}
$query = "player_game_ratings?player_id=eq.{$playerId}";
if ($gameKey) {
$query .= "&game_key=eq.{$gameKey}";
}
$query .= '&select=game_key,mode,rating,games_played,wins,losses,draws,win_streak,best_win_streak,last_played_at';
$res = supabase_rest('GET', $query, [], SUPABASE_SERVICE_KEY);
echo json_encode(['ratings' => $res['data'] ?? []]);
break;
case 'leaderboard':
$gameKey = $_GET['game'] ?? 'chess';
$mode = $_GET['mode'] ?? 'blitz';
$limit = min((int)($_GET['limit'] ?? 50), 100);
$validGames = ['chess', 'ludo', 'backgammon', 'domino'];
if (!in_array($gameKey, $validGames)) $gameKey = 'chess';
$minGames = ($gameKey === 'chess') ? 1 : 1;
$query = "player_game_ratings?game_key=eq.{$gameKey}&mode=eq.{$mode}&games_played=gte.{$minGames}&order=rating.desc&limit={$limit}&select=player_id,rating,games_played,wins,losses,draws,win_streak";
$res = supabase_rest('GET', $query, [], SUPABASE_SERVICE_KEY);
$ratings = $res['data'] ?? [];
$playerIds = array_map(fn($r) => $r['player_id'], $ratings);
$players = [];
if (!empty($playerIds)) {
$idList = implode(',', array_map(fn($id) => "\"$id\"", $playerIds));
$profileRes = supabase_rest('GET', "profiles?id=in.({$idList})&select=id,username,display_name,avatar_url,level,country_code", [], SUPABASE_SERVICE_KEY);
foreach (($profileRes['data'] ?? []) as $p) {
$players[$p['id']] = $p;
}
}
$leaderboard = [];
foreach ($ratings as $i => $r) {
$profile = $players[$r['player_id']] ?? [];
$leaderboard[] = [
'rank' => $i + 1,
'player_id' => $r['player_id'],
'username' => $profile['username'] ?? null,
'display_name' => $profile['display_name'] ?? null,
'avatar_url' => $profile['avatar_url'] ?? null,
'level' => $profile['level'] ?? 1,
'country_code' => $profile['country_code'] ?? null,
'rating' => $r['rating'],
'games_played' => $r['games_played'],
'wins' => $r['wins'],
'losses' => $r['losses'],
'win_streak' => $r['win_streak'],
];
}
echo json_encode(['leaderboard' => $leaderboard, 'game' => $gameKey, 'mode' => $mode]);
break;
case 'history':
$gameKey = $_GET['game'] ?? 'chess';
$mode = $_GET['mode'] ?? null;
$limit = min((int)($_GET['limit'] ?? 20), 50);
$userRes = supabase_auth('user', [], $token, 'GET');
$playerId = $userRes['data']['id'] ?? null;
$query = "rating_history?player_id=eq.{$playerId}&game_key=eq.{$gameKey}";
if ($mode) $query .= "&time_control_type=eq.{$mode}";
$query .= "&order=created_at.desc&limit={$limit}&select=rating_before,rating_after,rating_change,result,opponent_rating,created_at";
$res = supabase_rest('GET', $query, [], SUPABASE_SERVICE_KEY);
echo json_encode(['history' => $res['data'] ?? []]);
break;
case 'modes':
$gameKey = $_GET['game'] ?? 'chess';
$modes = [
'chess' => ['bullet', 'blitz', 'rapid', 'classical'],
'backgammon' => ['default'],
'domino' => ['default'],
'ludo' => ['default'],
];
echo json_encode(['modes' => $modes[$gameKey] ?? ['default']]);
break;
default:
http_response_code(400);
echo json_encode(['error' => 'invalid action']);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'method not allowed']);
}
</main>
<?php require __DIR__ . '/../templates/nav-bottom-v2.php'; ?>
</div>
<div class="toast-wrap" id="toast-wrap"></div>
<script src="/public/js/app.js"></script>
<script>
document.addEventListener('DOMContentLoaded', async () => {
if (typeof App !== 'undefined' && App.isLoggedIn()) {
const data = await App.fetch('/api/profile');
if (data && data.profile) {
const p = data.profile;
const elo = document.getElementById('hdr-elo-val');
if (elo) elo.textContent = p.elo_blitz || 1200;
}
}
});
</script>
</body>
</html>
<?php require_once __DIR__ . '/feature-flags.php'; ?>
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="theme-color" content="#030A12">
<title><?= $pageTitle ?? 'EL3AB' ?></title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Arabic:wght@400;500;600;700&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/public/css/app-v2.css">
<?php if (isset($extraCss)): ?>
<?php if (is_array($extraCss)): ?>
<?php foreach ($extraCss as $css): ?>
<link rel="stylesheet" href="<?= $css ?>">
<?php endforeach; ?>
<?php else: ?>
<link rel="stylesheet" href="<?= $extraCss ?>">
<?php endif; ?>
<?php endif; ?>
</head>
<body>
<div class="shell">
<?php require __DIR__ . '/../templates/sidebar-v2.php'; ?>
<header class="hdr">
<a href="/" class="hdr-brand">EL3AB</a>
<div class="hdr-center">
<div class="hdr-rating" id="hdr-elo" title="تصنيف بليتز">
<svg class="icon-sm icon-fill hdr-rating-icon"><use href="/public/icons/sprite.svg#icon-star"></use></svg>
<span id="hdr-elo-val">1200</span>
</div>
</div>
<div class="hdr-actions">
<a href="/notifications" class="hdr-btn" aria-label="الاشعارات">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-bell"></use></svg>
<span class="hdr-dot" id="hdr-notif-dot" style="display:none"></span>
</a>
<a href="/profile" class="hdr-btn" aria-label="الملف الشخصي">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-profile"></use></svg>
</a>
</div>
</header>
<main class="main">
......@@ -4,15 +4,15 @@ $route = $_GET['route'] ?? '';
$route = trim($route, '/');
if ($route === '' || $route === 'home') {
require 'pages/home.php';
require 'pages/home-v2.php';
} elseif ($route === 'login') {
require 'pages/login.php';
require 'pages/login-v2.php';
} elseif ($route === 'register') {
require 'pages/register.php';
} elseif ($route === 'games') {
require 'pages/games.php';
} elseif ($route === 'play') {
require 'pages/play.php';
require 'pages/play-v2.php';
} elseif ($route === 'game') {
require 'pages/game.php';
} elseif ($route === 'game-live') {
......@@ -22,9 +22,9 @@ if ($route === '' || $route === 'home') {
} elseif ($route === 'bots') {
require 'pages/bots.php';
} elseif ($route === 'profile') {
require 'pages/profile.php';
require 'pages/profile-v2.php';
} elseif ($route === 'leaderboard') {
require 'pages/leaderboard.php';
require 'pages/leaderboard-v2.php';
} elseif ($route === 'friends') {
require 'pages/friends.php';
} elseif ($route === 'tournaments') {
......
-- ============================================================
-- MIGRATION 001: Multi-game rating system
-- Run this via psql or Supabase SQL editor
-- ============================================================
-- 1. Per-game player ratings (replaces chess-specific elo_ columns)
CREATE TABLE IF NOT EXISTS player_game_ratings (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
player_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
game_key TEXT NOT NULL, -- chess, ludo, backgammon, domino
mode TEXT NOT NULL DEFAULT 'default', -- chess: bullet/blitz/rapid/classical, others: 'default'
rating INTEGER NOT NULL DEFAULT 1200,
rating_deviation INTEGER DEFAULT 350, -- for Glicko-2 later
games_played INTEGER NOT NULL DEFAULT 0,
wins INTEGER NOT NULL DEFAULT 0,
losses INTEGER NOT NULL DEFAULT 0,
draws INTEGER NOT NULL DEFAULT 0,
win_streak INTEGER NOT NULL DEFAULT 0,
best_win_streak INTEGER NOT NULL DEFAULT 0,
last_played_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(player_id, game_key, mode)
);
CREATE INDEX idx_pgr_player ON player_game_ratings(player_id);
CREATE INDEX idx_pgr_game_mode ON player_game_ratings(game_key, mode);
CREATE INDEX idx_pgr_leaderboard ON player_game_ratings(game_key, mode, rating DESC);
-- 2. Update XP levels to be game-neutral
UPDATE xp_levels SET title = 'Beginner', title_ar = 'مبتدئ' WHERE level = 1;
UPDATE xp_levels SET title = 'Rookie', title_ar = 'مستجد' WHERE level = 2;
UPDATE xp_levels SET title = 'Rookie II', title_ar = 'مستجد ٢' WHERE level = 3;
UPDATE xp_levels SET title = 'Regular', title_ar = 'لاعب' WHERE level = 4;
UPDATE xp_levels SET title = 'Regular II', title_ar = 'لاعب ٢' WHERE level = 5;
UPDATE xp_levels SET title = 'Skilled', title_ar = 'ماهر' WHERE level = 6;
UPDATE xp_levels SET title = 'Skilled II', title_ar = 'ماهر ٢' WHERE level = 7;
UPDATE xp_levels SET title = 'Expert', title_ar = 'خبير' WHERE level = 8;
UPDATE xp_levels SET title = 'Expert II', title_ar = 'خبير ٢' WHERE level = 9;
UPDATE xp_levels SET title = 'Master', title_ar = 'استاذ' WHERE level = 10;
-- 3. Migrate existing chess ratings from profiles to player_game_ratings
INSERT INTO player_game_ratings (player_id, game_key, mode, rating, games_played, wins, losses, draws)
SELECT
id, 'chess', 'bullet',
COALESCE(elo_bullet, 1200),
0, 0, 0, 0
FROM profiles
WHERE elo_bullet IS NOT NULL AND elo_bullet != 1200
ON CONFLICT (player_id, game_key, mode) DO NOTHING;
INSERT INTO player_game_ratings (player_id, game_key, mode, rating, games_played, wins, losses, draws)
SELECT
id, 'chess', 'blitz',
COALESCE(elo_blitz, 1200),
0, 0, 0, 0
FROM profiles
WHERE elo_blitz IS NOT NULL AND elo_blitz != 1200
ON CONFLICT (player_id, game_key, mode) DO NOTHING;
INSERT INTO player_game_ratings (player_id, game_key, mode, rating, games_played, wins, losses, draws)
SELECT
id, 'chess', 'rapid',
COALESCE(elo_rapid, 1200),
0, 0, 0, 0
FROM profiles
WHERE elo_rapid IS NOT NULL AND elo_rapid != 1200
ON CONFLICT (player_id, game_key, mode) DO NOTHING;
INSERT INTO player_game_ratings (player_id, game_key, mode, rating, games_played, wins, losses, draws)
SELECT
id, 'chess', 'classical',
COALESCE(elo_classical, 1200),
0, 0, 0, 0
FROM profiles
WHERE elo_classical IS NOT NULL AND elo_classical != 1200
ON CONFLICT (player_id, game_key, mode) DO NOTHING;
-- 4. Backfill per-game stats from rating_history
-- (count games per player per game_key per mode)
INSERT INTO player_game_ratings (player_id, game_key, mode, rating, games_played, wins, losses, draws, last_played_at)
SELECT
rh.player_id,
rh.game_key,
rh.time_control_type,
(SELECT rating_after FROM rating_history rh2
WHERE rh2.player_id = rh.player_id
AND rh2.game_key = rh.game_key
AND rh2.time_control_type = rh.time_control_type
ORDER BY rh2.created_at DESC LIMIT 1),
COUNT(*),
COUNT(*) FILTER (WHERE rh.result = 'win'),
COUNT(*) FILTER (WHERE rh.result = 'loss'),
COUNT(*) FILTER (WHERE rh.result = 'draw'),
MAX(rh.created_at)
FROM rating_history rh
GROUP BY rh.player_id, rh.game_key, rh.time_control_type
ON CONFLICT (player_id, game_key, mode)
DO UPDATE SET
games_played = EXCLUDED.games_played,
wins = EXCLUDED.wins,
losses = EXCLUDED.losses,
draws = EXCLUDED.draws,
rating = EXCLUDED.rating,
last_played_at = EXCLUDED.last_played_at;
-- 5. RLS policies for player_game_ratings
ALTER TABLE player_game_ratings ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Anyone can read ratings" ON player_game_ratings
FOR SELECT USING (true);
CREATE POLICY "Service role can insert/update" ON player_game_ratings
FOR ALL USING (true) WITH CHECK (true);
-- 6. Add last_daily_claim if it doesn't exist (some profiles use last_daily_reward)
-- ALTER TABLE profiles ADD COLUMN IF NOT EXISTS last_daily_claim DATE;
-- 7. Comment: Keep elo_* columns on profiles for now (backward compat)
-- They will be deprecated in favor of player_game_ratings
-- The API will write to BOTH during transition
COMMENT ON TABLE player_game_ratings IS 'Per-game, per-mode ratings. Replaces chess-specific elo_* columns on profiles.';
This diff is collapsed.
<?php $pageTitle = 'EL3AB - المتصدرين'; ?>
<?php require __DIR__ . '/../includes/header-v2.php'; ?>
<div class="stack-5">
<h2 class="t-display" style="text-align:center;">المتصدرين</h2>
<!-- Game selector -->
<div class="chip-group" id="lb-games" style="justify-content:center;">
<button class="chip chip-gold active" data-game="chess">شطرنج</button>
<button class="chip chip-gold" data-game="backgammon">طاولة</button>
<button class="chip chip-gold" data-game="domino">دومينو</button>
<button class="chip chip-gold" data-game="ludo">لودو</button>
</div>
<!-- Mode selector (changes per game) -->
<div class="chip-group" id="lb-modes" style="justify-content:center;"></div>
<!-- Podium -->
<div class="lb-podium" id="lb-podium">
<div class="lb-podium-item lb-podium-item--second" id="podium-2">
<div class="lb-rank lb-rank--2">2</div>
<div class="avatar avatar-sm"><svg class="icon"><use href="/public/icons/sprite.svg#icon-profile"></use></svg></div>
<p class="t-caption">---</p>
<p class="t-caption" style="color:var(--text-1);font-weight:700;">---</p>
</div>
<div class="lb-podium-item lb-podium-item--first" id="podium-1">
<div class="lb-rank lb-rank--1">1</div>
<div class="avatar avatar-lg avatar-ring"><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="t-caption" style="color:var(--gold);font-weight:700;">---</p>
</div>
<div class="lb-podium-item lb-podium-item--third" id="podium-3">
<div class="lb-rank lb-rank--3">3</div>
<div class="avatar avatar-sm"><svg class="icon"><use href="/public/icons/sprite.svg#icon-profile"></use></svg></div>
<p class="t-caption">---</p>
<p class="t-caption" style="color:var(--text-1);font-weight:700;">---</p>
</div>
</div>
<!-- Full List -->
<div class="card" id="lb-list">
<div class="empty">جاري التحميل...</div>
</div>
</div>
<script>
const GAME_MODES = {
chess: ['bullet', 'blitz', 'rapid', 'classical'],
backgammon: ['default'],
domino: ['default'],
ludo: ['default']
};
const MODE_NAMES = { bullet: 'بوليت', blitz: 'بليتز', rapid: 'رابيد', classical: 'كلاسيك', default: 'عام' };
let currentGame = 'chess';
let currentMode = 'blitz';
document.addEventListener('DOMContentLoaded', () => {
if (!App.isLoggedIn()) { window.location.href = '/login'; return; }
// Game selection
document.querySelectorAll('#lb-games .chip').forEach(chip => {
chip.addEventListener('click', () => {
document.querySelectorAll('#lb-games .chip').forEach(c => c.classList.remove('active'));
chip.classList.add('active');
currentGame = chip.dataset.game;
renderModes();
loadLeaderboard();
});
});
renderModes();
loadLeaderboard();
});
function renderModes() {
const modes = GAME_MODES[currentGame];
const container = document.getElementById('lb-modes');
if (modes.length <= 1) {
container.innerHTML = '';
currentMode = modes[0];
return;
}
container.innerHTML = modes.map((m, i) => {
const active = (currentGame === 'chess' && m === 'blitz') || (i === 0 && currentGame !== 'chess') ? ' active' : '';
if (active) currentMode = m;
return '<button class="chip' + active + '" data-mode="' + m + '">' + MODE_NAMES[m] + '</button>';
}).join('');
container.querySelectorAll('.chip').forEach(chip => {
chip.addEventListener('click', () => {
container.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
chip.classList.add('active');
currentMode = chip.dataset.mode;
loadLeaderboard();
});
});
}
async function loadLeaderboard() {
// Try new API first, fallback to legacy
let players = [];
try {
const data = await App.fetch('/api/ratings.php?action=leaderboard&game=' + currentGame + '&mode=' + currentMode);
if (data && data.leaderboard && data.leaderboard.length > 0) {
players = data.leaderboard;
}
} catch (e) {}
// Fallback to legacy leaderboard for chess
if (players.length === 0 && currentGame === 'chess') {
try {
const legacy = await App.fetch('/api/leaderboard?mode=' + currentMode);
if (legacy && legacy.players) {
players = legacy.players.map((p, i) => ({
rank: i + 1,
player_id: p.id,
username: p.username,
display_name: p.display_name,
rating: p['elo_' + currentMode] || 1200,
games_played: 0,
wins: 0
}));
}
} catch (e) {}
}
// Update podium
for (let i = 1; i <= 3; i++) {
const el = document.getElementById('podium-' + i);
const p = players[i - 1];
if (p) {
el.querySelector('p:not(.t-caption), p[style]').textContent = p.display_name || p.username || '---';
const ratingEl = el.querySelectorAll('p')[el.querySelectorAll('p').length - 1];
ratingEl.textContent = p.rating;
}
}
// Update list
const list = document.getElementById('lb-list');
if (players.length > 3) {
list.innerHTML = players.slice(3).map(p => {
const winRate = p.games_played > 0 ? Math.round((p.wins / p.games_played) * 100) + '%' : '--';
return '<div class="lb-list-item">' +
'<span class="lb-list-rank">' + p.rank + '</span>' +
'<div class="avatar avatar-sm"><svg class="icon-sm"><use href="/public/icons/sprite.svg#icon-profile"></use></svg></div>' +
'<span class="lb-list-name">' + (p.display_name || p.username || '---') + '</span>' +
'<span class="lb-list-elo">' + p.rating + '</span>' +
'</div>';
}).join('');
} else if (players.length === 0) {
list.innerHTML = '<div class="empty">لا يوجد لاعبين كافيين بعد (يلزم 5 مباريات)</div>';
} else {
list.innerHTML = '';
}
}
</script>
<?php require __DIR__ . '/../includes/footer-v2.php'; ?>
<?php $pageTitle = 'EL3AB - تسجيل الدخول'; ?>
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="theme-color" content="#030A12">
<title><?= $pageTitle ?></title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Arabic:wght@400;500;600;700&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/public/css/app-v2.css">
</head>
<body>
<div class="auth-page">
<div class="auth-card">
<h1 class="auth-brand">EL3AB</h1>
<p class="auth-subtitle">سجل دخولك وابدأ اللعب</p>
<form id="login-form" class="stack-4">
<div class="field">
<label class="field-label" for="login-email">البريد الالكتروني</label>
<input type="email" class="input" id="login-email" placeholder="email@example.com" required dir="ltr" autocomplete="email">
</div>
<div class="field">
<label class="field-label" for="login-password">كلمة المرور</label>
<input type="password" class="input" id="login-password" placeholder="••••••••" required dir="ltr" autocomplete="current-password">
</div>
<button type="submit" class="btn btn-primary btn-block btn-lg" id="login-btn">
تسجيل الدخول
</button>
</form>
<p class="auth-footer">
ما عندك حساب؟ <a href="/register">انشئ حساب</a>
</p>
<div id="login-error" class="auth-error" style="display:none;"></div>
</div>
</div>
<script src="/public/js/app.js"></script>
<script>
document.getElementById('login-form').addEventListener('submit', async (e) => {
e.preventDefault();
const btn = document.getElementById('login-btn');
const errEl = document.getElementById('login-error');
errEl.style.display = 'none';
btn.disabled = true;
btn.textContent = 'جاري الدخول...';
const email = document.getElementById('login-email').value;
const password = document.getElementById('login-password').value;
try {
const res = await fetch('/api/auth', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'login', email, password })
});
const data = await res.json();
if (data.error) {
errEl.textContent = data.error;
errEl.style.display = 'block';
} else {
App.setAuth(data.access_token, data.user);
window.location.href = '/';
}
} catch (err) {
errEl.textContent = 'حدث خطا في الاتصال';
errEl.style.display = 'block';
}
btn.disabled = false;
btn.textContent = 'تسجيل الدخول';
});
if (typeof App !== 'undefined' && App.isLoggedIn()) window.location.href = '/';
</script>
</body>
</html>
<?php $pageTitle = 'EL3AB - العب شطرنج'; ?>
<?php require __DIR__ . '/../includes/header-v2.php'; ?>
<div class="lobby stack-5">
<!-- Multiplayer (primary) -->
<div class="lobby-hero">
<div class="lobby-hero-header">
<div class="lobby-hero-icon">
<svg class="icon-lg"><use href="/public/icons/sprite.svg#icon-sword"></use></svg>
</div>
<div>
<p class="t-subhead">ضد لاعب حقيقي</p>
<p class="t-caption">ابحث عن خصم بمستواك</p>
</div>
</div>
<div class="stack-3">
<div class="chip-group" id="mp-cat">
<button class="chip chip-gold active" data-cat="bullet">Bullet</button>
<button class="chip chip-gold" data-cat="blitz">Blitz</button>
<button class="chip chip-gold" data-cat="rapid">Rapid</button>
<button class="chip chip-gold" data-cat="classical">Classical</button>
</div>
<div class="chip-group" id="mp-tc">
<button class="chip active" data-tc="bullet_1_0" data-time="60000" data-inc="0">1+0</button>
<button class="chip" data-tc="bullet_1_1" data-time="60000" data-inc="1000">1+1</button>
<button class="chip" data-tc="bullet_2_1" data-time="120000" data-inc="1000">2+1</button>
</div>
</div>
<button class="btn btn-primary btn-block btn-lg" onclick="startMP()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-search"></use></svg>
ابحث عن خصم
</button>
</div>
<!-- VS Bot -->
<a href="/bots" class="lobby-row">
<div class="lobby-row-icon">
<svg class="icon" style="color:var(--cyan)"><use href="/public/icons/sprite.svg#icon-bot"></use></svg>
</div>
<div class="lobby-row-text">
<p class="lobby-row-title">ضد البوت</p>
<p class="lobby-row-sub">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>
</a>
<!-- Quick Match -->
<div class="lobby-row" onclick="quickMatch()" role="button" tabindex="0">
<div class="lobby-row-icon">
<svg class="icon" style="color:var(--gold)"><use href="/public/icons/sprite.svg#icon-bot"></use></svg>
</div>
<div class="lobby-row-text">
<p class="lobby-row-title">مباراة سريعة</p>
<p class="lobby-row-sub">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>
<!-- Custom Game (collapsed by default) -->
<details class="card">
<summary class="card-pad flex items-center justify-between" style="cursor:pointer;list-style:none;">
<span class="t-subhead">مباراة مخصصة</span>
<svg class="icon" style="color:var(--text-3)"><use href="/public/icons/sprite.svg#icon-arrow-right"></use></svg>
</summary>
<div class="card-pad stack-4" style="border-top:1px solid var(--border);">
<div>
<label class="field-label">التوقيت</label>
<div class="chip-group" id="custom-cat">
<button class="chip chip-gold active" data-cat="bullet">Bullet</button>
<button class="chip chip-gold" data-cat="blitz">Blitz</button>
<button class="chip chip-gold" data-cat="rapid">Rapid</button>
</div>
<div class="chip-group" id="custom-tc" style="margin-top:var(--sp-2);">
<button class="chip active" data-time="60" data-inc="0">1+0</button>
<button class="chip" data-time="60" data-inc="1">1+1</button>
<button class="chip" data-time="120" data-inc="1">2+1</button>
</div>
</div>
<div>
<label class="field-label">اللون</label>
<div class="chip-group" id="custom-color">
<button class="chip active" data-color="w">ابيض</button>
<button class="chip" data-color="b">اسود</button>
<button class="chip" data-color="random">عشوائي</button>
</div>
</div>
<div>
<label class="field-label">الخصم</label>
<select class="input" id="bot-select" dir="ltr">
<option value="nour">جاري التحميل...</option>
</select>
</div>
<p class="t-caption" style="text-align:center;">مباراة تدريبية (غير مصنفة)</p>
<button class="btn btn-primary btn-block btn-lg" onclick="startCustom()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-play"></use></svg>
ابدأ المباراة
</button>
</div>
</details>
</div>
<script>
const TC_OPTIONS = {
bullet: [
{ time: '60', inc: '0', label: '1+0', tc: 'bullet_1_0', timeMs: '60000', incMs: '0' },
{ time: '60', inc: '1', label: '1+1', tc: 'bullet_1_1', timeMs: '60000', incMs: '1000' },
{ time: '120', inc: '1', label: '2+1', tc: 'bullet_2_1', timeMs: '120000', incMs: '1000' }
],
blitz: [
{ time: '180', inc: '0', label: '3+0', tc: 'blitz_3_0', timeMs: '180000', incMs: '0' },
{ time: '180', inc: '2', label: '3+2', tc: 'blitz_3_2', timeMs: '180000', incMs: '2000' },
{ time: '300', inc: '0', label: '5+0', tc: 'blitz_5_0', timeMs: '300000', incMs: '0' },
{ time: '300', inc: '3', label: '5+3', tc: 'blitz_5_3', timeMs: '300000', incMs: '3000' }
],
rapid: [
{ time: '600', inc: '0', label: '10+0', tc: 'rapid_10_0', timeMs: '600000', incMs: '0' },
{ time: '600', inc: '5', label: '10+5', tc: 'rapid_10_5', timeMs: '600000', incMs: '5000' },
{ time: '900', inc: '10', label: '15+10', tc: 'rapid_15_10', timeMs: '900000', incMs: '10000' }
],
classical: [
{ time: '1800', inc: '0', label: '30+0', tc: 'classical_30_0', timeMs: '1800000', incMs: '0' },
{ time: '1800', inc: '20', label: '30+20', tc: 'classical_30_20', timeMs: '1800000', incMs: '20000' },
{ time: '3600', inc: '0', label: '60+0', tc: 'classical_60_0', timeMs: '3600000', incMs: '0' }
]
};
document.addEventListener('DOMContentLoaded', async () => {
if (!App.isLoggedIn()) { window.location.href = '/login'; return; }
setupChips('mp-cat', 'mp-tc', true);
setupChips('custom-cat', 'custom-tc', false);
setupSelection('custom-color');
try {
const data = await App.cachedFetch('/api/bots.php', 60000);
if (data && data.bots) {
const select = document.getElementById('bot-select');
select.innerHTML = data.bots.map(bot => {
const avgElo = Math.round((bot.elo_min + bot.elo_max) / 2);
return '<option value="' + bot.id + '"' + (bot.id === 'nour' ? ' selected' : '') + '>' + bot.name + ' (' + avgElo + ')</option>';
}).join('');
window._botsData = data.bots;
}
} catch (e) {}
});
function setupChips(catId, tcId, isMP) {
const catEl = document.getElementById(catId);
const tcEl = document.getElementById(tcId);
catEl.querySelectorAll('.chip').forEach(chip => {
chip.addEventListener('click', () => {
catEl.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
chip.classList.add('active');
const opts = TC_OPTIONS[chip.dataset.cat];
tcEl.innerHTML = opts.map((o, i) => {
const cls = i === 0 ? ' active' : '';
if (isMP) return '<button class="chip' + cls + '" data-tc="' + o.tc + '" data-time="' + o.timeMs + '" data-inc="' + o.incMs + '">' + o.label + '</button>';
return '<button class="chip' + cls + '" data-time="' + o.time + '" data-inc="' + o.inc + '">' + o.label + '</button>';
}).join('');
setupSelection(tcId);
});
});
setupSelection(tcId);
}
function setupSelection(id) {
const el = document.getElementById(id);
if (!el) return;
el.querySelectorAll('.chip').forEach(chip => {
chip.addEventListener('click', () => {
el.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
chip.classList.add('active');
});
});
}
function startMP() {
const active = document.querySelector('#mp-tc .chip.active');
window.location.href = '/matchmaking?tc=' + active.dataset.tc + '&time=' + active.dataset.time + '&inc=' + active.dataset.inc;
}
function quickMatch() {
const bots = window._botsData ? window._botsData.filter(b => b.id !== 'grandmaster').map(b => b.id) : ['nour'];
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=false';
}
function startCustom() {
const tc = document.querySelector('#custom-tc .chip.active');
let color = document.querySelector('#custom-color .chip.active').dataset.color;
if (color === 'random') color = Math.random() < 0.5 ? 'w' : 'b';
const bot = document.getElementById('bot-select').value;
window.location.href = '/game?bot=' + bot + '&color=' + color + '&time=' + tc.dataset.time + '&inc=' + tc.dataset.inc + '&rated=false';
}
</script>
<?php require __DIR__ . '/../includes/footer-v2.php'; ?>
<?php $pageTitle = 'EL3AB - الملف الشخصي'; ?>
<?php require __DIR__ . '/../includes/header-v2.php'; ?>
<div class="stack-5" id="profile-page">
<!-- Profile Card -->
<div class="profile-header">
<div class="avatar avatar-xl avatar-ring">
<svg class="icon-xl"><use href="/public/icons/sprite.svg#icon-profile"></use></svg>
</div>
<h2 class="t-heading" id="profile-name">---</h2>
<p class="t-caption" id="profile-username">@---</p>
<div class="flex gap-2">
<span class="badge badge-gold" id="profile-level">Lv 1</span>
<span class="badge badge-cyan" id="profile-title">مبتدئ</span>
</div>
</div>
<!-- Account Level (XP progress) -->
<div class="card card-pad">
<div class="flex items-center justify-between" style="margin-bottom:var(--sp-2);">
<span class="t-caption">مستوى الحساب</span>
<span class="t-caption" id="xp-progress">0 / 100 XP</span>
</div>
<div style="width:100%;height:6px;background:var(--bg-3);border-radius:var(--r-full);overflow:hidden;">
<div id="xp-bar" style="height:100%;width:0%;background:linear-gradient(90deg,var(--cyan),var(--gold));border-radius:var(--r-full);transition:width 0.5s;"></div>
</div>
</div>
<!-- Per-Game Ratings -->
<section>
<div class="sec-header">
<h2 class="sec-title">تصنيفات الالعاب</h2>
</div>
<div class="stack-3" id="profile-ratings">
<div class="skel" style="height:60px;border-radius:var(--r-md);"></div>
</div>
</section>
<!-- Overall Stats -->
<section>
<div class="sec-header">
<h2 class="sec-title">احصائيات شاملة</h2>
</div>
<div class="stats-row" id="profile-stats">
<div class="stat"><div class="stat-val" id="stat-games">0</div><div class="stat-lbl">مباريات</div></div>
<div class="stat"><div class="stat-val color-success" id="stat-wins">0</div><div class="stat-lbl">فوز</div></div>
<div class="stat"><div class="stat-val" id="stat-draws">0</div><div class="stat-lbl">تعادل</div></div>
<div class="stat"><div class="stat-val color-error" id="stat-losses">0</div><div class="stat-lbl">خسارة</div></div>
</div>
</section>
<!-- Economy -->
<div class="card card-pad">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<svg class="icon icon-fill" style="color:var(--gold)"><use href="/public/icons/sprite.svg#icon-coin"></use></svg>
<div>
<p style="font-size:14px;font-weight:600;" id="stat-coins">0</p>
<p class="t-caption">عملات</p>
</div>
</div>
<div class="flex items-center gap-3">
<svg class="icon icon-fill" style="color:var(--purple)"><use href="/public/icons/sprite.svg#icon-gem"></use></svg>
<div>
<p style="font-size:14px;font-weight:600;" id="stat-gems">0</p>
<p class="t-caption">جواهر</p>
</div>
</div>
<div class="flex items-center gap-3">
<span style="font-size:18px;">🔥</span>
<div>
<p style="font-size:14px;font-weight:600;" id="stat-streak">0</p>
<p class="t-caption">ايام</p>
</div>
</div>
</div>
</div>
<!-- Actions -->
<div class="stack-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>
<script>
document.addEventListener('DOMContentLoaded', async () => {
if (!App.isLoggedIn()) { window.location.href = '/login'; return; }
const [profileData, ratingsData] = await Promise.all([
App.fetch('/api/profile'),
App.fetch('/api/ratings.php?action=player')
]);
if (profileData && profileData.profile) {
const p = profileData.profile;
document.getElementById('profile-name').textContent = p.display_name || p.username || '---';
document.getElementById('profile-username').textContent = '@' + (p.username || '---');
document.getElementById('profile-level').textContent = 'Lv ' + (p.level || 1);
document.getElementById('stat-games').textContent = p.total_games_played || p.games_played || 0;
document.getElementById('stat-wins').textContent = p.total_wins || 0;
document.getElementById('stat-draws').textContent = p.total_draws || 0;
document.getElementById('stat-losses').textContent = p.total_losses || 0;
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;
// XP bar
const xp = p.xp || 0;
const level = p.level || 1;
App.cachedFetch('/api/config.php?category=xp_levels', 300000).then(cfg => {
// Fallback: estimate next level XP
}).catch(() => {});
const nextLevelXp = [0, 100, 300, 600, 1000, 1500, 2100, 2800, 3600, 4500, 5500][level] || (level * 500);
const prevLevelXp = [0, 0, 100, 300, 600, 1000, 1500, 2100, 2800, 3600, 4500][level] || ((level - 1) * 500);
const progress = Math.min(100, Math.round(((xp - prevLevelXp) / (nextLevelXp - prevLevelXp)) * 100));
document.getElementById('xp-bar').style.width = progress + '%';
document.getElementById('xp-progress').textContent = xp + ' / ' + nextLevelXp + ' XP';
// Level title
const titles = { 1: 'مبتدئ', 2: 'مستجد', 3: 'مستجد ٢', 4: 'لاعب', 5: 'لاعب ٢', 6: 'ماهر', 7: 'ماهر ٢', 8: 'خبير', 9: 'خبير ٢', 10: 'استاذ' };
document.getElementById('profile-title').textContent = titles[level] || 'لاعب';
}
// Per-game ratings
const container = document.getElementById('profile-ratings');
const gameNames = { chess: 'شطرنج', ludo: 'لودو', backgammon: 'طاولة', domino: 'دومينو' };
const modeNames = { bullet: 'بوليت', blitz: 'بليتز', rapid: 'رابيد', classical: 'كلاسيك', default: '' };
const gameIcons = { chess: 'icon-play', ludo: 'icon-ludo', backgammon: 'icon-backgammon', domino: 'icon-domino' };
if (ratingsData && ratingsData.ratings && ratingsData.ratings.length > 0) {
// Group by game
const grouped = {};
ratingsData.ratings.forEach(r => {
if (!grouped[r.game_key]) grouped[r.game_key] = [];
grouped[r.game_key].push(r);
});
let html = '';
for (const [gameKey, modes] of Object.entries(grouped)) {
const gameName = gameNames[gameKey] || gameKey;
const icon = gameIcons[gameKey] || 'icon-play';
html += '<div class="card card-pad">';
html += '<div class="flex items-center gap-3" style="margin-bottom:var(--sp-3);">';
html += '<svg class="icon" style="color:var(--cyan)"><use href="/public/icons/sprite.svg#' + icon + '"></use></svg>';
html += '<span class="t-subhead">' + gameName + '</span>';
html += '</div>';
html += '<div class="stats-row">';
modes.forEach(r => {
const label = modeNames[r.mode] || r.mode;
const winRate = r.games_played > 0 ? Math.round((r.wins / r.games_played) * 100) : 0;
html += '<div class="stat">';
html += '<div class="stat-val">' + r.rating + '</div>';
html += '<div class="stat-lbl">' + (label || 'عام') + '</div>';
html += '</div>';
});
html += '</div>';
// Win/loss for this game
const totalGames = modes.reduce((s, m) => s + m.games_played, 0);
const totalWins = modes.reduce((s, m) => s + m.wins, 0);
if (totalGames > 0) {
html += '<p class="t-caption" style="margin-top:var(--sp-2);">' + totalGames + ' مباراة • ' + Math.round((totalWins/totalGames)*100) + '% فوز</p>';
}
html += '</div>';
}
container.innerHTML = html;
} else {
// Fallback to legacy profile elo fields
if (profileData && profileData.profile) {
const p = profileData.profile;
container.innerHTML = '<div class="card card-pad">' +
'<div class="flex items-center gap-3" style="margin-bottom:var(--sp-3);">' +
'<svg class="icon" style="color:var(--cyan)"><use href="/public/icons/sprite.svg#icon-play"></use></svg>' +
'<span class="t-subhead">شطرنج</span></div>' +
'<div class="stats-row">' +
'<div class="stat"><div class="stat-val">' + (p.elo_bullet || 1200) + '</div><div class="stat-lbl">بوليت</div></div>' +
'<div class="stat"><div class="stat-val">' + (p.elo_blitz || 1200) + '</div><div class="stat-lbl">بليتز</div></div>' +
'<div class="stat"><div class="stat-val">' + (p.elo_rapid || 1200) + '</div><div class="stat-lbl">رابيد</div></div>' +
'</div></div>';
}
}
});
</script>
<?php require __DIR__ . '/../includes/footer-v2.php'; ?>
This diff is collapsed.
<?php
$currentRoute = $_GET['route'] ?? '';
$navItems = [
['/', 'icon-home', 'الرئيسية'],
['/games', 'icon-games', 'العاب'],
['/leaderboard', 'icon-leaderboard', 'ترتيب'],
['/profile', 'icon-profile', 'حسابي'],
];
?>
<!-- Floating play button -->
<a href="/play" class="nav-m-play" aria-label="العب الان">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-play"></use></svg>
</a>
<nav class="nav-m" aria-label="التنقل">
<?php foreach ($navItems as $item):
$href = $item[0]; $icon = $item[1]; $label = $item[2];
$route = trim($href, '/');
$isActive = ($route === '' && $currentRoute === '') || ($route !== '' && $currentRoute === $route);
?>
<a href="<?= $href ?>" class="nav-m-item <?= $isActive ? 'active' : '' ?>" aria-current="<?= $isActive ? 'page' : 'false' ?>">
<svg class="icon"><use href="/public/icons/sprite.svg#<?= $icon ?>"></use></svg>
<span><?= $label ?></span>
</a>
<?php endforeach; ?>
</nav>
<nav class="sidebar" aria-label="التنقل الرئيسي">
<span class="sidebar-brand">EL3AB</span>
<a href="/play" class="sidebar-play-btn">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-play"></use></svg>
العب الان
</a>
<div class="sidebar-section">
<span class="sidebar-label">العاب</span>
<?php
$currentRoute = $_GET['route'] ?? '';
$gameItems = [
['/games', 'icon-games', 'جميع الالعاب'],
['/puzzles', 'icon-puzzle', 'تمارين'],
['/tournaments', 'icon-trophy', 'بطولات', 'tournaments_enabled'],
];
foreach ($gameItems as $item):
$href = $item[0]; $icon = $item[1]; $label = $item[2]; $flag = $item[3] ?? null;
if ($flag && !is_feature_enabled($flag)) continue;
$route = trim($href, '/');
$isActive = ($route !== '' && $currentRoute === $route);
?>
<a href="<?= $href ?>" class="sidebar-item <?= $isActive ? 'active' : '' ?>">
<svg class="icon"><use href="/public/icons/sprite.svg#<?= $icon ?>"></use></svg>
<?= $label ?>
</a>
<?php endforeach; ?>
</div>
<div class="sidebar-section">
<span class="sidebar-label">اجتماعي</span>
<?php
$socialItems = [
['/leaderboard', 'icon-leaderboard', 'المتصدرين'],
['/friends', 'icon-friends', 'الاصدقاء'],
['/orgs', 'icon-org', 'الاندية'],
];
foreach ($socialItems as $item):
$href = $item[0]; $icon = $item[1]; $label = $item[2];
$route = trim($href, '/');
$isActive = ($route !== '' && $currentRoute === $route);
?>
<a href="<?= $href ?>" class="sidebar-item <?= $isActive ? 'active' : '' ?>">
<svg class="icon"><use href="/public/icons/sprite.svg#<?= $icon ?>"></use></svg>
<?= $label ?>
</a>
<?php endforeach; ?>
</div>
<div class="sidebar-section">
<span class="sidebar-label">حسابي</span>
<?php
$accountItems = [
['/profile', 'icon-profile', 'الملف الشخصي'],
['/shop', 'icon-shop', 'المتجر', 'cosmetics_shop_enabled'],
['/achievements', 'icon-star', 'الانجازات'],
['/settings', 'icon-settings', 'الاعدادات'],
];
foreach ($accountItems as $item):
$href = $item[0]; $icon = $item[1]; $label = $item[2]; $flag = $item[3] ?? null;
if ($flag && !is_feature_enabled($flag)) continue;
$route = trim($href, '/');
$isActive = ($route !== '' && $currentRoute === $route);
?>
<a href="<?= $href ?>" class="sidebar-item <?= $isActive ? 'active' : '' ?>">
<svg class="icon"><use href="/public/icons/sprite.svg#<?= $icon ?>"></use></svg>
<?= $label ?>
</a>
<?php endforeach; ?>
</div>
</nav>
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