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.';
<?php $pageTitle = 'EL3AB'; ?>
<?php require __DIR__ . '/../includes/header-v2.php'; ?>
<div class="stack-5" id="home">
<!-- Hero: Quick Play (inline matchmaking from home) -->
<div class="hero-play">
<div class="hero-play-top">
<div>
<p class="hero-play-title" id="home-greeting">مرحبا</p>
<p class="hero-play-subtitle" id="home-rank"></p>
</div>
<div class="badge badge-gold" id="home-level">Lv 1</div>
</div>
<div class="chip-group" id="home-tc">
<button class="chip active" data-tc="bullet_1_1" data-time="60000" data-inc="1000">1+1</button>
<button class="chip" data-tc="blitz_3_0" data-time="180000" data-inc="0">3+0</button>
<button class="chip" data-tc="blitz_5_0" data-time="300000" data-inc="0">5+0</button>
<button class="chip" data-tc="rapid_10_0" data-time="600000" data-inc="0">10+0</button>
</div>
<button class="hero-play-btn" id="home-play-btn" onclick="quickPlay()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-search"></use></svg>
العب شطرنج الان
</button>
</div>
<!-- Daily Streak -->
<div class="streak-banner" id="streak-section">
<div class="streak-banner-icon">🔥</div>
<div class="streak-banner-text">
<p class="streak-banner-day" id="streak-day">اليوم 0</p>
<p class="streak-banner-reward" id="streak-reward">+50 عملة</p>
</div>
<button class="streak-banner-btn" id="streak-btn">اجمع</button>
</div>
<div class="streak-cal" id="streak-cal">
<div class="streak-cal-day" data-day="1">1</div>
<div class="streak-cal-day" data-day="2">2</div>
<div class="streak-cal-day" data-day="3">3</div>
<div class="streak-cal-day" data-day="4">4</div>
<div class="streak-cal-day" data-day="5">5</div>
<div class="streak-cal-day" data-day="6">6</div>
<div class="streak-cal-day" data-day="7">7</div>
</div>
<!-- All Games Grid -->
<section>
<div class="sec-header">
<h2 class="sec-title">العاب</h2>
<a href="/games" class="sec-link">الكل</a>
</div>
<div class="quick-play-grid" id="home-games">
<div class="skel" style="height:110px;border-radius:var(--r-lg);"></div>
<div class="skel" style="height:110px;border-radius:var(--r-lg);"></div>
<div class="skel" style="height:110px;border-radius:var(--r-lg);"></div>
<div class="skel" style="height:110px;border-radius:var(--r-lg);"></div>
</div>
</section>
<!-- My Ratings (multi-game) -->
<section id="ratings-section" style="display:none;">
<div class="sec-header">
<h2 class="sec-title">تصنيفاتي</h2>
</div>
<div class="stats-row" id="home-ratings"></div>
</section>
<!-- Recent Games -->
<section>
<div class="sec-header">
<h2 class="sec-title">اخر المباريات</h2>
</div>
<div class="card" id="home-recent">
<div class="empty">لم تلعب اي مباراة بعد</div>
</div>
</section>
</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('home-greeting').textContent = (p.display_name || p.username) + '، العب!';
document.getElementById('home-level').textContent = 'Lv ' + (p.level || 1);
const streak = p.daily_streak || 0;
document.getElementById('streak-day').textContent = 'اليوم ' + streak;
App.cachedFetch('/api/config.php?category=economy', 120000).then(cfg => {
const base = (cfg && cfg.config && cfg.config.daily_reward_base) || 50;
const bonus = (cfg && cfg.config && cfg.config.daily_reward_streak_bonus) || 10;
document.getElementById('streak-reward').textContent = '+' + (base + streak * bonus) + ' عملة';
}).catch(() => {
document.getElementById('streak-reward').textContent = '+' + (50 + streak * 10) + ' عملة';
});
const claimedToday = p.last_daily_claim === new Date().toISOString().slice(0, 10);
const days = document.querySelectorAll('.streak-cal-day');
days.forEach((day, i) => {
const dayNum = i + 1;
const streakDay = ((streak - 1) % 7) + 1;
if (dayNum < streakDay || (dayNum === streakDay && claimedToday)) {
day.classList.add('claimed');
} else if (dayNum === streakDay + (claimedToday ? 0 : 1) || (streak === 0 && dayNum === 1)) {
day.classList.add('current');
}
});
if (claimedToday) {
const btn = document.getElementById('streak-btn');
btn.textContent = 'تم ✓';
btn.disabled = true;
}
}
// Load multi-game ratings
try {
const ratingsData = await App.fetch('/api/ratings.php?action=player');
if (ratingsData && ratingsData.ratings && ratingsData.ratings.length > 0) {
document.getElementById('ratings-section').style.display = '';
const container = document.getElementById('home-ratings');
const gameNames = { chess: 'شطرنج', ludo: 'لودو', backgammon: 'طاولة', domino: 'دومينو' };
const modeNames = { bullet: 'بوليت', blitz: 'بليتز', rapid: 'رابيد', classical: 'كلاسيك', default: '' };
container.innerHTML = ratingsData.ratings.map(r => {
const gameName = gameNames[r.game_key] || r.game_key;
const modeName = modeNames[r.mode] || r.mode;
const label = modeName ? gameName + ' ' + modeName : gameName;
return '<div class="stat"><div class="stat-val">' + r.rating + '</div><div class="stat-lbl">' + label + '</div></div>';
}).join('');
// Also update home-rank subtitle with best rating
const best = ratingsData.ratings.reduce((a, b) => a.rating > b.rating ? a : b);
const bestMode = modeNames[best.mode] || '';
const bestGame = gameNames[best.game_key] || '';
document.getElementById('home-rank').textContent = bestGame + ' ' + bestMode + ' ' + best.rating;
} else {
// Fallback to legacy profile elo fields
if (data && data.profile) {
const p = data.profile;
document.getElementById('home-rank').textContent = 'بليتز ' + (p.elo_blitz || 1200);
}
}
} catch (e) {
if (data && data.profile) {
document.getElementById('home-rank').textContent = 'بليتز ' + (data.profile.elo_blitz || 1200);
}
}
// Games grid
try {
const gData = await App.cachedFetch('/api/games.php', 60000);
if (gData && gData.games) {
const routes = { chess: '/play', ludo: '/ludo', domino: '/domino', backgammon: '/backgammon' };
const heroes = { chess: 'qp-card-hero--chess', ludo: 'qp-card-hero--ludo', domino: 'qp-card-hero--domino', backgammon: 'qp-card-hero--backgammon' };
const icons = { chess: 'icon-play', ludo: 'icon-ludo', domino: 'icon-domino', backgammon: 'icon-backgammon' };
const grid = document.getElementById('home-games');
grid.innerHTML = gData.games.filter(g => routes[g.game_key]).map(g => {
const route = routes[g.game_key];
const enabled = g.is_enabled && route;
return '<a href="' + (enabled ? route : '#') + '" class="qp-card">' +
'<div class="qp-card-hero ' + (heroes[g.game_key] || '') + '">' +
'<svg class="icon-xl"><use href="/public/icons/sprite.svg#' + (icons[g.game_key] || 'icon-play') + '"></use></svg>' +
(enabled ? '' : '<span class="qp-card-soon">قريبا</span>') +
'</div>' +
'<div class="qp-card-body">' +
'<span class="qp-card-name">' + g.name_ar + '</span>' +
(enabled ? '<span class="qp-card-live"><span class="qp-card-live-dot"></span></span>' : '') +
'</div></a>';
}).join('');
}
} catch (e) {}
// Recent games
const [gamesData, botsData] = await Promise.all([
App.fetch('/api/game?action=recent'),
App.cachedFetch('/api/bots.php', 60000)
]);
const botMap = {};
if (botsData && botsData.bots) {
botsData.bots.forEach(b => { botMap[b.id] = b.name_ar ? b.name_ar.split(' ')[0] : b.name; });
}
if (gamesData && gamesData.games && gamesData.games.length > 0) {
const container = document.getElementById('home-recent');
container.innerHTML = '<div class="activity-list">' + gamesData.games.slice(0, 5).map(g => {
const iconClass = g.result === 'win' ? 'activity-icon--win' : g.result === 'loss' ? 'activity-icon--loss' : 'activity-icon--draw';
const resultText = g.result === 'win' ? 'فوز' : g.result === 'loss' ? 'خسارة' : 'تعادل';
const resultColor = g.result === 'win' ? 'color-success' : g.result === 'loss' ? 'color-error' : '';
const iconName = g.result === 'win' ? 'check' : g.result === 'loss' ? 'x' : 'clock';
const botName = botMap[g.bot_id] || g.bot_id || '?';
return '<div class="activity-item">' +
'<div class="activity-icon ' + iconClass + '"><svg class="icon-sm"><use href="/public/icons/sprite.svg#icon-' + iconName + '"></use></svg></div>' +
'<div class="activity-detail"><p class="activity-opponent">ضد ' + botName + '</p></div>' +
'<span class="activity-result ' + resultColor + '">' + resultText + '</span>' +
'</div>';
}).join('') + '</div>';
}
// Streak claim
document.getElementById('streak-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('streak-btn').textContent = 'تم ✓';
document.getElementById('streak-btn').disabled = true;
document.getElementById('streak-day').textContent = 'اليوم ' + res.streak;
App.loadProfile();
} else if (res && res.error === 'already_claimed') {
App.toast('لقد جمعت المكافأة اليوم', 'error');
}
});
// TC chip selection
document.querySelectorAll('#home-tc .chip').forEach(chip => {
chip.addEventListener('click', () => {
document.querySelectorAll('#home-tc .chip').forEach(c => c.classList.remove('active'));
chip.classList.add('active');
});
});
});
function quickPlay() {
const active = document.querySelector('#home-tc .chip.active');
const tc = active.dataset.tc;
const time = active.dataset.time;
const inc = active.dataset.inc;
window.location.href = '/matchmaking?tc=' + tc + '&time=' + time + '&inc=' + inc;
}
</script>
<?php require __DIR__ . '/../includes/footer-v2.php'; ?>
<?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'; ?>
/* EL3AB Design System v2 — Redesigned from scratch */
:root {
/* Backgrounds — deeper contrast between layers */
--bg-0: #030A12;
--bg-1: #081420;
--bg-2: #0F1F30;
--bg-3: #182B42;
--bg-surface: #0C1926;
--bg-elevated: #142438;
/* Brand */
--gold: #F5B731;
--gold-dim: rgba(245, 183, 49, 0.12);
--gold-dark: #D49A18;
--cyan: #00D4FF;
--cyan-dim: rgba(0, 212, 255, 0.10);
--cyan-dark: #00A8CC;
--blue: #3B82F6;
--purple: #8B5CF6;
--green: #10B981;
/* Status */
--success: #34D399;
--error: #F87171;
--warning: #FBBF24;
--online: #22C55E;
/* Text — WCAG AA compliant on bg-0 */
--text-1: #F8FAFC;
--text-2: #CBD5E1;
--text-3: #8B9DB7;
--text-inverse: #0F172A;
/* Border */
--border: rgba(255, 255, 255, 0.08);
--border-focus: rgba(0, 212, 255, 0.5);
/* Radius */
--r-xs: 6px;
--r-sm: 8px;
--r-md: 12px;
--r-lg: 16px;
--r-xl: 20px;
--r-full: 9999px;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.5);
--shadow-lg: 0 12px 48px rgba(0, 0, 0, 0.6);
--shadow-glow-gold: 0 0 20px rgba(245, 183, 49, 0.15);
--shadow-glow-cyan: 0 0 20px rgba(0, 212, 255, 0.15);
/* Layout */
--header-h: 52px;
--nav-h: 60px;
--sidebar-w: 240px;
--content-max: 640px;
--touch: 44px;
/* Fonts */
--font-ar: 'IBM Plex Sans Arabic', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', 'SF Mono', monospace;
/* Motion */
--ease: cubic-bezier(0.2, 0, 0, 1);
--ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
/* Spacing scale */
--sp-1: 4px;
--sp-2: 8px;
--sp-3: 12px;
--sp-4: 16px;
--sp-5: 20px;
--sp-6: 24px;
--sp-8: 32px;
--sp-10: 40px;
--sp-12: 48px;
}
/* === RESET === */
*, *::before, *::after {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
html {
direction: rtl;
font-family: var(--font-ar);
font-size: 16px;
line-height: 1.5;
color: var(--text-1);
background: var(--bg-0);
-webkit-font-smoothing: antialiased;
}
body {
min-height: 100dvh;
overflow-x: hidden;
}
a { color: inherit; text-decoration: none; }
button { cursor: pointer; border: none; background: none; font: inherit; color: inherit; }
input, select, textarea { font: inherit; color: inherit; background: none; border: none; outline: none; }
img { display: block; max-width: 100%; }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: var(--r-full); }
/* === TYPOGRAPHY === */
.t-display { font-size: 28px; font-weight: 700; line-height: 1.2; letter-spacing: -0.5px; }
.t-heading { font-size: 20px; font-weight: 700; line-height: 1.3; }
.t-subhead { font-size: 16px; font-weight: 600; line-height: 1.4; }
.t-body { font-size: 14px; font-weight: 400; line-height: 1.5; }
.t-caption { font-size: 12px; font-weight: 500; line-height: 1.4; color: var(--text-3); }
.t-mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
/* === LAYOUT SHELL === */
.shell {
display: grid;
grid-template-rows: var(--header-h) 1fr;
grid-template-columns: 1fr;
min-height: 100dvh;
}
@media (min-width: 1024px) {
.shell {
grid-template-columns: var(--sidebar-w) 1fr;
grid-template-rows: 1fr;
}
}
/* === HEADER === */
.hdr {
position: sticky;
top: 0;
z-index: 40;
height: var(--header-h);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--sp-4);
background: rgba(3, 10, 18, 0.85);
backdrop-filter: blur(16px) saturate(1.4);
border-bottom: 1px solid var(--border);
padding-top: env(safe-area-inset-top);
}
@media (min-width: 1024px) {
.hdr {
grid-column: 2;
padding: 0 var(--sp-6);
}
}
.hdr-brand {
font-weight: 800;
font-size: 22px;
color: var(--gold);
letter-spacing: -1px;
}
.hdr-center {
display: flex;
align-items: center;
gap: var(--sp-3);
}
.hdr-rating {
display: flex;
align-items: center;
gap: var(--sp-2);
padding: 4px 10px;
background: var(--bg-2);
border-radius: var(--r-full);
font-size: 13px;
font-weight: 600;
font-family: var(--font-mono);
}
.hdr-rating-icon {
width: 14px;
height: 14px;
color: var(--gold);
}
.hdr-actions {
display: flex;
align-items: center;
gap: var(--sp-2);
}
.hdr-btn {
position: relative;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--r-sm);
color: var(--text-3);
transition: background 0.15s, color 0.15s;
}
.hdr-btn:hover { background: var(--bg-2); color: var(--text-2); }
.hdr-dot {
position: absolute;
top: 6px;
left: 6px;
width: 8px;
height: 8px;
background: var(--error);
border-radius: 50%;
border: 2px solid var(--bg-0);
}
/* === SIDEBAR (Desktop) === */
.sidebar {
display: none;
position: fixed;
top: 0;
right: 0;
bottom: 0;
width: var(--sidebar-w);
flex-direction: column;
background: var(--bg-surface);
border-left: 1px solid var(--border);
padding: var(--sp-5) var(--sp-3);
overflow-y: auto;
z-index: 50;
}
@media (min-width: 1024px) {
.sidebar { display: flex; }
}
.sidebar-brand {
font-weight: 800;
font-size: 24px;
color: var(--gold);
letter-spacing: -1px;
padding: 0 var(--sp-3);
margin-bottom: var(--sp-6);
}
.sidebar-section {
margin-bottom: var(--sp-5);
}
.sidebar-label {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.8px;
color: var(--text-3);
padding: 0 var(--sp-3);
margin-bottom: var(--sp-2);
}
.sidebar-item {
display: flex;
align-items: center;
gap: var(--sp-3);
padding: var(--sp-2) var(--sp-3);
border-radius: var(--r-sm);
font-size: 14px;
font-weight: 500;
color: var(--text-2);
transition: background 0.15s, color 0.15s;
}
.sidebar-item:hover {
background: rgba(255,255,255,0.04);
color: var(--text-1);
}
.sidebar-item.active {
background: var(--cyan-dim);
color: var(--cyan);
}
.sidebar-item .icon {
width: 18px;
height: 18px;
flex-shrink: 0;
}
.sidebar-play-btn {
display: flex;
align-items: center;
justify-content: center;
gap: var(--sp-2);
width: 100%;
padding: var(--sp-3) var(--sp-4);
margin-bottom: var(--sp-5);
background: var(--gold);
color: var(--text-inverse);
font-weight: 700;
font-size: 15px;
border-radius: var(--r-md);
transition: transform 0.1s, box-shadow 0.2s;
box-shadow: var(--shadow-glow-gold);
}
.sidebar-play-btn:hover {
transform: translateY(-1px);
box-shadow: 0 0 30px rgba(245, 183, 49, 0.25);
}
.sidebar-play-btn:active { transform: scale(0.97); }
/* === BOTTOM NAV (Mobile) === */
.nav-m {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 40;
height: calc(var(--nav-h) + env(safe-area-inset-bottom));
display: grid;
grid-template-columns: repeat(4, 1fr);
align-items: start;
padding-top: var(--sp-2);
padding-bottom: env(safe-area-inset-bottom);
background: rgba(3, 10, 18, 0.92);
backdrop-filter: blur(16px) saturate(1.4);
border-top: 1px solid var(--border);
}
@media (min-width: 1024px) {
.nav-m { display: none; }
}
.nav-m-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
padding: var(--sp-2) 0;
color: var(--text-3);
font-size: 10px;
font-weight: 500;
transition: color 0.15s;
position: relative;
min-height: var(--touch);
justify-content: center;
}
.nav-m-item.active { color: var(--cyan); }
.nav-m-item.active::before {
content: '';
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
width: 24px;
height: 3px;
background: var(--cyan);
border-radius: 0 0 var(--r-full) var(--r-full);
}
.nav-m-item .icon {
width: 22px;
height: 22px;
}
/* Floating play button in center */
.nav-m-play {
position: fixed;
bottom: calc(var(--nav-h) + env(safe-area-inset-bottom) - 16px);
left: 50%;
transform: translateX(-50%);
z-index: 41;
width: 56px;
height: 56px;
display: flex;
align-items: center;
justify-content: center;
background: var(--gold);
border-radius: 50%;
box-shadow: 0 4px 20px rgba(245, 183, 49, 0.35);
transition: transform 0.15s var(--ease-bounce);
}
@media (min-width: 1024px) {
.nav-m-play { display: none; }
}
.nav-m-play:active { transform: translateX(-50%) scale(0.9); }
.nav-m-play .icon { width: 24px; height: 24px; color: var(--text-inverse); stroke-width: 2.5; }
/* === MAIN CONTENT === */
.main {
padding: var(--sp-5) var(--sp-4) calc(var(--nav-h) + env(safe-area-inset-bottom) + var(--sp-8));
max-width: var(--content-max);
margin: 0 auto;
width: 100%;
}
@media (min-width: 1024px) {
.main {
grid-column: 2;
padding: var(--sp-6) var(--sp-8) var(--sp-8);
max-width: 720px;
}
}
/* === CARD === */
.card {
background: var(--bg-1);
border: 1px solid var(--border);
border-radius: var(--r-lg);
overflow: hidden;
}
.card-pad { padding: var(--sp-4); }
.card-pad-lg { padding: var(--sp-5); }
/* === BUTTONS === */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--sp-2);
min-height: var(--touch);
padding: 10px 20px;
font-weight: 600;
font-size: 14px;
border-radius: var(--r-md);
transition: transform 0.1s, opacity 0.15s, box-shadow 0.2s;
position: relative;
overflow: hidden;
white-space: nowrap;
}
.btn:active { transform: scale(0.97); }
.btn:disabled { opacity: 0.4; pointer-events: none; }
.btn-primary {
background: var(--gold);
color: var(--text-inverse);
box-shadow: var(--shadow-glow-gold);
}
.btn-primary:hover { box-shadow: 0 0 30px rgba(245, 183, 49, 0.3); }
.btn-secondary {
background: var(--cyan);
color: var(--text-inverse);
}
.btn-ghost {
background: transparent;
color: var(--text-2);
border: 1px solid var(--border);
}
.btn-ghost:hover { background: rgba(255,255,255,0.04); }
.btn-block { display: flex; width: 100%; }
.btn-lg { min-height: 52px; font-size: 16px; font-weight: 700; border-radius: var(--r-lg); }
.btn-sm { min-height: 32px; padding: 6px 12px; font-size: 12px; }
/* === INPUTS === */
.field { margin-bottom: var(--sp-4); }
.field-label { display: block; font-size: 13px; font-weight: 500; color: var(--text-2); margin-bottom: var(--sp-2); }
.input {
width: 100%;
height: var(--touch);
padding: 0 var(--sp-4);
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--r-md);
color: var(--text-1);
font-size: 14px;
transition: border-color 0.15s, box-shadow 0.15s;
}
.input:focus {
border-color: var(--border-focus);
box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.08);
}
.input::placeholder { color: var(--text-3); }
/* === CHIPS (time controls, filters) === */
.chip-group {
display: flex;
gap: var(--sp-2);
flex-wrap: wrap;
}
.chip {
min-height: 36px;
padding: 6px 14px;
font-size: 13px;
font-weight: 600;
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--r-sm);
color: var(--text-2);
cursor: pointer;
transition: all 0.15s;
display: flex;
align-items: center;
justify-content: center;
}
.chip:hover { border-color: var(--text-3); }
.chip.active { background: var(--cyan); border-color: var(--cyan); color: var(--text-inverse); }
.chip-gold.active { background: var(--gold); border-color: var(--gold); color: var(--text-inverse); }
/* === STAT BLOCK === */
.stats-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
gap: var(--sp-2);
}
.stat {
background: var(--bg-2);
border-radius: var(--r-md);
padding: var(--sp-3);
text-align: center;
}
.stat-val {
font-size: 20px;
font-weight: 700;
font-family: var(--font-mono);
color: var(--text-1);
}
.stat-lbl {
font-size: 11px;
color: var(--text-3);
margin-top: 2px;
}
/* === BADGE === */
.badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
font-size: 11px;
font-weight: 600;
border-radius: var(--r-full);
}
.badge-gold { background: var(--gold-dim); color: var(--gold); }
.badge-cyan { background: var(--cyan-dim); color: var(--cyan); }
.badge-green { background: rgba(16, 185, 129, 0.12); color: var(--green); }
.badge-red { background: rgba(248, 113, 113, 0.12); color: var(--error); }
/* === AVATAR === */
.avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--bg-3);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
overflow: hidden;
}
.avatar img { width: 100%; height: 100%; object-fit: cover; }
.avatar-sm { width: 32px; height: 32px; }
.avatar-lg { width: 56px; height: 56px; }
.avatar-xl { width: 72px; height: 72px; }
.avatar-ring { box-shadow: 0 0 0 2px var(--gold); }
/* === ICON BASE === */
.icon {
width: 20px;
height: 20px;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
flex-shrink: 0;
}
.icon-sm { width: 16px; height: 16px; }
.icon-lg { width: 24px; height: 24px; }
.icon-xl { width: 32px; height: 32px; }
.icon-fill { fill: currentColor; stroke: none; }
/* === GAME CARD (Home) === */
.quick-play-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--sp-3);
}
.qp-card {
position: relative;
border-radius: var(--r-lg);
overflow: hidden;
background: var(--bg-1);
border: 1px solid var(--border);
cursor: pointer;
transition: transform 0.15s, box-shadow 0.15s;
display: flex;
flex-direction: column;
}
.qp-card:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
.qp-card:active { transform: translateY(0); }
.qp-card-hero {
height: 72px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.qp-card-hero--chess { background: linear-gradient(135deg, #1a2a4a 0%, #2a4a7a 100%); }
.qp-card-hero--ludo { background: linear-gradient(135deg, #2a1a4a 0%, #5a2a7a 100%); }
.qp-card-hero--domino { background: linear-gradient(135deg, #1a3a2a 0%, #2a5a4a 100%); }
.qp-card-hero--backgammon { background: linear-gradient(135deg, #3a2a1a 0%, #6a4a2a 100%); }
.qp-card-hero .icon-xl { color: rgba(255,255,255,0.85); }
.qp-card-body {
padding: var(--sp-3);
display: flex;
align-items: center;
justify-content: space-between;
}
.qp-card-name {
font-size: 13px;
font-weight: 700;
}
.qp-card-live {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: var(--text-3);
}
.qp-card-live-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--online);
animation: pulse-dot 2s infinite;
}
@keyframes pulse-dot {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.qp-card-soon {
position: absolute;
top: var(--sp-2);
left: var(--sp-2);
background: rgba(0,0,0,0.6);
backdrop-filter: blur(4px);
padding: 2px 8px;
border-radius: var(--r-full);
font-size: 10px;
font-weight: 600;
color: var(--text-2);
}
/* === FEATURED PLAY CARD (Home hero) === */
.hero-play {
position: relative;
border-radius: var(--r-xl);
overflow: hidden;
background: linear-gradient(135deg, var(--bg-2) 0%, var(--bg-1) 100%);
border: 1px solid rgba(245, 183, 49, 0.2);
padding: var(--sp-5);
display: flex;
flex-direction: column;
gap: var(--sp-4);
}
.hero-play::before {
content: '';
position: absolute;
top: -50%;
right: -50%;
width: 100%;
height: 100%;
background: radial-gradient(circle, rgba(245, 183, 49, 0.06) 0%, transparent 70%);
pointer-events: none;
}
.hero-play-top {
display: flex;
align-items: center;
justify-content: space-between;
}
.hero-play-title {
font-size: 18px;
font-weight: 700;
}
.hero-play-subtitle {
font-size: 12px;
color: var(--text-3);
margin-top: 2px;
}
.hero-play-btn {
display: flex;
align-items: center;
justify-content: center;
gap: var(--sp-2);
width: 100%;
padding: 14px;
background: var(--gold);
color: var(--text-inverse);
font-weight: 700;
font-size: 15px;
border-radius: var(--r-md);
box-shadow: var(--shadow-glow-gold);
transition: transform 0.1s, box-shadow 0.2s;
position: relative;
overflow: hidden;
}
.hero-play-btn::after {
content: '';
position: absolute;
top: 0;
right: -100%;
width: 60%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
animation: shimmer 4s ease-in-out infinite;
}
@keyframes shimmer {
0% { right: -100%; }
100% { right: 200%; }
}
.hero-play-btn:active { transform: scale(0.97); }
/* === STREAK BANNER === */
.streak-banner {
display: flex;
align-items: center;
gap: var(--sp-3);
padding: var(--sp-3) var(--sp-4);
background: var(--bg-1);
border: 1px solid var(--border);
border-radius: var(--r-lg);
}
.streak-banner-icon {
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(251, 191, 36, 0.1);
border-radius: var(--r-sm);
color: var(--warning);
font-size: 18px;
}
.streak-banner-text { flex: 1; }
.streak-banner-day { font-size: 14px; font-weight: 600; }
.streak-banner-reward { font-size: 12px; color: var(--text-3); }
.streak-banner-btn {
min-height: 32px;
padding: 6px 14px;
background: var(--cyan);
color: var(--text-inverse);
font-size: 12px;
font-weight: 700;
border-radius: var(--r-sm);
transition: transform 0.1s;
}
.streak-banner-btn:active { transform: scale(0.95); }
.streak-banner-btn:disabled { opacity: 0.4; background: var(--bg-3); color: var(--text-3); }
/* Streak calendar (compact) */
.streak-cal {
display: flex;
gap: 6px;
margin-top: var(--sp-2);
padding: var(--sp-2) var(--sp-4) var(--sp-3);
}
.streak-cal-day {
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: 600;
border: 2px solid var(--border);
color: var(--text-3);
transition: all 0.2s;
}
.streak-cal-day.claimed {
background: var(--gold);
border-color: var(--gold);
color: var(--text-inverse);
}
.streak-cal-day.current {
border-color: var(--cyan);
color: var(--cyan);
box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.12);
}
/* === RECENT ACTIVITY === */
.activity-list { display: flex; flex-direction: column; }
.activity-item {
display: flex;
align-items: center;
gap: var(--sp-3);
padding: var(--sp-3) var(--sp-4);
border-bottom: 1px solid var(--border);
transition: background 0.1s;
}
.activity-item:last-child { border-bottom: none; }
.activity-item:active { background: rgba(255,255,255,0.02); }
.activity-icon {
width: 32px;
height: 32px;
border-radius: var(--r-sm);
display: flex;
align-items: center;
justify-content: center;
}
.activity-icon--win { background: rgba(52, 211, 153, 0.12); color: var(--success); }
.activity-icon--loss { background: rgba(248, 113, 113, 0.12); color: var(--error); }
.activity-icon--draw { background: rgba(139, 157, 183, 0.12); color: var(--text-3); }
.activity-detail { flex: 1; min-width: 0; }
.activity-opponent { font-size: 13px; font-weight: 600; }
.activity-meta { font-size: 11px; color: var(--text-3); }
.activity-result { font-size: 12px; font-weight: 700; }
.activity-elo { font-size: 11px; font-family: var(--font-mono); color: var(--text-3); }
/* === SECTION HEADERS === */
.sec-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--sp-3);
}
.sec-title {
font-size: 15px;
font-weight: 700;
}
.sec-link {
font-size: 12px;
color: var(--cyan);
font-weight: 500;
}
/* === LIVE FRIENDS (social proof) === */
.friends-strip {
display: flex;
gap: var(--sp-2);
overflow-x: auto;
padding: var(--sp-2) 0;
scrollbar-width: none;
}
.friends-strip::-webkit-scrollbar { display: none; }
.friend-chip {
display: flex;
align-items: center;
gap: var(--sp-2);
padding: 6px 10px 6px 6px;
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--r-full);
flex-shrink: 0;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
}
.friend-chip-avatar {
width: 24px;
height: 24px;
border-radius: 50%;
background: var(--bg-3);
position: relative;
}
.friend-chip-online {
position: absolute;
bottom: -1px;
right: -1px;
width: 8px;
height: 8px;
background: var(--online);
border-radius: 50%;
border: 2px solid var(--bg-2);
}
/* === TOAST === */
.toast-wrap {
position: fixed;
top: calc(var(--header-h) + var(--sp-3));
left: 50%;
transform: translateX(-50%);
z-index: 100;
display: flex;
flex-direction: column;
gap: var(--sp-2);
pointer-events: none;
}
.toast {
display: flex;
align-items: center;
gap: var(--sp-2);
padding: var(--sp-3) var(--sp-4);
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: var(--r-md);
font-size: 13px;
font-weight: 500;
box-shadow: var(--shadow-lg);
animation: toast-enter 0.25s var(--ease);
pointer-events: auto;
}
.toast--success { border-color: var(--success); }
.toast--error { border-color: var(--error); }
@keyframes toast-enter {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
/* === LOBBY (Play page) === */
.lobby { display: flex; flex-direction: column; gap: var(--sp-4); }
.lobby-hero {
border-radius: var(--r-xl);
background: var(--bg-1);
border: 2px solid rgba(245, 183, 49, 0.2);
padding: var(--sp-5);
display: flex;
flex-direction: column;
gap: var(--sp-4);
}
.lobby-hero-header {
display: flex;
align-items: center;
gap: var(--sp-3);
}
.lobby-hero-icon {
width: 44px;
height: 44px;
border-radius: var(--r-md);
background: linear-gradient(135deg, var(--gold), var(--cyan));
display: flex;
align-items: center;
justify-content: center;
}
.lobby-hero-icon .icon-lg { color: white; }
.lobby-row {
display: flex;
align-items: center;
gap: var(--sp-4);
padding: var(--sp-4);
background: var(--bg-1);
border: 1px solid var(--border);
border-radius: var(--r-lg);
cursor: pointer;
transition: background 0.15s;
}
.lobby-row:hover { background: var(--bg-2); }
.lobby-row:active { background: rgba(255,255,255,0.03); }
.lobby-row-icon {
width: 40px;
height: 40px;
border-radius: var(--r-sm);
background: var(--bg-3);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.lobby-row-text { flex: 1; }
.lobby-row-title { font-size: 14px; font-weight: 600; }
.lobby-row-sub { font-size: 12px; color: var(--text-3); }
/* === EMPTY STATE === */
.empty {
padding: var(--sp-10) var(--sp-5);
text-align: center;
color: var(--text-3);
font-size: 13px;
}
/* === SKELETON === */
.skel {
background: var(--bg-2);
border-radius: var(--r-md);
position: relative;
overflow: hidden;
}
.skel::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.03), transparent);
animation: skel-shimmer 1.5s infinite;
}
@keyframes skel-shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
/* === UTILITIES === */
.stack-2 > * + * { margin-top: var(--sp-2); }
.stack-3 > * + * { margin-top: var(--sp-3); }
.stack-4 > * + * { margin-top: var(--sp-4); }
.stack-5 > * + * { margin-top: var(--sp-5); }
.stack-6 > * + * { margin-top: var(--sp-6); }
.flex { display: flex; }
.flex-col { flex-direction: column; }
.items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.gap-2 { gap: var(--sp-2); }
.gap-3 { gap: var(--sp-3); }
.gap-4 { gap: var(--sp-4); }
.text-center { text-align: center; }
.w-full { width: 100%; }
.color-gold { color: var(--gold); }
.color-cyan { color: var(--cyan); }
.color-success { color: var(--success); }
.color-error { color: var(--error); }
/* === PAGE ENTER === */
.main {
animation: page-in 0.25s var(--ease) both;
}
@keyframes page-in {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
/* === FOCUS / A11Y === */
:focus-visible {
outline: 2px solid var(--cyan);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
/* === RESPONSIVE === */
@media (max-width: 380px) {
.quick-play-grid { gap: var(--sp-2); }
.qp-card-hero { height: 60px; }
.hero-play { padding: var(--sp-4); }
.streak-cal-day { width: 24px; height: 24px; font-size: 9px; }
}
/* === LEADERBOARD === */
.lb-podium {
display: flex;
align-items: flex-end;
justify-content: center;
gap: var(--sp-3);
padding: var(--sp-5) 0;
}
.lb-podium-item {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--sp-2);
flex: 1;
}
.lb-podium-item--first { order: 0; }
.lb-podium-item--second { order: -1; }
.lb-podium-item--third { order: 1; }
.lb-rank {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 700;
}
.lb-rank--1 { background: var(--gold); color: var(--text-inverse); }
.lb-rank--2 { background: var(--text-3); color: var(--text-inverse); }
.lb-rank--3 { background: #CD7F32; color: var(--text-inverse); }
.lb-list-item {
display: flex;
align-items: center;
gap: var(--sp-3);
padding: var(--sp-3) var(--sp-4);
border-bottom: 1px solid var(--border);
}
.lb-list-item:last-child { border-bottom: none; }
.lb-list-rank {
width: 28px;
font-size: 13px;
font-weight: 700;
font-family: var(--font-mono);
color: var(--text-3);
text-align: center;
}
.lb-list-name { flex: 1; font-size: 14px; font-weight: 600; }
.lb-list-elo { font-size: 14px; font-weight: 700; font-family: var(--font-mono); }
/* === PROFILE === */
.profile-header {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--sp-3);
padding: var(--sp-6) var(--sp-4);
background: linear-gradient(180deg, rgba(0, 212, 255, 0.04) 0%, transparent 100%);
border-radius: var(--r-xl);
border: 1px solid var(--border);
}
.profile-ratings {
display: flex;
gap: var(--sp-4);
flex-wrap: wrap;
justify-content: center;
}
.profile-rating {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.profile-rating-val {
font-size: 18px;
font-weight: 700;
font-family: var(--font-mono);
}
.profile-rating-lbl {
font-size: 11px;
color: var(--text-3);
}
/* === SHOP === */
.shop-item {
display: flex;
align-items: center;
gap: var(--sp-3);
padding: var(--sp-3) var(--sp-4);
background: var(--bg-1);
border: 1px solid var(--border);
border-radius: var(--r-lg);
}
.shop-item-preview {
width: 44px;
height: 44px;
border-radius: var(--r-sm);
background: var(--bg-3);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.shop-item-info { flex: 1; min-width: 0; }
.shop-item-name { font-size: 14px; font-weight: 600; }
.shop-item-rarity { font-size: 11px; }
/* === LOGIN === */
.auth-page {
display: flex;
align-items: center;
justify-content: center;
min-height: 100dvh;
padding: var(--sp-5);
}
.auth-card {
width: 100%;
max-width: 380px;
}
.auth-brand {
font-weight: 800;
font-size: 36px;
color: var(--gold);
text-align: center;
letter-spacing: -1px;
margin-bottom: var(--sp-2);
}
.auth-subtitle {
text-align: center;
font-size: 14px;
color: var(--text-3);
margin-bottom: var(--sp-6);
}
.auth-error {
padding: var(--sp-3);
background: rgba(248, 113, 113, 0.08);
border: 1px solid rgba(248, 113, 113, 0.2);
border-radius: var(--r-md);
color: var(--error);
font-size: 13px;
text-align: center;
margin-top: var(--sp-4);
}
.auth-footer {
text-align: center;
font-size: 13px;
color: var(--text-3);
margin-top: var(--sp-5);
}
.auth-footer a { color: var(--cyan); font-weight: 500; }
<?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