Commit 9d4a6fa3 authored by Mahmoud Aglan's avatar Mahmoud Aglan

nuke: delete entire PHP frontend, rebuild as vanilla JS SPA

DELETED:
- All 50+ PHP pages, templates, includes
- app.css and app-v2.css (1782 + 700 lines of accumulated CSS)

CREATED:
- app/core/ — SPA engine (router, state, api, audio, haptics, particles, animate)
- app/screens/ — ES module screens (home, login + placeholders for all routes)
- app/styles/ — Design token system + animations + components
- index.php — now serves SPA shell for all non-API routes

ARCHITECTURE:
- Vanilla JS SPA with ES Modules (no build step, no framework)
- History API router with animated transitions
- Web Audio API synthesizer for game sounds
- Canvas particle system (confetti, coin bursts)
- Web Animations API for juice (number tickers, fly-to, pop, shake)
- Haptic feedback patterns via navigator.vibrate
- Game engines load on-demand as classic scripts (zero modifications)

API endpoints (24) completely unchanged. Game engine JS (25 files) unchanged.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 8c4dc029
export function tickNumber(el, from, to, duration = 600) {
const start = performance.now();
const diff = to - from;
function frame(now) {
const t = Math.min((now - start) / duration, 1);
const eased = 1 - Math.pow(1 - t, 3);
el.textContent = Math.round(from + diff * eased).toLocaleString();
if (t < 1) requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
export function flyTo(el, targetEl, duration = 500) {
const from = el.getBoundingClientRect();
const to = targetEl.getBoundingClientRect();
const dx = to.left - from.left + to.width / 2 - from.width / 2;
const dy = to.top - from.top + to.height / 2 - from.height / 2;
return el.animate([
{ transform: 'translate(0, 0) scale(1)', opacity: 1 },
{ transform: `translate(${dx}px, ${dy}px) scale(0.2)`, opacity: 0 }
], { duration, easing: 'cubic-bezier(0.4, 0, 0.2, 1)', fill: 'forwards' }).finished;
}
export function pop(el) {
el.animate([
{ transform: 'scale(0.8)', opacity: 0 },
{ transform: 'scale(1.05)', opacity: 1 },
{ transform: 'scale(1)' }
], { duration: 300, easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)' });
}
export function shake(el) {
el.animate([
{ transform: 'translateX(0)' },
{ transform: 'translateX(-4px)' },
{ transform: 'translateX(4px)' },
{ transform: 'translateX(-2px)' },
{ transform: 'translateX(2px)' },
{ transform: 'translateX(0)' }
], { duration: 300, easing: 'ease-out' });
}
export function fadeIn(el, duration = 250) {
el.animate([
{ opacity: 0, transform: 'translateY(8px)' },
{ opacity: 1, transform: 'translateY(0)' }
], { duration, easing: 'cubic-bezier(0.2, 0, 0, 1)', fill: 'forwards' });
}
export function scaleIn(el, duration = 300) {
el.animate([
{ transform: 'scale(0.85)', opacity: 0 },
{ transform: 'scale(1)', opacity: 1 }
], { duration, easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)', fill: 'forwards' });
}
import { state } from './state.js';
const cache = new Map();
export async function api(endpoint, options = {}) {
const url = endpoint.startsWith('http') ? endpoint : endpoint;
const headers = { 'Content-Type': 'application/json' };
const token = state.getToken();
if (token) headers['Authorization'] = `Bearer ${token}`;
const config = { headers, ...options };
if (config.body && typeof config.body === 'object') {
config.body = JSON.stringify(config.body);
}
try {
const res = await fetch(url, config);
if (res.status === 401) {
state.logout();
return null;
}
const text = await res.text();
try {
return JSON.parse(text);
} catch {
console.error('API parse error:', endpoint, text.slice(0, 200));
return null;
}
} catch (err) {
console.error('API fetch error:', endpoint, err);
return null;
}
}
export async function cachedApi(endpoint, ttlMs = 60000) {
const key = endpoint;
const cached = cache.get(key);
if (cached && Date.now() - cached.time < ttlMs) return cached.data;
const data = await api(endpoint);
if (data) cache.set(key, { data, time: Date.now() });
return data;
}
export function clearCache(pattern) {
if (!pattern) { cache.clear(); return; }
for (const key of cache.keys()) {
if (key.includes(pattern)) cache.delete(key);
}
}
let ctx = null;
let muted = localStorage.getItem('el3ab_muted') === '1';
function getCtx() {
if (!ctx) ctx = new (window.AudioContext || window.webkitAudioContext)();
if (ctx.state === 'suspended') ctx.resume();
return ctx;
}
function synth(freq, duration, type = 'sine', gain = 0.15) {
if (muted) return;
const c = getCtx();
const osc = c.createOscillator();
const g = c.createGain();
osc.type = type;
osc.frequency.value = freq;
g.gain.value = gain;
g.gain.exponentialRampToValueAtTime(0.001, c.currentTime + duration);
osc.connect(g).connect(c.destination);
osc.start();
osc.stop(c.currentTime + duration);
}
export const audio = {
tap() { synth(800, 0.05, 'sine', 0.08); },
move() { synth(300, 0.08, 'triangle', 0.1); },
capture() { synth(200, 0.15, 'sawtooth', 0.12); },
coin() { synth(1200, 0.1, 'sine', 0.1); synth(1600, 0.1, 'sine', 0.08); },
win() {
synth(523, 0.15, 'triangle', 0.12);
setTimeout(() => synth(659, 0.15, 'triangle', 0.12), 100);
setTimeout(() => synth(784, 0.3, 'triangle', 0.15), 200);
},
lose() {
synth(400, 0.2, 'triangle', 0.1);
setTimeout(() => synth(350, 0.3, 'triangle', 0.08), 150);
},
levelUp() {
[523, 659, 784, 1047].forEach((f, i) => {
setTimeout(() => synth(f, 0.2, 'sine', 0.12), i * 80);
});
},
error() { synth(200, 0.2, 'square', 0.08); },
dice() { for (let i = 0; i < 5; i++) setTimeout(() => synth(100 + Math.random() * 200, 0.04, 'noise' in window ? 'noise' : 'triangle', 0.06), i * 30); },
notify() { synth(880, 0.08, 'sine', 0.1); setTimeout(() => synth(1100, 0.12, 'sine', 0.08), 80); },
mute() { muted = true; localStorage.setItem('el3ab_muted', '1'); },
unmute() { muted = false; localStorage.removeItem('el3ab_muted'); },
isMuted() { return muted; },
toggle() { muted ? this.unmute() : this.mute(); return !muted; }
};
const can = 'vibrate' in navigator;
export const haptics = {
tap() { can && navigator.vibrate(10); },
heavy() { can && navigator.vibrate(30); },
success() { can && navigator.vibrate([20, 50, 20]); },
error() { can && navigator.vibrate([50, 30, 50, 30, 50]); },
selection() { can && navigator.vibrate(5); },
};
let canvas, ctx, particles = [], raf = null;
export function initCanvas() {
canvas = document.getElementById('particles');
if (!canvas) return;
ctx = canvas.getContext('2d');
resize();
window.addEventListener('resize', resize);
}
function resize() {
if (!canvas) return;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
function loop() {
if (!ctx) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
particles = particles.filter(p => {
p.update();
p.draw(ctx);
return p.alive;
});
if (particles.length > 0) {
raf = requestAnimationFrame(loop);
} else {
raf = null;
}
}
function startLoop() {
if (!raf) raf = requestAnimationFrame(loop);
}
export function confetti(x, y, count = 40) {
const colors = ['#F5B731', '#00D4FF', '#8B5CF6', '#34D399', '#F87171', '#FBBF24'];
for (let i = 0; i < count; i++) {
const angle = (Math.PI * 2 * i) / count + (Math.random() - 0.5);
const speed = 4 + Math.random() * 6;
const size = 4 + Math.random() * 4;
const color = colors[Math.floor(Math.random() * colors.length)];
const spin = (Math.random() - 0.5) * 0.3;
let life = 1;
particles.push({
x, y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed - 3,
size, color, rotation: Math.random() * Math.PI * 2, spin, alive: true,
update() {
this.x += this.vx;
this.y += this.vy;
this.vy += 0.15;
this.vx *= 0.98;
this.rotation += this.spin;
life -= 0.012;
if (life <= 0 || this.y > canvas.height) this.alive = false;
},
draw(c) {
c.save();
c.translate(this.x, this.y);
c.rotate(this.rotation);
c.globalAlpha = life;
c.fillStyle = this.color;
c.fillRect(-this.size / 2, -this.size / 2, this.size, this.size * 0.6);
c.restore();
}
});
}
startLoop();
}
export function coinBurst(x, y, count = 12) {
for (let i = 0; i < count; i++) {
const angle = -Math.PI / 2 + (Math.random() - 0.5) * 1.2;
const speed = 3 + Math.random() * 4;
let life = 1;
particles.push({
x, y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed,
size: 6 + Math.random() * 3, alive: true,
update() {
this.x += this.vx;
this.y += this.vy;
this.vy += 0.12;
life -= 0.018;
if (life <= 0) this.alive = false;
},
draw(c) {
c.globalAlpha = life;
c.fillStyle = '#F5B731';
c.beginPath();
c.arc(this.x, this.y, this.size, 0, Math.PI * 2);
c.fill();
c.globalAlpha = life * 0.6;
c.fillStyle = '#FFF';
c.beginPath();
c.arc(this.x - this.size * 0.2, this.y - this.size * 0.2, this.size * 0.3, 0, Math.PI * 2);
c.fill();
}
});
}
startLoop();
}
const routes = new Map();
let currentScreen = null;
let mainEl = null;
export function register(path, screenFn) {
routes.set(path, screenFn);
}
export function navigate(path, params = {}, replace = false) {
const url = new URL(path, window.location.origin);
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
if (replace) {
history.replaceState({ path, params }, '', url);
} else {
history.pushState({ path, params }, '', url);
}
render(path, params);
}
export function init(container) {
mainEl = container;
window.addEventListener('popstate', (e) => {
const s = e.state || {};
render(s.path || location.pathname, s.params || Object.fromEntries(new URLSearchParams(location.search)));
});
document.addEventListener('click', (e) => {
const link = e.target.closest('a[href]');
if (!link) return;
const href = link.getAttribute('href');
if (!href || href.startsWith('http') || href.startsWith('#') || link.target === '_blank') return;
if (href.startsWith('/api/')) return;
e.preventDefault();
navigate(href);
});
const path = location.pathname;
const params = Object.fromEntries(new URLSearchParams(location.search));
history.replaceState({ path, params }, '', location.href);
render(path, params);
}
async function render(path, params = {}) {
const screenFn = matchRoute(path);
if (!screenFn) {
mainEl.innerHTML = '<div class="empty">الصفحة غير موجودة</div>';
return;
}
if (currentScreen && currentScreen.destroy) {
mainEl.firstElementChild?.classList.add('screen-exit');
await sleep(150);
currentScreen.destroy();
}
mainEl.innerHTML = '';
const screen = await screenFn(params);
currentScreen = screen;
const wrapper = document.createElement('div');
wrapper.className = 'screen main-inner';
mainEl.appendChild(wrapper);
if (screen.render) {
const html = screen.render(params);
wrapper.innerHTML = html;
}
if (screen.mount) {
await screen.mount(wrapper, params);
}
}
function matchRoute(path) {
const clean = path.replace(/\/$/, '') || '/';
if (routes.has(clean)) return routes.get(clean);
for (const [pattern, fn] of routes) {
if (pattern.includes(':')) {
const regex = new RegExp('^' + pattern.replace(/:([^/]+)/g, '([^/]+)') + '$');
if (regex.test(clean)) return fn;
}
}
return null;
}
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
export function getParams() {
return Object.fromEntries(new URLSearchParams(location.search));
}
export const state = {
user: null,
screen: null,
notifications: [],
_subs: new Map(),
get(key) { return this[key]; },
set(key, value) {
const old = this[key];
this[key] = value;
if (this._subs.has(key)) {
this._subs.get(key).forEach(fn => fn(value, old));
}
},
on(key, fn) {
if (!this._subs.has(key)) this._subs.set(key, new Set());
this._subs.get(key).add(fn);
return () => this._subs.get(key).delete(fn);
},
isLoggedIn() {
return !!localStorage.getItem('el3ab_token');
},
getToken() {
return localStorage.getItem('el3ab_token') || '';
},
getUser() {
try { return JSON.parse(localStorage.getItem('el3ab_user')); } catch { return null; }
},
setAuth(token, user) {
localStorage.setItem('el3ab_token', token);
localStorage.setItem('el3ab_user', JSON.stringify(user));
this.set('user', user);
},
logout() {
localStorage.removeItem('el3ab_token');
localStorage.removeItem('el3ab_user');
this.set('user', null);
window.location.href = '/';
}
};
import { state } from './core/state.js';
import { router, navigate, register, init as initRouter } from './core/router.js';
import { initCanvas } from './core/particles.js';
import { api } from './core/api.js';
async function boot() {
initCanvas();
// Register routes
register('/', () => import('./screens/home.js').then(m => m.default));
register('/login', () => import('./screens/login.js').then(m => m.default));
register('/play', () => import('./screens/chess-lobby.js').then(m => m.default));
register('/ludo', () => import('./screens/ludo-lobby.js').then(m => m.default));
register('/domino', () => import('./screens/domino-lobby.js').then(m => m.default));
register('/backgammon', () => import('./screens/backgammon-lobby.js').then(m => m.default));
register('/game', () => import('./screens/game-chess.js').then(m => m.default));
register('/profile', () => import('./screens/profile.js').then(m => m.default));
register('/leaderboard', () => import('./screens/leaderboard.js').then(m => m.default));
register('/friends', () => import('./screens/friends.js').then(m => m.default));
register('/shop', () => import('./screens/shop.js').then(m => m.default));
register('/achievements', () => import('./screens/achievements.js').then(m => m.default));
register('/settings', () => import('./screens/settings.js').then(m => m.default));
register('/games', () => import('./screens/home.js').then(m => m.default));
// Auth check
if (!state.isLoggedIn() && location.pathname !== '/login' && location.pathname !== '/register') {
navigate('/login', {}, true);
initRouter(document.getElementById('main'));
return;
}
// Load profile if logged in
if (state.isLoggedIn()) {
const data = await api('/api/profile');
if (data && data.profile) {
state.set('user', data.profile);
updateShell(data.profile);
}
}
initRouter(document.getElementById('main'));
setupNav();
}
function updateShell(profile) {
const lvl = document.getElementById('hdr-level');
if (lvl) lvl.textContent = `Lv ${profile.level || 1}`;
const coins = document.getElementById('hdr-coins');
if (coins) coins.textContent = (profile.coins || 0).toLocaleString();
const gems = document.getElementById('hdr-gems');
if (gems) gems.textContent = profile.gems || 0;
}
function setupNav() {
const navItems = document.querySelectorAll('.nav-item');
const updateActive = () => {
const path = location.pathname;
navItems.forEach(item => {
const href = item.getAttribute('href');
const match = (href === '/' && path === '/') || (href !== '/' && path.startsWith(href));
item.classList.toggle('active', match);
});
};
updateActive();
window.addEventListener('popstate', updateActive);
// Also listen for navigate events
const orig = history.pushState;
history.pushState = function() {
orig.apply(this, arguments);
updateActive();
};
}
// Boot when DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', boot);
} else {
boot();
}
// Export for global access by game engines
window.El3ab = { state, navigate, api };
export default {
render() { return '<div class="empty" style="padding:80px 20px;text-align:center;"><p style="font-size:48px;margin-bottom:16px;">🚧</p><p>قيد الانشاء</p></div>'; },
mount() {},
destroy() {}
};
export default {
render() { return '<div class="empty" style="padding:80px 20px;text-align:center;"><p style="font-size:48px;margin-bottom:16px;">🚧</p><p>قيد الانشاء</p></div>'; },
mount() {},
destroy() {}
};
export default {
render() { return '<div class="empty" style="padding:80px 20px;text-align:center;"><p style="font-size:48px;margin-bottom:16px;">🚧</p><p>قيد الانشاء</p></div>'; },
mount() {},
destroy() {}
};
export default {
render() { return '<div class="empty" style="padding:80px 20px;text-align:center;"><p style="font-size:48px;margin-bottom:16px;">🚧</p><p>قيد الانشاء</p></div>'; },
mount() {},
destroy() {}
};
export default {
render() { return '<div class="empty" style="padding:80px 20px;text-align:center;"><p style="font-size:48px;margin-bottom:16px;">🚧</p><p>قيد الانشاء</p></div>'; },
mount() {},
destroy() {}
};
export default {
render() { return '<div class="empty" style="padding:80px 20px;text-align:center;"><p style="font-size:48px;margin-bottom:16px;">🚧</p><p>قيد الانشاء</p></div>'; },
mount() {},
destroy() {}
};
import { state } from '../core/state.js';
import { api, cachedApi } from '../core/api.js';
import { haptics } from '../core/haptics.js';
import { audio } from '../core/audio.js';
import { coinBurst } from '../core/particles.js';
import { tickNumber, pop } from '../core/animate.js';
export default {
render() {
return `
<!-- Daily Streak -->
<div class="streak" id="streak">
<span class="streak-fire">🔥</span>
<div class="streak-text">
<p class="streak-day" id="streak-day">اليوم 0</p>
<p class="streak-reward" id="streak-reward">+50 عملة</p>
</div>
<button class="streak-btn" id="streak-btn">اجمع</button>
</div>
<!-- Games Grid -->
<section>
<div class="sec-hdr">
<h2 class="sec-title">العب</h2>
</div>
<div class="game-grid" id="games-grid">
<div class="skeleton" style="height:120px;border-radius:var(--r-xl)"></div>
<div class="skeleton" style="height:120px;border-radius:var(--r-xl)"></div>
<div class="skeleton" style="height:120px;border-radius:var(--r-xl)"></div>
<div class="skeleton" style="height:120px;border-radius:var(--r-xl)"></div>
</div>
</section>
<!-- Ratings -->
<section id="ratings-sec" style="display:none">
<div class="sec-hdr">
<h2 class="sec-title">تصنيفاتي</h2>
<a href="/profile" class="sec-link">الكل</a>
</div>
<div class="stats-row" id="ratings-grid"></div>
</section>
<!-- Recent Games -->
<section>
<div class="sec-hdr">
<h2 class="sec-title">اخر المباريات</h2>
</div>
<div class="card" id="recent-games">
<div class="empty">لم تلعب اي مباراة بعد</div>
</div>
</section>
`;
},
async mount(el) {
const profile = state.get('user');
// Streak
if (profile) {
const streak = profile.daily_streak || 0;
el.querySelector('#streak-day').textContent = `اليوم ${streak}`;
const cfgData = await cachedApi('/api/config.php?category=economy', 120000);
const base = cfgData?.config?.daily_reward_base || 50;
const bonus = cfgData?.config?.daily_reward_streak_bonus || 10;
el.querySelector('#streak-reward').textContent = `+${base + streak * bonus} عملة`;
const today = new Date().toISOString().slice(0, 10);
const claimed = profile.last_daily_reward === today;
if (claimed) {
const btn = el.querySelector('#streak-btn');
btn.textContent = 'تم ✓';
btn.disabled = true;
}
}
// Streak claim
el.querySelector('#streak-btn').addEventListener('click', async (e) => {
haptics.tap();
const btn = e.target;
btn.disabled = true;
const res = await api('/api/daily-reward', { method: 'POST' });
if (res?.ok) {
audio.coin();
haptics.success();
const rect = btn.getBoundingClientRect();
coinBurst(rect.left + rect.width / 2, rect.top);
btn.textContent = 'تم ✓';
el.querySelector('#streak-day').textContent = `اليوم ${res.streak}`;
const coinsEl = document.getElementById('hdr-coins');
if (coinsEl) tickNumber(coinsEl, parseInt(coinsEl.textContent.replace(/,/g, '')) || 0, res.coins, 800);
} else if (res?.error === 'already_claimed') {
btn.textContent = 'تم ✓';
} else {
btn.disabled = false;
audio.error();
haptics.error();
}
});
// Games grid
const gamesData = await cachedApi('/api/games.php', 60000);
if (gamesData?.games) {
const routes = { chess: '/play', ludo: '/ludo', domino: '/domino', backgammon: '/backgammon' };
const heroes = { chess: 'game-card-hero--chess', ludo: 'game-card-hero--ludo', domino: 'game-card-hero--domino', backgammon: 'game-card-hero--backgammon' };
const icons = { chess: 'icon-play', ludo: 'icon-ludo', domino: 'icon-domino', backgammon: 'icon-backgammon' };
const grid = el.querySelector('#games-grid');
grid.innerHTML = gamesData.games.filter(g => routes[g.game_key]).map(g => {
const route = routes[g.game_key];
const enabled = g.is_enabled;
return `<a href="${enabled ? route : '#'}" class="game-card" ${!enabled ? 'style="opacity:0.5;pointer-events:none"' : ''}>
<div class="game-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="game-card-soon">قريبا</span>' : ''}
</div>
<div class="game-card-body">
<span class="game-card-name">${g.name_ar}</span>
${enabled ? '<span class="game-card-live"><span class="game-card-live-dot"></span></span>' : ''}
</div>
</a>`;
}).join('');
grid.querySelectorAll('.game-card').forEach((card, i) => {
card.style.animationDelay = `${i * 80}ms`;
card.classList.add('anim-pop');
});
}
// Ratings
try {
const ratingsData = await api('/api/ratings.php?action=player');
if (ratingsData?.ratings?.length) {
el.querySelector('#ratings-sec').style.display = '';
const names = { chess: 'شطرنج', ludo: 'لودو', backgammon: 'طاولة', domino: 'دومينو' };
const modes = { bullet: 'بوليت', blitz: 'بليتز', rapid: 'رابيد', classical: 'كلاسيك', default: '' };
el.querySelector('#ratings-grid').innerHTML = ratingsData.ratings.map(r => {
const label = (modes[r.mode] || '') ? `${names[r.game_key] || r.game_key} ${modes[r.mode]}` : (names[r.game_key] || r.game_key);
return `<div class="stat"><div class="stat-val">${r.rating}</div><div class="stat-lbl">${label}</div></div>`;
}).join('');
}
} catch {}
// Recent games
const [gamesRes, botsRes] = await Promise.all([
api('/api/game?action=recent'),
cachedApi('/api/bots.php', 60000)
]);
const botMap = {};
if (botsRes?.bots) botsRes.bots.forEach(b => { botMap[b.id] = b.name_ar?.split(' ')[0] || b.name; });
if (gamesRes?.games?.length) {
el.querySelector('#recent-games').innerHTML = `<div class="activity-list">${gamesRes.games.slice(0, 5).map(g => {
const cls = g.result === 'win' ? 'activity-icon--win' : g.result === 'loss' ? 'activity-icon--loss' : 'activity-icon--draw';
const text = g.result === 'win' ? 'فوز' : g.result === 'loss' ? 'خسارة' : 'تعادل';
const color = g.result === 'win' ? 'color:var(--win)' : g.result === 'loss' ? 'color:var(--loss)' : '';
const icon = g.result === 'win' ? 'check' : g.result === 'loss' ? 'x' : 'clock';
const name = botMap[g.bot_id] || g.bot_id || '?';
return `<div class="activity-item">
<div class="activity-icon ${cls}"><svg class="icon-sm"><use href="/public/icons/sprite.svg#icon-${icon}"></use></svg></div>
<div class="activity-detail"><p class="activity-name">ضد ${name}</p></div>
<span class="activity-result" style="${color}">${text}</span>
</div>`;
}).join('')}</div>`;
}
},
destroy() {}
};
export default {
render() { return '<div class="empty" style="padding:80px 20px;text-align:center;"><p style="font-size:48px;margin-bottom:16px;">🚧</p><p>قيد الانشاء</p></div>'; },
mount() {},
destroy() {}
};
import { state } from '../core/state.js';
import { navigate } from '../core/router.js';
import { haptics } from '../core/haptics.js';
import { audio } from '../core/audio.js';
import { shake } from '../core/animate.js';
export default {
render() {
return `
<div class="login-page">
<div class="login-card">
<h1 class="login-brand">EL3AB</h1>
<p class="login-subtitle">سجل دخولك وابدأ اللعب</p>
<form id="login-form">
<div class="input-group">
<label class="input-label">البريد الالكتروني</label>
<input type="email" class="input" id="login-email" placeholder="email@example.com" required dir="ltr" autocomplete="email">
</div>
<div class="input-group">
<label class="input-label">كلمة المرور</label>
<input type="password" class="input" id="login-pass" 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="login-footer">ما عندك حساب؟ <a href="/register">انشئ حساب</a></p>
<div id="login-error" class="login-error" style="display:none"></div>
</div>
</div>
`;
},
mount(el) {
const form = el.querySelector('#login-form');
const btn = el.querySelector('#login-btn');
const errEl = el.querySelector('#login-error');
form.addEventListener('submit', async (e) => {
e.preventDefault();
haptics.tap();
errEl.style.display = 'none';
btn.disabled = true;
btn.textContent = 'جاري الدخول...';
const email = el.querySelector('#login-email').value;
const password = el.querySelector('#login-pass').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';
shake(errEl);
haptics.error();
audio.error();
} else {
state.setAuth(data.access_token, data.user);
audio.coin();
haptics.success();
navigate('/', {}, true);
window.location.reload();
}
} catch {
errEl.textContent = 'حدث خطأ في الاتصال';
errEl.style.display = 'block';
shake(errEl);
haptics.error();
}
btn.disabled = false;
btn.textContent = 'تسجيل الدخول';
});
},
destroy() {}
};
export default {
render() { return '<div class="empty" style="padding:80px 20px;text-align:center;"><p style="font-size:48px;margin-bottom:16px;">🚧</p><p>قيد الانشاء</p></div>'; },
mount() {},
destroy() {}
};
export default {
render() { return '<div class="empty" style="padding:80px 20px;text-align:center;"><p style="font-size:48px;margin-bottom:16px;">🚧</p><p>قيد الانشاء</p></div>'; },
mount() {},
destroy() {}
};
export default {
render() { return '<div class="empty" style="padding:80px 20px;text-align:center;"><p style="font-size:48px;margin-bottom:16px;">🚧</p><p>قيد الانشاء</p></div>'; },
mount() {},
destroy() {}
};
export default {
render() { return '<div class="empty" style="padding:80px 20px;text-align:center;"><p style="font-size:48px;margin-bottom:16px;">🚧</p><p>قيد الانشاء</p></div>'; },
mount() {},
destroy() {}
};
/* === KEYFRAME LIBRARY === */
@keyframes pulse-dot {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(0.8); }
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
20% { transform: translateX(-3px); }
40% { transform: translateX(3px); }
60% { transform: translateX(-2px); }
80% { transform: translateX(2px); }
}
@keyframes pop {
0% { transform: scale(0.8); opacity: 0; }
60% { transform: scale(1.1); }
100% { transform: scale(1); opacity: 1; }
}
@keyframes slide-up {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
@keyframes slide-down {
from { transform: translateY(-20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes scale-in {
from { transform: scale(0.9); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
@keyframes glow-pulse {
0%, 100% { box-shadow: 0 0 0 0 var(--gold-glow); }
50% { box-shadow: 0 0 20px 4px var(--gold-glow); }
}
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
@keyframes count-up {
from { transform: translateY(100%); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
@keyframes radar {
0% { transform: scale(0.3); opacity: 1; }
100% { transform: scale(2); opacity: 0; }
}
@keyframes bounce-in {
0% { transform: scale(0); }
50% { transform: scale(1.15); }
70% { transform: scale(0.95); }
100% { transform: scale(1); }
}
@keyframes confetti-fall {
0% { transform: translateY(0) rotate(0deg); opacity: 1; }
100% { transform: translateY(100vh) rotate(720deg); opacity: 0; }
}
@keyframes coin-fly {
0% { transform: translate(0, 0) scale(1); opacity: 1; }
100% { transform: translate(var(--fly-x), var(--fly-y)) scale(0.3); opacity: 0; }
}
@keyframes fire-flicker {
0%, 100% { transform: scale(1) rotate(-2deg); }
50% { transform: scale(1.1) rotate(2deg); }
}
@keyframes skeleton-wave {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
/* Utility animation classes */
.anim-pop { animation: pop var(--duration-normal) var(--ease-bounce) both; }
.anim-shake { animation: shake 0.4s ease both; }
.anim-float { animation: float 3s ease-in-out infinite; }
.anim-pulse { animation: glow-pulse 2s ease-in-out infinite; }
.anim-bounce-in { animation: bounce-in 0.5s var(--ease-bounce) both; }
.anim-fade-in { animation: fade-in var(--duration-normal) var(--ease) both; }
/* Skeleton loading */
.skeleton {
background: linear-gradient(90deg, var(--bg-2) 25%, var(--bg-3) 50%, var(--bg-2) 75%);
background-size: 200% 100%;
animation: skeleton-wave 1.5s infinite linear;
border-radius: var(--r-md);
}
/* === 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 var(--duration-fast), box-shadow var(--duration-normal);
position: relative;
overflow: hidden;
white-space: nowrap;
}
.btn:active { transform: scale(0.95); }
.btn:disabled { opacity: 0.4; pointer-events: none; }
.btn-primary {
background: var(--gold);
color: var(--text-inverse);
box-shadow: var(--shadow-gold);
}
.btn-secondary {
background: var(--cyan);
color: var(--text-inverse);
box-shadow: var(--shadow-cyan);
}
.btn-ghost {
background: transparent;
color: var(--text-2);
border: 1px solid var(--border-light);
}
.btn-ghost:active { background: rgba(255,255,255,0.04); }
.btn-danger { background: var(--loss-dim); color: var(--loss); border: 1px solid rgba(248,113,113,0.2); }
.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 14px; font-size: 12px; border-radius: var(--r-sm); }
/* === CARDS === */
.card {
background: var(--bg-1);
border: 1px solid var(--border);
border-radius: var(--r-lg);
overflow: hidden;
transition: transform var(--duration-fast), box-shadow var(--duration-fast);
}
.card:active { transform: scale(0.98); }
.card-pad { padding: var(--sp-4); }
/* === GAME CARDS (home grid) === */
.game-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--sp-3);
}
.game-card {
position: relative;
border-radius: var(--r-xl);
overflow: hidden;
background: var(--bg-1);
border: 1px solid var(--border);
cursor: pointer;
transition: transform var(--duration-fast) var(--ease-bounce), box-shadow var(--duration-fast);
}
.game-card:active {
transform: scale(0.96);
}
.game-card-hero {
height: 80px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.game-card-hero--chess { background: linear-gradient(135deg, #1a2a4a, #2a5a8a); }
.game-card-hero--ludo { background: linear-gradient(135deg, #2a1a4a, #6a2a9a); }
.game-card-hero--domino { background: linear-gradient(135deg, #1a3a2a, #2a7a5a); }
.game-card-hero--backgammon { background: linear-gradient(135deg, #3a2a1a, #7a5a2a); }
.game-card-hero .icon-xl { color: rgba(255,255,255,0.9); }
.game-card-body {
padding: var(--sp-3);
display: flex;
align-items: center;
justify-content: space-between;
}
.game-card-name {
font-size: 14px;
font-weight: 700;
}
.game-card-live {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: var(--text-3);
}
.game-card-live-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--online);
animation: pulse-dot 2s infinite;
}
.game-card-soon {
position: absolute;
top: var(--sp-2);
left: var(--sp-2);
padding: 2px 8px;
background: rgba(0,0,0,0.6);
backdrop-filter: blur(4px);
border-radius: var(--r-full);
font-size: 10px;
font-weight: 600;
color: var(--text-2);
}
/* === INPUTS === */
.input-group { margin-bottom: var(--sp-4); }
.input-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 var(--duration-fast), box-shadow var(--duration-fast);
}
.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 === */
.chip-row {
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 var(--duration-fast);
}
.chip:active { transform: scale(0.95); }
.chip.active { background: var(--cyan); border-color: var(--cyan); color: var(--text-inverse); }
/* === BADGES === */
.badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 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-win { background: var(--win-dim); color: var(--win); }
.badge-loss { background: var(--loss-dim); color: var(--loss); }
/* === STREAK BANNER === */
.streak {
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-fire {
font-size: 24px;
animation: fire-flicker 1.5s ease-in-out infinite;
}
.streak-text { flex: 1; }
.streak-day { font-size: 14px; font-weight: 600; }
.streak-reward { font-size: 12px; color: var(--text-3); }
.streak-btn {
min-height: 32px;
padding: 6px 14px;
background: var(--gold);
color: var(--text-inverse);
font-size: 12px;
font-weight: 700;
border-radius: var(--r-sm);
transition: transform var(--duration-fast);
box-shadow: var(--shadow-gold);
}
.streak-btn:active { transform: scale(0.92); }
.streak-btn:disabled { opacity: 0.4; background: var(--bg-3); color: var(--text-3); box-shadow: none; }
/* === STATS === */
.stats-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(70px, 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: 18px;
font-weight: 700;
font-family: var(--font-mono);
font-variant-numeric: tabular-nums;
}
.stat-lbl {
font-size: 10px;
color: var(--text-3);
margin-top: 2px;
}
/* === SECTION HEADERS === */
.sec-hdr {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--sp-3);
}
.sec-title {
font-size: 16px;
font-weight: 700;
}
.sec-link {
font-size: 12px;
color: var(--cyan);
font-weight: 500;
}
/* === ACTIVITY LIST === */
.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);
}
.activity-item:last-child { border-bottom: none; }
.activity-icon {
width: 32px;
height: 32px;
border-radius: var(--r-sm);
display: flex;
align-items: center;
justify-content: center;
}
.activity-icon--win { background: var(--win-dim); color: var(--win); }
.activity-icon--loss { background: var(--loss-dim); color: var(--loss); }
.activity-icon--draw { background: rgba(148,163,184,0.12); color: var(--draw); }
.activity-detail { flex: 1; }
.activity-name { font-size: 13px; font-weight: 600; }
.activity-meta { font-size: 11px; color: var(--text-3); }
.activity-result { font-size: 12px; font-weight: 700; }
/* === TOAST === */
.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: slide-down var(--duration-normal) var(--ease-bounce);
pointer-events: auto;
}
.toast--success { border-color: var(--win); }
.toast--error { border-color: var(--loss); }
.toast--info { border-color: var(--cyan); }
/* === EMPTY STATE === */
.empty {
padding: var(--sp-10) var(--sp-5);
text-align: center;
color: var(--text-3);
font-size: 14px;
}
/* === LOGIN === */
.login-page {
display: flex;
align-items: center;
justify-content: center;
min-height: 100dvh;
padding: var(--sp-5);
}
.login-card { width: 100%; max-width: 380px; }
.login-brand {
font-weight: 800;
font-size: 40px;
color: var(--gold);
text-align: center;
letter-spacing: -1px;
margin-bottom: var(--sp-2);
text-shadow: 0 0 40px var(--gold-glow);
}
.login-subtitle {
text-align: center;
font-size: 14px;
color: var(--text-3);
margin-bottom: var(--sp-6);
}
.login-error {
padding: var(--sp-3);
background: var(--loss-dim);
border: 1px solid rgba(248,113,113,0.2);
border-radius: var(--r-md);
color: var(--loss);
font-size: 13px;
text-align: center;
margin-top: var(--sp-4);
animation: shake 0.4s ease;
}
.login-footer {
text-align: center;
font-size: 13px;
color: var(--text-3);
margin-top: var(--sp-5);
}
.login-footer a { color: var(--cyan); font-weight: 500; }
*, *::before, *::after {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
html {
direction: rtl;
font-family: var(--font);
font-size: 16px;
line-height: 1.5;
color: var(--text-1);
background: var(--bg-0);
-webkit-font-smoothing: antialiased;
height: 100%;
}
body {
min-height: 100dvh;
overflow-x: hidden;
overscroll-behavior: none;
}
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: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: var(--r-full); }
: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;
}
}
/* === APP SHELL === */
#app {
display: flex;
flex-direction: column;
min-height: 100dvh;
position: relative;
}
/* Header */
.hdr {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
height: var(--header-h);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--sp-4);
background: var(--bg-glass);
backdrop-filter: blur(20px) saturate(1.4);
border-bottom: 1px solid var(--border);
padding-top: env(safe-area-inset-top);
transition: transform var(--duration-normal) var(--ease);
}
.hdr--hidden { transform: translateY(-100%); }
.hdr-brand {
font-weight: 800;
font-size: 20px;
color: var(--gold);
letter-spacing: -0.5px;
}
.hdr-center {
display: flex;
align-items: center;
gap: var(--sp-3);
}
.hdr-level {
display: flex;
align-items: center;
gap: var(--sp-2);
padding: 4px 10px;
background: var(--bg-2);
border-radius: var(--r-full);
font-size: 12px;
font-weight: 700;
color: var(--gold);
}
.hdr-currency {
display: flex;
align-items: center;
gap: var(--sp-1);
font-size: 13px;
font-weight: 600;
font-family: var(--font-mono);
font-variant-numeric: tabular-nums;
}
.hdr-currency-icon {
width: 16px;
height: 16px;
}
.hdr-actions {
display: flex;
align-items: center;
gap: var(--sp-1);
}
.hdr-btn {
position: relative;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--r-sm);
color: var(--text-3);
transition: background var(--duration-fast), color var(--duration-fast);
}
.hdr-btn:active { background: var(--bg-2); color: var(--text-1); }
.hdr-btn-dot {
position: absolute;
top: 8px;
left: 8px;
width: 8px;
height: 8px;
background: var(--loss);
border-radius: 50%;
border: 2px solid var(--bg-0);
animation: pulse-dot 2s infinite;
}
/* Main content area */
.main {
flex: 1;
padding-top: var(--header-h);
padding-bottom: calc(var(--nav-h) + env(safe-area-inset-bottom) + var(--sp-4));
width: 100%;
max-width: var(--content-max);
margin: 0 auto;
}
.main-inner {
padding: var(--sp-4);
display: flex;
flex-direction: column;
gap: var(--sp-5);
}
/* Bottom Navigation */
.nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 100;
height: calc(var(--nav-h) + env(safe-area-inset-bottom));
display: grid;
grid-template-columns: repeat(5, 1fr);
align-items: center;
padding-bottom: env(safe-area-inset-bottom);
background: var(--bg-glass);
backdrop-filter: blur(20px) saturate(1.4);
border-top: 1px solid var(--border);
transition: transform var(--duration-normal) var(--ease);
}
.nav--hidden { transform: translateY(100%); }
.nav-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
height: var(--nav-h);
color: var(--text-3);
font-size: 10px;
font-weight: 500;
transition: color var(--duration-fast);
position: relative;
}
.nav-item.active { color: var(--cyan); }
.nav-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-item .icon {
width: 22px;
height: 22px;
transition: transform var(--duration-fast) var(--ease-bounce);
}
.nav-item:active .icon { transform: scale(0.85); }
.nav-badge {
position: absolute;
top: 6px;
left: 50%;
margin-left: 6px;
min-width: 16px;
height: 16px;
padding: 0 4px;
background: var(--loss);
border-radius: var(--r-full);
font-size: 9px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
color: white;
}
/* Desktop sidebar */
@media (min-width: 768px) {
.nav { display: none; }
.sidebar {
display: flex;
flex-direction: column;
position: fixed;
top: 0;
right: 0;
bottom: 0;
width: var(--sidebar-w);
background: var(--bg-surface);
border-left: 1px solid var(--border);
padding: var(--sp-5) var(--sp-3);
overflow-y: auto;
z-index: 90;
}
.main {
margin-right: var(--sidebar-w);
max-width: 600px;
}
.hdr {
right: var(--sidebar-w);
}
}
/* Sidebar (hidden on mobile) */
.sidebar {
display: none;
}
.sidebar-brand {
font-weight: 800;
font-size: 22px;
color: var(--gold);
padding: 0 var(--sp-3);
margin-bottom: var(--sp-6);
}
.sidebar-section {
margin-bottom: var(--sp-4);
}
.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 var(--duration-fast), color var(--duration-fast);
}
.sidebar-item:active { background: rgba(255,255,255,0.04); color: var(--text-1); }
.sidebar-item.active { background: var(--cyan-dim); color: var(--cyan); }
/* Screen transitions */
.screen {
animation: screen-enter var(--duration-normal) var(--ease) both;
}
.screen-exit {
animation: screen-exit var(--duration-fast) var(--ease) both;
}
@keyframes screen-enter {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes screen-exit {
from { opacity: 1; transform: translateY(0); }
to { opacity: 0; transform: translateY(-8px); }
}
/* Toast container */
.toast-layer {
position: fixed;
top: calc(var(--header-h) + var(--sp-3));
left: 50%;
transform: translateX(-50%);
z-index: 200;
display: flex;
flex-direction: column;
gap: var(--sp-2);
pointer-events: none;
width: calc(100% - var(--sp-8));
max-width: 360px;
}
/* Overlay / modals */
.overlay {
position: fixed;
inset: 0;
z-index: 150;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(4px);
opacity: 0;
transition: opacity var(--duration-normal);
pointer-events: none;
}
.overlay.active {
opacity: 1;
pointer-events: auto;
}
/* Particle canvas */
#particles {
position: fixed;
inset: 0;
z-index: 300;
pointer-events: none;
}
/* 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 immersive mode */
.immersive .hdr,
.immersive .nav { transform: translateY(-100%); }
.immersive .nav { transform: translateY(100%); }
.immersive .main { padding-top: 0; padding-bottom: 0; max-width: 100%; }
:root {
/* Backgrounds */
--bg-0: #030A12;
--bg-1: #081420;
--bg-2: #0F1F30;
--bg-3: #182B42;
--bg-surface: #0C1926;
--bg-elevated: #142438;
--bg-glass: rgba(8, 20, 32, 0.85);
/* Brand */
--gold: #F5B731;
--gold-dim: rgba(245, 183, 49, 0.12);
--gold-glow: rgba(245, 183, 49, 0.25);
--cyan: #00D4FF;
--cyan-dim: rgba(0, 212, 255, 0.10);
--cyan-glow: rgba(0, 212, 255, 0.2);
--purple: #8B5CF6;
--purple-dim: rgba(139, 92, 246, 0.12);
/* Status */
--win: #34D399;
--win-dim: rgba(52, 211, 153, 0.12);
--loss: #F87171;
--loss-dim: rgba(248, 113, 113, 0.12);
--draw: #94A3B8;
--online: #22C55E;
--warning: #FBBF24;
/* Text */
--text-1: #F8FAFC;
--text-2: #CBD5E1;
--text-3: #8B9DB7;
--text-inverse: #0F172A;
/* Border */
--border: rgba(255, 255, 255, 0.07);
--border-light: rgba(255, 255, 255, 0.12);
--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-2xl: 28px;
--r-full: 9999px;
/* Spacing */
--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;
/* Shadows */
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 12px 48px rgba(0, 0, 0, 0.5);
--shadow-gold: 0 4px 20px rgba(245, 183, 49, 0.2);
--shadow-cyan: 0 4px 20px rgba(0, 212, 255, 0.15);
/* Layout */
--header-h: 56px;
--nav-h: 64px;
--sidebar-w: 220px;
--content-max: 480px;
--touch: 44px;
/* Type */
--font: 'IBM Plex Sans Arabic', system-ui, sans-serif;
--font-mono: 'SF Mono', 'Fira Code', monospace;
/* Motion */
--ease: cubic-bezier(0.2, 0, 0, 1);
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
--duration-fast: 150ms;
--duration-normal: 250ms;
--duration-slow: 400ms;
}
<?php
/**
* Feature flags with file-based caching (60s TTL).
* Include this file to use is_feature_enabled($flag).
*/
require_once __DIR__ . '/../config/database.php';
function _load_feature_flags(): array {
$cacheFile = '/tmp/el3ab_feature_flags.json';
$cacheTTL = 60; // seconds
if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < $cacheTTL) {
$cached = json_decode(file_get_contents($cacheFile), true);
if (is_array($cached)) {
return $cached;
}
}
// Fetch all flags from DB
$res = supabase_rest('GET', 'feature_flags?select=id,is_enabled', [], SUPABASE_SERVICE_KEY);
$flags = [];
if (!empty($res['data']) && is_array($res['data'])) {
foreach ($res['data'] as $row) {
$flags[$row['id']] = (bool)$row['is_enabled'];
}
}
// Write cache
file_put_contents($cacheFile, json_encode($flags));
return $flags;
}
function is_feature_enabled(string $flag): bool {
static $flags = null;
if ($flags === null) {
$flags = _load_feature_flags();
}
return $flags[$flag] ?? false;
}
</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 lvl = document.getElementById('hdr-level-val');
if (lvl) lvl.textContent = 'Lv ' + (p.level || 1);
}
}
});
</script>
</body>
</html>
</div>
</main>
<?php require __DIR__ . '/../templates/nav-bottom.php'; ?>
</div>
<div class="toast-container" id="toast-container"></div>
<script src="/public/js/app.js"></script>
<script>
if (window.__themeAssets) {
document.addEventListener('DOMContentLoaded', () => {
const a = window.__themeAssets;
if (a['logo-text']) {
document.querySelectorAll('.header-logo, .nav-desktop-logo').forEach(el => { el.textContent = a['logo-text']; });
}
if (a['logo-image']) {
document.querySelectorAll('.header-logo').forEach(el => {
el.innerHTML = '<img src="' + a['logo-image'] + '" alt="Logo" style="height:28px;">';
});
}
if (a['favicon']) {
let link = document.querySelector('link[rel="icon"]') || document.createElement('link');
link.rel = 'icon'; link.href = a['favicon'];
document.head.appendChild(link);
}
if (a['sprite-svg']) {
document.querySelectorAll('use[href^="/public/icons/sprite.svg"]').forEach(el => {
el.setAttribute('href', el.getAttribute('href').replace('/public/icons/sprite.svg', a['sprite-svg']));
});
}
// Individual icon overrides
Object.keys(a).forEach(k => {
if (k.startsWith('icon-')) {
const iconName = k;
document.querySelectorAll('use[href$="#' + iconName + '"]').forEach(useEl => {
const svg = useEl.closest('svg');
if (svg) {
const img = document.createElement('img');
img.src = a[k];
img.style.cssText = 'width:100%;height:100%;object-fit:contain;';
img.alt = iconName;
svg.replaceWith(img);
}
});
}
if (k.startsWith('piece-')) {
const piece = k.replace('piece-', '');
document.documentElement.style.setProperty('--piece-' + piece, 'url(' + a[k] + ')');
}
});
});
}
</script>
<?php if (isset($extraJs)): ?>
<?php if (is_array($extraJs)): ?>
<?php foreach ($extraJs as $js): ?>
<script src="<?= $js ?>"></script>
<?php endforeach; ?>
<?php else: ?>
<script src="<?= $extraJs ?>"></script>
<?php endif; ?>
<?php endif; ?>
<?php
// Prefetch adjacent pages
$prefetchMap = [
'' => ['/games', '/profile'],
'home' => ['/games', '/profile'],
'games' => ['/play', '/ludo'],
'play' => ['/games', '/game'],
'ludo' => ['/games', '/ludo-game'],
];
$currentRoute = $route ?? '';
if (isset($prefetchMap[$currentRoute])):
foreach ($prefetchMap[$currentRoute] as $pf): ?>
<link rel="prefetch" href="<?= $pf ?>">
<?php endforeach; endif; ?>
</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-level" title="مستوى الحساب">
<span id="hdr-level-val">Lv 1</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">
<?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="#050D17">
<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.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; ?>
<?php require_once __DIR__ . '/theme-loader.php'; ?>
</head>
<body>
<div class="app">
<?php require __DIR__ . '/../templates/nav-desktop.php'; ?>
<header class="header">
<div class="header-inner">
<a href="/" class="header-logo">EL3AB</a>
<div class="header-stats">
<div class="header-stat">
<svg class="icon-sm icon-fill" style="color:var(--gold)"><use href="/public/icons/sprite.svg#icon-coin"></use></svg>
<span id="header-coins">0</span>
</div>
<div class="header-stat">
<svg class="icon-sm icon-fill" style="color:var(--purple)"><use href="/public/icons/sprite.svg#icon-gem"></use></svg>
<span id="header-gems">0</span>
</div>
<a href="/notifications" class="header-bell">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-bell"></use></svg>
<span class="header-badge" id="header-notif-badge" style="display:none">0</span>
</a>
</div>
</div>
</header>
<main class="main">
<div class="main-inner">
<?php
/**
* Loads theme overrides from DB and outputs a <style> block with CSS variable overrides.
* Caches in a local file for 60 seconds to avoid hitting Supabase on every request.
*/
require_once __DIR__ . '/../config/database.php';
function get_theme_overrides(): array {
$cacheFile = __DIR__ . '/../storage/theme-cache.json';
$cacheTTL = 60;
if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < $cacheTTL) {
$cached = json_decode(file_get_contents($cacheFile), true);
if (is_array($cached)) return $cached;
}
$res = supabase_rest('GET', 'theme_settings?select=key,value,category', [], SUPABASE_SERVICE_KEY);
$settings = ($res['status'] === 200 && is_array($res['data']) && !isset($res['data']['code'])) ? $res['data'] : [];
$cacheDir = dirname($cacheFile);
if (!is_dir($cacheDir)) mkdir($cacheDir, 0755, true);
file_put_contents($cacheFile, json_encode($settings));
return $settings;
}
function render_theme_style(): void {
$settings = get_theme_overrides();
if (empty($settings)) return;
$cssVars = [];
$assetOverrides = [];
foreach ($settings as $s) {
$key = $s['key'];
$value = $s['value'];
$category = $s['category'];
if ($category === 'assets' || $category === 'icons' || $category === 'pieces') {
$assetOverrides[$key] = $value;
} else {
$cssVars[] = ' --' . htmlspecialchars($key) . ': ' . htmlspecialchars($value) . ';';
}
}
if (!empty($cssVars)) {
echo "<style id=\"theme-overrides\">\n:root {\n" . implode("\n", $cssVars) . "\n}\n</style>\n";
}
if (!empty($assetOverrides)) {
echo "<script>window.__themeAssets = " . json_encode($assetOverrides) . ";</script>\n";
}
}
render_theme_style();
<?php <?php
$route = $_GET['route'] ?? ''; $route = $_GET['route'] ?? '';
$route = trim($route, '/'); $route = trim($route, '/');
if ($route === '' || $route === 'home') { // API routes — pass through to PHP endpoints
require 'pages/home-v2.php'; if (str_starts_with($route, 'api/')) {
} elseif ($route === 'login') {
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-v2.php';
} elseif ($route === 'game') {
require 'pages/game.php';
} elseif ($route === 'game-live') {
require 'pages/game-live.php';
} elseif ($route === 'matchmaking') {
require 'pages/matchmaking.php';
} elseif ($route === 'bots') {
require 'pages/bots.php';
} elseif ($route === 'profile') {
require 'pages/profile-v2.php';
} elseif ($route === 'leaderboard') {
require 'pages/leaderboard-v2.php';
} elseif ($route === 'friends') {
require 'pages/friends.php';
} elseif ($route === 'tournaments') {
require 'pages/tournaments.php';
} elseif ($route === 'tournament') {
require 'pages/tournament.php';
} elseif ($route === 'shop') {
require 'pages/shop.php';
} elseif ($route === 'achievements') {
require 'pages/achievements.php';
} elseif ($route === 'analysis') {
require 'pages/analysis.php';
} elseif ($route === 'puzzles') {
require 'pages/puzzles.php';
} elseif ($route === 'notifications') {
require 'pages/notifications.php';
} elseif ($route === 'settings') {
require 'pages/settings.php';
} elseif ($route === 'orgs') {
require 'pages/orgs.php';
} elseif ($route === 'org') {
require 'pages/org.php';
} elseif ($route === 'ludo') {
require 'pages/ludo.php';
} elseif ($route === 'ludo-game') {
require 'pages/ludo-game.php';
} elseif ($route === 'ludo-live') {
require 'pages/ludo-live.php';
} elseif ($route === 'ludo-matchmaking') {
require 'pages/ludo-matchmaking.php';
} elseif ($route === 'domino') {
require 'pages/domino.php';
} elseif ($route === 'domino-game') {
require 'pages/domino-game.php';
} elseif ($route === 'domino-live') {
require 'pages/domino-live.php';
} elseif ($route === 'domino-matchmaking') {
require 'pages/domino-matchmaking.php';
} elseif ($route === 'backgammon') {
require 'pages/backgammon.php';
} elseif ($route === 'backgammon-game') {
require 'pages/backgammon-game.php';
} elseif ($route === 'backgammon-live') {
require 'pages/backgammon-live.php';
} elseif ($route === 'backgammon-matchmaking') {
require 'pages/backgammon-matchmaking.php';
} elseif ($route === 'admin/theme') {
require 'pages/admin-theme.php';
} elseif (str_starts_with($route, 'api/')) {
$apiFile = str_replace('api/', '', $route); $apiFile = str_replace('api/', '', $route);
$apiPath = __DIR__ . '/api/' . basename($apiFile) . '.php'; $apiPath = __DIR__ . '/api/' . basename($apiFile) . '.php';
if (file_exists($apiPath)) { if (file_exists($apiPath)) {
...@@ -82,7 +12,85 @@ if ($route === '' || $route === 'home') { ...@@ -82,7 +12,85 @@ if ($route === '' || $route === 'home') {
http_response_code(404); http_response_code(404);
echo json_encode(['error' => 'endpoint not found']); echo json_encode(['error' => 'endpoint not found']);
} }
} else { exit;
http_response_code(404);
require 'pages/404.php';
} }
// Everything else: serve the SPA shell
?>
<!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>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;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/app/styles/tokens.css">
<link rel="stylesheet" href="/app/styles/reset.css">
<link rel="stylesheet" href="/app/styles/animations.css">
<link rel="stylesheet" href="/app/styles/shell.css">
<link rel="stylesheet" href="/app/styles/components.css">
</head>
<body>
<div id="app">
<!-- Header -->
<header class="hdr" id="hdr">
<a href="/" class="hdr-brand">EL3AB</a>
<div class="hdr-center">
<span class="hdr-level" id="hdr-level">Lv 1</span>
<div class="hdr-currency">
<svg class="hdr-currency-icon icon-sm icon-fill" style="color:var(--gold)"><use href="/public/icons/sprite.svg#icon-coin"></use></svg>
<span id="hdr-coins">0</span>
</div>
<div class="hdr-currency">
<svg class="hdr-currency-icon icon-sm icon-fill" style="color:var(--purple)"><use href="/public/icons/sprite.svg#icon-gem"></use></svg>
<span id="hdr-gems">0</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>
</a>
</div>
</header>
<!-- Main Content (SPA renders here) -->
<div class="main" id="main"></div>
<!-- Bottom Navigation -->
<nav class="nav" id="nav">
<a href="/" class="nav-item active">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-home"></use></svg>
<span>الرئيسية</span>
</a>
<a href="/games" class="nav-item">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-games"></use></svg>
<span>العاب</span>
</a>
<a href="/leaderboard" class="nav-item">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-leaderboard"></use></svg>
<span>ترتيب</span>
</a>
<a href="/friends" class="nav-item">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-friends"></use></svg>
<span>اجتماعي</span>
</a>
<a href="/profile" class="nav-item">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-profile"></use></svg>
<span>حسابي</span>
</a>
</nav>
<!-- Toast Layer -->
<div class="toast-layer" id="toasts"></div>
<!-- Particle Canvas -->
<canvas id="particles"></canvas>
</div>
<script type="module" src="/app/main.js"></script>
</body>
</html>
<?php $pageTitle = 'EL3AB - 404'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="text-center" style="padding:64px 20px;">
<svg class="icon-xl" style="color:var(--text-3);margin:0 auto 16px;width:48px;height:48px;">
<use href="/public/icons/sprite.svg#icon-search"></use>
</svg>
<h1 style="font-size:24px;font-weight:700;margin-bottom:8px;">404</h1>
<p class="text-muted" style="margin-bottom:24px;">الصفحة غير موجودة</p>
<a href="/" class="btn btn-cyan">الرئيسية</a>
</div>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB - الانجازات'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="space-y-6">
<div class="text-center">
<h2 style="font-size:22px;font-weight:700;">الانجازات</h2>
<p class="text-muted text-sm" id="ach-progress">0 / 0 مكتمل</p>
</div>
<!-- Progress Bar -->
<div class="stat-bar" style="height:8px;">
<div class="stat-bar-fill" id="ach-bar" style="width:0%;background:var(--gold);"></div>
</div>
<!-- Categories -->
<div class="tab-group" id="ach-tabs">
<button class="tab active" data-cat="all">الكل</button>
<button class="tab" data-cat="games">مباريات</button>
<button class="tab" data-cat="social">اجتماعي</button>
<button class="tab" data-cat="streak">متتالية</button>
</div>
<!-- Achievement List -->
<div class="space-y-2" id="achievements-list">
<div class="card"><div class="empty-state">جاري التحميل...</div></div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
let currentCat = 'all';
document.querySelectorAll('#ach-tabs .tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('#ach-tabs .tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
currentCat = tab.dataset.cat;
loadAchievements(currentCat);
});
});
loadAchievements(currentCat);
});
async function loadAchievements(category) {
const url = '/api/achievements' + (category !== 'all' ? '?category=' + category : '');
const data = await App.fetch(url);
const container = document.getElementById('achievements-list');
if (!data || !data.achievements) {
container.innerHTML = '<div class="card"><div class="empty-state">لا يوجد انجازات</div></div>';
return;
}
const achievements = data.achievements;
const completed = achievements.filter(a => a.unlocked).length;
document.getElementById('ach-progress').textContent = completed + ' / ' + achievements.length + ' مكتمل';
const pct = achievements.length > 0 ? (completed / achievements.length * 100) : 0;
document.getElementById('ach-bar').style.width = pct + '%';
container.innerHTML = achievements.map(a => {
const unlocked = a.unlocked;
const opacity = unlocked ? '' : 'opacity:0.5;';
return `
<div class="card" style="${opacity}">
<div class="card-body" style="display:flex;align-items:center;gap:16px;">
<div class="avatar" style="background:${unlocked ? 'var(--gold-dim)' : 'var(--bg-3)'};">
<svg class="icon-lg" style="color:${unlocked ? 'var(--gold)' : 'var(--text-3)'}"><use href="/public/icons/sprite.svg#icon-star"></use></svg>
</div>
<div style="flex:1;">
<p style="font-size:14px;font-weight:600;">${a.name}</p>
<p class="text-muted text-xs">${a.description}</p>
${a.progress !== undefined && !unlocked ? `
<div class="stat-bar" style="margin-top:6px;height:4px;">
<div class="stat-bar-fill" style="width:${Math.min((a.progress / a.target) * 100, 100)}%;background:var(--cyan);"></div>
</div>
<p class="text-muted text-xs" style="margin-top:2px;">${a.progress} / ${a.target}</p>
` : ''}
</div>
${unlocked ? '<svg class="icon" style="color:var(--success);"><use href="/public/icons/sprite.svg#icon-check"></use></svg>' : ''}
</div>
</div>
`;
}).join('');
}
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB - Theme Editor'; ?>
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EL3AB Admin - Theme Editor</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:'Inter',sans-serif; background:#0a0f1a; color:#e2e8f0; min-height:100vh; }
.login-screen { display:flex; align-items:center; justify-content:center; min-height:100vh; padding:20px; }
.login-box { background:#1a2332; border:1px solid rgba(255,255,255,0.1); border-radius:12px; padding:32px; max-width:360px; width:100%; }
.login-box h1 { font-size:20px; margin-bottom:20px; text-align:center; color:#e7a832; }
.login-box input { width:100%; padding:12px; margin-bottom:12px; background:#0f1925; border:1px solid rgba(255,255,255,0.1); border-radius:8px; color:#e2e8f0; font-size:14px; }
.login-box button { width:100%; padding:12px; background:#e7a832; color:#0f172a; border:none; border-radius:8px; font-weight:700; font-size:14px; cursor:pointer; }
.login-box button:hover { background:#c48b1a; }
.login-error { color:#ef4444; font-size:12px; text-align:center; margin-top:8px; display:none; }
.admin-layout { display:flex; min-height:100vh; }
.admin-sidebar { width:220px; background:#111827; border-right:1px solid rgba(255,255,255,0.08); padding:20px 0; flex-shrink:0; }
.admin-sidebar h2 { font-size:14px; padding:0 16px; margin-bottom:16px; color:#e7a832; }
.admin-sidebar .nav-item { display:block; padding:10px 16px; font-size:13px; cursor:pointer; color:#94a3b8; transition:all 0.15s; border-right:3px solid transparent; }
.admin-sidebar .nav-item:hover { color:#e2e8f0; background:rgba(255,255,255,0.04); }
.admin-sidebar .nav-item.active { color:#15d7ff; background:rgba(21,215,255,0.08); border-right-color:#15d7ff; }
.admin-main { flex:1; padding:24px 32px; overflow-y:auto; max-height:100vh; }
.admin-main h1 { font-size:22px; font-weight:700; margin-bottom:8px; }
.admin-main .subtitle { color:#64748b; font-size:13px; margin-bottom:24px; }
.section { margin-bottom:32px; }
.section-title { font-size:15px; font-weight:600; margin-bottom:14px; padding-bottom:8px; border-bottom:1px solid rgba(255,255,255,0.08); display:flex; align-items:center; gap:8px; }
.section-title .badge { font-size:10px; background:#7c4dff; padding:2px 6px; border-radius:4px; }
.field-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(260px, 1fr)); gap:12px; }
.field { background:#1a2332; border:1px solid rgba(255,255,255,0.08); border-radius:8px; padding:14px; }
.field label { display:block; font-size:11px; color:#94a3b8; margin-bottom:6px; font-weight:500; text-transform:uppercase; letter-spacing:0.5px; }
.field .input-row { display:flex; gap:8px; align-items:center; }
.field input[type="text"], .field input[type="color"] { flex:1; padding:8px 10px; background:#0f1925; border:1px solid rgba(255,255,255,0.1); border-radius:6px; color:#e2e8f0; font-size:13px; font-family:monospace; }
.field input[type="color"] { width:40px; height:34px; padding:2px; cursor:pointer; flex:none; }
.field input[type="file"] { font-size:12px; color:#94a3b8; }
.field .preview { margin-top:8px; }
.field .preview img { max-height:40px; border-radius:4px; }
.field .remove-btn { font-size:11px; color:#ef4444; cursor:pointer; background:none; border:none; padding:4px 8px; }
.save-bar { position:sticky; bottom:0; background:#111827; border-top:1px solid rgba(255,255,255,0.1); padding:16px 32px; display:flex; align-items:center; gap:12px; }
.save-bar .btn-save { padding:10px 24px; background:#15d7ff; color:#0f172a; border:none; border-radius:8px; font-weight:700; font-size:13px; cursor:pointer; }
.save-bar .btn-save:hover { background:#0ba8c9; }
.save-bar .btn-reset { padding:10px 24px; background:transparent; color:#ef4444; border:1px solid #ef4444; border-radius:8px; font-size:13px; cursor:pointer; }
.save-bar .status { font-size:12px; color:#34d399; }
.upload-area { border:2px dashed rgba(255,255,255,0.1); border-radius:8px; padding:16px; text-align:center; cursor:pointer; transition:border-color 0.2s; }
.upload-area:hover { border-color:rgba(21,215,255,0.4); }
.upload-area p { font-size:12px; color:#64748b; }
.rgba-picker { display:flex; gap:6px; align-items:center; }
.rgba-picker input[type="color"] { width:40px; height:32px; border:none; cursor:pointer; }
.rgba-picker .alpha-slider { width:80px; }
.rgba-picker .alpha-label { font-size:11px; min-width:30px; }
.rgba-picker .rgba-output { font-size:11px; width:160px; background:var(--bg-3); border:1px solid var(--border); border-radius:4px; padding:4px 6px; color:var(--text-2); }
.theme-field { margin-bottom:12px; }
.theme-field label { display:block; font-size:11px; color:#94a3b8; margin-bottom:6px; font-weight:500; }
.spacing-slider { flex:1; }
.slider-value { font-size:12px; color:#e2e8f0; min-width:48px; text-align:right; font-family:monospace; }
.icon-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(100px, 1fr)); gap:12px; }
.icon-item { display:flex; flex-direction:column; align-items:center; gap:6px; padding:12px 8px; background:#1a2332; border-radius:8px; border:1px solid rgba(255,255,255,0.08); }
.icon-item svg { width:24px; height:24px; color:#94a3b8; }
.icon-item img { width:24px; height:24px; object-fit:contain; }
.icon-item span { font-size:10px; color:#64748b; }
.icon-item input[type="file"] { display:none; }
.icon-item .icon-upload-btn { font-size:10px; padding:3px 8px; background:#15d7ff; color:#fff; border:none; border-radius:4px; cursor:pointer; }
.icon-item .icon-reset-btn { font-size:10px; padding:3px 6px; background:#0f1925; color:#64748b; border:1px solid rgba(255,255,255,0.1); border-radius:4px; cursor:pointer; }
.upload-hint { display:block; margin-top:4px; font-size:10px; color:#64748b; }
@media (max-width: 768px) {
.admin-layout { flex-direction:column; }
.admin-sidebar { width:100%; border-right:none; border-bottom:1px solid rgba(255,255,255,0.08); padding:12px 0; display:flex; overflow-x:auto; gap:0; }
.admin-sidebar h2 { display:none; }
.admin-sidebar .nav-item { white-space:nowrap; border-right:none; border-bottom:3px solid transparent; padding:8px 14px; }
.admin-sidebar .nav-item.active { border-bottom-color:#15d7ff; }
.admin-main { padding:16px; max-height:none; }
.field-grid { grid-template-columns:1fr; }
}
</style>
</head>
<body>
<!-- Login Screen -->
<div class="login-screen" id="login-screen">
<div class="login-box">
<h1>Theme Editor</h1>
<input type="text" id="admin-user" placeholder="Username" autocomplete="username">
<input type="password" id="admin-pass" placeholder="Password" autocomplete="current-password">
<button onclick="doLogin()">Login</button>
<p class="login-error" id="login-err">Invalid credentials</p>
</div>
</div>
<!-- Admin Panel -->
<div class="admin-layout" id="admin-panel" style="display:none;">
<aside class="admin-sidebar">
<h2>Theme Editor</h2>
<p style="font-size:9px;color:#666;padding:8px 12px 4px;text-transform:uppercase;">عام</p>
<div class="nav-item active" data-section="colors">🎨 الألوان</div>
<div class="nav-item" data-section="spacing">📐 المسافات</div>
<div class="nav-item" data-section="ui">🖥 الواجهة</div>
<div class="nav-item" data-section="assets">📁 الشعار والملفات</div>
<div class="nav-item" data-section="icons-grid">🎨 الأيقونات</div>
<div class="nav-item" data-section="bots">🤖 البوتات</div>
<p style="font-size:9px;color:#666;padding:12px 12px 4px;text-transform:uppercase;">شطرنج</p>
<div class="nav-item" data-section="board">♟ ألوان الرقعة</div>
<div class="nav-item" data-section="chess">♟ تصميم اللعبة</div>
<div class="nav-item" data-section="pieces">♟ القطع</div>
<div class="nav-item" data-section="moves">♟ تصنيف النقلات</div>
<p style="font-size:9px;color:#666;padding:12px 12px 4px;text-transform:uppercase;">ألعاب أخرى</p>
<div class="nav-item" data-section="ludo">🎲 لودو</div>
<div class="nav-item" data-section="domino">🁣 دومينو</div>
<div class="nav-item" data-section="backgammon">🎯 طاولة</div>
</aside>
<div class="admin-main">
<h1>Theme Customization</h1>
<p class="subtitle">Changes apply globally to all players. Leave fields empty to use defaults.</p>
<!-- Colors Section -->
<div class="section" data-panel="colors">
<div class="section-title">Background & Brand <span class="badge">CSS Vars</span></div>
<div class="field-grid" id="fields-colors"></div>
</div>
<!-- Board Section -->
<div class="section" data-panel="board">
<div class="section-title">Board Theme</div>
<div class="field-grid" id="fields-board"></div>
</div>
<!-- Chess Styling -->
<div class="section" data-panel="chess">
<div class="section-title">♟ Chess Styling — Pieces, Clock, Panel, Layout</div>
<div class="field-grid" id="fields-chess"></div>
</div>
<!-- Move Classes -->
<div class="section" data-panel="moves">
<div class="section-title">Move Classifications</div>
<div class="field-grid" id="fields-moves"></div>
</div>
<!-- UI -->
<div class="section" data-panel="ui">
<div class="section-title">UI / Overlays / Eval Bar</div>
<div class="field-grid" id="fields-ui"></div>
</div>
<!-- Bots -->
<div class="section" data-panel="bots">
<div class="section-title">Bot Avatar Gradients</div>
<div class="field-grid" id="fields-bots"></div>
</div>
<!-- Assets -->
<div class="section" data-panel="assets">
<div class="section-title">Assets / Logo / Favicon</div>
<div class="field-grid" id="fields-assets"></div>
</div>
<!-- Chess Pieces -->
<div class="section" data-panel="pieces">
<div class="section-title">Chess Pieces (upload custom images)</div>
<div class="field-grid" id="fields-pieces"></div>
</div>
<!-- Ludo -->
<div class="section" data-panel="ludo" style="display:none;">
<div class="section-title">Ludo Game Colors</div>
<div class="field-grid" id="fields-ludo"></div>
</div>
<!-- Icons -->
<div class="section" data-panel="icons">
<div class="section-title">Custom SVG Sprite</div>
<div class="field-grid" id="fields-icons"></div>
</div>
<!-- Spacing & Layout -->
<div class="section" data-panel="spacing">
<div class="section-title">&#x1F4D0; Spacing & Layout</div>
<div id="fields-spacing"></div>
</div>
<!-- Domino -->
<div class="section" data-panel="domino">
<div class="section-title">&#x1F3B2; Domino</div>
<div class="field-grid" id="fields-domino"></div>
</div>
<!-- Backgammon -->
<div class="section" data-panel="backgammon">
<div class="section-title">&#x1F3AF; Backgammon</div>
<div class="field-grid" id="fields-backgammon"></div>
</div>
<!-- Icons Grid -->
<div class="section" data-panel="icons-grid" id="section-icons-grid">
<div class="section-title">&#x1F3A8; Icons (Individual)</div>
<p style="color:#64748b;font-size:12px;margin-bottom:12px;">Replace any icon with an SVG or PNG file — required size: 24x24px</p>
<div class="icon-grid"></div>
</div>
<div class="save-bar">
<button class="btn-save" onclick="saveAll()">Save All Changes</button>
<button class="btn-reset" onclick="clearCache()">Clear Cache</button>
<span class="status" id="save-status"></span>
</div>
</div>
</div>
<script>
const THEME_FIELDS = {
colors: [
{ key:'bg-0', label:'Background Base', type:'color', default:'#050D17' },
{ key:'bg-1', label:'Background Layer 1', type:'color', default:'#0A1525' },
{ key:'bg-2', label:'Background Layer 2', type:'color', default:'#142640' },
{ key:'bg-3', label:'Background Layer 3', type:'color', default:'#1C3254' },
{ key:'gold', label:'Gold (Brand)', type:'color', default:'#E7A832' },
{ key:'gold-dark', label:'Gold Dark', type:'color', default:'#C48B1A' },
{ key:'cyan', label:'Cyan (Brand)', type:'color', default:'#15D7FF' },
{ key:'cyan-dark', label:'Cyan Dark', type:'color', default:'#0BA8C9' },
{ key:'blue', label:'Blue', type:'color', default:'#2979FF' },
{ key:'purple', label:'Purple', type:'color', default:'#7C4DFF' },
{ key:'success', label:'Success', type:'color', default:'#34D399' },
{ key:'error', label:'Error', type:'color', default:'#EF4444' },
{ key:'warning', label:'Warning', type:'color', default:'#F59E0B' },
{ key:'online', label:'Online Indicator', type:'color', default:'#22C55E' },
{ key:'text-1', label:'Text Primary', type:'color', default:'#F1F5F9' },
{ key:'text-2', label:'Text Secondary', type:'color', default:'#94A3B8' },
{ key:'text-3', label:'Text Tertiary', type:'color', default:'#64748B' },
{ key:'text-inverse', label:'Text Inverse', type:'color', default:'#0F172A' },
],
board: [
{ key:'board-light', label:'Light Square', type:'color', default:'#E8EDF9' },
{ key:'board-dark', label:'Dark Square', type:'color', default:'#7195D1' },
{ key:'board-selected', label:'Selected Square', type:'text', default:'rgba(21, 215, 255, 0.45)' },
{ key:'board-last-move', label:'Last Move Highlight', type:'text', default:'rgba(255, 199, 40, 0.35)' },
{ key:'board-check', label:'Check Highlight', type:'text', default:'rgba(239, 68, 68, 0.55)' },
{ key:'board-premove', label:'Premove Highlight', type:'text', default:'rgba(21, 180, 240, 0.3)' },
{ key:'board-legal', label:'Legal Move Dots', type:'text', default:'rgba(0, 0, 0, 0.18)' },
{ key:'board-highlight-green', label:'Highlight Green', type:'text', default:'rgba(21, 180, 90, 0.4)' },
{ key:'board-highlight-red', label:'Highlight Red', type:'text', default:'rgba(220, 50, 50, 0.4)' },
{ key:'board-highlight-yellow', label:'Highlight Yellow', type:'text', default:'rgba(220, 180, 30, 0.4)' },
],
chess: [
{ key:'chess-board-radius', label:'Board Radius', type:'text', default:'4px' },
{ key:'chess-board-shadow', label:'Board Shadow', type:'text', default:'0 2px 10px rgba(0,0,0,0.4)' },
{ key:'chess-piece-size', label:'Piece Size', type:'text', default:'min(11vw, 62px)' },
{ key:'chess-piece-white', label:'White Piece Color', type:'color', default:'#ffffff' },
{ key:'chess-piece-white-stroke', label:'White Stroke', type:'text', default:'1px #555' },
{ key:'chess-piece-black', label:'Black Piece Color', type:'color', default:'#222222' },
{ key:'chess-piece-black-stroke', label:'Black Stroke', type:'text', default:'0.5px #000' },
{ key:'chess-piece-shadow', label:'Piece Shadow', type:'text', default:'0 1px 2px rgba(0,0,0,0.3)' },
{ key:'chess-piece-hover-scale', label:'Hover Scale', type:'text', default:'1.05' },
{ key:'chess-piece-drag-scale', label:'Drag Scale', type:'text', default:'1.15' },
{ key:'chess-legal-dot-size', label:'Legal Move Dot Size', type:'text', default:'28%' },
{ key:'chess-legal-capture-size', label:'Capture Ring Size', type:'text', default:'88%' },
{ key:'chess-legal-capture-border', label:'Capture Ring Border', type:'text', default:'4px' },
{ key:'chess-clock-bg', label:'Clock BG', type:'color', default:'#1C3254' },
{ key:'chess-clock-active-bg', label:'Clock Active BG', type:'color', default:'#E7A832' },
{ key:'chess-clock-radius', label:'Clock Radius', type:'text', default:'8px' },
{ key:'chess-clock-font-size', label:'Clock Font Size', type:'text', default:'20px' },
{ key:'chess-clock-padding', label:'Clock Padding', type:'text', default:'8px 14px' },
{ key:'chess-coords-size', label:'Coordinates Size', type:'text', default:'11px' },
{ key:'chess-coords-weight', label:'Coordinates Weight', type:'text', default:'700' },
{ key:'chess-panel-width', label:'Side Panel Width', type:'text', default:'260px' },
{ key:'chess-panel-bg', label:'Panel BG', type:'color', default:'#142640' },
{ key:'chess-panel-radius', label:'Panel Radius', type:'text', default:'12px' },
{ key:'chess-move-font-size', label:'Move List Font', type:'text', default:'13px' },
{ key:'chess-eval-width', label:'Eval Bar Width', type:'text', default:'18px' },
{ key:'chess-eval-radius', label:'Eval Bar Radius', type:'text', default:'4px' },
{ key:'chess-header-padding', label:'Header Padding', type:'text', default:'10px 14px' },
{ key:'chess-header-radius', label:'Header Radius', type:'text', default:'12px' },
{ key:'chess-header-bg', label:'Header BG', type:'color', default:'#142640' },
{ key:'chess-player-font-size', label:'Player Name Size', type:'text', default:'14px' },
{ key:'chess-rating-font-size', label:'Rating Font Size', type:'text', default:'11px' },
{ key:'chess-board-max', label:'Board Max Width', type:'text', default:'580px' },
],
moves: [
{ key:'move-brilliant', label:'Brilliant', type:'color', default:'#26c6da' },
{ key:'move-great', label:'Great', type:'color', default:'#66bb6a' },
{ key:'move-good', label:'Good', type:'color', default:'#81c784' },
{ key:'move-book', label:'Book', type:'color', default:'#9e9e9e' },
{ key:'move-inaccuracy', label:'Inaccuracy', type:'color', default:'#fdd835' },
{ key:'move-mistake', label:'Mistake', type:'color', default:'#ef6c00' },
{ key:'move-blunder', label:'Blunder', type:'color', default:'#e53935' },
{ key:'move-text-brilliant', label:'Text Brilliant', type:'color', default:'#00bcd4' },
{ key:'move-text-great', label:'Text Great', type:'color', default:'#2196f3' },
{ key:'move-text-good', label:'Text Good', type:'color', default:'#4caf50' },
{ key:'move-text-inaccuracy', label:'Text Inaccuracy', type:'color', default:'#ff9800' },
{ key:'move-text-mistake', label:'Text Mistake', type:'color', default:'#f44336' },
{ key:'move-text-blunder', label:'Text Blunder', type:'color', default:'#d32f2f' },
],
ui: [
{ key:'eval-bg', label:'Eval Bar BG', type:'color', default:'#1a1a2e' },
{ key:'eval-white', label:'Eval White Fill', type:'color', default:'#f0f0f0' },
{ key:'eval-label-light', label:'Eval Label Light', type:'color', default:'#ffffff' },
{ key:'eval-label-dark', label:'Eval Label Dark', type:'color', default:'#333333' },
{ key:'arrow-green', label:'Arrow Green', type:'text', default:'rgba(21, 180, 90, 0.7)' },
{ key:'arrow-red', label:'Arrow Red', type:'text', default:'rgba(220, 50, 50, 0.7)' },
{ key:'arrow-yellow', label:'Arrow Yellow', type:'text', default:'rgba(220, 180, 30, 0.7)' },
{ key:'graph-bg', label:'Graph Background', type:'color', default:'#0a1628' },
{ key:'graph-grid', label:'Graph Grid', type:'color', default:'#333333' },
{ key:'graph-accent', label:'Graph Accent Line', type:'color', default:'#15d7ff' },
{ key:'graph-error', label:'Graph Error Dots', type:'color', default:'#f44336' },
{ key:'overlay-dark', label:'Overlay Dark', type:'text', default:'rgba(0, 0, 0, 0.8)' },
{ key:'overlay-result', label:'Game Result Overlay', type:'text', default:'rgba(5, 13, 23, 0.92)' },
{ key:'overlay-error-bg', label:'Error Alert BG', type:'text', default:'rgba(239, 68, 68, 0.1)' },
{ key:'overlay-error-border', label:'Error Alert Border', type:'text', default:'rgba(239, 68, 68, 0.2)' },
],
bots: [
{ key:'bot-amina', label:'Amina (Beginner)', type:'text', default:'#4ade80, #22c55e' },
{ key:'bot-tarek', label:'Tarek (Amateur)', type:'text', default:'#38bdf8, #0ea5e9' },
{ key:'bot-nour', label:'Nour (Intermediate)', type:'text', default:'#a78bfa, #7c3aed' },
{ key:'bot-omar', label:'Omar (Good)', type:'text', default:'#fb923c, #ea580c' },
{ key:'bot-layla', label:'Layla (Strong)', type:'text', default:'#f472b6, #db2777' },
{ key:'bot-ziad', label:'Ziad (Expert)', type:'text', default:'#f87171, #dc2626' },
{ key:'bot-gm', label:'Grandmaster', type:'text', default:'var(--gold), #b45309' },
],
assets: [
{ key:'logo-text', label:'Logo Text Override', type:'text', default:'', category:'assets' },
{ key:'logo-image', label:'Logo Image', type:'file', category:'assets' },
{ key:'favicon', label:'Favicon', type:'file', category:'assets' },
],
pieces: [
{ key:'piece-wK', label:'White King', type:'file', category:'pieces' },
{ key:'piece-wQ', label:'White Queen', type:'file', category:'pieces' },
{ key:'piece-wR', label:'White Rook', type:'file', category:'pieces' },
{ key:'piece-wB', label:'White Bishop', type:'file', category:'pieces' },
{ key:'piece-wN', label:'White Knight', type:'file', category:'pieces' },
{ key:'piece-wP', label:'White Pawn', type:'file', category:'pieces' },
{ key:'piece-bK', label:'Black King', type:'file', category:'pieces' },
{ key:'piece-bQ', label:'Black Queen', type:'file', category:'pieces' },
{ key:'piece-bR', label:'Black Rook', type:'file', category:'pieces' },
{ key:'piece-bB', label:'Black Bishop', type:'file', category:'pieces' },
{ key:'piece-bN', label:'Black Knight', type:'file', category:'pieces' },
{ key:'piece-bP', label:'Black Pawn', type:'file', category:'pieces' },
],
ludo: [
{ key:'ludo-p1', label:'Player 1 (Red)', type:'color', default:'#E53935' },
{ key:'ludo-p2', label:'Player 2 (Green)', type:'color', default:'#43A047' },
{ key:'ludo-p3', label:'Player 3 (Yellow)', type:'color', default:'#FDD835' },
{ key:'ludo-p4', label:'Player 4 (Blue)', type:'color', default:'#1E88E5' },
{ key:'ludo-board-bg', label:'Board Background', type:'color', default:'#1a2332' },
{ key:'ludo-path', label:'Path Cell Color', type:'color', default:'#f5f5f5' },
{ key:'ludo-safe', label:'Safe Position Color', type:'color', default:'#FFD54F' },
{ key:'ludo-path-border', label:'Path Cell Border', type:'text', default:'rgba(0,0,0,0.1)' },
{ key:'ludo-home-p1', label:'Home Entrance P1', type:'text', default:'rgba(229,57,53,0.3)' },
{ key:'ludo-home-p2', label:'Home Entrance P2', type:'text', default:'rgba(67,160,71,0.3)' },
{ key:'ludo-home-p3', label:'Home Entrance P3', type:'text', default:'rgba(253,216,53,0.3)' },
{ key:'ludo-home-p4', label:'Home Entrance P4', type:'text', default:'rgba(30,136,229,0.3)' },
{ key:'ludo-dice-bg', label:'Dice Background', type:'text', default:'var(--bg-2)' },
{ key:'ludo-dice-dot', label:'Dice Dot Color', type:'text', default:'var(--text-inverse)' },
{ key:'ludo-chat-bg', label:'Chat Background', type:'text', default:'var(--bg-2)' },
],
icons: [
{ key:'sprite-svg', label:'Custom SVG Sprite (replaces default)', type:'file', category:'icons' },
],
domino: [
{ key:'domino-face', label:'Domino Face', type:'color', default:'#f5f0e8' },
{ key:'domino-pip', label:'Domino Pips', type:'color', default:'#1a1a1a' },
{ key:'domino-back', label:'Domino Back', type:'color', default:'#1a2a4a' },
{ key:'domino-board-bg', label:'Domino Board BG', type:'color', default:'#0a2a1a' },
{ key:'domino-playable-glow', label:'Playable Glow', type:'text', default:'rgba(255,200,50,0.4)' },
],
backgammon: [
{ key:'bg-felt', label:'Felt Color', type:'color', default:'#1a5c32' },
{ key:'bg-point-light', label:'Light Point', type:'color', default:'#d4a76a' },
{ key:'bg-point-dark', label:'Dark Point', type:'color', default:'#8b4513' },
{ key:'bg-bar', label:'Bar Color', type:'color', default:'#3a2418' },
{ key:'bg-checker-white', label:'White Checker', type:'color', default:'#f0ebe0' },
{ key:'bg-checker-black', label:'Black Checker', type:'color', default:'#1a1a1a' },
{ key:'bg-board-wood', label:'Board Wood', type:'color', default:'#5c3d2e' },
{ key:'bg-frame', label:'Frame Color', type:'color', default:'#3a2418' },
],
};
let adminUser = '';
let adminPass = '';
let currentSettings = {};
function doLogin() {
adminUser = document.getElementById('admin-user').value;
adminPass = document.getElementById('admin-pass').value;
if (adminUser === 'admin' && adminPass === 'Alarcade123#') {
document.getElementById('login-screen').style.display = 'none';
document.getElementById('admin-panel').style.display = 'flex';
loadTheme();
} else {
document.getElementById('login-err').style.display = 'block';
}
}
document.getElementById('admin-pass').addEventListener('keydown', e => { if(e.key==='Enter') doLogin(); });
async function loadTheme() {
const res = await fetch('/api/theme');
const data = await res.json();
currentSettings = {};
(data.settings || []).forEach(s => { currentSettings[s.key] = s.value; });
renderAllFields();
}
function renderAllFields() {
Object.keys(THEME_FIELDS).forEach(section => {
const container = document.getElementById('fields-' + section);
if (!container) return;
container.innerHTML = '';
THEME_FIELDS[section].forEach(f => {
const saved = currentSettings[f.key] || '';
container.innerHTML += renderField(f, saved);
});
});
}
function renderField(f, saved) {
if (f.type === 'color') {
const colorVal = saved || f.default;
return `<div class="field">
<label>${f.label} <code style="font-size:10px;color:#64748b;">(--${f.key})</code></label>
<div class="input-row">
<input type="color" value="${colorVal}" data-key="${f.key}" data-category="${f.category || 'colors'}" onchange="onColorChange(this)">
<input type="text" value="${saved}" data-key="${f.key}" data-category="${f.category || 'colors'}" placeholder="${f.default}">
${saved ? '<button class="remove-btn" onclick="removeField(this,\'' + f.key + '\')">X</button>' : ''}
</div>
</div>`;
} else if (f.type === 'text') {
return `<div class="field">
<label>${f.label} <code style="font-size:10px;color:#64748b;">(--${f.key})</code></label>
<div class="input-row">
<input type="text" value="${saved}" data-key="${f.key}" data-category="${f.category || 'colors'}" placeholder="${f.default}">
${saved ? '<button class="remove-btn" onclick="removeField(this,\'' + f.key + '\')">X</button>' : ''}
</div>
</div>`;
} else if (f.type === 'file') {
const hint = getUploadHint(f.key, f.category);
return `<div class="field">
<label>${f.label} <code style="font-size:10px;color:#64748b;">(${f.key})</code></label>
<input type="file" accept="image/*,.svg" data-key="${f.key}" data-category="${f.category || 'assets'}" onchange="uploadFile(this)">
${hint ? '<small class="upload-hint">' + hint + '</small>' : ''}
${saved ? '<div class="preview"><img src="' + saved + '"><button class="remove-btn" onclick="removeAsset(this,\'' + f.key + '\')">Remove</button></div>' : ''}
</div>`;
}
return '';
}
function onColorChange(colorInput) {
const row = colorInput.closest('.input-row');
const textInput = row.querySelector('input[type="text"]');
textInput.value = colorInput.value;
}
async function uploadFile(input) {
const file = input.files[0];
if (!file) return;
const fd = new FormData();
fd.append('file', file);
fd.append('key', input.dataset.key);
fd.append('category', input.dataset.category);
fd.append('admin_user', adminUser);
fd.append('admin_pass', adminPass);
fd.append('label', input.dataset.key);
const res = await fetch('/api/theme-upload', { method:'POST', body:fd });
const data = await res.json();
if (data.ok) {
currentSettings[input.dataset.key] = data.url;
showStatus('Uploaded: ' + input.dataset.key);
renderAllFields();
} else {
showStatus('Error: ' + (data.error || 'upload failed'), true);
}
}
async function removeField(btn, key) {
await fetch('/api/theme', {
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({ admin_user:adminUser, admin_pass:adminPass, action:'delete', key })
});
delete currentSettings[key];
renderAllFields();
showStatus('Removed: ' + key);
}
async function removeAsset(btn, key) {
await removeField(btn, key);
}
async function saveAll() {
const inputs = document.querySelectorAll('.field input[type="text"][data-key]');
const settings = [];
inputs.forEach(input => {
const key = input.dataset.key;
const value = input.value.trim();
const category = input.dataset.category || 'colors';
if (value) {
settings.push({ key, value, category, label:key });
}
});
if (settings.length === 0) {
showStatus('Nothing to save');
return;
}
const res = await fetch('/api/theme', {
method:'POST',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({ admin_user:adminUser, admin_pass:adminPass, action:'save', settings })
});
const data = await res.json();
if (data.ok) {
showStatus('Saved ' + settings.length + ' settings!');
currentSettings = {};
settings.forEach(s => { currentSettings[s.key] = s.value; });
} else {
showStatus('Error saving', true);
}
}
async function clearCache() {
showStatus('Cache cleared - next page load will fetch fresh data');
}
function showStatus(msg, isErr) {
const el = document.getElementById('save-status');
el.textContent = msg;
el.style.color = isErr ? '#ef4444' : '#34d399';
setTimeout(() => { el.textContent = ''; }, 4000);
}
// Sidebar nav
document.querySelectorAll('.admin-sidebar .nav-item').forEach(item => {
item.addEventListener('click', () => {
document.querySelectorAll('.admin-sidebar .nav-item').forEach(n => n.classList.remove('active'));
item.classList.add('active');
const section = item.dataset.section;
document.querySelectorAll('.section').forEach(s => {
s.style.display = s.dataset.panel === section ? 'block' : 'none';
});
});
});
// Show only first section on load
document.querySelectorAll('.section').forEach((s, i) => { s.style.display = i === 0 ? 'block' : 'none'; });
// Upload hint helper
function getUploadHint(key, category) {
if (key.startsWith('piece-')) return '128×128px PNG أو SVG';
if (key === 'logo-image') return '200×40px PNG/SVG (خلفية شفافة)';
if (key === 'favicon') return '32×32px PNG أو ICO';
if (key === 'sprite-svg') return 'ملف SVG — كل رمز 24px viewBox';
if (category === 'icons') return '24×24px SVG مفضل';
return '';
}
// Spacing & Layout sliders
const SPACING_FIELDS = [
{ key:'--radius-sm', label:'زوايا صغيرة', default:8, min:0, max:24, step:1, suffix:'px' },
{ key:'--radius-md', label:'زوايا متوسطة', default:12, min:0, max:32, step:1, suffix:'px' },
{ key:'--radius-lg', label:'زوايا كبيرة', default:16, min:0, max:40, step:1, suffix:'px' },
{ key:'--content-max', label:'عرض المحتوى', default:600, min:400, max:1200, step:10, suffix:'px' },
{ key:'--header-h', label:'ارتفاع الهيدر', default:52, min:40, max:80, step:1, suffix:'px' },
{ key:'--nav-bottom-h', label:'ارتفاع النافبار', default:56, min:40, max:72, step:1, suffix:'px' },
{ key:'--card-padding', label:'حشو الكرت', default:16, min:8, max:32, step:1, suffix:'px' },
{ key:'--section-gap', label:'مسافة الأقسام', default:16, min:8, max:40, step:1, suffix:'px' },
{ key:'--touch-min', label:'حجم اللمس', default:44, min:36, max:56, step:1, suffix:'px' },
];
function renderSpacingFields() {
const container = document.getElementById('fields-spacing');
if (!container) return;
container.innerHTML = SPACING_FIELDS.map(f => {
const saved = currentSettings[f.key] || '';
const val = saved ? parseInt(saved) : f.default;
return `<div class="theme-field">
<label>${f.label} <code style="font-size:10px;color:#64748b;">(${f.key})</code></label>
<div style="display:flex;gap:8px;align-items:center;">
<input type="range" min="${f.min}" max="${f.max}" value="${val}" step="${f.step}" data-key="${f.key}" class="spacing-slider">
<span class="slider-value">${val}${f.suffix || ''}</span>
</div>
</div>`;
}).join('');
}
function initSpacingSliders() {
document.querySelectorAll('.spacing-slider').forEach(slider => {
const valueSpan = slider.parentElement.querySelector('.slider-value');
slider.addEventListener('input', () => {
const val = slider.value + 'px';
valueSpan.textContent = val;
document.documentElement.style.setProperty(slider.dataset.key, val);
});
});
}
// RGBA Picker
function initRgbaPickers() {
document.querySelectorAll('.rgba-picker').forEach(picker => {
const colorInput = picker.querySelector('input[type="color"]');
const alphaSlider = picker.querySelector('.alpha-slider');
const alphaLabel = picker.querySelector('.alpha-label');
const output = picker.querySelector('.rgba-output');
const key = picker.dataset.key;
function update() {
const hex = colorInput.value;
const alpha = alphaSlider.value / 100;
const r = parseInt(hex.slice(1,3), 16);
const g = parseInt(hex.slice(3,5), 16);
const b = parseInt(hex.slice(5,7), 16);
const rgba = `rgba(${r}, ${g}, ${b}, ${alpha})`;
output.value = rgba;
alphaLabel.textContent = alphaSlider.value + '%';
document.documentElement.style.setProperty(key, rgba);
}
colorInput.addEventListener('input', update);
alphaSlider.addEventListener('input', update);
});
}
// RGBA fields - detect which fields need RGBA pickers
const RGBA_KEYS = ['board-selected','board-last-move','board-check','board-premove','board-legal',
'board-highlight-green','board-highlight-red','board-highlight-yellow',
'overlay-dark','overlay-result','overlay-error-bg','overlay-error-border',
'ludo-path-border','ludo-home-p1','ludo-home-p2','ludo-home-p3','ludo-home-p4',
'domino-playable-glow'];
function parseRgba(str) {
const m = str.match(/rgba?\((\d+),\s*(\d+),\s*(\d+),?\s*([\d.]*)\)/);
if (m) {
const r = parseInt(m[1]), g = parseInt(m[2]), b = parseInt(m[3]);
const a = m[4] !== '' ? parseFloat(m[4]) : 1;
const hex = '#' + [r,g,b].map(x => x.toString(16).padStart(2,'0')).join('');
return { hex, alpha: Math.round(a * 100) };
}
return { hex:'#000000', alpha:100 };
}
// Override renderField for RGBA fields
const originalRenderField = renderField;
renderField = function(f, saved) {
if (f.type === 'text' && RGBA_KEYS.includes(f.key)) {
const val = saved || f.default;
const parsed = parseRgba(val);
return `<div class="field">
<label>${f.label} <code style="font-size:10px;color:#64748b;">(--${f.key})</code></label>
<div class="rgba-picker" data-key="--${f.key}">
<input type="color" value="${parsed.hex}">
<input type="range" class="alpha-slider" min="0" max="100" value="${parsed.alpha}">
<span class="alpha-label">${parsed.alpha}%</span>
<input type="text" class="rgba-output" value="${val}" data-key="${f.key}" data-category="${f.category || 'colors'}">
</div>
</div>`;
}
return originalRenderField(f, saved);
};
// Icon Grid
const ICONS = ['home','play','trophy','leaderboard','friends','shop','star','settings','profile','bell','coin','gem','dice','users','lock','games','domino','backgammon','cards','plus','check','x','arrow-left','arrow-right','flag','crown','clock','key','bot','search','lightning'];
function renderIconGrid() {
const container = document.querySelector('.icon-grid');
if (!container) return;
container.innerHTML = ICONS.map(icon => `
<div class="icon-item" data-icon="${icon}">
<svg style="width:24px;height:24px;"><use href="/public/icons/sprite.svg#icon-${icon}"></use></svg>
<span>icon-${icon}</span>
<label class="icon-upload-btn">تغيير<input type="file" accept=".svg,.png" onchange="uploadIcon('${icon}', this.files[0])"></label>
</div>
`).join('');
}
async function uploadIcon(iconName, file) {
if (!file) return;
const fd = new FormData();
fd.append('file', file);
fd.append('key', 'icon-' + iconName);
fd.append('category', 'icons');
fd.append('admin_user', adminUser);
fd.append('admin_pass', adminPass);
fd.append('label', 'icon-' + iconName);
const res = await fetch('/api/theme-upload', { method:'POST', body:fd });
const data = await res.json();
if (data.ok) {
showStatus('Uploaded icon: ' + iconName);
} else {
showStatus('Error: ' + (data.error || 'upload failed'), true);
}
}
// Extend saveAll to include spacing sliders
const originalSaveAll = saveAll;
saveAll = async function() {
// Collect spacing slider values into hidden inputs before saving
document.querySelectorAll('.spacing-slider').forEach(slider => {
const key = slider.dataset.key;
const val = slider.value + 'px';
// Create a temporary text input so the save loop picks it up
let existing = document.querySelector(`input[type="text"][data-key="${key}"]`);
if (!existing) {
const hidden = document.createElement('input');
hidden.type = 'text';
hidden.dataset.key = key;
hidden.dataset.category = 'spacing';
hidden.value = val;
hidden.style.display = 'none';
hidden.classList.add('spacing-hidden-input');
document.querySelector('.admin-main').appendChild(hidden);
} else {
existing.value = val;
}
});
// Also collect RGBA picker values
document.querySelectorAll('.rgba-picker .rgba-output').forEach(output => {
// These already have data-key and data-category, they'll be picked up by saveAll
});
await originalSaveAll();
// Remove temp hidden inputs
document.querySelectorAll('.spacing-hidden-input').forEach(el => el.remove());
};
// Extend loadTheme to also render spacing and icon grid
const originalLoadTheme = loadTheme;
loadTheme = async function() {
await originalLoadTheme();
renderSpacingFields();
initSpacingSliders();
initRgbaPickers();
renderIconGrid();
};
</script>
</body>
</html>
<?php
$pageTitle = 'EL3AB - تحليل المباراة';
$extraCss = '/public/css/chessboard.css';
?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="analysis-page" id="analysis-page">
<!-- Loading state -->
<div class="analysis-loading" id="analysis-loading">
<div class="thinking">
<div class="thinking-dots">
<span></span><span></span><span></span>
</div>
<span>تحميل المباراة...</span>
</div>
</div>
<!-- Main content (hidden until loaded) -->
<div class="analysis-content" id="analysis-content" style="display:none;">
<!-- Header with game info -->
<div class="analysis-header">
<div class="analysis-players">
<div class="analysis-player">
<svg class="icon-sm"><use href="/public/icons/sprite.svg#icon-profile"></use></svg>
<span id="analysis-player-name">اللاعب</span>
<span class="text-muted text-sm" id="analysis-player-accuracy"></span>
</div>
<span class="text-muted">ضد</span>
<div class="analysis-player">
<svg class="icon-sm"><use href="/public/icons/sprite.svg#icon-bot"></use></svg>
<span id="analysis-opponent-name">الخصم</span>
<span class="text-muted text-sm" id="analysis-opponent-accuracy"></span>
</div>
</div>
<div class="analysis-opening" id="analysis-opening"></div>
</div>
<!-- Board + Eval Bar -->
<div class="analysis-board-section">
<!-- Eval bar (vertical, left side) -->
<div class="eval-bar" id="eval-bar">
<div class="eval-bar-fill" id="eval-bar-fill"></div>
<div class="eval-bar-value" id="eval-bar-value">0.0</div>
</div>
<!-- Board -->
<div class="board-wrapper">
<div class="board" id="board"></div>
</div>
</div>
<!-- Move navigation -->
<div class="analysis-nav">
<button class="btn btn-ghost btn-sm" id="btn-first" title="البداية">
<svg class="icon" style="transform:scaleX(-1)"><use href="/public/icons/sprite.svg#icon-arrows"></use></svg>
</button>
<button class="btn btn-ghost btn-sm" id="btn-prev" title="السابق">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-arrow-right"></use></svg>
</button>
<button class="btn btn-ghost btn-sm" id="btn-next" title="التالي">
<svg class="icon" style="transform:scaleX(-1)"><use href="/public/icons/sprite.svg#icon-arrow-right"></use></svg>
</button>
<button class="btn btn-ghost btn-sm" id="btn-last" title="النهاية">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-arrows"></use></svg>
</button>
<button class="btn btn-ghost btn-sm" onclick="Board.flip()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-arrows"></use></svg>
اقلب
</button>
</div>
<!-- Move list with classifications -->
<div class="analysis-moves" id="analysis-moves"></div>
<!-- Eval graph (canvas chart) -->
<div class="analysis-section">
<h3 class="analysis-section-title">
<svg class="icon-sm"><use href="/public/icons/sprite.svg#icon-star"></use></svg>
رسم التقييم
</h3>
<div class="eval-graph-wrapper">
<canvas id="eval-graph" width="600" height="150"></canvas>
</div>
</div>
<!-- Time per move chart -->
<div class="analysis-section">
<h3 class="analysis-section-title">
<svg class="icon-sm"><use href="/public/icons/sprite.svg#icon-clock"></use></svg>
الوقت لكل نقلة
</h3>
<div class="eval-graph-wrapper">
<canvas id="time-graph" width="600" height="120"></canvas>
</div>
</div>
<!-- Critical moments -->
<div class="analysis-section">
<h3 class="analysis-section-title">
<svg class="icon-sm" style="color:var(--error)"><use href="/public/icons/sprite.svg#icon-fire"></use></svg>
اللحظات الحاسمة
</h3>
<div class="critical-moments" id="critical-moments"></div>
</div>
<!-- Action buttons -->
<div class="analysis-actions">
<button class="btn btn-cyan btn-sm" id="btn-analyze" onclick="Analysis.startAnalysis()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-star"></use></svg>
تحليل بالمحرك
</button>
<button class="btn btn-ghost btn-sm" id="btn-review-mistakes" onclick="Analysis.reviewMistakes()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-search"></use></svg>
مراجعة الاخطاء
</button>
<button class="btn btn-ghost btn-sm" onclick="Analysis.exportPGN()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-send"></use></svg>
تصدير PGN
</button>
<button class="btn btn-ghost btn-sm" onclick="Analysis.copyFEN()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-edit"></use></svg>
نسخ FEN
</button>
</div>
<!-- Analysis progress -->
<div class="analysis-progress" id="analysis-progress" style="display:none;">
<div class="progress-bar">
<div class="progress-bar-fill" id="analysis-progress-fill"></div>
</div>
<span class="text-sm text-muted" id="analysis-progress-text">جاري التحليل...</span>
</div>
</div>
</div>
<style>
.analysis-page {
max-width: 100%;
}
.analysis-loading {
display: flex;
align-items: center;
justify-content: center;
min-height: 300px;
}
.analysis-header {
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 12px 16px;
margin-bottom: 12px;
}
.analysis-players {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.analysis-player {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
font-weight: 600;
}
.analysis-opening {
margin-top: 8px;
font-size: 12px;
color: var(--text-3);
font-family: var(--font-en);
}
.analysis-board-section {
display: flex;
gap: 8px;
align-items: stretch;
margin-bottom: 12px;
}
/* Eval bar */
.eval-bar {
width: 24px;
min-height: 100%;
background: var(--eval-bg);
border-radius: var(--radius-sm);
position: relative;
overflow: hidden;
border: 1px solid var(--border);
flex-shrink: 0;
}
.eval-bar-fill {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 50%;
background: var(--eval-white);
transition: height 0.3s ease;
}
.eval-bar-value {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 9px;
font-weight: 700;
font-family: var(--font-en);
color: var(--text-1);
writing-mode: vertical-lr;
text-orientation: mixed;
z-index: 1;
text-shadow: 0 0 3px var(--overlay-dark);
}
.analysis-board-section .board-wrapper {
flex: 1;
}
/* Navigation */
.analysis-nav {
display: flex;
gap: 6px;
justify-content: center;
margin-bottom: 12px;
flex-wrap: wrap;
}
/* Move list */
.analysis-moves {
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 12px;
max-height: 200px;
overflow-y: auto;
font-family: var(--font-en);
font-size: 13px;
direction: ltr;
margin-bottom: 16px;
}
.analysis-move-pair {
display: flex;
gap: 4px;
padding: 2px 0;
align-items: center;
}
.analysis-move-number {
color: var(--text-3);
min-width: 28px;
}
.analysis-move {
padding: 2px 6px;
border-radius: 4px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 2px;
}
.analysis-move:hover { background: var(--bg-3); }
.analysis-move.current { background: var(--cyan); color: var(--text-inverse); }
/* Move classifications */
.move-class {
font-size: 10px;
font-weight: 700;
margin-right: 2px;
}
.move-class-brilliant { color: var(--move-text-brilliant); }
.move-class-great { color: var(--move-text-great); }
.move-class-good { color: var(--move-text-good); }
.move-class-book { color: var(--move-book); }
.move-class-inaccuracy { color: var(--move-text-inaccuracy); }
.move-class-mistake { color: var(--move-text-mistake); }
.move-class-blunder { color: var(--move-text-blunder); }
/* Sections */
.analysis-section {
margin-bottom: 16px;
}
.analysis-section-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
margin-bottom: 8px;
}
/* Eval graph */
.eval-graph-wrapper {
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 8px;
overflow-x: auto;
}
.eval-graph-wrapper canvas {
width: 100%;
height: auto;
display: block;
}
/* Critical moments */
.critical-moments {
display: flex;
flex-direction: column;
gap: 8px;
}
.critical-moment {
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 10px 14px;
cursor: pointer;
transition: background 0.2s;
display: flex;
align-items: center;
gap: 10px;
}
.critical-moment:hover { background: var(--bg-3); }
.critical-moment-move {
font-family: var(--font-en);
font-size: 14px;
font-weight: 600;
}
.critical-moment-eval {
font-family: var(--font-en);
font-size: 12px;
color: var(--text-3);
}
/* Actions */
.analysis-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 16px;
}
/* Progress bar */
.analysis-progress {
margin-bottom: 16px;
}
.progress-bar {
width: 100%;
height: 6px;
background: var(--bg-3);
border-radius: 3px;
overflow: hidden;
margin-bottom: 6px;
}
.progress-bar-fill {
height: 100%;
background: var(--cyan);
border-radius: 3px;
transition: width 0.3s ease;
width: 0%;
}
</style>
<script src="/public/js/chess.min.js"></script>
<script src="/public/js/board.js"></script>
<script src="/public/js/analysis.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
const params = new URLSearchParams(window.location.search);
const gameId = params.get('id');
if (!gameId) {
App.toast('لم يتم تحديد مباراة', 'error');
return;
}
Analysis.init(gameId);
});
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB - طاولة'; $hideNav = true; ?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<link rel="stylesheet" href="/public/css/backgammon.css">
<style>.main-inner { max-width: 960px !important; padding: 8px 12px !important; }</style>
<div class="bg-layout">
<div class="bg-board-column">
<!-- Player bar top (opponent) -->
<div class="bg-players-bar">
<div class="bg-player-info bg-player-info--black" id="bg-player-1">
<span class="bg-player-dot bg-player-dot--black"></span>
<span class="bg-player-name" id="bg-name-1">اللاعب 2</span>
<span class="bg-player-pip">نقاط: <span id="bg-pip-1">167</span></span>
</div>
</div>
<!-- Board -->
<div class="bg-board" id="bg-board"></div>
<!-- Player bar bottom (you) -->
<div class="bg-players-bar">
<div class="bg-player-info bg-player-info--white" id="bg-player-0">
<span class="bg-player-dot bg-player-dot--white"></span>
<span class="bg-player-name" id="bg-name-0">أنت</span>
<span class="bg-player-pip">نقاط: <span id="bg-pip-0">167</span></span>
</div>
</div>
</div>
<!-- Side panel -->
<div class="bg-side-panel">
<div id="bg-dice-container" class="bg-dice-area"></div>
<div id="bg-status" class="bg-status"></div>
<div class="bg-controls">
<button id="bg-roll-btn" class="bg-roll-btn">ارمِ النرد</button>
<button id="bg-undo-btn" class="bg-pass-btn" style="display:none;">تراجع</button>
</div>
<div id="bg-log" class="bg-log"></div>
</div>
</div>
<!-- Result overlay -->
<div id="bg-result-overlay" class="bg-result-overlay" style="display:none;"></div>
<script src="/public/js/backgammon-constants.js"></script>
<script src="/public/js/backgammon-ui.js"></script>
<script src="/public/js/backgammon-bot.js"></script>
<script src="/public/js/backgammon-game.js"></script>
<script>
(function() {
var params = new URLSearchParams(window.location.search);
var mode = params.get('mode') || 'bot';
var difficulty = params.get('difficulty') || 'medium';
var players = [
{ id: 'p0', name: 'أنت', type: 'human' },
{ id: 'p1', name: 'اللاعب 2', type: 'human' }
];
var bots = [];
if (mode === 'bot') {
players[1] = { id: 'bot_0', name: 'بوت', type: 'bot', difficulty: difficulty };
bots.push({ index: 1, difficulty: difficulty });
}
BackgammonGame.init({
mode: mode,
players: players,
bots: bots,
matchLength: 1
});
})();
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB - طاولة'; $hideNav = true; ?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<link rel="stylesheet" href="/public/css/backgammon.css">
<style>.main-inner { max-width: 960px !important; padding: 8px 12px !important; }</style>
<!-- Waiting Room -->
<div id="bg-waiting" class="lobby-page" style="max-width:500px;margin:0 auto;padding:24px;">
<div class="text-center" style="margin-bottom:24px;">
<h2 class="lobby-title">طاولة — غرفة خاصة</h2>
<p class="text-muted text-sm" id="bg-waiting-msg">جاري الإعداد...</p>
</div>
<div class="card" style="padding:24px;text-align:center;">
<p class="text-muted text-sm">كود الغرفة</p>
<h1 id="bg-room-code" style="font-size:2.5rem;letter-spacing:8px;margin:8px 0;font-family:monospace;">------</h1>
<p class="text-muted text-sm" id="bg-players-count">0 / 2 لاعبين</p>
</div>
<div id="bg-waiting-players" style="margin-top:16px;"></div>
<button id="bg-start-btn" class="btn btn-gold btn-block btn-lg" style="margin-top:16px;display:none;">
ابدأ اللعبة
</button>
</div>
<!-- Game Area (hidden until game starts) -->
<div id="bg-game-area" style="display:none;">
<div class="bg-layout">
<div class="bg-board-column">
<div class="bg-players-bar">
<div class="bg-player-info bg-player-info--black" id="bg-player-1">
<span class="bg-player-dot bg-player-dot--black"></span>
<span class="bg-player-name" id="bg-name-1">الخصم</span>
<span class="bg-player-pip">نقاط: <span id="bg-pip-1">167</span></span>
</div>
</div>
<div class="bg-board" id="bg-board"></div>
<div class="bg-players-bar">
<div class="bg-player-info bg-player-info--white" id="bg-player-0">
<span class="bg-player-dot bg-player-dot--white"></span>
<span class="bg-player-name" id="bg-name-0">أنت</span>
<span class="bg-player-pip">نقاط: <span id="bg-pip-0">167</span></span>
</div>
</div>
</div>
<div class="bg-side-panel">
<div id="bg-turn" class="bg-turn-indicator"></div>
<div id="bg-dice-container" class="bg-dice-area"></div>
<div class="bg-controls">
<button id="bg-roll-btn" class="bg-roll-btn">ارمِ النرد</button>
</div>
<div id="bg-log" class="bg-log"></div>
</div>
</div>
<div id="bg-result-overlay" class="bg-result-overlay" style="display:none;"></div>
</div>
<script src="/public/js/backgammon-constants.js"></script>
<script src="/public/js/backgammon-ui.js"></script>
<script src="/public/js/backgammon-live.js"></script>
<script>
(function() {
var params = new URLSearchParams(window.location.search);
var action = params.get('action');
var code = params.get('code');
var matchId = params.get('match_id');
BackgammonLive.init({
action: action,
code: code,
matchId: matchId
});
})();
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB - طاولة'; $hideNav = true; ?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<link rel="stylesheet" href="/public/css/backgammon.css">
<div class="lobby-page" style="max-width:500px;margin:0 auto;padding:24px;text-align:center;">
<h2 class="lobby-title" style="margin-bottom:8px;">طاولة</h2>
<p class="text-muted text-sm" style="margin-bottom:32px;">البحث عن خصم...</p>
<div class="card" style="padding:32px;">
<div class="matchmaking-spinner"></div>
<p id="bg-mm-timer" style="font-size:1.5rem;margin-top:16px;font-weight:700;">0:00</p>
<p class="text-muted text-sm" style="margin-top:8px;">في الانتظار</p>
</div>
<button class="btn btn-outline btn-block" style="margin-top:24px;" onclick="cancelMatchmaking()">
إلغاء
</button>
</div>
<script>
(function() {
var token = localStorage.getItem('el3ab_token');
var user = JSON.parse(localStorage.getItem('el3ab_user') || 'null');
if (!token || !user) { window.location.href = '/login'; return; }
var startTime = Date.now();
var timerEl = document.getElementById('bg-mm-timer');
var polling = null;
var cancelled = false;
function updateTimer() {
var elapsed = Math.floor((Date.now() - startTime) / 1000);
var m = Math.floor(elapsed / 60);
var s = elapsed % 60;
timerEl.textContent = m + ':' + (s < 10 ? '0' : '') + s;
}
setInterval(updateTimer, 1000);
function joinQueue() {
fetch('/api/backgammon.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
body: JSON.stringify({ action: 'matchmake', sub_action: 'join' })
}).then(function(r) { return r.json(); }).then(function(data) {
if (data.match_id) {
window.location.href = '/backgammon-live?match_id=' + data.match_id;
} else {
polling = setInterval(pollQueue, 3000);
}
});
}
function pollQueue() {
if (cancelled) return;
fetch('/api/backgammon.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
body: JSON.stringify({ action: 'matchmake', sub_action: 'poll' })
}).then(function(r) { return r.json(); }).then(function(data) {
if (data.match_id) {
clearInterval(polling);
window.location.href = '/backgammon-live?match_id=' + data.match_id;
}
});
}
window.cancelMatchmaking = function() {
cancelled = true;
if (polling) clearInterval(polling);
fetch('/api/backgammon.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
body: JSON.stringify({ action: 'matchmake', sub_action: 'leave' })
}).then(function() {
window.location.href = '/backgammon';
});
};
joinQueue();
})();
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB - طاولة'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="lobby-page">
<a href="/games" class="breadcrumb">
<svg class="icon" style="width:14px;height:14px;"><use href="/public/icons/sprite.svg#icon-arrow-right"></use></svg>
العاب
</a>
<div class="text-center" style="margin-top:16px;">
<h2 class="lobby-title">طاولة</h2>
<p class="text-muted text-sm">اختر نوع اللعب</p>
</div>
<div class="lobby-cards">
<!-- Local (Pass & Play) -->
<div class="card lobby-card">
<div class="card-body space-y-4">
<div class="lobby-card-header">
<div class="avatar avatar-game" style="background:linear-gradient(135deg, var(--cyan), var(--gold));">
<svg class="icon-lg" style="color:var(--text-1)"><use href="/public/icons/sprite.svg#icon-users"></use></svg>
</div>
<div>
<p class="lobby-card-title-sm">لعب محلي</p>
<p class="text-muted text-sm">لاعبين على نفس الجهاز</p>
</div>
</div>
<button class="btn btn-gold btn-block btn-lg" onclick="startLocal()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-play"></use></svg>
ابدأ اللعب
</button>
</div>
</div>
<!-- VS Bot -->
<div class="card lobby-card">
<div class="card-body space-y-4">
<div class="lobby-card-header">
<div class="avatar avatar-game" style="background:var(--bg-3);">
<svg class="icon-lg" style="color:var(--cyan)"><use href="/public/icons/sprite.svg#icon-bot"></use></svg>
</div>
<div>
<p class="lobby-card-title-sm">ضد البوت</p>
<p class="text-muted text-sm">العب ضد بوتات ذكية</p>
</div>
</div>
<div>
<label class="input-label">الصعوبة</label>
<div class="tab-group" id="bot-diff-tabs">
<button class="tab" data-diff="easy">سهل</button>
<button class="tab active" data-diff="medium">متوسط</button>
<button class="tab" data-diff="hard">صعب</button>
</div>
</div>
<button class="btn btn-gold btn-block btn-lg" onclick="startBot()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-play"></use></svg>
ابدأ اللعب
</button>
</div>
</div>
<!-- Online Multiplayer -->
<div class="card lobby-card card-featured">
<div class="card-body space-y-4">
<div class="lobby-card-header">
<div class="avatar avatar-game" style="background:linear-gradient(135deg, var(--gold), var(--gold-dark));">
<svg class="icon-lg" style="color:var(--text-1)"><use href="/public/icons/sprite.svg#icon-lightning"></use></svg>
</div>
<div>
<p class="lobby-card-title-sm">ضد لاعب حقيقي</p>
<p class="text-muted text-sm">العب اونلاين ضد لاعبين حقيقيين</p>
</div>
</div>
<button class="btn btn-gold btn-block btn-lg" onclick="startMatchmaking()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-play"></use></svg>
ابحث عن خصم
</button>
</div>
</div>
<!-- Private Room -->
<div class="card lobby-card">
<div class="card-body space-y-4">
<div class="lobby-card-header">
<div class="avatar avatar-game" style="background:var(--bg-3);">
<svg class="icon-lg" style="color:var(--gold)"><use href="/public/icons/sprite.svg#icon-key"></use></svg>
</div>
<div>
<p class="lobby-card-title-sm">غرفة خاصة</p>
<p class="text-muted text-sm">العب مع صديق بكود</p>
</div>
</div>
<div style="display:flex;gap:8px;">
<button class="btn btn-cyan flex-1" onclick="createRoom()">إنشاء غرفة</button>
<button class="btn btn-outline flex-1" onclick="joinRoom()">انضم بكود</button>
</div>
</div>
</div>
</div>
</div>
<script>
function getActiveDiff() {
var el = document.querySelector('#bot-diff-tabs .tab.active');
return el ? el.dataset.diff : 'medium';
}
document.querySelectorAll('.tab-group').forEach(function(group) {
group.querySelectorAll('.tab').forEach(function(tab) {
tab.addEventListener('click', function() {
group.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
tab.classList.add('active');
});
});
});
function startLocal() {
window.location.href = '/backgammon-game?mode=local';
}
function startBot() {
var diff = getActiveDiff();
window.location.href = '/backgammon-game?mode=bot&difficulty=' + diff;
}
function startMatchmaking() {
window.location.href = '/backgammon-matchmaking';
}
function createRoom() {
window.location.href = '/backgammon-live?action=create';
}
function joinRoom() {
var code = prompt('ادخل كود الغرفة:');
if (code && code.trim()) {
window.location.href = '/backgammon-live?action=join&code=' + code.trim().toUpperCase();
}
}
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB - البوتات'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="lobby-page">
<a href="/play" class="breadcrumb">
<svg class="icon" style="width:14px;height:14px;"><use href="/public/icons/sprite.svg#icon-arrow-right"></use></svg>
العب شطرنج
</a>
<div class="text-center" style="margin-top:16px;">
<h2 class="lobby-title">اختر خصمك</h2>
<p class="text-muted text-sm" id="bots-subtitle">جاري التحميل...</p>
</div>
<div class="lobby-cards" id="bots-grid">
<div class="skeleton skeleton-card" style="height:80px;"></div>
<div class="skeleton skeleton-card" style="height:80px;"></div>
<div class="skeleton skeleton-card" style="height:80px;"></div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', async () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
const grid = document.getElementById('bots-grid');
const subtitle = document.getElementById('bots-subtitle');
try {
const data = await App.cachedFetch('/api/bots.php', 60000);
if (!data || !data.bots) throw new Error('no data');
const bots = data.bots;
subtitle.textContent = bots.length + ' بوتات بمستويات مختلفة';
const barColors = {
amina: 'var(--success)',
tarek: 'var(--success)',
nour: 'var(--warning)',
omar: 'var(--warning)',
layla: 'var(--error)',
ziad: 'var(--error)',
grandmaster: 'var(--purple)'
};
grid.innerHTML = bots.map((bot, i) => {
const barPct = Math.round(((i + 1) / bots.length) * 100);
const barColor = barColors[bot.id] || 'var(--cyan)';
const avgElo = Math.round((bot.elo_min + bot.elo_max) / 2);
const portraitUrl = 'https://stockfishapi.caprover.al-arcade.com' + bot.portrait_url;
const letter = bot.id === 'grandmaster' ? 'GM' : bot.name.charAt(0).toUpperCase();
return '<div class="card lobby-card card-hover bot-card" data-bot="' + bot.id + '" data-elo="' + avgElo + '" style="cursor:pointer;">' +
'<div class="card-body lobby-card-row">' +
'<img src="' + portraitUrl + '" class="avatar" style="object-fit:cover;" ' +
'onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\';">' +
'<div class="avatar" style="display:none;background:linear-gradient(135deg,var(--bot-' + bot.id + '));color:var(--text-1);font-weight:700;font-size:18px;align-items:center;justify-content:center;">' + letter + '</div>' +
'<div style="flex:1;">' +
'<p style="font-size:16px;font-weight:600;">' + bot.name_ar + '</p>' +
'<p class="text-muted text-xs">' + bot.style_ar + ' - ELO ' + avgElo + '</p>' +
'<div class="stat-bar" style="margin-top:6px;"><div class="stat-bar-fill" style="width:' + barPct + '%;background:' + barColor + ';"></div></div>' +
'</div>' +
'<svg class="icon" style="color:var(--text-3);transform:scaleX(-1)"><use href="/public/icons/sprite.svg#icon-arrow-right"></use></svg>' +
'</div>' +
'</div>';
}).join('');
grid.querySelectorAll('.bot-card').forEach(card => {
card.addEventListener('click', () => {
const bot = card.dataset.bot;
const color = Math.random() < 0.5 ? 'w' : 'b';
window.location.href = '/game?bot=' + bot + '&color=' + color + '&time=600&inc=0&rated=true';
});
});
} catch (e) {
subtitle.textContent = 'تعذر تحميل البوتات';
grid.innerHTML = '<div class="empty-state"><p>حدث خطأ في تحميل البوتات. حاول مرة أخرى.</p></div>';
}
});
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB - دومينو'; $hideNav = true; ?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<link rel="stylesheet" href="/public/css/domino.css">
<div class="domino-game">
<!-- Top area: opponent + scores -->
<div class="domino-top-area">
<div id="domino-opponent-top" class="domino-opponent-top"></div>
<div id="domino-scores"></div>
</div>
<!-- Side opponents (4P mode) -->
<div id="domino-opponent-left" class="domino-opponent-left"></div>
<div id="domino-opponent-right" class="domino-opponent-right"></div>
<!-- Board -->
<div class="domino-board-area">
<div id="domino-board"></div>
<div id="domino-boneyard"></div>
</div>
<!-- Status -->
<div id="domino-status" class="domino-status"></div>
<!-- Actions -->
<div id="domino-actions"></div>
<!-- End buttons (choose which side to play on) -->
<div id="domino-end-buttons"></div>
<!-- Player hand -->
<div class="domino-hand-area">
<div id="domino-hand"></div>
</div>
</div>
<!-- Round overlay -->
<div id="domino-round-overlay"></div>
<script src="/public/js/domino-constants.js"></script>
<script src="/public/js/domino-ui.js"></script>
<script src="/public/js/domino-bot.js"></script>
<script src="/public/js/domino-game.js"></script>
<script>
(function() {
var params = new URLSearchParams(window.location.search);
var mode = params.get('mode') || '2p';
var type = params.get('type') || 'bot';
var playerCount = parseInt(params.get('players')) || 2;
var difficulty = params.get('difficulty') || 'medium';
DominoGame.init({
mode: mode,
playerCount: playerCount,
difficulty: difficulty,
isLocal: type === 'local'
});
})();
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB - دومينو اونلاين'; $hideNav = true; ?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<link rel="stylesheet" href="/public/css/domino.css">
<!-- Waiting Room -->
<div id="domino-waiting" class="space-y-6" style="padding:24px;text-align:center;">
<h2 style="font-size:22px;font-weight:700;">غرفة دومينو</h2>
<div class="card">
<div class="card-body space-y-4">
<div id="room-code-display">
<p class="text-muted text-sm">كود الغرفة</p>
<p id="room-code-value" style="font-size:32px;font-weight:700;letter-spacing:6px;color:var(--gold);"></p>
<button class="btn btn-ghost btn-sm" onclick="copyCode()" style="margin-top:8px;">نسخ الكود</button>
</div>
<div id="waiting-players" class="space-y-3"></div>
<button class="btn btn-gold btn-block btn-lg" id="start-game-btn" style="display:none;" onclick="DominoLive.startGame()">
ابدأ اللعب
</button>
<button class="btn btn-ghost btn-sm" onclick="window.location.href='/domino'">مغادرة</button>
</div>
</div>
</div>
<!-- Game Area -->
<div id="domino-game-area" class="domino-game" style="display:none;">
<div class="domino-top-area">
<div id="domino-opponent-top" class="domino-opponent-top"></div>
<div id="domino-scores"></div>
</div>
<div id="domino-opponent-left" class="domino-opponent-left"></div>
<div id="domino-opponent-right" class="domino-opponent-right"></div>
<div class="domino-board-area">
<div id="domino-board"></div>
<div id="domino-boneyard"></div>
</div>
<div id="domino-status" class="domino-status"></div>
<div id="domino-actions"></div>
<div id="domino-end-buttons"></div>
<div class="domino-hand-area">
<div id="domino-hand"></div>
</div>
</div>
<!-- Round overlay -->
<div id="domino-round-overlay"></div>
<style>
.domino-waiting-player {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 14px;
background: var(--bg-2);
border-radius: 8px;
border: 1px solid rgba(255,255,255,0.05);
}
.domino-waiting-player--empty {
border-style: dashed;
opacity: 0.6;
}
</style>
<script src="/public/js/domino-constants.js"></script>
<script src="/public/js/domino-ui.js"></script>
<script src="/public/js/domino-live.js"></script>
<script>
(function() {
var params = new URLSearchParams(window.location.search);
var action = params.get('action');
var matchId = params.get('match_id');
var code = params.get('code');
var token = localStorage.getItem('sb_access_token') || '';
var userId = '';
try {
var payload = JSON.parse(atob(token.split('.')[1]));
userId = payload.sub || '';
} catch(e) {}
if (!token || !userId) {
window.location.href = '/login';
return;
}
if (action === 'create') {
App.fetch('/api/domino.php', {
method: 'POST',
body: JSON.stringify({ action: 'create', mode: params.get('mode') || '2p' })
}).then(function(res) {
if (res && res.ok) {
var newMatchId = res.match.id;
history.replaceState(null, '', '/domino-live?match_id=' + newMatchId);
DominoLive.init({ matchId: newMatchId, userId: userId, token: token });
} else {
App.toast('فشل إنشاء الغرفة', 'error');
}
});
} else if (action === 'join' && code) {
App.fetch('/api/domino.php', {
method: 'POST',
body: JSON.stringify({ action: 'join', room_code: code })
}).then(function(res) {
if (res && res.ok) {
var joinMatchId = res.match.id;
history.replaceState(null, '', '/domino-live?match_id=' + joinMatchId);
DominoLive.init({ matchId: joinMatchId, userId: userId, token: token });
} else {
App.toast(res.error || 'فشل الانضمام', 'error');
setTimeout(function() { window.location.href = '/domino'; }, 1500);
}
});
} else if (matchId) {
DominoLive.init({ matchId: matchId, userId: userId, token: token });
} else {
window.location.href = '/domino';
}
})();
function copyCode() {
var code = document.getElementById('room-code-value').textContent;
if (navigator.clipboard) {
navigator.clipboard.writeText(code);
App.toast('تم نسخ الكود', 'success');
}
}
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB - البحث عن خصم'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="lobby-page" style="display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:60vh;text-align:center;">
<div class="matchmaking-spinner">
<svg class="icon-lg" style="color:var(--gold);width:48px;height:48px;animation:spin 2s linear infinite;">
<use href="/public/icons/sprite.svg#icon-search"></use>
</svg>
</div>
<h2 style="margin-top:20px;color:var(--text-1);">جاري البحث عن خصم...</h2>
<p class="text-muted" id="matchmaking-status">يتم البحث عن لاعبين مناسبين</p>
<p class="text-muted text-sm" id="matchmaking-timer" style="margin-top:8px;"></p>
<button class="btn btn-ghost" style="margin-top:24px;" onclick="cancelMatchmaking()">إلغاء</button>
</div>
<style>
@keyframes spin { to { transform: rotate(360deg); } }
</style>
<script>
(function() {
var params = new URLSearchParams(window.location.search);
var mode = params.get('mode') || '2p';
var pollInterval = null;
var startTime = Date.now();
function updateTimer() {
var elapsed = Math.floor((Date.now() - startTime) / 1000);
var mins = Math.floor(elapsed / 60);
var secs = elapsed % 60;
document.getElementById('matchmaking-timer').textContent = (mins > 0 ? mins + ':' : '') + (secs < 10 ? '0' : '') + secs;
}
function pollMatchmaking() {
App.fetch('/api/domino.php', {
method: 'POST',
body: JSON.stringify({ action: 'matchmake', sub_action: 'join', mode: mode })
}).then(function(res) {
if (res.status === 'matched' && res.match_id) {
clearInterval(pollInterval);
document.getElementById('matchmaking-status').textContent = 'تم العثور على خصم!';
setTimeout(function() {
window.location.href = '/domino-live?match_id=' + res.match_id;
}, 1000);
}
}).catch(function() {});
}
pollInterval = setInterval(function() {
updateTimer();
pollMatchmaking();
}, 3000);
updateTimer();
pollMatchmaking();
window.cancelMatchmaking = function() {
clearInterval(pollInterval);
App.fetch('/api/domino.php', {
method: 'POST',
body: JSON.stringify({ action: 'matchmake', sub_action: 'leave', mode: mode })
}).then(function() {
window.location.href = '/domino';
});
};
})();
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB - دومينو'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="lobby-page">
<a href="/games" class="breadcrumb">
<svg class="icon" style="width:14px;height:14px;"><use href="/public/icons/sprite.svg#icon-arrow-right"></use></svg>
العاب
</a>
<div class="text-center" style="margin-top:16px;">
<h2 class="lobby-title">دومينو</h2>
<p class="text-muted text-sm">اختر نوع اللعب</p>
</div>
<div class="lobby-cards">
<!-- Local (Pass & Play) -->
<div class="card lobby-card">
<div class="card-body space-y-4">
<div class="lobby-card-header">
<div class="avatar avatar-game" style="background:linear-gradient(135deg, var(--cyan), var(--gold));">
<svg class="icon-lg" style="color:var(--text-1)"><use href="/public/icons/sprite.svg#icon-users"></use></svg>
</div>
<div>
<p class="lobby-card-title-sm">لعب محلي</p>
<p class="text-muted text-sm">لاعبين على نفس الجهاز</p>
</div>
</div>
<div>
<label class="input-label">عدد اللاعبين</label>
<div class="tab-group" id="local-count-tabs">
<button class="tab active" data-count="2">2 لاعبين</button>
<button class="tab" data-count="4">4 لاعبين</button>
</div>
</div>
<button class="btn btn-gold btn-block btn-lg" onclick="startLocal()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-play"></use></svg>
ابدأ اللعب
</button>
</div>
</div>
<!-- VS Bot -->
<div class="card lobby-card">
<div class="card-body space-y-4">
<div class="lobby-card-header">
<div class="avatar avatar-game" style="background:var(--bg-3);">
<svg class="icon-lg" style="color:var(--cyan)"><use href="/public/icons/sprite.svg#icon-bot"></use></svg>
</div>
<div>
<p class="lobby-card-title-sm">ضد البوت</p>
<p class="text-muted text-sm">العب ضد بوتات ذكية</p>
</div>
</div>
<div>
<label class="input-label">الوضع</label>
<div class="tab-group" id="bot-mode-tabs">
<button class="tab active" data-mode="2p">1 ضد 1</button>
<button class="tab" data-mode="4p_teams">فرق (2 ضد 2)</button>
</div>
</div>
<div>
<label class="input-label">الصعوبة</label>
<div class="tab-group" id="bot-diff-tabs">
<button class="tab" data-diff="easy">سهل</button>
<button class="tab active" data-diff="medium">متوسط</button>
<button class="tab" data-diff="hard">صعب</button>
</div>
</div>
<button class="btn btn-gold btn-block btn-lg" onclick="startBot()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-play"></use></svg>
ابدأ اللعب
</button>
</div>
</div>
<!-- Multiplayer -->
<div class="card lobby-card lobby-card--featured">
<div class="card-body space-y-4">
<div class="lobby-card-header">
<div class="avatar avatar-game" style="background:linear-gradient(135deg, var(--gold), var(--cyan));">
<svg class="icon-lg" style="color:var(--text-1)"><use href="/public/icons/sprite.svg#icon-sword"></use></svg>
</div>
<div>
<p class="lobby-card-title">ضد لاعب حقيقي</p>
<p class="text-muted text-sm">العب اونلاين ضد لاعبين حقيقيين</p>
</div>
</div>
<div>
<label class="input-label">الوضع</label>
<div class="tab-group" id="mp-mode-tabs">
<button class="tab active" data-mode="2p">1 ضد 1</button>
<button class="tab" data-mode="4p_teams">فرق (2 ضد 2)</button>
</div>
</div>
<button class="btn btn-gold btn-block btn-lg" onclick="startMatchmaking()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-search"></use></svg>
ابحث عن خصم
</button>
</div>
</div>
<!-- Private Room -->
<div class="card lobby-card">
<div class="card-body space-y-4">
<div class="lobby-card-header">
<div class="avatar avatar-game" style="background:var(--bg-3);">
<svg class="icon-lg" style="color:var(--gold)"><use href="/public/icons/sprite.svg#icon-lock"></use></svg>
</div>
<div>
<p class="lobby-card-title-sm">غرفة خاصة</p>
<p class="text-muted text-sm">انشئ غرفة وادعو اصحابك</p>
</div>
</div>
<div class="lobby-btn-pair">
<button class="btn btn-gold" onclick="createRoom()">انشئ غرفة</button>
<button class="btn btn-ghost" onclick="showJoinRoom()">انضم بكود</button>
</div>
<div id="join-room-form" style="display:none;">
<div class="lobby-code-row">
<input type="text" class="input" id="room-code-input" placeholder="ادخل كود الغرفة" maxlength="6" style="text-transform:uppercase;letter-spacing:4px;text-align:center;">
<button class="btn btn-gold" onclick="joinRoom()">دخول</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.tab-group').forEach(function(group) {
group.querySelectorAll('.tab').forEach(function(tab) {
tab.addEventListener('click', function() {
group.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
tab.classList.add('active');
});
});
});
// Load config from games API
App.cachedFetch('/api/games.php', 60000).then(function(data) {
if (!data || !data.games) return;
var domino = data.games.find(function(g) { return g.game_key === 'domino'; });
if (!domino || !domino.config || !domino.config.difficulties) return;
var diffTabs = document.getElementById('bot-diff-tabs');
if (!diffTabs) return;
diffTabs.innerHTML = domino.config.difficulties.map(function(d, i) {
return '<button class="tab' + (i === 1 ? ' active' : '') + '" data-diff="' + d.id + '">' + d.label + '</button>';
}).join('');
diffTabs.querySelectorAll('.tab').forEach(function(tab) {
tab.addEventListener('click', function() {
diffTabs.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
tab.classList.add('active');
});
});
}).catch(function() {});
});
function startLocal() {
var count = document.querySelector('#local-count-tabs .tab.active').dataset.count;
var mode = count === '4' ? '4p_teams' : '2p';
window.location.href = '/domino-game?mode=' + mode + '&type=local&players=' + count;
}
function startBot() {
var mode = document.querySelector('#bot-mode-tabs .tab.active').dataset.mode;
var diff = document.querySelector('#bot-diff-tabs .tab.active').dataset.diff;
var count = mode === '4p_teams' ? 4 : 2;
window.location.href = '/domino-game?mode=' + mode + '&type=bot&players=' + count + '&difficulty=' + diff;
}
function startMatchmaking() {
if (!App.isLoggedIn()) { window.location.href = '/login'; return; }
var mode = document.querySelector('#mp-mode-tabs .tab.active').dataset.mode;
window.location.href = '/domino-matchmaking?mode=' + mode;
}
function createRoom() {
if (!App.isLoggedIn()) { window.location.href = '/login'; return; }
window.location.href = '/domino-live?action=create';
}
function showJoinRoom() {
var form = document.getElementById('join-room-form');
form.style.display = form.style.display === 'none' ? 'block' : 'none';
if (form.style.display === 'block') {
document.getElementById('room-code-input').focus();
}
}
function joinRoom() {
if (!App.isLoggedIn()) { window.location.href = '/login'; return; }
var code = document.getElementById('room-code-input').value.trim().toUpperCase();
if (!code || code.length < 4) {
App.toast('ادخل كود صحيح', 'error');
return;
}
window.location.href = '/domino-live?action=join&code=' + code;
}
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB - الاصدقاء'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="space-y-6">
<div class="text-center">
<h2 style="font-size:22px;font-weight:700;">الاصدقاء</h2>
</div>
<!-- Search -->
<div class="input-group">
<svg class="icon input-icon"><use href="/public/icons/sprite.svg#icon-search"></use></svg>
<input type="text" class="input" id="friend-search" placeholder="ابحث عن لاعب...">
</div>
<!-- Tabs -->
<div class="tab-group" id="friends-tabs">
<button class="tab active" data-tab="friends">اصدقائي</button>
<button class="tab" data-tab="requests">طلبات</button>
<button class="tab" data-tab="search">بحث</button>
</div>
<!-- Friends List -->
<div id="tab-friends" class="space-y-2">
<div class="card" id="friends-list">
<div class="empty-state">
<svg class="icon-lg" style="color:var(--text-3);margin-bottom:8px;"><use href="/public/icons/sprite.svg#icon-friends"></use></svg>
<p>لا يوجد اصدقاء بعد</p>
<p class="text-muted text-xs">ابحث عن لاعبين واضفهم</p>
</div>
</div>
</div>
<!-- Requests -->
<div id="tab-requests" class="space-y-2" style="display:none;">
<div class="card" id="requests-list">
<div class="empty-state">
<p>لا يوجد طلبات صداقة</p>
</div>
</div>
</div>
<!-- Search Results -->
<div id="tab-search" class="space-y-2" style="display:none;">
<div class="card" id="search-results">
<div class="empty-state">
<p>اكتب اسم اللاعب للبحث</p>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
const tabs = document.querySelectorAll('#friends-tabs .tab');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
tabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
document.querySelectorAll('[id^="tab-"]').forEach(p => p.style.display = 'none');
document.getElementById('tab-' + tab.dataset.tab).style.display = 'block';
});
});
loadFriends();
let searchTimeout;
document.getElementById('friend-search').addEventListener('input', (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => searchPlayers(e.target.value), 500);
});
});
async function loadFriends() {
const data = await App.fetch('/api/friends?action=list');
if (data && data.friends && data.friends.length > 0) {
const list = document.getElementById('friends-list');
list.innerHTML = data.friends.map(f => `
<div class="card-body" style="display:flex;align-items:center;gap:12px;padding:12px;border-bottom:1px solid var(--border);">
<div class="avatar avatar-sm">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-profile"></use></svg>
</div>
<div style="flex:1;">
<p style="font-size:14px;font-weight:600;">${f.display_name || f.username}</p>
<p class="text-muted text-xs">${f.elo_blitz || 1200} ELO</p>
</div>
<span class="badge ${f.online ? 'badge-success' : ''}">${f.online ? 'متصل' : 'غير متصل'}</span>
</div>
`).join('');
}
const reqData = await App.fetch('/api/friends?action=requests');
if (reqData && reqData.requests && reqData.requests.length > 0) {
const reqList = document.getElementById('requests-list');
reqList.innerHTML = reqData.requests.map(r => `
<div class="card-body" style="display:flex;align-items:center;gap:12px;padding:12px;border-bottom:1px solid var(--border);">
<div class="avatar avatar-sm">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-profile"></use></svg>
</div>
<div style="flex:1;">
<p style="font-size:14px;font-weight:600;">${r.display_name || r.username}</p>
</div>
<button class="btn btn-cyan btn-xs" onclick="acceptFriend('${r.id}')">قبول</button>
<button class="btn btn-ghost btn-xs" onclick="rejectFriend('${r.id}')">رفض</button>
</div>
`).join('');
}
}
async function searchPlayers(query) {
if (!query || query.length < 2) return;
const data = await App.fetch('/api/friends?action=search&q=' + encodeURIComponent(query));
const results = document.getElementById('search-results');
if (data && data.players && data.players.length > 0) {
results.innerHTML = data.players.map(p => `
<div class="card-body" style="display:flex;align-items:center;gap:12px;padding:12px;border-bottom:1px solid var(--border);">
<div class="avatar avatar-sm">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-profile"></use></svg>
</div>
<div style="flex:1;">
<p style="font-size:14px;font-weight:600;">${p.display_name || p.username}</p>
<p class="text-muted text-xs">${p.elo_blitz || 1200} ELO</p>
</div>
<button class="btn btn-cyan btn-xs" onclick="addFriend('${p.id}')">اضافة</button>
</div>
`).join('');
} else {
results.innerHTML = '<div class="empty-state"><p>لا توجد نتائج</p></div>';
}
}
async function addFriend(userId) {
await App.fetch('/api/friends', {
method: 'POST',
body: JSON.stringify({ action: 'add', user_id: userId })
});
App.toast('تم ارسال طلب الصداقة', 'success');
}
async function acceptFriend(requestId) {
await App.fetch('/api/friends', {
method: 'POST',
body: JSON.stringify({ action: 'accept', request_id: requestId })
});
App.toast('تم قبول الصداقة', 'success');
loadFriends();
}
async function rejectFriend(requestId) {
await App.fetch('/api/friends', {
method: 'POST',
body: JSON.stringify({ action: 'reject', request_id: requestId })
});
loadFriends();
}
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php
$pageTitle = 'EL3AB - مباراة مباشرة';
$extraCss = '/public/css/chessboard.css';
?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="game-layout" id="game-container">
<!-- Board Column -->
<div class="game-board-column">
<!-- Opponent info + clock -->
<div class="game-header">
<div class="game-player">
<div class="avatar avatar-sm" id="opponent-avatar">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-profile"></use></svg>
</div>
<div>
<div class="game-player-name" id="opponent-name">خصم</div>
<div class="game-player-rating" id="opponent-rating">1200</div>
</div>
</div>
<div class="game-clock" id="clock-top">10:00</div>
</div>
<!-- Board + Eval Bar -->
<div class="game-board-section">
<div class="eval-bar" id="eval-bar">
<span class="eval-bar-label eval-bar-label-top" id="eval-label-top"></span>
<div class="eval-bar-fill" id="eval-bar-fill" style="height:50%;"></div>
<span class="eval-bar-label eval-bar-label-bottom" id="eval-label-bottom"></span>
</div>
<div class="board-wrapper">
<div class="board" id="board"></div>
</div>
</div>
<!-- Thinking indicator (opponent's turn) -->
<div class="thinking" id="thinking-indicator" style="display:none;">
<div class="thinking-dots">
<span></span><span></span><span></span>
</div>
<span>ينتظر نقلة الخصم...</span>
</div>
<!-- Player info + clock -->
<div class="game-header">
<div class="game-player">
<div class="avatar avatar-sm">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-profile"></use></svg>
</div>
<div>
<div class="game-player-name" id="player-name">انت</div>
<div class="game-player-rating" id="player-rating">1200</div>
</div>
</div>
<div class="game-clock" id="clock-bottom">10:00</div>
</div>
</div>
<!-- Side Panel (Desktop) -->
<div class="game-side-panel" id="side-panel">
<div class="analysis-panel">
<div class="opening-display" id="opening-display" style="display:none;">
<span class="opening-eco" id="opening-eco"></span>
<span class="opening-name" id="opening-name"></span>
</div>
<div class="move-list-pro" id="move-list"></div>
<div class="panel-status" id="game-status">بانتظار الخصم...</div>
<!-- Draw offer banner -->
<div class="draw-offer-banner" id="draw-offer-banner" style="display:none;">
<span>الخصم يعرض التعادل</span>
<div class="draw-offer-actions">
<button class="btn btn-success btn-sm" onclick="LiveGame.acceptDraw()">قبول</button>
<button class="btn btn-ghost btn-sm" onclick="LiveGame.declineDraw()">رفض</button>
</div>
</div>
<div class="panel-controls" id="game-controls">
<button class="btn btn-ghost btn-sm" onclick="LiveGame.resign()" id="btn-resign">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-flag"></use></svg>
استسلام
</button>
<button class="btn btn-ghost btn-sm" onclick="LiveGame.offerDraw()" id="btn-draw">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-shield"></use></svg>
تعادل
</button>
<button class="btn btn-ghost btn-sm" onclick="Board.flip()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-eye"></use></svg>
اقلب
</button>
</div>
<div class="postgame-controls" id="postgame-controls" style="display:none;">
<button class="btn btn-ghost btn-sm" onclick="LiveGame.exportPGN()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-download"></use></svg>
PGN
</button>
<button class="btn btn-ghost btn-sm" onclick="LiveGame.copyFEN()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-copy"></use></svg>
FEN
</button>
<button class="btn btn-cyan btn-sm" onclick="window.location.href='/play'">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-arrow-right"></use></svg>
رجوع
</button>
</div>
</div>
</div>
<!-- Mobile Panel -->
<div class="game-mobile-panel" id="mobile-panel">
<div class="analysis-panel">
<div class="opening-display" id="opening-display-mobile" style="display:none;">
<span class="opening-eco" id="opening-eco-mobile"></span>
<span class="opening-name" id="opening-name-mobile"></span>
</div>
<div class="move-list-pro" id="move-list-mobile"></div>
<div class="panel-status" id="game-status-mobile">بانتظار الخصم...</div>
<div class="draw-offer-banner" id="draw-offer-banner-mobile" style="display:none;">
<span>الخصم يعرض التعادل</span>
<div class="draw-offer-actions">
<button class="btn btn-success btn-sm" onclick="LiveGame.acceptDraw()">قبول</button>
<button class="btn btn-ghost btn-sm" onclick="LiveGame.declineDraw()">رفض</button>
</div>
</div>
<div class="panel-controls" id="game-controls-mobile">
<button class="btn btn-ghost btn-sm" onclick="LiveGame.resign()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-flag"></use></svg>
استسلام
</button>
<button class="btn btn-ghost btn-sm" onclick="LiveGame.offerDraw()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-shield"></use></svg>
تعادل
</button>
<button class="btn btn-ghost btn-sm" onclick="Board.flip()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-eye"></use></svg>
اقلب
</button>
</div>
</div>
</div>
</div>
<script src="/public/js/chess.min.js"></script>
<script src="/public/js/board.js"></script>
<script src="/public/js/openings.js"></script>
<script src="/public/js/realtime.js"></script>
<script src="/public/js/game-live.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
const params = new URLSearchParams(window.location.search);
const matchId = params.get('id');
if (!matchId) {
App.toast('معرف المباراة مفقود', 'error');
setTimeout(() => window.location.href = '/play', 1500);
return;
}
LiveGame.init(matchId);
});
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php
$pageTitle = 'EL3AB - المباراة';
$extraCss = '/public/css/chessboard.css';
?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="game-layout" id="game-container">
<!-- Board Column (eval bar + opponent/player info + board) -->
<div class="game-board-column">
<!-- Opponent info + clock -->
<div class="game-header">
<div class="game-player">
<div class="avatar avatar-sm" id="opponent-avatar">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-bot"></use></svg>
</div>
<div>
<div class="game-player-name" id="opponent-name">Bot</div>
<div class="game-player-rating" id="opponent-rating">1200</div>
</div>
</div>
<div class="game-clock" id="clock-top">10:00</div>
</div>
<!-- Board + Eval Bar -->
<div class="game-board-section">
<!-- Eval Bar -->
<div class="eval-bar" id="eval-bar">
<span class="eval-bar-label eval-bar-label-top" id="eval-label-top"></span>
<div class="eval-bar-fill" id="eval-bar-fill" style="height:50%;"></div>
<span class="eval-bar-label eval-bar-label-bottom" id="eval-label-bottom"></span>
</div>
<!-- Board -->
<div class="board-wrapper">
<div class="board" id="board"></div>
</div>
</div>
<!-- Thinking indicator -->
<div class="thinking" id="thinking-indicator" style="display:none;">
<div class="thinking-dots">
<span></span><span></span><span></span>
</div>
<span>يفكر...</span>
</div>
<!-- Pre-move indicator -->
<div class="premove-indicator" id="premove-indicator" style="display:none;">
نقلة مسبقة محجوزة
</div>
<!-- Player info + clock -->
<div class="game-header">
<div class="game-player">
<div class="avatar avatar-sm">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-profile"></use></svg>
</div>
<div>
<div class="game-player-name" id="player-name">انت</div>
<div class="game-player-rating" id="player-rating">1200</div>
</div>
</div>
<div class="game-clock" id="clock-bottom">10:00</div>
</div>
</div>
<!-- Side Panel (Desktop) -->
<div class="game-side-panel" id="side-panel">
<div class="analysis-panel">
<!-- Opening name -->
<div class="opening-display" id="opening-display" style="display:none;">
<span class="opening-eco" id="opening-eco"></span>
<span class="opening-name" id="opening-name"></span>
</div>
<!-- Move list -->
<div class="move-list-pro" id="move-list"></div>
<!-- Game status -->
<div class="panel-status" id="game-status">دورك</div>
<!-- Controls -->
<div class="panel-controls" id="game-controls">
<button class="btn btn-ghost btn-sm" onclick="Game.resign()" id="btn-resign">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-flag"></use></svg>
استسلام
</button>
<button class="btn btn-ghost btn-sm" onclick="Board.flip()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-arrows"></use></svg>
اقلب
</button>
<button class="btn btn-ghost btn-sm" onclick="Game.toggleArrows()" id="btn-arrows">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-eye"></use></svg>
اسهم
</button>
</div>
<!-- Post-game controls (hidden initially) -->
<div class="postgame-controls" id="postgame-controls" style="display:none;">
<button class="btn btn-ghost btn-sm" onclick="Game.exportPGN()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-download"></use></svg>
PGN
</button>
<button class="btn btn-ghost btn-sm" onclick="Game.copyFEN()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-copy"></use></svg>
FEN
</button>
<button class="btn btn-ghost btn-sm" onclick="Game.analyze()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-chart"></use></svg>
تحليل
</button>
</div>
</div>
</div>
<!-- Mobile Panel (shown only on mobile) -->
<div class="game-mobile-panel" id="mobile-panel">
<div class="analysis-panel">
<!-- Opening name (mobile) -->
<div class="opening-display" id="opening-display-mobile" style="display:none;">
<span class="opening-eco" id="opening-eco-mobile"></span>
<span class="opening-name" id="opening-name-mobile"></span>
</div>
<!-- Move list (mobile) -->
<div class="move-list-pro" id="move-list-mobile"></div>
<!-- Game status (mobile) -->
<div class="panel-status" id="game-status-mobile">دورك</div>
<!-- Controls (mobile) -->
<div class="panel-controls" id="game-controls-mobile">
<button class="btn btn-ghost btn-sm" onclick="Game.resign()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-flag"></use></svg>
استسلام
</button>
<button class="btn btn-ghost btn-sm" onclick="Board.flip()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-arrows"></use></svg>
اقلب
</button>
<button class="btn btn-ghost btn-sm" onclick="Game.toggleArrows()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-eye"></use></svg>
اسهم
</button>
</div>
<!-- Post-game controls (mobile, hidden initially) -->
<div class="postgame-controls" id="postgame-controls-mobile" style="display:none;">
<button class="btn btn-ghost btn-sm" onclick="Game.exportPGN()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-download"></use></svg>
PGN
</button>
<button class="btn btn-ghost btn-sm" onclick="Game.copyFEN()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-copy"></use></svg>
FEN
</button>
<button class="btn btn-ghost btn-sm" onclick="Game.analyze()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-chart"></use></svg>
تحليل
</button>
</div>
</div>
</div>
</div>
<script src="/public/js/chess.min.js"></script>
<script src="/public/js/board.js"></script>
<script src="/public/js/openings.js"></script>
<script src="/public/js/game.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
const params = new URLSearchParams(window.location.search);
const botId = params.get('bot') || 'amina';
const color = params.get('color') || 'w';
const time = parseInt(params.get('time') || '600');
const inc = parseInt(params.get('inc') || '0');
const rated = params.get('rated') !== 'false';
const avatarEl = document.getElementById('opponent-avatar');
avatarEl.innerHTML = '<img src="https://stockfishapi.caprover.al-arcade.com/portraits/' + botId + '.jpg" alt="' + botId + '" style="width:100%;height:100%;border-radius:50%;object-fit:cover;">';
// Load bot name from API
(async () => {
try {
const data = await App.cachedFetch('/api/bots.php', 60000);
if (data && data.bots) {
const bot = data.bots.find(b => b.id === botId);
if (bot) {
document.getElementById('opponent-name').textContent = bot.name_ar.split(' ')[0];
} else {
document.getElementById('opponent-name').textContent = botId;
}
}
} catch (e) {
document.getElementById('opponent-name').textContent = botId;
}
})();
Game.start({ botId, color, time, increment: inc, rated });
});
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB - العاب'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="space-y-6 bg-animated" id="games-content">
<div class="text-center">
<h2 class="page-title" style="margin-bottom:8px;">العاب</h2>
<p class="text-muted text-sm">اختر لعبتك المفضلة وابدأ</p>
</div>
<div class="games-grid" id="games-grid">
<div class="skeleton skeleton-card" style="height:140px;border-radius:var(--radius-lg);"></div>
<div class="skeleton skeleton-card" style="height:140px;border-radius:var(--radius-lg);"></div>
<div class="skeleton skeleton-card" style="height:140px;border-radius:var(--radius-lg);"></div>
<div class="skeleton skeleton-card" style="height:140px;border-radius:var(--radius-lg);"></div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', async () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
const grid = document.getElementById('games-grid');
const gameRoutes = {
chess: '/play',
ludo: '/ludo',
domino: '/domino',
backgammon: '/backgammon'
};
const gameIcons = {
chess: 'icon-play',
ludo: 'icon-ludo',
domino: 'icon-domino',
backgammon: 'icon-backgammon',
trix: 'icon-cards',
baloot: 'icon-cards',
trivia: 'icon-trophy'
};
const gameCoverClasses = {
chess: 'game-card-cover--chess',
ludo: 'game-card-cover--ludo',
domino: 'game-card-cover--domino',
backgammon: 'game-card-cover--backgammon',
trix: 'game-card-cover--trix',
baloot: 'game-card-cover--baloot',
trivia: 'game-card-cover--trix'
};
try {
const data = await App.cachedFetch('/api/games.php', 60000);
if (!data || !data.games) throw new Error('no data');
const games = data.games;
grid.innerHTML = games.map(game => {
const route = gameRoutes[game.game_key];
const isPlayable = route && game.is_enabled;
const icon = gameIcons[game.game_key] || 'icon-play';
const coverClass = gameCoverClasses[game.game_key] || 'game-card-cover--chess';
const cardClass = isPlayable ? 'game-card card-accent' : 'game-card game-card--soon';
let html = '<div class="' + cardClass + '" data-game="' + game.game_key + '">';
html += '<div class="game-card-cover ' + coverClass + '">';
html += '<svg class="game-card-icon"><use href="/public/icons/sprite.svg#' + icon + '"></use></svg>';
if (!isPlayable) {
html += '<span class="game-card-badge">قريبا</span>';
}
html += '</div>';
html += '<div class="game-card-info">';
html += '<div class="game-card-meta">';
html += '<span class="game-card-name">' + game.name_ar + '</span>';
if (isPlayable) {
html += '<span class="online-badge" id="online-' + game.game_key + '"><span class="online-dot"></span> --</span>';
}
html += '</div>';
if (isPlayable) {
html += '<a href="' + route + '" class="btn btn-cyan btn-sm game-card-play">العب</a>';
}
html += '</div></div>';
return html;
}).join('');
grid.querySelectorAll('.game-card[data-game]').forEach(card => {
card.addEventListener('click', (e) => {
if (e.target.closest('.game-card-play')) return;
const key = card.dataset.game;
if (gameRoutes[key]) window.location.href = gameRoutes[key];
});
});
} catch (e) {
grid.innerHTML = '<div class="empty-state" style="grid-column:1/-1;"><p>حدث خطأ في تحميل الالعاب</p></div>';
}
});
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB'; ?>
<?php require __DIR__ . '/../includes/header-v2.php'; ?>
<div class="stack-5" id="home">
<!-- Welcome + Daily Streak (compact bar) -->
<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>
<!-- GAMES: The hero of the app -->
<section>
<div class="sec-header">
<h2 class="sec-title">العب</h2>
</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>
<a href="/profile" class="sec-link">الكل</a>
</div>
<div class="stats-row" id="home-ratings"></div>
</section>
<!-- Recent Games (all games, not just chess) -->
<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('hdr-level-val').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_reward === new Date().toISOString().slice(0, 10);
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('');
}
} catch (e) {}
// Games grid — this is the HERO of the page
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');
}
});
});
</script>
<?php require __DIR__ . '/../includes/footer-v2.php'; ?>
<?php $pageTitle = 'EL3AB - الرئيسية'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="space-y-6" id="home-content">
<!-- Welcome -->
<div class="text-center">
<h2 style="font-size:22px;font-weight:700;" id="home-welcome">اهلا يا لاعب</h2>
<p class="text-muted text-sm" id="home-subtitle">المستوى 1</p>
</div>
<!-- Play Button -->
<a href="/games" class="btn btn-gold btn-block btn-lg">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-games"></use></svg>
العب الان
</a>
<!-- Daily Reward + Streak Calendar -->
<div class="card">
<div class="card-body" style="display:flex;align-items:center;justify-content:space-between;">
<div style="display:flex;align-items:center;gap:12px;">
<svg class="icon-lg" style="color:var(--warning)"><use href="/public/icons/sprite.svg#icon-fire"></use></svg>
<div>
<p style="font-size:14px;font-weight:600;" id="home-streak">اليوم 0</p>
<p class="text-muted text-xs" id="home-streak-reward">+50 عملة</p>
</div>
</div>
<button class="btn btn-cyan btn-sm" id="home-claim-btn">اجمع</button>
</div>
<div class="streak-strip" id="streak-strip">
<div class="streak-day" data-day="1"><span class="streak-day-num">1</span></div>
<div class="streak-day" data-day="2"><span class="streak-day-num">2</span></div>
<div class="streak-day" data-day="3"><span class="streak-day-num">3</span></div>
<div class="streak-day" data-day="4"><span class="streak-day-num">4</span></div>
<div class="streak-day" data-day="5"><span class="streak-day-num">5</span></div>
<div class="streak-day" data-day="6"><span class="streak-day-num">6</span></div>
<div class="streak-day" data-day="7"><span class="streak-day-num">7</span></div>
</div>
</div>
<!-- Recent Games -->
<section>
<p class="section-title">اخر المباريات</p>
<div class="card" id="home-recent-games">
<div class="empty-state">لم تلعب اي مباراة بعد</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-welcome').textContent = 'اهلا يا ' + (p.display_name || p.username);
document.getElementById('home-subtitle').textContent = 'المستوى ' + (p.level || 1) + ' • ' + (p.elo_blitz || 1200) + ' بليتز';
const streak = p.daily_streak || 0;
document.getElementById('home-streak').textContent = 'اليوم ' + streak;
// Load reward config from API
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('home-streak-reward').textContent = '+' + (base + streak * bonus) + ' عملة';
}).catch(() => {
document.getElementById('home-streak-reward').textContent = '+' + (50 + streak * 10) + ' عملة';
});
// Update streak calendar strip
const claimedToday = p.last_daily_claim === new Date().toISOString().slice(0, 10);
const days = document.querySelectorAll('.streak-day');
days.forEach((day, i) => {
const dayNum = i + 1;
const streakDay = ((streak - 1) % 7) + 1;
if (dayNum < streakDay || (dayNum === streakDay && claimedToday)) {
day.classList.add('streak-day--claimed');
} else if (dayNum === streakDay + (claimedToday ? 0 : 1) || (streak === 0 && dayNum === 1)) {
day.classList.add('streak-day--current');
}
});
if (claimedToday) {
document.getElementById('home-claim-btn').textContent = 'تم';
document.getElementById('home-claim-btn').disabled = true;
document.getElementById('home-claim-btn').classList.add('btn-ghost');
document.getElementById('home-claim-btn').classList.remove('btn-cyan');
}
}
const [gamesData, 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.split(' ')[0]; });
}
if (gamesData && gamesData.games && gamesData.games.length > 0) {
const container = document.getElementById('home-recent-games');
container.innerHTML = gamesData.games.slice(0, 5).map(g => {
const resultClass = g.result === 'win' ? 'color:var(--success)' : g.result === 'loss' ? 'color:var(--error)' : 'color:var(--text-3)';
const resultText = g.result === 'win' ? 'فوز' : g.result === 'loss' ? 'خسارة' : 'تعادل';
const resultIcon = g.result === 'win' ? 'check' : g.result === 'loss' ? 'x' : 'clock';
const botName = botMap[g.bot_id] || g.bot_id || '?';
return '<div style="display:flex;align-items:center;gap:12px;padding:10px 16px;border-bottom:1px solid var(--border);">' +
'<svg class="icon" style="' + resultClass + '"><use href="/public/icons/sprite.svg#icon-' + resultIcon + '"></use></svg>' +
'<div style="flex:1;"><p style="font-size:13px;font-weight:600;">ضد ' + botName + '</p></div>' +
'<span class="badge" style="' + resultClass + '">' + resultText + '</span>' +
'</div>';
}).join('');
}
document.getElementById('home-claim-btn').addEventListener('click', async () => {
const res = await App.fetch('/api/daily-reward', { method: 'POST' });
if (res && res.ok) {
App.toast('+' + res.reward + ' عملة!', 'success');
document.getElementById('home-claim-btn').textContent = 'تم';
document.getElementById('home-claim-btn').disabled = true;
document.getElementById('home-streak').textContent = 'اليوم ' + res.streak;
App.loadProfile();
} else if (res && res.error === 'already_claimed') {
App.toast('لقد جمعت المكافأة اليوم', 'error');
}
});
});
</script>
<?php require __DIR__ . '/../includes/footer.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 - المتصدرين'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="space-y-6">
<div class="text-center">
<h2 style="font-size:22px;font-weight:700;">المتصدرين</h2>
</div>
<!-- Category Tabs -->
<div class="tab-group" id="lb-tabs">
<button class="tab active" data-mode="blitz">بليتز</button>
<button class="tab" data-mode="rapid">رابيد</button>
<button class="tab" data-mode="bullet">بوليت</button>
</div>
<!-- Top 3 Podium -->
<div id="podium" style="display:flex;align-items:flex-end;justify-content:center;gap:8px;padding:16px 0;">
<div class="text-center" id="podium-2" style="flex:1;">
<div class="avatar" style="margin:0 auto 8px;background:var(--bg-3);">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-profile"></use></svg>
</div>
<p class="text-xs" style="font-weight:600;">---</p>
<p class="text-xs text-muted">---</p>
</div>
<div class="text-center" id="podium-1" style="flex:1;">
<svg class="icon-lg" style="color:var(--gold);margin-bottom:4px;"><use href="/public/icons/sprite.svg#icon-crown"></use></svg>
<div class="avatar avatar-lg" style="margin:0 auto 8px;border:2px solid var(--gold);background:var(--bg-3);">
<svg class="icon-lg"><use href="/public/icons/sprite.svg#icon-profile"></use></svg>
</div>
<p style="font-size:14px;font-weight:700;">---</p>
<p class="text-xs text-muted">---</p>
</div>
<div class="text-center" id="podium-3" style="flex:1;">
<div class="avatar" style="margin:0 auto 8px;background:var(--bg-3);">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-profile"></use></svg>
</div>
<p class="text-xs" style="font-weight:600;">---</p>
<p class="text-xs text-muted">---</p>
</div>
</div>
<!-- Full List -->
<div class="card" id="leaderboard-list">
<div class="empty-state">جاري التحميل...</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
let currentMode = 'blitz';
document.querySelectorAll('#lb-tabs .tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('#lb-tabs .tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
currentMode = tab.dataset.mode;
loadLeaderboard(currentMode);
});
});
loadLeaderboard(currentMode);
});
async function loadLeaderboard(mode) {
const data = await App.fetch('/api/leaderboard?mode=' + mode);
if (!data || !data.players) return;
const players = data.players;
if (players.length >= 1) {
updatePodium('podium-1', players[0], mode);
}
if (players.length >= 2) {
updatePodium('podium-2', players[1], mode);
}
if (players.length >= 3) {
updatePodium('podium-3', players[2], mode);
}
const list = document.getElementById('leaderboard-list');
if (players.length > 3) {
list.innerHTML = players.slice(3).map((p, i) => `
<div style="display:flex;align-items:center;gap:12px;padding:12px 16px;border-bottom:1px solid var(--border);">
<span style="font-weight:700;font-family:var(--font-en);min-width:24px;color:var(--text-3);">${i + 4}</span>
<div class="avatar avatar-sm" style="background:var(--bg-3);">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-profile"></use></svg>
</div>
<div style="flex:1;">
<p style="font-size:14px;font-weight:600;">${p.display_name || p.username}</p>
</div>
<span style="font-weight:700;font-family:var(--font-en);">${p['elo_' + mode] || 1200}</span>
</div>
`).join('');
} else if (players.length === 0) {
list.innerHTML = '<div class="empty-state">لا يوجد لاعبين بعد</div>';
} else {
list.innerHTML = '';
}
}
function updatePodium(elementId, player, mode) {
const el = document.getElementById(elementId);
const nameEl = el.querySelector('p:not(.text-muted)');
const ratingEl = el.querySelector('.text-muted:last-child');
if (nameEl) nameEl.textContent = player.display_name || player.username || '---';
if (ratingEl) ratingEl.textContent = (player['elo_' + mode] || 1200) + ' ELO';
}
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $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 - تسجيل الدخول'; ?>
<!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="#050D17">
<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.css">
</head>
<body>
<div class="app" style="justify-content:center;align-items:center;padding:20px;">
<div style="width:100%;max-width:380px;">
<div class="text-center mb-6">
<h1 style="font-family:var(--font-en);font-size:36px;font-weight:800;color:var(--gold);margin-bottom:8px;">EL3AB</h1>
<p style="color:var(--text-2);font-size:14px;">سجل دخولك للعب</p>
</div>
<form id="login-form" class="space-y-4">
<div class="input-group">
<label class="input-label">البريد الالكتروني</label>
<input type="email" class="input" id="login-email" placeholder="email@example.com" required dir="ltr">
</div>
<div class="input-group">
<label class="input-label">كلمة المرور</label>
<input type="password" class="input" id="login-password" placeholder="********" required dir="ltr">
</div>
<button type="submit" class="btn btn-gold btn-block btn-lg" id="login-btn">
تسجيل الدخول
</button>
</form>
<p class="text-center mt-4" style="font-size:13px;color:var(--text-3);">
ما عندك حساب؟ <a href="/register" style="color:var(--cyan);">سجل الان</a>
</p>
<div id="login-error" style="display:none;margin-top:16px;padding:12px;background:var(--overlay-error-bg);border:1px solid var(--overlay-error-border);border-radius:var(--radius-md);color:var(--error);font-size:13px;text-align:center;"></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 (App.isLoggedIn()) window.location.href = '/';
</script>
</body>
</html>
<?php
$pageTitle = 'EL3AB - لودو';
$extraCss = ['/public/css/ludo.css'];
$extraJs = ['/public/js/ludo-constants.js', '/public/js/ludo-ui.js', '/public/js/ludo-bot.js', '/public/js/ludo-game.js'];
?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="ludo-layout">
<div class="ludo-board-column">
<div class="ludo-players-row" id="ludo-top-players"></div>
<div class="ludo-board-wrapper">
<div id="ludo-board"></div>
</div>
<div class="ludo-players-row" id="ludo-bottom-players"></div>
<div class="ludo-mobile-panel">
<div class="ludo-turn-indicator" id="ludo-turn-mobile"></div>
<div id="ludo-dice-container-mobile"></div>
</div>
</div>
<div class="ludo-side-panel">
<div class="ludo-turn-indicator" id="ludo-turn"></div>
<div id="ludo-dice-container"></div>
<div class="ludo-log" id="ludo-log"></div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var params = new URLSearchParams(window.location.search);
var mode = params.get('mode') || 'local';
var playerCount = parseInt(params.get('players') || '4', 10);
var botCount = parseInt(params.get('bots') || '1', 10);
var difficulty = params.get('difficulty') || 'easy';
var allPlayers = ['P1', 'P2', 'P3', 'P4'];
var activePlayers, bots, playerNames;
if (mode === 'bot') {
var totalPlayers = 1 + botCount;
if (totalPlayers > 4) totalPlayers = 4;
activePlayers = allPlayers.slice(0, totalPlayers);
bots = {};
playerNames = {};
playerNames['P1'] = 'انت';
for (var i = 1; i < totalPlayers; i++) {
bots[allPlayers[i]] = difficulty;
playerNames[allPlayers[i]] = 'بوت ' + i;
}
} else {
if (playerCount < 2) playerCount = 2;
if (playerCount > 4) playerCount = 4;
activePlayers = allPlayers.slice(0, playerCount);
bots = {};
playerNames = {};
activePlayers.forEach(function(p) {
playerNames[p] = LudoConstants.PLAYER_LABELS[p];
});
}
LudoUI.renderBoard('#ludo-board');
var isMobile = window.innerWidth < 768;
var diceContainer = isMobile ? '#ludo-dice-container-mobile' : '#ludo-dice-container';
var turnEl = isMobile ? document.getElementById('ludo-turn-mobile') : document.getElementById('ludo-turn');
LudoUI.renderDice(diceContainer);
LudoUI.setTurnElement(turnEl);
LudoUI.setLogElement(document.getElementById('ludo-log'));
LudoUI.renderPlayerCards(
document.getElementById('ludo-top-players'),
document.getElementById('ludo-bottom-players'),
activePlayers,
playerNames
);
LudoGame.init({
players: activePlayers,
mode: mode,
bots: bots,
difficulty: difficulty,
playerNames: playerNames,
onGameEnd: function(winners) {
var playAgainBtn = document.getElementById('ludo-play-again');
if (playAgainBtn) {
playAgainBtn.addEventListener('click', function() {
LudoGame.restart({
players: activePlayers,
mode: mode,
bots: bots,
difficulty: difficulty,
playerNames: playerNames
});
});
}
}
});
});
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php
$pageTitle = 'EL3AB - لودو اونلاين';
$extraCss = ['/public/css/ludo.css'];
$extraJs = ['/public/js/ludo-constants.js', '/public/js/ludo-ui.js', '/public/js/ludo-bot.js', '/public/js/ludo-chat.js', '/public/js/ludo-live.js'];
?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="ludo-layout" id="ludo-page" style="display:none;">
<div class="ludo-board-column">
<div class="ludo-players-row" id="ludo-top-players"></div>
<div class="ludo-board-wrapper">
<div id="ludo-board"></div>
</div>
<div class="ludo-players-row" id="ludo-bottom-players"></div>
<div class="ludo-mobile-panel">
<div class="ludo-turn-indicator" id="ludo-turn-mobile"></div>
<div id="ludo-dice-container-mobile"></div>
</div>
</div>
<div class="ludo-side-panel">
<div class="ludo-turn-indicator" id="ludo-turn"></div>
<div id="ludo-dice-container"></div>
<div class="ludo-log" id="ludo-log"></div>
<div id="ludo-chat"></div>
</div>
</div>
<!-- Waiting Room -->
<div id="ludo-waiting" class="space-y-6" style="padding:24px;text-align:center;">
<h2 style="font-size:22px;font-weight:700;">غرفة لودو</h2>
<div class="card" id="waiting-card">
<div class="card-body space-y-4">
<div id="room-code-display" style="display:none;">
<p class="text-muted text-sm">كود الغرفة</p>
<p id="room-code-value" style="font-size:32px;font-weight:700;letter-spacing:6px;color:var(--gold);"></p>
<button class="btn btn-ghost btn-sm" onclick="copyRoomCode()">نسخ الكود</button>
</div>
<div id="waiting-players" class="space-y-2"></div>
<div id="waiting-status">
<div class="spinner" style="margin:0 auto;"></div>
<p class="text-muted text-sm" style="margin-top:8px;">في انتظار اللاعبين...</p>
</div>
<button class="btn btn-gold btn-block" id="start-game-btn" style="display:none;" onclick="startGame()">ابدأ اللعبة</button>
<button class="btn btn-ghost btn-block" onclick="leaveRoom()">مغادرة</button>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var params = new URLSearchParams(window.location.search);
var action = params.get('action');
var code = params.get('code');
var matchId = params.get('id');
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
var token = App.token;
var userId = App.user ? App.user.id : null;
LudoLive.setAuth(token, userId);
if (matchId) {
startLiveGame(matchId);
return;
}
if (action === 'create') {
LudoLive.createRoom(4, [], function(data) {
if (data.ok) {
showWaitingRoom(data.match, data.room_code);
initLiveConnection(data.match.id);
} else {
App.toast(data.error || 'خطأ', 'error');
}
});
} else if (action === 'join' && code) {
LudoLive.joinRoom(code, function(data) {
if (data.ok) {
showWaitingRoom(data.match, data.match.room_code);
initLiveConnection(data.match.id);
} else {
App.toast(data.error === 'room_not_found' ? 'الغرفة غير موجودة' : (data.error || 'خطأ'), 'error');
setTimeout(function() { window.location.href = '/ludo'; }, 1500);
}
});
}
function initLiveConnection(id) {
LudoLive.init({
matchId: id,
userId: userId,
token: token
});
}
function showWaitingRoom(match, roomCode) {
var codeDisplay = document.getElementById('room-code-display');
var codeValue = document.getElementById('room-code-value');
codeDisplay.style.display = 'block';
codeValue.textContent = roomCode || match.room_code || '';
updateWaitingPlayers(match);
if (match.host_id === userId) {
document.getElementById('start-game-btn').style.display = 'block';
}
// Watch for status change
var checkInterval = setInterval(function() {
var st = LudoLive.getState();
if (st.match) {
var m = st.match;
var status = m.status;
updateWaitingPlayers(m);
if (status === 'in_progress') {
clearInterval(checkInterval);
startLiveGame(m.id);
}
}
}, 1000);
}
function updateWaitingPlayers(match) {
var players = typeof match.players === 'string' ? JSON.parse(match.players) : match.players;
var container = document.getElementById('waiting-players');
container.innerHTML = '';
players.forEach(function(p) {
var el = document.createElement('div');
el.style.cssText = 'display:flex;align-items:center;gap:8px;padding:8px;border-radius:8px;background:var(--bg-3);';
el.innerHTML = '<div style="width:12px;height:12px;border-radius:50%;background:var(--ludo-' + p.color.toLowerCase() + ');"></div>' +
'<span>' + p.name + '</span>' +
'<span class="text-muted text-sm" style="margin-right:auto;">' + (p.type === 'bot' ? 'بوت' : '') + '</span>';
container.appendChild(el);
});
}
window.startGame = function() {
LudoLive.startGame(function(data) {
if (data.ok) {
startLiveGame(data.match.id);
} else {
App.toast(data.error || 'خطأ', 'error');
}
});
};
window.startLiveGame = startLiveGame;
function startLiveGame(id) {
document.getElementById('ludo-waiting').style.display = 'none';
document.getElementById('ludo-page').style.display = '';
if (!LudoLive.getState().matchId) {
LudoLive.init({
matchId: id,
userId: userId,
token: token
});
}
LudoLive.bindUI();
LudoLive.fetchState();
}
window.leaveRoom = function() {
LudoLive.leave();
window.location.href = '/ludo';
};
window.copyRoomCode = function() {
var code = document.getElementById('room-code-value').textContent;
if (navigator.clipboard) {
navigator.clipboard.writeText(code);
App.toast('تم نسخ الكود', 'success');
}
};
});
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php
$pageTitle = 'EL3AB - البحث عن خصم';
$extraCss = ['/public/css/ludo.css'];
?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="space-y-6" style="padding:24px;text-align:center;">
<h2 style="font-size:22px;font-weight:700;">البحث عن خصم - لودو</h2>
<div class="card">
<div class="card-body space-y-4" style="padding:32px;">
<div id="mm-searching">
<div style="width:80px;height:80px;margin:0 auto;border-radius:50%;background:linear-gradient(135deg, var(--ludo-p1), var(--ludo-p3));display:flex;align-items:center;justify-content:center;">
<svg class="icon-lg" style="color:var(--text-1);animation:spin 2s linear infinite;"><use href="/public/icons/sprite.svg#icon-search"></use></svg>
</div>
<p style="font-size:18px;font-weight:600;margin-top:16px;">جاري البحث...</p>
<p class="text-muted text-sm">نبحث عن لاعب بمستواك</p>
<div id="mm-timer" style="font-size:24px;font-weight:700;color:var(--gold);margin-top:12px;">0:00</div>
</div>
<button class="btn btn-ghost btn-block" onclick="cancelSearch()">الغاء</button>
</div>
</div>
</div>
<style>
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
var token = App.token;
var timerEl = document.getElementById('mm-timer');
var startTime = Date.now();
var pollInterval = null;
var timerInterval = null;
var cancelled = false;
function updateTimer() {
var elapsed = Math.floor((Date.now() - startTime) / 1000);
var mins = Math.floor(elapsed / 60);
var secs = elapsed % 60;
timerEl.textContent = mins + ':' + (secs < 10 ? '0' : '') + secs;
}
function poll() {
if (cancelled) return;
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/ludo', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Authorization', 'Bearer ' + token);
xhr.onload = function() {
try {
var data = JSON.parse(xhr.responseText);
if (data.ok && data.status === 'matched' && data.match_id) {
clearInterval(pollInterval);
clearInterval(timerInterval);
window.location.href = '/ludo-live?id=' + data.match_id;
}
} catch(e) {}
};
xhr.send(JSON.stringify({ action: 'matchmake', sub_action: 'join' }));
}
timerInterval = setInterval(updateTimer, 1000);
poll();
pollInterval = setInterval(poll, 3000);
window.cancelSearch = function() {
cancelled = true;
clearInterval(pollInterval);
clearInterval(timerInterval);
var xhr = new XMLHttpRequest();
xhr.open('POST', '/api/ludo', true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Authorization', 'Bearer ' + token);
xhr.onload = function() { window.location.href = '/ludo'; };
xhr.send(JSON.stringify({ action: 'matchmake', sub_action: 'leave' }));
};
});
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB - لودو'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="lobby-page">
<a href="/games" class="breadcrumb">
<svg class="icon" style="width:14px;height:14px;"><use href="/public/icons/sprite.svg#icon-arrow-right"></use></svg>
العاب
</a>
<div class="text-center" style="margin-top:16px;">
<h2 class="lobby-title">لودو</h2>
<p class="text-muted text-sm">اختر نوع اللعب</p>
</div>
<div class="lobby-cards">
<!-- Local (Pass & Play) -->
<div class="card lobby-card card-hover" style="cursor:pointer;" onclick="startLocal()">
<div class="card-body lobby-card-row">
<div class="avatar avatar-game" style="background:linear-gradient(135deg, var(--ludo-p1), var(--ludo-p3));">
<svg class="icon-lg" style="color:var(--text-1)"><use href="/public/icons/sprite.svg#icon-users"></use></svg>
</div>
<div style="flex:1;">
<p class="lobby-card-title-sm">لعب محلي</p>
<p class="text-muted text-sm">2-4 لاعبين على نفس الجهاز</p>
</div>
<svg class="icon" style="color:var(--text-3);transform:scaleX(-1)"><use href="/public/icons/sprite.svg#icon-arrow-right"></use></svg>
</div>
</div>
<!-- VS Bot -->
<div class="card lobby-card">
<div class="card-body space-y-4">
<div class="lobby-card-header">
<div class="avatar avatar-game" style="background:var(--bg-3);">
<svg class="icon-lg" style="color:var(--cyan)"><use href="/public/icons/sprite.svg#icon-bot"></use></svg>
</div>
<div>
<p class="lobby-card-title-sm">ضد البوت</p>
<p class="text-muted text-sm">العب ضد 1-3 بوتات</p>
</div>
</div>
<div>
<label class="input-label">عدد البوتات</label>
<div class="tab-group" id="bot-count-tabs">
<button class="tab active" data-count="1">1</button>
<button class="tab" data-count="2">2</button>
<button class="tab" data-count="3">3</button>
</div>
</div>
<div>
<label class="input-label">الصعوبة</label>
<div class="tab-group" id="bot-diff-tabs">
<!-- Loaded from API, fallback below -->
<button class="tab active" data-diff="easy">سهل</button>
<button class="tab" data-diff="hard">صعب</button>
</div>
</div>
<button class="btn btn-gold btn-block btn-lg" onclick="startBot()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-play"></use></svg>
ابدأ اللعب
</button>
</div>
</div>
<!-- Multiplayer -->
<div class="card lobby-card lobby-card--featured">
<div class="card-body space-y-4">
<div class="lobby-card-header">
<div class="avatar avatar-game" style="background:linear-gradient(135deg, var(--gold), var(--cyan));">
<svg class="icon-lg" style="color:var(--text-1)"><use href="/public/icons/sprite.svg#icon-sword"></use></svg>
</div>
<div>
<p class="lobby-card-title">ضد لاعب حقيقي</p>
<p class="text-muted text-sm">العب اونلاين ضد لاعبين حقيقيين</p>
</div>
</div>
<button class="btn btn-gold btn-block btn-lg" onclick="startMatchmaking()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-search"></use></svg>
ابحث عن خصم
</button>
</div>
</div>
<!-- Private Room -->
<div class="card lobby-card">
<div class="card-body space-y-4">
<div class="lobby-card-header">
<div class="avatar avatar-game" style="background:var(--bg-3);">
<svg class="icon-lg" style="color:var(--ludo-p4)"><use href="/public/icons/sprite.svg#icon-lock"></use></svg>
</div>
<div>
<p class="lobby-card-title-sm">غرفة خاصة</p>
<p class="text-muted text-sm">انشئ غرفة وادعو اصحابك</p>
</div>
</div>
<div class="lobby-btn-pair">
<button class="btn btn-gold" onclick="createRoom()">انشئ غرفة</button>
<button class="btn btn-ghost" onclick="showJoinRoom()">انضم بكود</button>
</div>
<div id="join-room-form" style="display:none;">
<div class="lobby-code-row">
<input type="text" class="input" id="room-code-input" placeholder="ادخل كود الغرفة" maxlength="6" style="text-transform:uppercase;letter-spacing:4px;text-align:center;">
<button class="btn btn-gold" onclick="joinRoom()">دخول</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.tab-group').forEach(function(group) {
group.querySelectorAll('.tab').forEach(function(tab) {
tab.addEventListener('click', function() {
group.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
tab.classList.add('active');
});
});
});
// Load ludo config from games API
App.cachedFetch('/api/games.php', 60000).then(function(data) {
if (!data || !data.games) return;
var ludo = data.games.find(function(g) { return g.game_key === 'ludo'; });
if (!ludo || !ludo.config || !ludo.config.difficulties) return;
var diffTabs = document.getElementById('bot-diff-tabs');
if (!diffTabs) return;
diffTabs.innerHTML = ludo.config.difficulties.map(function(d, i) {
return '<button class="tab' + (i === 0 ? ' active' : '') + '" data-diff="' + d.id + '">' + d.label + '</button>';
}).join('');
diffTabs.querySelectorAll('.tab').forEach(function(tab) {
tab.addEventListener('click', function() {
diffTabs.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
tab.classList.add('active');
});
});
}).catch(function() {});
});
function startLocal() {
window.location.href = '/ludo-game?mode=local&players=4';
}
function startBot() {
var count = document.querySelector('#bot-count-tabs .tab.active').dataset.count;
var diff = document.querySelector('#bot-diff-tabs .tab.active').dataset.diff;
window.location.href = '/ludo-game?mode=bot&bots=' + count + '&difficulty=' + diff;
}
function startMatchmaking() {
if (!App.isLoggedIn()) { window.location.href = '/login'; return; }
window.location.href = '/ludo-matchmaking';
}
function createRoom() {
if (!App.isLoggedIn()) { window.location.href = '/login'; return; }
window.location.href = '/ludo-live?action=create';
}
function showJoinRoom() {
var form = document.getElementById('join-room-form');
form.style.display = form.style.display === 'none' ? 'block' : 'none';
if (form.style.display === 'block') {
document.getElementById('room-code-input').focus();
}
}
function joinRoom() {
if (!App.isLoggedIn()) { window.location.href = '/login'; return; }
var code = document.getElementById('room-code-input').value.trim().toUpperCase();
if (!code || code.length < 4) {
App.toast('ادخل كود صحيح', 'error');
return;
}
window.location.href = '/ludo-live?action=join&code=' + code;
}
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php
$pageTitle = 'EL3AB - البحث عن خصم';
$extraCss = '/public/css/chessboard.css';
?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="matchmaking-page" id="matchmaking-page">
<!-- Searching state -->
<div class="mm-searching" id="mm-searching">
<div class="mm-animation">
<div class="mm-ring"></div>
<div class="mm-ring mm-ring-2"></div>
<svg class="mm-icon"><use href="/public/icons/sprite.svg#icon-sword"></use></svg>
</div>
<h2 class="mm-title">جاري البحث عن خصم...</h2>
<p class="mm-subtitle" id="mm-time-label">بلتز 5 دقائق</p>
<div class="mm-stats">
<div class="mm-stat">
<span class="mm-stat-label">الوقت</span>
<span class="mm-stat-value" id="mm-wait-time">0:00</span>
</div>
<div class="mm-stat">
<span class="mm-stat-label">التصنيف</span>
<span class="mm-stat-value" id="mm-rating">1200</span>
</div>
<div class="mm-stat">
<span class="mm-stat-label">النطاق</span>
<span class="mm-stat-value" id="mm-range">+/- 200</span>
</div>
</div>
<button class="btn btn-ghost btn-block" id="btn-cancel-search" onclick="Matchmaking.cancel()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-x"></use></svg>
الغاء البحث
</button>
</div>
<!-- Found state -->
<div class="mm-found" id="mm-found" style="display:none;">
<div class="mm-found-animation">
<svg class="mm-found-icon"><use href="/public/icons/sprite.svg#icon-check"></use></svg>
</div>
<h2 class="mm-title">تم ايجاد خصم!</h2>
<p class="mm-subtitle">جاري تحضير المباراة...</p>
</div>
</div>
<style>
.matchmaking-page {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 60vh;
text-align: center;
padding: 24px;
}
.mm-searching {
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
width: 100%;
max-width: 360px;
}
.mm-animation {
position: relative;
width: 120px;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
}
.mm-ring {
position: absolute;
inset: 0;
border: 3px solid transparent;
border-top-color: var(--cyan);
border-radius: 50%;
animation: mm-spin 1.5s linear infinite;
}
.mm-ring-2 {
inset: 10px;
border-top-color: var(--gold);
animation-direction: reverse;
animation-duration: 2s;
}
@keyframes mm-spin {
to { transform: rotate(360deg); }
}
.mm-icon {
width: 40px;
height: 40px;
color: var(--gold);
}
.mm-title {
font-size: 20px;
font-weight: 700;
}
.mm-subtitle {
font-size: 14px;
color: var(--text-2);
margin-top: -12px;
}
.mm-stats {
display: flex;
gap: 16px;
width: 100%;
justify-content: center;
}
.mm-stat {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 12px 16px;
min-width: 80px;
}
.mm-stat-label {
font-size: 11px;
color: var(--text-3);
}
.mm-stat-value {
font-size: 16px;
font-weight: 700;
font-family: var(--font-en);
}
/* Found state */
.mm-found {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.mm-found-animation {
width: 80px;
height: 80px;
background: var(--success);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
animation: mm-pop 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes mm-pop {
from { transform: scale(0); }
to { transform: scale(1); }
}
.mm-found-icon {
width: 40px;
height: 40px;
color: var(--text-1);
}
</style>
<script>
const Matchmaking = {
queueId: null,
pollInterval: null,
waitStart: null,
waitInterval: null,
timeControl: 'blitz_5_0',
initialTime: 300000,
increment: 0,
init() {
const params = new URLSearchParams(window.location.search);
this.timeControl = params.get('tc') || 'blitz_5_0';
this.initialTime = parseInt(params.get('time') || '300000');
this.increment = parseInt(params.get('inc') || '0');
const tcLabels = {
'bullet_1_0': 'بوليت 1 دقيقة',
'bullet_1_1': 'بوليت 1|1',
'bullet_2_1': 'بوليت 2|1',
'blitz_3_0': 'بلتز 3 دقائق',
'blitz_3_2': 'بلتز 3|2',
'blitz_5_0': 'بلتز 5 دقائق',
'blitz_5_3': 'بلتز 5|3',
'rapid_10_0': 'سريع 10 دقائق',
'rapid_10_5': 'سريع 10|5',
'rapid_15_10': 'سريع 15|10',
'rapid_30_0': 'كلاسيكي 30 دقيقة',
};
document.getElementById('mm-time-label').textContent = tcLabels[this.timeControl] || this.timeControl;
this.joinQueue();
},
async joinQueue() {
this.waitStart = Date.now();
this.startWaitTimer();
const res = await App.fetch('/api/matchmaking', {
method: 'POST',
body: JSON.stringify({
action: 'join',
time_control: this.timeControl,
initial_time_ms: this.initialTime,
increment_ms: this.increment,
is_rated: true
})
});
if (res && res.queue_id) {
this.queueId = res.queue_id;
if (res.rating) {
document.getElementById('mm-rating').textContent = res.rating;
}
if (res.matched) {
this.onMatched(res.match_id);
} else {
this.startPolling();
}
} else if (res && res.match_id) {
this.onMatched(res.match_id);
} else {
App.toast('خطأ في الاتصال', 'error');
setTimeout(() => window.location.href = '/play', 2000);
}
},
startPolling() {
this.pollInterval = setInterval(async () => {
const res = await App.fetch('/api/matchmaking', {
method: 'POST',
body: JSON.stringify({
action: 'status',
queue_id: this.queueId
})
});
if (res && res.status === 'matched' && res.match_id) {
this.onMatched(res.match_id);
}
}, 2000);
},
startWaitTimer() {
const el = document.getElementById('mm-wait-time');
this.waitInterval = setInterval(() => {
const elapsed = Math.floor((Date.now() - this.waitStart) / 1000);
const mins = Math.floor(elapsed / 60);
const secs = elapsed % 60;
el.textContent = mins + ':' + secs.toString().padStart(2, '0');
// Expand range display after time
const range = 200 + Math.floor(elapsed / 10) * 50;
document.getElementById('mm-range').textContent = '+/- ' + Math.min(range, 600);
}, 1000);
},
onMatched(matchId) {
this.stopPolling();
document.getElementById('mm-searching').style.display = 'none';
document.getElementById('mm-found').style.display = 'flex';
setTimeout(() => {
window.location.href = '/game-live?id=' + matchId;
}, 1500);
},
async cancel() {
this.stopPolling();
if (this.queueId) {
await App.fetch('/api/matchmaking', {
method: 'POST',
body: JSON.stringify({
action: 'leave',
queue_id: this.queueId
})
});
}
window.location.href = '/play';
},
stopPolling() {
if (this.pollInterval) clearInterval(this.pollInterval);
if (this.waitInterval) clearInterval(this.waitInterval);
this.pollInterval = null;
this.waitInterval = null;
}
};
document.addEventListener('DOMContentLoaded', () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
Matchmaking.init();
});
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB - الاشعارات'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="space-y-6">
<div style="display:flex;align-items:center;justify-content:space-between;">
<h2 style="font-size:22px;font-weight:700;">الاشعارات</h2>
<button class="btn btn-ghost btn-xs" id="mark-all-read">قراءة الكل</button>
</div>
<div id="notifications-list" class="space-y-2">
<div class="card">
<div class="empty-state">
<svg class="icon-lg" style="color:var(--text-3);margin-bottom:8px;"><use href="/public/icons/sprite.svg#icon-bell"></use></svg>
<p>لا يوجد اشعارات</p>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', async () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
const data = await App.fetch('/api/notifications');
if (data && data.notifications && data.notifications.length > 0) {
const list = document.getElementById('notifications-list');
list.innerHTML = data.notifications.map(n => {
const icons = {
friend_request: 'friends', game_invite: 'sword', achievement: 'star',
reward: 'coin', tournament: 'trophy', system: 'bell'
};
const icon = icons[n.type] || 'bell';
const unread = !n.read_at ? 'border-right:3px solid var(--cyan);' : '';
return `
<div class="card" style="${unread}">
<div class="card-body" style="display:flex;align-items:center;gap:12px;">
<svg class="icon" style="color:var(--cyan);flex-shrink:0;"><use href="/public/icons/sprite.svg#icon-${icon}"></use></svg>
<div style="flex:1;">
<p style="font-size:14px;">${n.message}</p>
<p class="text-muted text-xs">${timeAgo(n.created_at)}</p>
</div>
</div>
</div>
`;
}).join('');
}
document.getElementById('mark-all-read').addEventListener('click', async () => {
await App.fetch('/api/notifications', {
method: 'POST',
body: JSON.stringify({ action: 'read_all' })
});
document.querySelectorAll('#notifications-list .card').forEach(c => {
c.style.borderRight = '';
});
App.toast('تم تحديد الكل كمقروء');
});
});
function timeAgo(dateStr) {
const diff = Date.now() - new Date(dateStr).getTime();
const mins = Math.floor(diff / 60000);
if (mins < 1) return 'الان';
if (mins < 60) return mins + ' دقيقة';
const hours = Math.floor(mins / 60);
if (hours < 24) return hours + ' ساعة';
const days = Math.floor(hours / 24);
return days + ' يوم';
}
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB - المنظمة'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="space-y-6" id="org-page">
<!-- Header -->
<div class="card">
<div class="card-body text-center" style="padding:24px;">
<div class="avatar avatar-lg" style="margin:0 auto 12px;background:var(--bg-3);">
<svg class="icon-xl" style="color:var(--cyan)"><use href="/public/icons/sprite.svg#icon-org"></use></svg>
</div>
<h2 style="font-size:20px;font-weight:700;" id="org-name">---</h2>
<p class="text-muted text-sm" id="org-desc">---</p>
<div style="display:flex;gap:12px;justify-content:center;margin-top:12px;">
<div class="badge" id="org-members-count">0 عضو</div>
<div class="badge" id="org-created">---</div>
</div>
</div>
</div>
<!-- Join/Leave -->
<button class="btn btn-cyan btn-block" id="org-join-btn" style="display:none;" onclick="joinOrg()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-plus"></use></svg>
انضم
</button>
<button class="btn btn-ghost btn-block" id="org-leave-btn" style="display:none;" onclick="leaveOrg()">
تسجيل خروج من المنظمة
</button>
<!-- Stats -->
<div class="stat-grid">
<div class="stat-item">
<div class="stat-value" id="org-total-games">0</div>
<div class="stat-label">مباريات</div>
</div>
<div class="stat-item">
<div class="stat-value" id="org-avg-elo">0</div>
<div class="stat-label">متوسط ELO</div>
</div>
<div class="stat-item">
<div class="stat-value" id="org-wins">0</div>
<div class="stat-label">انتصارات</div>
</div>
</div>
<!-- Members -->
<div class="card">
<div class="card-body">
<p class="section-title" style="margin-bottom:12px;">الاعضاء</p>
<div id="org-members">
<div class="empty-state text-sm">جاري التحميل...</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', async () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
const params = new URLSearchParams(window.location.search);
const id = params.get('id');
if (!id) {
window.location.href = '/orgs';
return;
}
const data = await App.fetch('/api/orgs?action=detail&id=' + id);
if (!data || !data.org) {
App.toast('المنظمة غير موجودة', 'error');
return;
}
const org = data.org;
document.getElementById('org-name').textContent = org.name;
document.getElementById('org-desc').textContent = org.description || '';
document.getElementById('org-members-count').textContent = (org.members_count || 0) + ' عضو';
if (data.is_member) {
document.getElementById('org-leave-btn').style.display = 'flex';
} else {
document.getElementById('org-join-btn').style.display = 'flex';
}
if (data.members && data.members.length > 0) {
document.getElementById('org-members').innerHTML = data.members.map(m => `
<div style="display:flex;align-items:center;gap:12px;padding:8px 0;border-bottom:1px solid var(--border);">
<div class="avatar avatar-sm" style="background:var(--bg-3);">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-profile"></use></svg>
</div>
<div style="flex:1;">
<p style="font-size:14px;font-weight:600;">${m.display_name || m.username}</p>
<p class="text-muted text-xs">${m.role || 'عضو'}</p>
</div>
<span style="font-family:var(--font-en);font-size:12px;color:var(--text-3);">${m.elo_blitz || 1200}</span>
</div>
`).join('');
}
});
async function joinOrg() {
const params = new URLSearchParams(window.location.search);
const res = await App.fetch('/api/orgs', {
method: 'POST',
body: JSON.stringify({ action: 'join', org_id: params.get('id') })
});
if (res && res.ok) {
App.toast('تم الانضمام', 'success');
location.reload();
}
}
async function leaveOrg() {
if (!confirm('هل تريد مغادرة المنظمة؟')) return;
const params = new URLSearchParams(window.location.search);
const res = await App.fetch('/api/orgs', {
method: 'POST',
body: JSON.stringify({ action: 'leave', org_id: params.get('id') })
});
if (res && res.ok) {
window.location.href = '/orgs';
}
}
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB - المنظمات'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="space-y-6">
<div class="text-center">
<h2 style="font-size:22px;font-weight:700;">المنظمات</h2>
<p class="text-muted text-sm">انضم لمنظمة او انشئ واحدة</p>
</div>
<!-- Tabs -->
<div class="tab-group" id="orgs-tabs">
<button class="tab active" data-tab="my">منظماتي</button>
<button class="tab" data-tab="browse">تصفح</button>
</div>
<!-- My Orgs -->
<div id="tab-my" class="space-y-3">
<div id="my-orgs">
<div class="card"><div class="empty-state">
<svg class="icon-lg" style="color:var(--text-3);margin-bottom:8px;"><use href="/public/icons/sprite.svg#icon-org"></use></svg>
<p>لم تنضم لأي منظمة</p>
</div></div>
</div>
</div>
<!-- Browse -->
<div id="tab-browse" class="space-y-3" style="display:none;">
<div class="input-group">
<svg class="icon input-icon"><use href="/public/icons/sprite.svg#icon-search"></use></svg>
<input type="text" class="input" id="org-search" placeholder="ابحث عن منظمة...">
</div>
<div id="browse-orgs">
<div class="card"><div class="empty-state">جاري التحميل...</div></div>
</div>
</div>
<!-- Create -->
<button class="btn btn-cyan btn-block" onclick="showCreateOrg()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-plus"></use></svg>
انشاء منظمة
</button>
</div>
<!-- Create Modal (hidden) -->
<div id="create-org-modal" style="display:none;position:fixed;inset:0;background:var(--overlay-dark);z-index:100;display:none;align-items:center;justify-content:center;padding:20px;">
<div class="card" style="max-width:400px;width:100%;">
<div class="card-body space-y-4" style="padding:24px;">
<p style="font-size:18px;font-weight:700;text-align:center;">انشاء منظمة</p>
<div>
<label class="input-label">اسم المنظمة</label>
<input type="text" class="input" id="org-name" placeholder="اسم المنظمة">
</div>
<div>
<label class="input-label">الوصف</label>
<input type="text" class="input" id="org-desc" placeholder="وصف قصير">
</div>
<div style="display:flex;gap:8px;">
<button class="btn btn-gold" style="flex:1;" onclick="createOrg()">انشاء</button>
<button class="btn btn-ghost" style="flex:1;" onclick="hideCreateOrg()">الغاء</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
const tabs = document.querySelectorAll('#orgs-tabs .tab');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
tabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
document.getElementById('tab-my').style.display = tab.dataset.tab === 'my' ? 'block' : 'none';
document.getElementById('tab-browse').style.display = tab.dataset.tab === 'browse' ? 'block' : 'none';
});
});
loadOrgs();
let searchTimeout;
document.getElementById('org-search').addEventListener('input', (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => searchOrgs(e.target.value), 500);
});
});
async function loadOrgs() {
const data = await App.fetch('/api/orgs?action=my');
if (data && data.orgs && data.orgs.length > 0) {
document.getElementById('my-orgs').innerHTML = data.orgs.map(o => `
<a href="/org?id=${o.id}" class="card card-hover" style="display:block;text-decoration:none;">
<div class="card-body" style="display:flex;align-items:center;gap:16px;">
<div class="avatar" style="background:var(--bg-3);">
<svg class="icon-lg" style="color:var(--cyan)"><use href="/public/icons/sprite.svg#icon-org"></use></svg>
</div>
<div style="flex:1;">
<p style="font-size:15px;font-weight:600;">${o.name}</p>
<p class="text-muted text-xs">${o.members_count || 0} عضو</p>
</div>
</div>
</a>
`).join('');
}
const browseData = await App.fetch('/api/orgs?action=browse');
if (browseData && browseData.orgs) {
renderBrowseOrgs(browseData.orgs);
}
}
async function searchOrgs(query) {
if (!query || query.length < 2) return;
const data = await App.fetch('/api/orgs?action=search&q=' + encodeURIComponent(query));
if (data && data.orgs) {
renderBrowseOrgs(data.orgs);
}
}
function renderBrowseOrgs(orgs) {
const container = document.getElementById('browse-orgs');
if (orgs.length === 0) {
container.innerHTML = '<div class="card"><div class="empty-state">لا توجد نتائج</div></div>';
return;
}
container.innerHTML = orgs.map(o => `
<a href="/org?id=${o.id}" class="card card-hover" style="display:block;text-decoration:none;margin-bottom:12px;">
<div class="card-body" style="display:flex;align-items:center;gap:16px;">
<div class="avatar" style="background:var(--bg-3);">
<svg class="icon-lg" style="color:var(--cyan)"><use href="/public/icons/sprite.svg#icon-org"></use></svg>
</div>
<div style="flex:1;">
<p style="font-size:15px;font-weight:600;">${o.name}</p>
<p class="text-muted text-xs">${o.description || ''}</p>
</div>
<span class="badge">${o.members_count || 0} عضو</span>
</div>
</a>
`).join('');
}
function showCreateOrg() {
document.getElementById('create-org-modal').style.display = 'flex';
}
function hideCreateOrg() {
document.getElementById('create-org-modal').style.display = 'none';
}
async function createOrg() {
const name = document.getElementById('org-name').value.trim();
const desc = document.getElementById('org-desc').value.trim();
if (!name) { App.toast('ادخل اسم المنظمة', 'error'); return; }
const res = await App.fetch('/api/orgs', {
method: 'POST',
body: JSON.stringify({ action: 'create', name, description: desc })
});
if (res && res.ok) {
App.toast('تم انشاء المنظمة', 'success');
hideCreateOrg();
loadOrgs();
} else {
App.toast(res?.error || 'خطأ', 'error');
}
}
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $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.php'; ?>
<style>
.tc-categories {
display: flex;
gap: 4px;
margin-bottom: 8px;
}
.tc-cat {
flex: 1;
padding: 6px 4px;
font-size: 11px;
font-weight: 600;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--bg-3);
color: var(--text-2);
cursor: pointer;
text-align: center;
transition: all 0.15s;
}
.tc-cat.active {
background: var(--gold);
color: var(--text-inverse);
border-color: var(--gold);
}
.tc-cat:hover:not(.active) {
background: var(--bg-2);
}
.tc-options {
flex-wrap: wrap;
}
</style>
<div class="lobby-page">
<a href="/games" class="breadcrumb">
<svg class="icon" style="width:14px;height:14px;"><use href="/public/icons/sprite.svg#icon-arrow-right"></use></svg>
العاب
</a>
<div class="text-center" style="margin-top:16px;">
<h2 class="lobby-title">العب شطرنج</h2>
<p class="text-muted text-sm">اختر نوع المباراة</p>
</div>
<div class="lobby-cards">
<!-- VS Human - MULTIPLAYER -->
<div class="card lobby-card lobby-card--featured">
<div class="card-body space-y-4">
<div class="lobby-card-header">
<div class="avatar avatar-game" style="background:linear-gradient(135deg, var(--gold), var(--cyan));">
<svg class="icon-lg" style="color:var(--text-1)"><use href="/public/icons/sprite.svg#icon-sword"></use></svg>
</div>
<div>
<p class="lobby-card-title">ضد لاعب حقيقي</p>
<p class="text-muted text-sm">ابحث عن خصم بمستواك</p>
</div>
</div>
<!-- Time Control Categories -->
<div class="tc-categories" id="mp-tc-categories">
<button class="tc-cat active" data-cat="bullet">⚡ Bullet</button>
<button class="tc-cat" data-cat="blitz">🔥 Blitz</button>
<button class="tc-cat" data-cat="rapid">⏱ Rapid</button>
<button class="tc-cat" data-cat="classical">♟ Classical</button>
</div>
<div class="tab-group tc-options" id="mp-time-tabs">
<button class="tab" data-tc="bullet_1_0" data-time="60000" data-inc="0">1+0</button>
<button class="tab active" data-tc="bullet_1_1" data-time="60000" data-inc="1000">1+1</button>
<button class="tab" data-tc="bullet_2_1" data-time="120000" data-inc="1000">2+1</button>
</div>
<button class="btn btn-gold btn-block btn-lg" onclick="startMultiplayer()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-search"></use></svg>
ابحث عن خصم
</button>
</div>
</div>
<!-- VS Bot -->
<a href="/bots" class="card lobby-card card-hover" style="display:block;text-decoration:none;">
<div class="card-body lobby-card-row">
<div class="avatar avatar-game" style="background:var(--bg-3);">
<svg class="icon-lg" style="color:var(--cyan)"><use href="/public/icons/sprite.svg#icon-bot"></use></svg>
</div>
<div style="flex:1;">
<p class="lobby-card-title-sm">ضد البوت</p>
<p class="text-muted text-sm">7 مستويات مختلفة</p>
</div>
<svg class="icon" style="color:var(--text-3);transform:scaleX(-1)"><use href="/public/icons/sprite.svg#icon-arrow-right"></use></svg>
</div>
</a>
<!-- Quick Match vs Bot -->
<div class="card lobby-card card-hover" style="cursor:pointer;" onclick="startQuickMatch()">
<div class="card-body lobby-card-row">
<div class="avatar avatar-game" style="background:var(--bg-3);">
<svg class="icon-lg" style="color:var(--gold)"><use href="/public/icons/sprite.svg#icon-bot"></use></svg>
</div>
<div style="flex:1;">
<p class="lobby-card-title-sm">مباراة سريعة ضد بوت</p>
<p class="text-muted text-sm">5 دقائق ضد بوت عشوائي</p>
</div>
<svg class="icon" style="color:var(--text-3);transform:scaleX(-1)"><use href="/public/icons/sprite.svg#icon-arrow-right"></use></svg>
</div>
</div>
<!-- Custom Game -->
<div class="card lobby-card">
<div class="card-body space-y-5">
<p class="lobby-card-title-sm" style="text-align:center;">مباراة مخصصة</p>
<!-- Time Control -->
<div>
<label class="input-label">التوقيت</label>
<div class="tc-categories" id="custom-tc-categories">
<button class="tc-cat active" data-cat="bullet">⚡ Bullet</button>
<button class="tc-cat" data-cat="blitz">🔥 Blitz</button>
<button class="tc-cat" data-cat="rapid">⏱ Rapid</button>
<button class="tc-cat" data-cat="classical">♟ Classical</button>
</div>
<div class="tab-group tc-options" id="time-tabs">
<button class="tab" data-time="60" data-inc="0">1+0</button>
<button class="tab active" data-time="60" data-inc="1">1+1</button>
<button class="tab" data-time="120" data-inc="1">2+1</button>
</div>
</div>
<!-- Color -->
<div>
<label class="input-label">اللون</label>
<div class="tab-group" id="color-tabs">
<button class="tab active" data-color="w">ابيض</button>
<button class="tab" data-color="b">اسود</button>
<button class="tab" data-color="random">عشوائي</button>
</div>
</div>
<!-- Bot Selection -->
<div>
<label class="input-label">الخصم</label>
<select class="input" id="bot-select" style="direction:ltr;">
<option value="nour">جاري التحميل...</option>
</select>
</div>
<!-- Note: Bot games are never rated -->
<p class="text-muted text-sm" style="text-align:center;">مباراة تدريبية (غير مصنفة)</p>
<button class="btn btn-gold btn-block btn-lg" onclick="startCustomGame()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-play"></use></svg>
ابدأ المباراة
</button>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', async () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
document.querySelectorAll('.tab-group').forEach(group => {
group.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
group.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
});
});
});
// Load bots from API
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);
const selected = bot.id === 'nour' ? ' selected' : '';
return '<option value="' + bot.id + '"' + selected + '>' + bot.name + ' (' + avgElo + ')</option>';
}).join('');
window._botsData = data.bots;
}
} catch (e) {}
});
function getSelectedTime() {
const active = document.querySelector('#time-tabs .tab.active');
return { time: active.dataset.time, inc: active.dataset.inc };
}
function getSelectedColor() {
const active = document.querySelector('#color-tabs .tab.active');
let color = active.dataset.color;
if (color === 'random') color = Math.random() < 0.5 ? 'w' : 'b';
return color;
}
function startMultiplayer() {
const active = document.querySelector('#mp-time-tabs .tab.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;
}
function startQuickMatch() {
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 startCustomGame() {
const { time, inc } = getSelectedTime();
const color = getSelectedColor();
const bot = document.getElementById('bot-select').value;
window.location.href = '/game?bot=' + bot + '&color=' + color + '&time=' + time + '&inc=' + inc + '&rated=false';
}
// Time control category switching
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' },
{ time: '1200', inc: '0', label: '20+0', tc: 'rapid_20_0', timeMs: '1200000', incMs: '0' }
],
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: '2700', inc: '0', label: '45+0', tc: 'classical_45_0', timeMs: '2700000', incMs: '0' },
{ time: '3600', inc: '0', label: '60+0', tc: 'classical_60_0', timeMs: '3600000', incMs: '0' }
]
};
function setupTcCategories(catContainerId, tabContainerId, isMultiplayer) {
const catContainer = document.getElementById(catContainerId);
const tabContainer = document.getElementById(tabContainerId);
if (!catContainer || !tabContainer) return;
catContainer.querySelectorAll('.tc-cat').forEach(function(cat) {
cat.addEventListener('click', function() {
catContainer.querySelectorAll('.tc-cat').forEach(function(c) { c.classList.remove('active'); });
cat.classList.add('active');
var category = cat.dataset.cat;
var options = TC_OPTIONS[category];
var html = '';
options.forEach(function(opt, i) {
var cls = i === 0 ? ' active' : '';
if (isMultiplayer) {
html += '<button class="tab' + cls + '" data-tc="' + opt.tc + '" data-time="' + opt.timeMs + '" data-inc="' + opt.incMs + '">' + opt.label + '</button>';
} else {
html += '<button class="tab' + cls + '" data-time="' + opt.time + '" data-inc="' + opt.inc + '">' + opt.label + '</button>';
}
});
tabContainer.innerHTML = html;
// Rebind tab clicks
tabContainer.querySelectorAll('.tab').forEach(function(tab) {
tab.addEventListener('click', function() {
tabContainer.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
tab.classList.add('active');
});
});
});
});
}
setupTcCategories('mp-tc-categories', 'mp-time-tabs', true);
setupTcCategories('custom-tc-categories', 'time-tabs', false);
</script>
<?php require __DIR__ . '/../includes/footer.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'; ?>
<?php $pageTitle = 'EL3AB - الملف الشخصي'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="space-y-6" id="profile-page">
<!-- Profile Header -->
<div class="card">
<div class="card-body text-center" style="padding:24px;">
<div class="avatar avatar-lg" style="margin:0 auto 12px;">
<svg class="icon-lg"><use href="/public/icons/sprite.svg#icon-profile"></use></svg>
</div>
<h2 style="font-size:20px;font-weight:700;" id="profile-name">---</h2>
<p class="text-muted text-sm" id="profile-username">@---</p>
<div style="display:flex;gap:12px;justify-content:center;margin-top:12px;">
<div class="badge badge-gold" id="profile-level">المستوى 1</div>
<div class="badge" id="profile-title">لاعب</div>
</div>
</div>
</div>
<!-- Stats Grid -->
<div class="stat-grid stat-grid-4">
<div class="stat-card">
<div class="stat-value" id="stat-games">0</div>
<div class="stat-label">مباريات</div>
</div>
<div class="stat-card">
<div class="stat-value text-success" id="stat-wins">0</div>
<div class="stat-label">فوز</div>
</div>
<div class="stat-card">
<div class="stat-value text-muted" id="stat-draws">0</div>
<div class="stat-label">تعادل</div>
</div>
<div class="stat-card">
<div class="stat-value text-error" id="stat-losses">0</div>
<div class="stat-label">خسارة</div>
</div>
</div>
<!-- Ratings -->
<div class="card">
<div class="card-body">
<p class="section-title" style="margin-bottom:12px;">التصنيفات</p>
<div class="space-y-3">
<div style="display:flex;align-items:center;justify-content:space-between;">
<div style="display:flex;align-items:center;gap:8px;">
<svg class="icon" style="color:var(--gold)"><use href="/public/icons/sprite.svg#icon-clock"></use></svg>
<span>بليتز</span>
</div>
<span style="font-weight:700;font-family:var(--font-en);" id="rating-blitz">1200</span>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;">
<div style="display:flex;align-items:center;gap:8px;">
<svg class="icon" style="color:var(--cyan)"><use href="/public/icons/sprite.svg#icon-clock"></use></svg>
<span>رابيد</span>
</div>
<span style="font-weight:700;font-family:var(--font-en);" id="rating-rapid">1200</span>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;">
<div style="display:flex;align-items:center;gap:8px;">
<svg class="icon" style="color:var(--purple)"><use href="/public/icons/sprite.svg#icon-clock"></use></svg>
<span>بوليت</span>
</div>
<span style="font-weight:700;font-family:var(--font-en);" id="rating-bullet">1200</span>
</div>
</div>
</div>
</div>
<!-- Economy -->
<div class="card">
<div class="card-body">
<p class="section-title" style="margin-bottom:12px;">الاقتصاد</p>
<div class="stat-grid" style="grid-template-columns:1fr 1fr 1fr;">
<div class="stat-item">
<div class="stat-value" id="stat-coins">0</div>
<div class="stat-label">عملات</div>
</div>
<div class="stat-item">
<div class="stat-value" id="stat-gems">0</div>
<div class="stat-label">جواهر</div>
</div>
<div class="stat-item">
<div class="stat-value" id="stat-streak">0</div>
<div class="stat-label">ايام متتالية</div>
</div>
</div>
</div>
</div>
<!-- Actions -->
<div class="space-y-2">
<a href="/settings" class="btn btn-ghost btn-block">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-settings"></use></svg>
الاعدادات
</a>
<button class="btn btn-ghost btn-block" style="color:var(--error);" onclick="App.logout()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-logout"></use></svg>
تسجيل خروج
</button>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', async () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
const data = await App.fetch('/api/profile');
if (data && data.profile) {
const p = data.profile;
document.getElementById('profile-name').textContent = p.display_name || p.username || '---';
document.getElementById('profile-username').textContent = '@' + (p.username || '---');
document.getElementById('profile-level').textContent = 'المستوى ' + (p.level || 1);
document.getElementById('stat-games').textContent = p.games_played || 0;
document.getElementById('stat-wins').textContent = p.wins || 0;
document.getElementById('stat-draws').textContent = p.draws || 0;
document.getElementById('stat-losses').textContent = p.losses || 0;
document.getElementById('rating-blitz').textContent = p.elo_blitz || 1200;
document.getElementById('rating-rapid').textContent = p.elo_rapid || 1200;
document.getElementById('rating-bullet').textContent = p.elo_bullet || 1200;
document.getElementById('stat-coins').textContent = (p.coins || 0).toLocaleString();
document.getElementById('stat-gems').textContent = p.gems || 0;
document.getElementById('stat-streak').textContent = p.daily_streak || 0;
}
});
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php
$pageTitle = 'EL3AB - الالغاز';
$extraCss = '/public/css/chessboard.css';
?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="puzzles-page" id="puzzles-page">
<!-- Header -->
<div class="text-center" style="margin-bottom:16px;">
<h2 style="font-size:22px;font-weight:700;">الالغاز</h2>
<p class="text-muted text-sm">حل الالغاز وطور مستواك</p>
</div>
<!-- Stats bar -->
<div class="puzzle-stats" id="puzzle-stats">
<div class="puzzle-stat">
<svg class="icon-sm" style="color:var(--gold)"><use href="/public/icons/sprite.svg#icon-star"></use></svg>
<div>
<div class="puzzle-stat-value" id="puzzle-rating">1200</div>
<div class="puzzle-stat-label">التصنيف</div>
</div>
</div>
<div class="puzzle-stat">
<svg class="icon-sm" style="color:var(--cyan)"><use href="/public/icons/sprite.svg#icon-fire"></use></svg>
<div>
<div class="puzzle-stat-value" id="puzzle-streak">0</div>
<div class="puzzle-stat-label">السلسلة</div>
</div>
</div>
<div class="puzzle-stat">
<svg class="icon-sm" style="color:var(--purple)"><use href="/public/icons/sprite.svg#icon-trophy"></use></svg>
<div>
<div class="puzzle-stat-value" id="puzzle-best-streak">0</div>
<div class="puzzle-stat-label">افضل سلسلة</div>
</div>
</div>
</div>
<!-- Tabs -->
<div class="puzzle-tabs" id="puzzle-tabs">
<button class="tab active" data-tab="daily">اليومي</button>
<button class="tab" data-tab="streak">السلسلة</button>
<button class="tab" data-tab="themes">المواضيع</button>
<button class="tab" data-tab="rush">سباق</button>
</div>
<!-- Daily tab content -->
<div class="puzzle-tab-content" id="tab-daily">
<div class="card" style="margin-bottom:12px;">
<div class="card-body">
<p style="font-size:14px;font-weight:600;margin-bottom:8px;">
<svg class="icon-sm" style="color:var(--gold)"><use href="/public/icons/sprite.svg#icon-star"></use></svg>
الالغاز اليومية
</p>
<p class="text-sm text-muted" style="margin-bottom:12px;">3 الغاز جديدة كل يوم</p>
<div class="daily-progress" id="daily-progress">
<div class="daily-dot"></div>
<div class="daily-dot"></div>
<div class="daily-dot"></div>
</div>
</div>
</div>
</div>
<!-- Streak tab content -->
<div class="puzzle-tab-content" id="tab-streak" style="display:none;">
<div class="card" style="margin-bottom:12px;">
<div class="card-body text-center">
<p style="font-size:14px;font-weight:600;margin-bottom:4px;">وضع السلسلة</p>
<p class="text-sm text-muted">حل اكبر عدد على التوالي</p>
</div>
</div>
</div>
<!-- Themes tab content -->
<div class="puzzle-tab-content" id="tab-themes" style="display:none;">
<div class="puzzle-themes-grid" id="puzzle-themes-grid">
<div class="puzzle-theme-card" data-theme="fork">
<svg class="icon" style="color:var(--cyan)"><use href="/public/icons/sprite.svg#icon-sword"></use></svg>
<span>شوكة</span>
</div>
<div class="puzzle-theme-card" data-theme="pin">
<svg class="icon" style="color:var(--gold)"><use href="/public/icons/sprite.svg#icon-shield"></use></svg>
<span>تثبيت</span>
</div>
<div class="puzzle-theme-card" data-theme="skewer">
<svg class="icon" style="color:var(--error)"><use href="/public/icons/sprite.svg#icon-arrow-right"></use></svg>
<span>سيخ</span>
</div>
<div class="puzzle-theme-card" data-theme="mate">
<svg class="icon" style="color:var(--purple)"><use href="/public/icons/sprite.svg#icon-crown"></use></svg>
<span>مات</span>
</div>
<div class="puzzle-theme-card" data-theme="endgame">
<svg class="icon" style="color:var(--text-2)"><use href="/public/icons/sprite.svg#icon-flag"></use></svg>
<span>نهايات</span>
</div>
<div class="puzzle-theme-card" data-theme="opening">
<svg class="icon" style="color:var(--success)"><use href="/public/icons/sprite.svg#icon-board"></use></svg>
<span>افتتاحيات</span>
</div>
</div>
</div>
<!-- Rush tab content -->
<div class="puzzle-tab-content" id="tab-rush" style="display:none;">
<div class="card" style="margin-bottom:12px;">
<div class="card-body text-center">
<p style="font-size:14px;font-weight:600;margin-bottom:4px;">سباق الالغاز</p>
<p class="text-sm text-muted" style="margin-bottom:12px;">حل اكبر عدد خلال 3 دقائق</p>
<div class="rush-timer" id="rush-timer" style="display:none;">
<svg class="icon-sm" style="color:var(--error)"><use href="/public/icons/sprite.svg#icon-clock"></use></svg>
<span id="rush-time">3:00</span>
<span class="text-muted">|</span>
<span id="rush-count">0 حل</span>
</div>
<button class="btn btn-gold" id="btn-start-rush" onclick="Puzzles.startRush()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-fire"></use></svg>
ابدأ السباق
</button>
</div>
</div>
</div>
<!-- Puzzle board area (appears when solving) -->
<div class="puzzle-board-area" id="puzzle-board-area" style="display:none;">
<!-- Puzzle info -->
<div class="puzzle-info">
<div class="puzzle-info-rating">
<svg class="icon-sm" style="color:var(--gold)"><use href="/public/icons/sprite.svg#icon-star"></use></svg>
<span id="current-puzzle-rating">1200</span>
</div>
<div class="puzzle-info-turn" id="puzzle-turn">دور الابيض</div>
</div>
<!-- Board -->
<div class="board-wrapper">
<div class="board" id="board"></div>
</div>
<!-- Puzzle status -->
<div class="puzzle-status" id="puzzle-status">
<span>جد افضل نقلة</span>
</div>
<!-- Puzzle result overlay -->
<div class="puzzle-result" id="puzzle-result" style="display:none;">
<div class="puzzle-result-icon" id="puzzle-result-icon"></div>
<div class="puzzle-result-text" id="puzzle-result-text"></div>
<button class="btn btn-cyan btn-sm" id="btn-next-puzzle" onclick="Puzzles.nextPuzzle()">
التالي
<svg class="icon" style="transform:scaleX(-1)"><use href="/public/icons/sprite.svg#icon-arrow-right"></use></svg>
</button>
</div>
</div>
</div>
<style>
.puzzles-page {
max-width: 100%;
}
/* Stats */
.puzzle-stats {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.puzzle-stat {
flex: 1;
display: flex;
align-items: center;
gap: 8px;
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 10px 12px;
}
.puzzle-stat-value {
font-size: 16px;
font-weight: 700;
font-family: var(--font-en);
}
.puzzle-stat-label {
font-size: 11px;
color: var(--text-3);
}
/* Tabs */
.puzzle-tabs {
display: flex;
gap: 4px;
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 4px;
margin-bottom: 16px;
}
.puzzle-tabs .tab {
flex: 1;
padding: 8px 12px;
font-size: 13px;
font-weight: 600;
background: transparent;
border: none;
border-radius: var(--radius-sm);
color: var(--text-2);
cursor: pointer;
transition: all 0.2s;
}
.puzzle-tabs .tab.active {
background: var(--cyan);
color: var(--text-inverse);
}
/* Daily progress dots */
.daily-progress {
display: flex;
gap: 8px;
justify-content: center;
}
.daily-dot {
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--bg-3);
border: 2px solid var(--border);
display: flex;
align-items: center;
justify-content: center;
}
.daily-dot.solved {
background: var(--success);
border-color: var(--success);
}
.daily-dot.current {
border-color: var(--cyan);
animation: pulse 1.5s ease infinite;
}
@keyframes pulse {
50% { transform: scale(1.1); }
}
/* Themes grid */
.puzzle-themes-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
.puzzle-theme-card {
display: flex;
align-items: center;
gap: 10px;
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 14px 16px;
cursor: pointer;
transition: background 0.2s;
font-size: 14px;
font-weight: 600;
}
.puzzle-theme-card:hover { background: var(--bg-3); }
/* Rush timer */
.rush-timer {
display: flex;
align-items: center;
gap: 8px;
justify-content: center;
font-family: var(--font-en);
font-size: 18px;
font-weight: 700;
margin-bottom: 12px;
}
/* Puzzle board area */
.puzzle-board-area {
margin-top: 16px;
}
.puzzle-info {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--radius-md);
margin-bottom: 8px;
}
.puzzle-info-rating {
display: flex;
align-items: center;
gap: 4px;
font-family: var(--font-en);
font-weight: 600;
}
.puzzle-info-turn {
font-size: 13px;
color: var(--text-2);
}
.puzzle-status {
text-align: center;
padding: 10px;
font-size: 14px;
font-weight: 600;
color: var(--text-2);
}
.puzzle-status.correct {
color: var(--success);
}
.puzzle-status.wrong {
color: var(--error);
}
/* Puzzle result */
.puzzle-result {
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 16px;
text-align: center;
margin-top: 8px;
}
.puzzle-result-icon {
font-size: 32px;
margin-bottom: 8px;
}
.puzzle-result-text {
font-size: 14px;
font-weight: 600;
margin-bottom: 12px;
}
</style>
<script src="/public/js/chess.min.js"></script>
<script src="/public/js/board.js"></script>
<script src="/public/js/puzzles.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
Puzzles.init();
});
</script>
<?php require __DIR__ . '/../includes/footer.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="#050D17">
<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.css">
</head>
<body>
<div class="app" style="justify-content:center;align-items:center;padding:20px;">
<div style="width:100%;max-width:380px;">
<div class="text-center mb-6">
<h1 style="font-family:var(--font-en);font-size:36px;font-weight:800;color:var(--gold);margin-bottom:8px;">EL3AB</h1>
<p style="color:var(--text-2);font-size:14px;">انشئ حسابك وابدا اللعب</p>
</div>
<form id="register-form" class="space-y-4">
<div class="input-group">
<label class="input-label">اسم المستخدم</label>
<input type="text" class="input" id="reg-username" placeholder="player123" required dir="ltr">
</div>
<div class="input-group">
<label class="input-label">البريد الالكتروني</label>
<input type="email" class="input" id="reg-email" placeholder="email@example.com" required dir="ltr">
</div>
<div class="input-group">
<label class="input-label">كلمة المرور</label>
<input type="password" class="input" id="reg-password" placeholder="6 احرف على الاقل" required dir="ltr" minlength="6">
</div>
<button type="submit" class="btn btn-gold btn-block btn-lg" id="reg-btn">
انشاء حساب
</button>
</form>
<p class="text-center mt-4" style="font-size:13px;color:var(--text-3);">
عندك حساب؟ <a href="/login" style="color:var(--cyan);">سجل دخول</a>
</p>
<div id="reg-error" style="display:none;margin-top:16px;padding:12px;background:var(--overlay-error-bg);border:1px solid var(--overlay-error-border);border-radius:var(--radius-md);color:var(--error);font-size:13px;text-align:center;"></div>
</div>
</div>
<script src="/public/js/app.js"></script>
<script>
document.getElementById('register-form').addEventListener('submit', async (e) => {
e.preventDefault();
const btn = document.getElementById('reg-btn');
const errEl = document.getElementById('reg-error');
errEl.style.display = 'none';
btn.disabled = true;
btn.textContent = 'جاري التسجيل...';
const username = document.getElementById('reg-username').value;
const email = document.getElementById('reg-email').value;
const password = document.getElementById('reg-password').value;
try {
const res = await fetch('/api/auth', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'register', email, password, username })
});
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 (App.isLoggedIn()) window.location.href = '/';
</script>
</body>
</html>
<?php $pageTitle = 'EL3AB - الاعدادات'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="space-y-6">
<div class="text-center">
<h2 style="font-size:22px;font-weight:700;">الاعدادات</h2>
</div>
<!-- Profile Settings -->
<div class="card">
<div class="card-body space-y-4">
<p class="section-title">الملف الشخصي</p>
<div>
<label class="input-label">الاسم</label>
<input type="text" class="input" id="set-display-name" placeholder="الاسم المعروض">
</div>
<div>
<label class="input-label">اسم المستخدم</label>
<input type="text" class="input" id="set-username" placeholder="username" style="direction:ltr;">
</div>
<button class="btn btn-cyan btn-block" onclick="saveProfile()">حفظ</button>
</div>
</div>
<!-- Game Settings -->
<div class="card">
<div class="card-body space-y-4">
<p class="section-title">اعدادات اللعب</p>
<div style="display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:14px;">اصوات الحركة</span>
<label class="toggle">
<input type="checkbox" id="set-sound" checked>
<span class="toggle-slider"></span>
</label>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:14px;">تأكيد الحركة</span>
<label class="toggle">
<input type="checkbox" id="set-confirm-move">
<span class="toggle-slider"></span>
</label>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:14px;">اظهار احداثيات الرقعة</span>
<label class="toggle">
<input type="checkbox" id="set-coords" checked>
<span class="toggle-slider"></span>
</label>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:14px;">اظهار الحركات القانونية</span>
<label class="toggle">
<input type="checkbox" id="set-legal-moves" checked>
<span class="toggle-slider"></span>
</label>
</div>
<div>
<label class="input-label">سمة الرقعة</label>
<select class="input" id="set-board-theme">
<option value="default">الافتراضي (ازرق)</option>
<option value="green">اخضر كلاسيكي</option>
<option value="brown">بني خشبي</option>
<option value="purple">بنفسجي</option>
</select>
</div>
</div>
</div>
<!-- Notifications -->
<div class="card">
<div class="card-body space-y-4">
<p class="section-title">الاشعارات</p>
<div style="display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:14px;">اشعارات طلبات الصداقة</span>
<label class="toggle">
<input type="checkbox" id="set-notif-friends" checked>
<span class="toggle-slider"></span>
</label>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:14px;">اشعارات البطولات</span>
<label class="toggle">
<input type="checkbox" id="set-notif-tournaments" checked>
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
<!-- Account -->
<div class="card">
<div class="card-body space-y-4">
<p class="section-title">الحساب</p>
<button class="btn btn-ghost btn-block" style="color:var(--error);" onclick="App.logout()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-logout"></use></svg>
تسجيل خروج
</button>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', async () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
const settings = JSON.parse(localStorage.getItem('el3ab_settings') || '{}');
if (settings.sound !== undefined) document.getElementById('set-sound').checked = settings.sound;
if (settings.confirmMove !== undefined) document.getElementById('set-confirm-move').checked = settings.confirmMove;
if (settings.coords !== undefined) document.getElementById('set-coords').checked = settings.coords;
if (settings.legalMoves !== undefined) document.getElementById('set-legal-moves').checked = settings.legalMoves;
if (settings.boardTheme) document.getElementById('set-board-theme').value = settings.boardTheme;
const data = await App.fetch('/api/profile');
if (data && data.profile) {
document.getElementById('set-display-name').value = data.profile.display_name || '';
document.getElementById('set-username').value = data.profile.username || '';
}
document.querySelectorAll('[id^="set-"]').forEach(el => {
if (el.type === 'checkbox' || el.tagName === 'SELECT') {
el.addEventListener('change', saveSettings);
}
});
});
function saveSettings() {
const settings = {
sound: document.getElementById('set-sound').checked,
confirmMove: document.getElementById('set-confirm-move').checked,
coords: document.getElementById('set-coords').checked,
legalMoves: document.getElementById('set-legal-moves').checked,
boardTheme: document.getElementById('set-board-theme').value,
notifFriends: document.getElementById('set-notif-friends').checked,
notifTournaments: document.getElementById('set-notif-tournaments').checked,
};
localStorage.setItem('el3ab_settings', JSON.stringify(settings));
App.toggleSound(settings.sound);
}
async function saveProfile() {
const displayName = document.getElementById('set-display-name').value.trim();
const username = document.getElementById('set-username').value.trim();
const res = await App.fetch('/api/profile', {
method: 'PATCH',
body: JSON.stringify({ display_name: displayName, username })
});
if (res && !res.error) {
App.toast('تم حفظ الملف الشخصي', 'success');
} else {
App.toast(res?.error || 'خطأ في الحفظ', 'error');
}
}
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB - المتجر'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="space-y-6">
<div class="text-center">
<h2 style="font-size:22px;font-weight:700;">المتجر</h2>
<p class="text-muted text-sm">اشتري مظاهر وعناصر</p>
</div>
<!-- Balance -->
<div class="card">
<div class="card-body" style="display:flex;align-items:center;justify-content:space-around;padding:16px;">
<div class="text-center">
<div style="display:flex;align-items:center;gap:6px;justify-content:center;">
<svg class="icon" style="color:var(--gold)"><use href="/public/icons/sprite.svg#icon-coin"></use></svg>
<span style="font-size:18px;font-weight:700;" id="shop-coins">0</span>
</div>
<p class="text-muted text-xs">عملات</p>
</div>
<div class="text-center">
<div style="display:flex;align-items:center;gap:6px;justify-content:center;">
<svg class="icon" style="color:var(--purple)"><use href="/public/icons/sprite.svg#icon-gem"></use></svg>
<span style="font-size:18px;font-weight:700;" id="shop-gems">0</span>
</div>
<p class="text-muted text-xs">جواهر</p>
</div>
</div>
</div>
<!-- Category Tabs -->
<div class="tab-group" id="shop-tabs">
<button class="tab active" data-cat="board_theme">رقعة اللعب</button>
<button class="tab" data-cat="piece_set">طقم القطع</button>
<button class="tab" data-cat="avatar_frame">إطار الصورة</button>
<button class="tab" data-cat="trail_effect">تأثيرات الحركة</button>
</div>
<!-- Items Grid -->
<div class="space-y-3" id="shop-items">
<div class="card"><div class="empty-state">جاري التحميل...</div></div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', async () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
let currentType = 'board_theme';
document.querySelectorAll('#shop-tabs .tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('#shop-tabs .tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
currentType = tab.dataset.cat;
loadShopItems(currentType);
});
});
const profileData = await App.fetch('/api/profile');
if (profileData && profileData.profile) {
document.getElementById('shop-coins').textContent = (profileData.profile.coins || 0).toLocaleString();
document.getElementById('shop-gems').textContent = profileData.profile.gems || 0;
}
loadShopItems(currentType);
});
async function loadShopItems(type) {
const data = await App.fetch('/api/shop?type=' + type);
const container = document.getElementById('shop-items');
if (!data || !data.items || data.items.length === 0) {
container.innerHTML = '<div class="card"><div class="empty-state">لا يوجد عناصر في هذا القسم</div></div>';
return;
}
container.innerHTML = data.items.map(item => {
const owned = item.owned ? ' style="opacity:0.6;"' : '';
const hasGems = item.price_gems && item.price_gems > 0;
const hasCoins = item.price_coins && item.price_coins > 0;
const rarityColors = { common: 'var(--text-muted)', uncommon: 'var(--green)', rare: 'var(--cyan)', epic: 'var(--purple)', legendary: 'var(--gold)' };
const rarityColor = rarityColors[item.rarity] || 'var(--text-muted)';
return `
<div class="card card-hover"${owned}>
<div class="card-body" style="display:flex;align-items:center;gap:16px;">
<div class="avatar" style="background:var(--bg-3);border:2px solid ${rarityColor};">
${item.preview_url ? `<img src="${item.preview_url}" style="width:100%;height:100%;object-fit:cover;border-radius:50%;" />` : `<svg class="icon-lg" style="color:${rarityColor}"><use href="/public/icons/sprite.svg#icon-star"></use></svg>`}
</div>
<div style="flex:1;">
<p style="font-size:15px;font-weight:600;">${item.name_ar || item.name}</p>
<p class="text-muted text-xs">${item.description || ''}</p>
<span class="text-xs" style="color:${rarityColor}">${item.rarity}</span>
</div>
${item.owned ? (item.equipped ? '<span class="badge badge-success">مُجهّز</span>' : `<button class="btn btn-sm btn-outline" onclick="equipItem('${item.id}')">تجهيز</button>`) : `
<button class="btn btn-sm btn-gold" onclick="buyItem('${item.id}')">
<svg class="icon-sm" style="color:var(--gold)"><use href="/public/icons/sprite.svg#icon-coin"></use></svg>
${item.price_coins || 0}
</button>
`}
</div>
</div>
`;
}).join('');
}
async function buyItem(itemId) {
const res = await App.fetch('/api/shop', {
method: 'POST',
body: JSON.stringify({ action: 'buy', item_id: itemId })
});
if (res && res.ok) {
App.toast('تم الشراء بنجاح!', 'success');
const profileData = await App.fetch('/api/profile');
if (profileData && profileData.profile) {
document.getElementById('shop-coins').textContent = (profileData.profile.coins || 0).toLocaleString();
document.getElementById('shop-gems').textContent = profileData.profile.gems || 0;
}
const active = document.querySelector('#shop-tabs .tab.active');
loadShopItems(active.dataset.cat);
} else {
App.toast(res?.error || 'خطأ في الشراء', 'error');
}
}
async function equipItem(itemId) {
const res = await App.fetch('/api/shop', {
method: 'POST',
body: JSON.stringify({ action: 'equip', item_id: itemId, equip: true })
});
if (res && res.ok) {
App.toast('تم التجهيز!', 'success');
const active = document.querySelector('#shop-tabs .tab.active');
loadShopItems(active.dataset.cat);
} else {
App.toast(res?.error || 'خطأ في التجهيز', 'error');
}
}
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB - البطولة'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="space-y-6" id="tournament-page">
<!-- Header -->
<div class="card">
<div class="card-body text-center" style="padding:24px;">
<svg class="icon-xl" style="color:var(--gold);margin-bottom:12px;width:48px;height:48px;"><use href="/public/icons/sprite.svg#icon-trophy"></use></svg>
<h2 style="font-size:20px;font-weight:700;" id="t-name">---</h2>
<p class="text-muted text-sm" id="t-desc">---</p>
<div style="display:flex;gap:12px;justify-content:center;margin-top:16px;">
<div class="badge" id="t-status">---</div>
<div class="badge" id="t-time">---</div>
<div class="badge" id="t-players">---</div>
</div>
</div>
</div>
<!-- Join Button -->
<button class="btn btn-gold btn-block" id="t-join-btn" style="display:none;" onclick="joinTournament()">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-sword"></use></svg>
انضم للبطولة
</button>
<!-- Standings -->
<div class="card">
<div class="card-body">
<p class="section-title" style="margin-bottom:12px;">الترتيب</p>
<div id="t-standings">
<div class="empty-state text-sm">لا يوجد مشاركين بعد</div>
</div>
</div>
</div>
<!-- Rounds -->
<div class="card">
<div class="card-body">
<p class="section-title" style="margin-bottom:12px;">الجولات</p>
<div id="t-rounds">
<div class="empty-state text-sm">لم تبدأ بعد</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', async () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
const params = new URLSearchParams(window.location.search);
const id = params.get('id');
if (!id) {
window.location.href = '/tournaments';
return;
}
const data = await App.fetch('/api/tournaments?id=' + id);
if (!data || !data.tournament) {
App.toast('البطولة غير موجودة', 'error');
return;
}
const t = data.tournament;
document.getElementById('t-name').textContent = t.name;
document.getElementById('t-desc').textContent = t.description || '';
document.getElementById('t-status').textContent = t.status === 'active' ? 'جارية' : t.status === 'upcoming' ? 'قادمة' : 'منتهية';
document.getElementById('t-time').textContent = t.time_control || '5+0';
document.getElementById('t-players').textContent = (t.participants_count || 0) + ' مشارك';
if (t.status === 'upcoming' || t.status === 'active') {
document.getElementById('t-join-btn').style.display = 'flex';
}
if (data.standings && data.standings.length > 0) {
document.getElementById('t-standings').innerHTML = data.standings.map((s, i) => `
<div style="display:flex;align-items:center;gap:12px;padding:8px 0;border-bottom:1px solid var(--border);">
<span style="font-weight:700;font-family:var(--font-en);min-width:24px;color:var(--text-3);">${i + 1}</span>
<div style="flex:1;">
<p style="font-size:14px;font-weight:600;">${s.display_name || s.username}</p>
</div>
<span style="font-family:var(--font-en);font-weight:600;">${s.points || 0} نقطة</span>
</div>
`).join('');
}
});
async function joinTournament() {
const params = new URLSearchParams(window.location.search);
const id = params.get('id');
const res = await App.fetch('/api/tournaments', {
method: 'POST',
body: JSON.stringify({ action: 'join', tournament_id: id })
});
if (res && res.ok) {
App.toast('تم الانضمام بنجاح', 'success');
document.getElementById('t-join-btn').style.display = 'none';
} else {
App.toast('خطأ في الانضمام', 'error');
}
}
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
<?php $pageTitle = 'EL3AB - البطولات'; ?>
<?php require __DIR__ . '/../includes/header.php'; ?>
<div class="space-y-6">
<div class="text-center">
<h2 style="font-size:22px;font-weight:700;">البطولات</h2>
<p class="text-muted text-sm">شارك في بطولات واربح جوائز</p>
</div>
<!-- Tabs -->
<div class="tab-group" id="tourney-tabs">
<button class="tab active" data-tab="active">جارية</button>
<button class="tab" data-tab="upcoming">قادمة</button>
<button class="tab" data-tab="completed">منتهية</button>
</div>
<!-- Active Tournaments -->
<div id="tab-active" class="space-y-3">
<div id="active-tournaments">
<div class="card">
<div class="empty-state">جاري التحميل...</div>
</div>
</div>
</div>
<!-- Upcoming -->
<div id="tab-upcoming" class="space-y-3" style="display:none;">
<div id="upcoming-tournaments">
<div class="card">
<div class="empty-state">جاري التحميل...</div>
</div>
</div>
</div>
<!-- Completed -->
<div id="tab-completed" class="space-y-3" style="display:none;">
<div id="completed-tournaments">
<div class="card">
<div class="empty-state">جاري التحميل...</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
if (!App.isLoggedIn()) {
window.location.href = '/login';
return;
}
const tabs = document.querySelectorAll('#tourney-tabs .tab');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
tabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
document.querySelectorAll('[id^="tab-"]').forEach(p => p.style.display = 'none');
document.getElementById('tab-' + tab.dataset.tab).style.display = 'block';
});
});
loadTournaments();
});
async function loadTournaments() {
const data = await App.fetch('/api/tournaments');
if (!data || !data.tournaments) {
document.getElementById('active-tournaments').innerHTML = '<div class="card"><div class="empty-state">لا توجد بطولات جارية</div></div>';
document.getElementById('upcoming-tournaments').innerHTML = '<div class="card"><div class="empty-state">لا توجد بطولات قادمة</div></div>';
document.getElementById('completed-tournaments').innerHTML = '<div class="card"><div class="empty-state">لا توجد بطولات منتهية</div></div>';
return;
}
const now = new Date();
const active = [];
const upcoming = [];
const completed = [];
data.tournaments.forEach(t => {
const start = t.started_at ? new Date(t.started_at) : null;
const end = t.completed_at ? new Date(t.completed_at) : null;
if (t.status === 'completed' || (end && end < now)) {
completed.push(t);
} else if (t.status === 'active' || (start && start <= now && (!end || end > now))) {
active.push(t);
} else {
upcoming.push(t);
}
});
renderTournaments('active-tournaments', active, 'لا توجد بطولات جارية');
renderTournaments('upcoming-tournaments', upcoming, 'لا توجد بطولات قادمة');
renderTournaments('completed-tournaments', completed, 'لا توجد بطولات منتهية');
}
function renderTournaments(containerId, tournaments, emptyMsg) {
const container = document.getElementById(containerId);
if (tournaments.length === 0) {
container.innerHTML = '<div class="card"><div class="empty-state">' + emptyMsg + '</div></div>';
return;
}
container.innerHTML = tournaments.map(t => `
<a href="/tournament?id=${t.id}" class="card card-hover" style="display:block;text-decoration:none;margin-bottom:12px;">
<div class="card-body" style="display:flex;align-items:center;gap:16px;">
<div class="avatar" style="background:var(--bg-3);">
<svg class="icon-lg" style="color:var(--gold)"><use href="/public/icons/sprite.svg#icon-trophy"></use></svg>
</div>
<div style="flex:1;">
<p style="font-size:15px;font-weight:600;">${t.name}</p>
<p class="text-muted text-xs">${t.participants_count || 0} مشارك</p>
</div>
<div class="text-left">
<p class="text-xs" style="font-family:var(--font-en);font-weight:600;">${t.time_control || '5+0'}</p>
<p class="text-muted text-xs">${t.prize || ''}</p>
</div>
</div>
</a>
`).join('');
}
</script>
<?php require __DIR__ . '/../includes/footer.php'; ?>
/* 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 removed — games are individual nav items now) */
/* === 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(5, 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 FAB removed — games are the nav now) */
/* === 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);
}
/* (hero-play removed — home page is now a game hub grid, not a chess launcher) */
/* === 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; }
/* EL3AB Design System */
:root {
/* Background Layers */
--bg-0: #050D17;
--bg-1: #0A1525;
--bg-2: #142640;
--bg-3: #1C3254;
/* Brand */
--gold: #E7A832;
--gold-dark: #C48B1A;
--cyan: #15D7FF;
--cyan-dark: #0BA8C9;
--blue: #2979FF;
--purple: #7C4DFF;
/* Status */
--success: #34D399;
--error: #EF4444;
--warning: #F59E0B;
--online: #22C55E;
/* Text */
--text-1: #F1F5F9;
--text-2: #94A3B8;
--text-3: #64748B;
--text-inverse: #0F172A;
/* Border */
--border: rgba(255, 255, 255, 0.10);
--border-strong: rgba(255, 255, 255, 0.16);
/* Radius */
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-xl: 24px;
--radius-full: 9999px;
/* Shadows */
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 12px 40px rgba(0, 0, 0, 0.5);
/* Sizes */
--header-h: 56px;
--nav-bottom-h: 64px;
--nav-desktop-w: 72px;
--content-max: 600px;
--touch-min: 48px;
/* Fonts */
--font-ar: 'IBM Plex Sans Arabic', 'Segoe UI', sans-serif;
--font-en: 'Inter', 'Segoe UI', sans-serif;
/* Transitions */
--ease: cubic-bezier(0.4, 0, 0.2, 1);
/* Elevation system */
--elevation-1: 0 1px 2px rgba(0,0,0,0.2);
--elevation-2: 0 4px 12px rgba(0,0,0,0.3);
--elevation-3: 0 8px 24px rgba(0,0,0,0.4);
--elevation-4: 0 12px 40px rgba(0,0,0,0.5);
/* Responsive typography */
--text-page: clamp(22px, 2vw + 16px, 28px);
--text-section: clamp(12px, 0.5vw + 10px, 14px);
--text-body: clamp(13px, 0.5vw + 11px, 15px);
/* Spacing & Layout (admin-editable) */
--card-padding: 16px;
--section-gap: 16px;
/* Board Theme — Chess.com green */
--board-light: #EBECD0;
--board-dark: #779556;
--board-selected: rgba(255, 255, 50, 0.5);
--board-legal: rgba(0, 0, 0, 0.15);
--board-last-move: rgba(255, 255, 50, 0.4);
--board-check: rgba(255, 0, 0, 0.6);
--board-premove: rgba(21, 180, 240, 0.3);
--board-highlight-green: rgba(21, 180, 90, 0.4);
--board-highlight-red: rgba(220, 50, 50, 0.4);
--board-highlight-yellow: rgba(220, 180, 30, 0.4);
/* Eval Bar */
--eval-bg: #1a1a2e;
--eval-white: #f0f0f0;
--eval-label-light: #fff;
--eval-label-dark: #333;
/* Move Classifications */
--move-brilliant: #26c6da;
--move-great: #66bb6a;
--move-good: #81c784;
--move-book: #9e9e9e;
--move-inaccuracy: #fdd835;
--move-mistake: #ef6c00;
--move-blunder: #e53935;
/* Analysis Move Text (slightly different palette) */
--move-text-brilliant: #00bcd4;
--move-text-great: #2196f3;
--move-text-good: #4caf50;
--move-text-inaccuracy: #ff9800;
--move-text-mistake: #f44336;
--move-text-blunder: #d32f2f;
/* Arrows */
--arrow-green: rgba(21, 180, 90, 0.7);
--arrow-red: rgba(220, 50, 50, 0.7);
--arrow-yellow: rgba(220, 180, 30, 0.7);
/* Graph / Canvas */
--graph-bg: #0a1628;
--graph-grid: #333;
--graph-accent: #15d7ff;
--graph-error: #f44336;
/* Overlays */
--overlay-dark: rgba(0, 0, 0, 0.8);
--overlay-result: rgba(5, 13, 23, 0.92);
--overlay-error-bg: rgba(239, 68, 68, 0.1);
--overlay-error-border: rgba(239, 68, 68, 0.2);
/* Bot Avatars */
--bot-amina: #4ade80, #22c55e;
--bot-tarek: #38bdf8, #0ea5e9;
--bot-nour: #a78bfa, #7c3aed;
--bot-omar: #fb923c, #ea580c;
--bot-layla: #f472b6, #db2777;
--bot-ziad: #f87171, #dc2626;
--bot-gm: var(--gold), #b45309;
/* Ludo */
--ludo-p1: #E53935;
--ludo-p2: #43A047;
--ludo-p3: #FDD835;
--ludo-p4: #1E88E5;
--ludo-board-bg: #1a2332;
--ludo-path: #f5f5f5;
--ludo-path-border: rgba(0,0,0,0.1);
--ludo-safe: #FFD54F;
--ludo-home-p1: rgba(229,57,53,0.3);
--ludo-home-p2: rgba(67,160,71,0.3);
--ludo-home-p3: rgba(253,216,53,0.3);
--ludo-home-p4: rgba(30,136,229,0.3);
--ludo-dice-bg: var(--bg-2);
--ludo-dice-dot: var(--text-inverse);
--ludo-chat-bg: var(--bg-2);
}
/* 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;
-moz-osx-font-smoothing: grayscale;
}
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%;
}
/* Scrollbar */
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: var(--radius-full); }
/* Latin text override */
.font-en { font-family: var(--font-en); direction: ltr; unicode-bidi: embed; }
/* 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, .icon-lg, .icon-xl { 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; }
/* ==================== LAYOUT ==================== */
.app {
display: flex;
flex-direction: column;
min-height: 100dvh;
}
/* Header */
.header {
position: sticky;
top: 0;
z-index: 40;
height: var(--header-h);
background: rgba(10, 21, 37, 0.75);
border-bottom: 1px solid var(--border);
backdrop-filter: blur(12px) saturate(1.2);
-webkit-backdrop-filter: blur(12px) saturate(1.2);
padding-top: env(safe-area-inset-top);
}
.header-inner {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
max-width: calc(var(--content-max) + 40px);
margin: 0 auto;
padding: 0 20px;
}
.header-logo {
font-family: var(--font-en);
font-weight: 800;
font-size: 20px;
color: var(--gold);
letter-spacing: -0.5px;
}
.header-stats {
display: flex;
align-items: center;
gap: 16px;
}
.header-stat {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-weight: 500;
font-family: var(--font-en);
}
.header-bell {
position: relative;
padding: 8px;
margin: -8px;
color: var(--text-2);
}
.header-badge {
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
background: var(--error);
border-radius: var(--radius-full);
font-size: 9px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
color: var(--eval-label-light);
}
/* Main content */
.main {
flex: 1;
padding-bottom: calc(var(--nav-bottom-h) + env(safe-area-inset-bottom) + 40px);
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.main-inner {
width: 100%;
max-width: var(--content-max);
margin: 0 auto;
padding: 24px 16px 48px;
}
@media (min-width: 400px) {
.main-inner {
padding: 28px 20px 48px;
}
}
/* Desktop nav */
.nav-desktop {
display: none;
position: fixed;
left: 0;
top: 0;
bottom: 0;
width: var(--nav-desktop-w);
flex-direction: column;
align-items: center;
padding: 16px 0;
gap: 4px;
background: rgba(10, 21, 37, 0.8);
border-right: 1px solid var(--border);
backdrop-filter: blur(12px) saturate(1.2);
-webkit-backdrop-filter: blur(12px) saturate(1.2);
z-index: 50;
overflow-y: auto;
}
.nav-desktop-logo {
font-family: var(--font-en);
font-weight: 800;
font-size: 14px;
color: var(--gold);
margin-bottom: 16px;
}
.nav-desktop-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
border-radius: var(--radius-sm);
color: var(--text-3);
transition: color 0.2s var(--ease), background 0.2s var(--ease);
text-decoration: none;
}
.nav-desktop-item:hover { color: var(--text-2); }
.nav-desktop-item.active { background: rgba(21, 215, 255, 0.08); color: var(--cyan); }
.nav-desktop-label {
font-size: 9px;
margin-top: 2px;
}
/* Bottom nav (mobile) */
.nav-bottom {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 40;
height: calc(var(--nav-bottom-h) + env(safe-area-inset-bottom));
background: rgba(10, 21, 37, 0.92);
border-top: 1px solid var(--border);
backdrop-filter: blur(16px) saturate(1.3);
-webkit-backdrop-filter: blur(16px) saturate(1.3);
display: flex;
align-items: flex-start;
justify-content: space-around;
padding-top: 8px;
padding-bottom: env(safe-area-inset-bottom);
}
.nav-bottom-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 3px;
min-width: var(--touch-min);
min-height: var(--touch-min);
color: var(--text-3);
transition: color 0.2s var(--ease);
text-decoration: none;
padding: 4px 0;
}
.nav-bottom-item.active { color: var(--cyan); }
.nav-bottom-label {
font-size: 10px;
font-weight: 500;
line-height: 1;
}
/* Tablet responsive */
@media (min-width: 768px) and (max-width: 1023px) {
.main-inner {
max-width: 720px;
padding: 32px 24px 48px;
}
.games-grid {
grid-template-columns: repeat(3, 1fr);
}
.nav-bottom-label {
font-size: 11px;
}
.lobby-card .card-body {
padding: 24px;
}
.lobby-card-row {
padding: 20px 24px;
}
}
/* Desktop responsive */
@media (min-width: 1024px) {
.nav-desktop { display: flex; }
.nav-bottom { display: none; }
.main { padding-bottom: 24px; margin-right: var(--nav-desktop-w); }
.header { margin-right: var(--nav-desktop-w); }
}
/* ==================== COMPONENTS ==================== */
/* Cards */
.card {
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
overflow: hidden;
}
.card-body { padding: 18px; }
.card-body-lg { padding: 24px; }
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: var(--touch-min);
padding: 12px 24px;
font-weight: 600;
font-size: 14px;
border-radius: var(--radius-md);
transition: transform 0.1s var(--ease), opacity 0.2s var(--ease);
text-decoration: none;
white-space: nowrap;
}
.btn:active { transform: scale(0.97); }
.btn:disabled { opacity: 0.5; pointer-events: none; }
.btn-gold { background: var(--gold); color: var(--text-inverse); position: relative; overflow: hidden; }
.btn-gold::after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 60%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
animation: shimmer-sweep 3s ease-in-out infinite;
}
.btn-cyan { background: var(--cyan); color: var(--text-inverse); }
.btn-outline { background: transparent; border: 1px solid var(--border); color: var(--text-2); }
.btn-error { background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.2); color: var(--error); }
.btn-ghost { background: transparent; color: var(--text-2); }
.btn-block { display: flex; width: 100%; }
.btn-lg { min-height: 56px; font-size: 16px; font-weight: 700; border-radius: var(--radius-lg); }
.btn-sm { min-height: 36px; padding: 8px 16px; font-size: 12px; }
/* Button ripple */
.btn {
position: relative;
overflow: hidden;
}
.btn .ripple {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.25);
transform: scale(0);
animation: ripple-expand 0.5s ease-out forwards;
pointer-events: none;
}
@keyframes ripple-expand {
to { transform: scale(4); opacity: 0; }
}
/* Card hover/tap lift */
.card {
transition: transform 0.2s var(--ease), box-shadow 0.2s var(--ease);
}
.card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.card:active {
transform: translateY(-1px);
}
/* Inputs */
.input-group { margin-bottom: 16px; }
.input-label {
display: block;
font-size: 13px;
font-weight: 500;
color: var(--text-2);
margin-bottom: 6px;
}
.input {
width: 100%;
height: var(--touch-min);
padding: 0 16px;
background: var(--bg-3);
border: 1px solid var(--border);
border-radius: var(--radius-md);
color: var(--text-1);
font-size: 14px;
transition: border-color 0.2s var(--ease);
}
.input:focus { border-color: var(--cyan); }
.input::placeholder { color: var(--text-3); }
/* Section headers */
.section-title {
font-size: 12px;
font-weight: 600;
color: var(--text-3);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 12px;
}
/* Page title */
.page-title {
font-size: 28px;
font-weight: 700;
text-align: center;
margin-bottom: 24px;
}
/* Tabs */
.tabs {
display: flex;
gap: 8px;
justify-content: center;
overflow-x: auto;
padding-bottom: 4px;
margin-bottom: 24px;
}
.tabs::-webkit-scrollbar { display: none; }
.tab {
padding: 10px 18px;
font-size: 13px;
font-weight: 500;
border-radius: var(--radius-md);
white-space: nowrap;
background: var(--bg-2);
border: 1px solid var(--border);
color: var(--text-2);
transition: all 0.2s var(--ease);
min-height: 40px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.tab.active { background: var(--cyan); border-color: var(--cyan); color: var(--text-inverse); }
/* Tab group (inline selection - time control, bot difficulty, etc.) */
.tab-group {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.tab-group .tab {
flex: 1;
min-width: 56px;
text-align: center;
padding: 10px 12px;
}
/* List items */
.list-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
border-bottom: 1px solid var(--border);
}
.list-item:last-child { border-bottom: none; }
/* Avatar */
.avatar {
width: 40px;
height: 40px;
border-radius: var(--radius-full);
background: var(--bg-3);
overflow: hidden;
flex-shrink: 0;
}
.avatar img { width: 100%; height: 100%; object-fit: cover; }
.avatar-sm { width: 32px; height: 32px; }
.avatar-lg { width: 64px; height: 64px; }
.avatar-xl { width: 96px; height: 96px; }
/* Badge */
.badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
font-size: 10px;
font-weight: 600;
border-radius: var(--radius-full);
}
.badge-gold { background: rgba(231,168,50,0.15); color: var(--gold); }
.badge-cyan { background: rgba(21,215,255,0.15); color: var(--cyan); }
.badge-error { background: rgba(239,68,68,0.15); color: var(--error); }
.badge-success { background: rgba(52,211,153,0.15); color: var(--success); }
/* Toast */
.toast-container {
position: fixed;
top: calc(var(--header-h) + 12px);
left: 50%;
transform: translateX(-50%);
z-index: 100;
display: flex;
flex-direction: column;
gap: 8px;
pointer-events: none;
}
.toast {
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 12px 20px;
font-size: 13px;
font-weight: 500;
box-shadow: var(--shadow-md);
animation: toast-in 0.3s var(--ease);
pointer-events: auto;
}
.toast-success { border-color: var(--success); }
.toast-error { border-color: var(--error); }
@keyframes toast-in {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
/* Empty state */
.empty-state {
padding: 56px 24px;
text-align: center;
color: var(--text-3);
font-size: 14px;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.empty-state .icon,
.empty-state .icon-lg {
color: var(--text-3);
opacity: 0.5;
margin-bottom: 4px;
}
.empty-state p {
max-width: 240px;
line-height: 1.5;
}
/* Skeleton loader */
.skeleton {
background: var(--bg-2);
border-radius: var(--radius-md);
animation: skeleton-pulse 1.5s ease-in-out infinite;
}
@keyframes skeleton-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
/* Stat grid */
.stat-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.stat-grid-4 {
grid-template-columns: repeat(4, 1fr);
}
@media (max-width: 400px) {
.stat-grid-4 {
grid-template-columns: repeat(2, 1fr);
}
}
.stat-card {
background: var(--bg-2);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 12px;
text-align: center;
}
.stat-value {
font-size: 18px;
font-weight: 700;
font-family: var(--font-en);
}
.stat-label {
font-size: 10px;
color: var(--text-3);
margin-top: 2px;
}
/* Toggle (class-based, legacy) */
.toggle-legacy {
width: 44px;
height: 24px;
border-radius: var(--radius-full);
background: var(--bg-3);
position: relative;
cursor: pointer;
transition: background 0.2s var(--ease);
}
.toggle-legacy.active { background: var(--cyan); }
.toggle-knob {
position: absolute;
top: 2px;
right: 2px;
width: 20px;
height: 20px;
border-radius: var(--radius-full);
background: var(--eval-label-light);
transition: right 0.2s var(--ease);
}
.toggle-legacy.active .toggle-knob { right: 22px; }
/* Spacing utilities */
.space-y-2 > * + * { margin-top: 8px; }
.space-y-3 > * + * { margin-top: 12px; }
.space-y-4 > * + * { margin-top: 16px; }
.space-y-5 > * + * { margin-top: 20px; }
.space-y-6 > * + * { margin-top: 24px; }
.space-y-8 > * + * { margin-top: 32px; }
.gap-3 { gap: 12px; }
.gap-4 { gap: 16px; }
.mb-2 { margin-bottom: 8px; }
.mb-3 { margin-bottom: 12px; }
.mb-4 { margin-bottom: 16px; }
.mb-6 { margin-bottom: 24px; }
.mt-2 { margin-top: 8px; }
.mt-4 { margin-top: 16px; }
.pt-2 { padding-top: 8px; }
.pb-4 { padding-bottom: 16px; }
.text-center { text-align: center; }
.text-gold { color: var(--gold); }
.text-cyan { color: var(--cyan); }
.text-error { color: var(--error); }
.text-success { color: var(--success); }
.text-muted { color: var(--text-3); }
.text-sm { font-size: 13px; }
.text-xs { font-size: 11px; }
.fw-bold { font-weight: 700; }
/* ===== Lazy Image Placeholder ===== */
.img-lazy {
position: relative;
background: var(--bg-3);
overflow: hidden;
}
.img-lazy::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.04), transparent);
animation: skeleton-shimmer 1.5s infinite;
}
.img-lazy img {
opacity: 0;
transition: opacity 0.3s;
}
.img-lazy--loaded::before {
display: none;
}
.img-lazy--loaded img {
opacity: 1;
}
/* ===== Streak Calendar Strip ===== */
.streak-strip {
display: flex;
align-items: center;
justify-content: space-around;
padding: 12px 16px;
border-top: 1px solid var(--border);
}
.streak-day {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid var(--border-strong);
transition: all 0.3s var(--ease);
}
.streak-day--claimed {
background: var(--gold);
border-color: var(--gold);
color: var(--text-inverse);
}
.streak-day--claimed .streak-day-num {
display: none;
}
.streak-day--claimed::after {
content: '✓';
font-size: 14px;
font-weight: 700;
}
.streak-day--current {
border-color: var(--cyan);
animation: streak-pulse 1.5s ease-in-out infinite;
}
@keyframes streak-pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(21, 215, 255, 0.3); }
50% { box-shadow: 0 0 0 4px rgba(21, 215, 255, 0.1); }
}
.streak-day-num {
font-size: 11px;
font-weight: 600;
color: var(--text-3);
}
.streak-day--claimed .streak-day-num,
.streak-day--current .streak-day-num {
color: var(--text-1);
}
/* ===== Skeleton Loading ===== */
.skeleton {
background: var(--bg-3);
border-radius: var(--radius-sm);
position: relative;
overflow: hidden;
}
.skeleton::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.04), transparent);
animation: skeleton-shimmer 1.5s infinite;
}
@keyframes skeleton-shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.skeleton-text {
height: 14px;
width: 60%;
margin-bottom: 8px;
}
.skeleton-text--short { width: 40%; }
.skeleton-text--full { width: 100%; }
.skeleton-avatar {
width: 40px;
height: 40px;
border-radius: var(--radius-full);
}
.skeleton-card {
height: 80px;
border-radius: var(--radius-lg);
}
/* ===== Page Transitions ===== */
.main-inner {
animation: page-enter 0.3s var(--ease) both;
}
@keyframes page-enter {
from {
opacity: 0;
transform: translateX(12px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.page-exit {
animation: page-exit 0.2s var(--ease) both;
}
@keyframes page-exit {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(-12px);
}
}
/* ===== Gradient Accent Border ===== */
.card-accent {
position: relative;
border: none;
}
.card-accent::before {
content: '';
position: absolute;
inset: 0;
border-radius: var(--radius-lg);
padding: 2px;
background: linear-gradient(135deg, var(--gold), var(--cyan));
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
pointer-events: none;
}
/* ===== Breadcrumb ===== */
.breadcrumb {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--text-3);
text-decoration: none;
transition: color 0.2s;
}
.breadcrumb:hover {
color: var(--cyan);
}
/* ===== Games Hub ===== */
.games-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.game-card {
border-radius: var(--radius-lg);
overflow: hidden;
background: var(--bg-2);
border: 1px solid var(--border);
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.game-card:hover,
.game-card:active {
transform: translateY(-3px);
box-shadow: var(--shadow-lg);
}
.game-card--soon {
opacity: 0.6;
pointer-events: none;
}
.game-card-cover {
position: relative;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
}
.game-card-cover--chess {
background: linear-gradient(135deg, #1a2a4a, #2a4a6a);
}
.game-card-cover--ludo {
background: linear-gradient(135deg, #2a1a3a, #4a2a5a);
}
.game-card-cover--domino {
background: linear-gradient(135deg, #1a3a2a, #2a5a4a);
}
.game-card-cover--backgammon {
background: linear-gradient(135deg, #3a2a1a, #5a4a2a);
}
.game-card-cover--trix {
background: linear-gradient(135deg, #1a1a3a, #3a2a5a);
}
.game-card-cover--baloot {
background: linear-gradient(135deg, #3a1a1a, #5a2a2a);
}
.game-card-icon {
width: 40px;
height: 40px;
color: rgba(255, 255, 255, 0.9);
}
.game-card-badge {
position: absolute;
top: 8px;
left: 8px;
background: var(--gold);
color: var(--text-inverse);
font-size: 11px;
font-weight: 700;
padding: 2px 8px;
border-radius: var(--radius-full);
}
.game-card-info {
padding: 12px;
display: flex;
align-items: center;
justify-content: space-between;
}
.game-card-meta {
display: flex;
flex-direction: column;
gap: 4px;
}
.game-card-name {
font-size: 14px;
font-weight: 700;
color: var(--text-1);
}
.online-badge {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
color: var(--text-3);
}
.online-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--online);
}
.game-card-play {
white-space: nowrap;
}
@media (max-width: 767px) {
.games-grid {
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.game-card-cover {
height: 80px;
}
}
@media (min-width: 1024px) {
.games-grid {
grid-template-columns: repeat(3, 1fr);
}
.game-card-cover {
height: 120px;
}
}
/* ===== Visual Improvements ===== */
/* Better avatar placeholder */
.avatar {
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, var(--bg-3), var(--bg-2));
}
/* Profile header gradient */
.profile-card-header {
background: linear-gradient(135deg, rgba(21, 215, 255, 0.08), rgba(231, 168, 50, 0.05));
}
/* Better list items */
.list-item {
transition: background 0.15s var(--ease);
}
.list-item:active {
background: rgba(255, 255, 255, 0.03);
}
/* Better tabs */
.tab {
min-width: 64px;
text-align: center;
}
/* Section divider */
.section-divider {
height: 1px;
background: var(--border);
margin: 24px 0;
}
/* Better input focus glow */
.input:focus {
box-shadow: 0 0 0 3px rgba(21, 215, 255, 0.1);
}
/* Game mode card improvements */
.card-body {
transition: background 0.15s var(--ease);
}
/* Better button hover states */
.btn-gold:active { background: var(--gold-dark); }
.btn-cyan:active { background: var(--cyan-dark); }
/* Selection pills (time control, bot count, etc) */
.pill-group {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.pill {
min-width: 44px;
height: 36px;
padding: 0 14px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-md);
background: var(--bg-3);
border: 1px solid var(--border);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s var(--ease);
}
.pill.active,
.pill:active {
background: var(--cyan);
border-color: var(--cyan);
color: var(--text-inverse);
}
/* Progress bar component */
.progress-bar {
width: 100%;
height: 6px;
background: var(--bg-3);
border-radius: var(--radius-full);
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
border-radius: var(--radius-full);
background: linear-gradient(90deg, var(--cyan), var(--gold));
transition: width 0.5s var(--ease);
}
/* Notification dot */
.notif-dot {
position: absolute;
top: 4px;
right: 4px;
width: 8px;
height: 8px;
background: var(--error);
border-radius: 50%;
border: 2px solid var(--bg-1);
}
/* Card-body padding on mobile */
@media (max-width: 767px) {
.card-body { padding: 16px; }
.card-body-lg { padding: 20px; }
}
/* Divider with text */
.divider-text {
display: flex;
align-items: center;
gap: 12px;
color: var(--text-3);
font-size: 12px;
margin: 16px 0;
}
.divider-text::before,
.divider-text::after {
content: '';
flex: 1;
height: 1px;
background: var(--border);
}
/* Animated gradient text */
.text-gradient {
background: linear-gradient(135deg, var(--gold), var(--cyan));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Focus visible for a11y */
.btn:focus-visible,
.tab:focus-visible,
.input:focus-visible {
outline: 2px solid var(--cyan);
outline-offset: 2px;
}
/* Game result overlay improvements */
.game-result {
animation: result-in 0.4s var(--ease);
}
@keyframes result-in {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
}
/* Haptic feedback visual pulse on game actions */
.pulse-feedback {
animation: action-pulse 0.3s var(--ease);
}
@keyframes action-pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
/* ===== Win Streak Fire Badge ===== */
.streak-fire::before {
content: '🔥';
position: absolute;
top: -2px;
right: -2px;
font-size: 10px;
animation: fire-flicker 1s ease-in-out infinite alternate;
}
@keyframes fire-flicker {
from { transform: scale(1); }
to { transform: scale(1.2); }
}
/* ===== Bottom Sheet Modal ===== */
.sheet-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 90;
opacity: 0;
transition: opacity 0.3s var(--ease);
pointer-events: none;
}
.sheet-backdrop.active {
opacity: 1;
pointer-events: auto;
}
.sheet {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 91;
background: var(--bg-1);
border-top-left-radius: var(--radius-xl);
border-top-right-radius: var(--radius-xl);
padding: 16px 20px calc(20px + env(safe-area-inset-bottom));
transform: translateY(100%);
transition: transform 0.3s var(--ease);
max-height: 80vh;
overflow-y: auto;
}
.sheet.active {
transform: translateY(0);
}
.sheet-handle {
width: 36px;
height: 4px;
background: var(--text-3);
border-radius: var(--radius-full);
margin: 0 auto 16px;
}
/* ===== Pull to Refresh ===== */
.ptr-indicator {
position: fixed;
top: calc(var(--header-h) + 8px);
left: 50%;
transform: translateX(-50%) scale(0);
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--bg-2);
border: 2px solid var(--cyan);
display: flex;
align-items: center;
justify-content: center;
z-index: 35;
transition: transform 0.2s var(--ease);
}
.ptr-indicator.active {
transform: translateX(-50%) scale(1);
}
.ptr-indicator.loading {
transform: translateX(-50%) scale(1);
animation: ptr-spin 0.8s linear infinite;
}
@keyframes ptr-spin {
to { transform: translateX(-50%) scale(1) rotate(360deg); }
}
/* ===== Enhanced Toasts ===== */
.toast {
display: flex;
align-items: center;
gap: 10px;
position: relative;
overflow: hidden;
}
.toast-icon {
width: 18px;
height: 18px;
flex-shrink: 0;
}
.toast-progress {
position: absolute;
bottom: 0;
left: 0;
height: 2px;
background: var(--cyan);
animation: toast-countdown 3s linear forwards;
}
.toast-success .toast-progress { background: var(--success); }
.toast-error .toast-progress { background: var(--error); }
@keyframes toast-countdown {
from { width: 100%; }
to { width: 0%; }
}
/* ===== Nav Sliding Indicator ===== */
.nav-desktop-item.active {
position: relative;
}
.nav-desktop-item.active::after {
content: '';
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 24px;
background: var(--cyan);
border-radius: var(--radius-full);
}
.nav-bottom-item.active::after {
content: '';
position: absolute;
bottom: 8px;
left: 50%;
transform: translateX(-50%);
width: 20px;
height: 3px;
background: var(--cyan);
border-radius: var(--radius-full);
}
.nav-bottom-item {
position: relative;
}
/* ===== Animated Background (Games Hub) ===== */
.bg-animated {
background: linear-gradient(135deg, var(--bg-0), var(--bg-1), var(--bg-2), var(--bg-1));
background-size: 400% 400%;
animation: bg-shift 15s ease infinite;
}
@keyframes bg-shift {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
/* ===== Gold Shimmer ===== */
.shimmer {
position: relative;
overflow: hidden;
}
.shimmer::after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 60%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.15), transparent);
animation: shimmer-sweep 3s ease-in-out infinite;
}
@keyframes shimmer-sweep {
0% { left: -100%; }
100% { left: 150%; }
}
/* ===== Celebration Overlay ===== */
.celebrate-overlay {
position: fixed;
inset: 0;
z-index: 200;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.7);
animation: celebrate-in 0.3s var(--ease);
}
@keyframes celebrate-in {
from { opacity: 0; }
to { opacity: 1; }
}
.celebrate-title {
font-size: 28px;
font-weight: 700;
color: var(--gold);
text-shadow: 0 0 20px rgba(231, 168, 50, 0.5);
animation: celebrate-bounce 0.5s var(--ease) 0.2s both;
}
@keyframes celebrate-bounce {
0% { transform: scale(0.5); opacity: 0; }
60% { transform: scale(1.1); }
100% { transform: scale(1); opacity: 1; }
}
.confetti-particle {
position: absolute;
width: 8px;
height: 8px;
border-radius: 2px;
animation: confetti-fall 2.5s ease-out forwards;
}
@keyframes confetti-fall {
0% {
transform: translateY(0) rotate(0deg);
opacity: 1;
}
100% {
transform: translateY(100vh) rotate(720deg);
opacity: 0;
}
}
/* ===== Responsive Typography ===== */
.page-title {
font-size: var(--text-page);
}
.section-title {
font-size: var(--text-section);
}
/* ===== Safe Area Handling ===== */
@supports (padding: env(safe-area-inset-top)) {
.main-inner {
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}
}
/* ===== Landscape Game Layout ===== */
@media (orientation: landscape) and (max-height: 500px) {
.header { height: 40px; }
.header-inner { padding: 0 12px; }
.nav-bottom { height: 48px; }
.main { padding-bottom: 48px; }
}
/* ===== Clock Urgency (Chess) ===== */
.clock-urgent {
animation: clock-pulse 1s ease-in-out infinite;
color: var(--error) !important;
}
.clock-critical {
animation: clock-shake 0.3s ease-in-out infinite;
color: var(--error) !important;
}
@keyframes clock-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
@keyframes clock-shake {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-2px); }
75% { transform: translateX(2px); }
}
/* ===== Lobby Pages (Play / Ludo) ===== */
.lobby-page {
display: flex;
flex-direction: column;
gap: 16px;
}
.lobby-title {
font-size: 24px;
font-weight: 700;
margin-bottom: 4px;
}
.lobby-cards {
display: flex;
flex-direction: column;
gap: 16px;
padding-bottom: 24px;
}
.lobby-card {
border-radius: var(--radius-lg);
}
.lobby-card--featured {
border: 2px solid var(--gold);
box-shadow: 0 0 20px rgba(231, 168, 50, 0.08);
}
.lobby-card .card-body {
padding: 20px;
}
@media (max-width: 400px) {
.lobby-card .card-body {
padding: 16px;
}
}
.lobby-card-header {
display: flex;
align-items: center;
gap: 14px;
}
.lobby-card-title {
font-size: 18px;
font-weight: 700;
line-height: 1.3;
}
.lobby-card-title-sm {
font-size: 16px;
font-weight: 600;
line-height: 1.3;
}
.lobby-card-row {
display: flex;
align-items: center;
gap: 16px;
padding: 18px 20px;
}
@media (max-width: 400px) {
.lobby-card-row {
padding: 16px;
gap: 12px;
}
}
.lobby-toggle-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 0;
font-size: 14px;
}
.lobby-btn-pair {
display: flex;
gap: 10px;
}
.lobby-btn-pair .btn {
flex: 1;
}
.lobby-code-row {
display: flex;
gap: 10px;
}
.lobby-code-row .input {
flex: 1;
}
.avatar-game {
width: 48px;
height: 48px;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
/* ===== Input Label Spacing ===== */
.input-label {
margin-bottom: 8px;
}
/* ===== Toggle (checkbox-based) ===== */
.toggle {
position: relative;
display: inline-block;
width: 48px;
height: 26px;
flex-shrink: 0;
}
.toggle input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
inset: 0;
background: var(--bg-3);
border-radius: var(--radius-full);
transition: background 0.2s var(--ease);
cursor: pointer;
}
.toggle-slider::before {
content: '';
position: absolute;
top: 3px;
right: 3px;
width: 20px;
height: 20px;
border-radius: 50%;
background: white;
transition: transform 0.2s var(--ease);
}
.toggle input:checked + .toggle-slider {
background: var(--cyan);
}
.toggle input:checked + .toggle-slider::before {
transform: translateX(-22px);
}
/* ===== Better select dropdown ===== */
select.input {
appearance: none;
-webkit-appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2394A3B8' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: left 12px center;
padding-left: 36px;
}
/* ===== Scrollable page guarantee ===== */
@media (max-width: 767px) {
.main {
min-height: calc(100dvh - var(--header-h));
}
.lobby-cards {
padding-bottom: 16px;
}
}
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
<?php
$currentRoute = $_GET['route'] ?? '';
$navItems = [
['/', 'icon-home', 'الرئيسية'],
['/games', 'icon-games', 'العاب'],
['/leaderboard', 'icon-leaderboard', 'ترتيب'],
['/friends', 'icon-friends', 'اجتماعي'],
['/profile', 'icon-profile', 'حسابي'],
];
?>
<nav class="nav-m" aria-label="التنقل">
<?php foreach ($navItems as $i => $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="nav-bottom">
<?php
$currentRoute = $_GET['route'] ?? '';
$bottomItems = [
['/', 'icon-home', 'الرئيسية', null],
['/games', 'icon-games', 'العاب', null],
['/tournaments', 'icon-trophy', 'بطولات', 'tournaments_enabled'],
['/friends', 'icon-friends', 'اجتماعي', null],
['/profile', 'icon-profile', 'حسابي', null],
];
foreach ($bottomItems as $item):
$href = $item[0];
$icon = $item[1];
$label = $item[2];
$flag = $item[3];
if ($flag && !is_feature_enabled($flag)) continue;
$route = trim($href, '/');
$isActive = ($route === '' && $currentRoute === '') || ($route !== '' && $currentRoute === $route);
?>
<a href="<?= $href ?>" class="nav-bottom-item <?= $isActive ? 'active' : '' ?>">
<svg class="icon"><use href="/public/icons/sprite.svg#<?= $icon ?>"></use></svg>
<span class="nav-bottom-label"><?= $label ?></span>
</a>
<?php endforeach; ?>
</nav>
<nav class="nav-desktop">
<span class="nav-desktop-logo">E3</span>
<?php
$currentRoute = $_GET['route'] ?? '';
$navItems = [
['/', 'icon-home', 'الرئيسية', null],
['/games', 'icon-games', 'العاب', null],
['/puzzles', 'icon-puzzle', 'تمارين', null],
['/tournaments', 'icon-trophy', 'بطولات', 'tournaments_enabled'],
['/leaderboard', 'icon-leaderboard', 'متصدرون', null],
['/friends', 'icon-friends', 'اجتماعي', null],
['/orgs', 'icon-org', 'اندية', null],
['/shop', 'icon-shop', 'متجر', 'cosmetics_shop_enabled'],
['/achievements', 'icon-star', 'انجازات', null],
['/profile', 'icon-profile', 'حسابي', null],
['/settings', 'icon-settings', 'اعدادات', null],
];
foreach ($navItems as $item):
$href = $item[0];
$icon = $item[1];
$label = $item[2];
$flag = $item[3];
if ($flag && !is_feature_enabled($flag)) continue;
$route = trim($href, '/');
$isActive = ($route === '' && $currentRoute === '') || ($route !== '' && $currentRoute === $route);
?>
<a href="<?= $href ?>" class="nav-desktop-item <?= $isActive ? 'active' : '' ?>">
<svg class="icon"><use href="/public/icons/sprite.svg#<?= $icon ?>"></use></svg>
<span class="nav-desktop-label"><?= $label ?></span>
</a>
<?php endforeach; ?>
</nav>
<nav class="sidebar" aria-label="التنقل الرئيسي">
<span class="sidebar-brand">EL3AB</span>
<?php
$currentRoute = $_GET['route'] ?? '';
?>
<!-- Games ARE the primary nav -->
<div class="sidebar-section">
<span class="sidebar-label">العاب</span>
<?php
$gameNav = [
['/play', 'icon-play', 'شطرنج'],
['/ludo', 'icon-ludo', 'لودو'],
['/backgammon', 'icon-backgammon', 'طاولة'],
['/domino', 'icon-domino', 'دومينو'],
];
foreach ($gameNav 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
$competeItems = [
['/tournaments', 'icon-trophy', 'بطولات', 'tournaments_enabled'],
['/leaderboard', 'icon-leaderboard', 'المتصدرين'],
];
foreach ($competeItems 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 = [
['/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