Commit e620362f authored by Mahmoud Aglan's avatar Mahmoud Aglan

nuke v2 SPA, rebuild as single-file app with working games

The ES modules approach was overengineered and broken:
- Import maps failed silently
- Game engines couldn't find window.App
- Nothing was responsive

New approach: everything in index.php + one CSS file.
- Loads public/js/app.js (game engines need App.fetch, App.toast etc)
- Simple defineScreen() router — no modules, no imports, no build step
- Chess game works: loads chess.min.js + board.js + game.js, enters immersive mode
- Ludo/Domino/Backgammon lobbies create games via API
- All screens responsive (max-width adapts, clamp() on hero heights)
- Visual identity from DESIGN.md: game-specific gradients, 20px radius cards, gold brand
- Header shows level + coins + gems (the game currencies, not chess ELO)
- DESIGN.md added as the visual specification document
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent f39a8dcb
# EL3AB Design Specification v3
## Identity
EL3AB is a **social gaming hub for Arabic speakers** wrapping 4 classic board games (Chess, Ludo, Domino, Backgammon) in a platform that feels alive, dense, and responsive. Reference: Yalla Ludo + competitive depth.
## Design Pillars
1. **One Thumb, Full Experience** — Every primary action reachable with one thumb. < 3 taps to play.
2. **Touch = Feedback** — Every interaction responds with visual + audio + haptic simultaneously.
3. **Cards, Not Pages** — One UI primitive (the card). Cards stack, scroll, expand into sheets.
4. **Color = Game** — Each game owns a color identity. You always know which game context you're in.
5. **Always Alive** — No screen is static. Minimum: floating glow, pulsing dots, breathing buttons.
## Anti-Pillars
- NOT a web app (no material design, no breadcrumbs, no "dashboard")
- NOT minimal (dense = purposeful, every pixel works)
- NOT chess-first (all 4 games equal)
- NOT dark-for-trendy (dark bg is functional — makes colors pop)
## Color System
| Context | Primary | Secondary | Gradient |
|---------|---------|-----------|----------|
| Chess | #2563EB (blue) | #F5B731 (gold) | blue → gold |
| Ludo | #8B5CF6 (purple) | #EC4899 (pink) | purple → pink |
| Domino | #10B981 (emerald) | #06B6D4 (cyan) | emerald → cyan |
| Backgammon | #F59E0B (amber) | #EF4444 (red) | amber → red |
| Platform/Brand | #F5B731 (gold) | #00D4FF (cyan) | — |
| Background | #030A12 | #081420 | — |
| Text Primary | #F8FAFC | — | — |
| Text Secondary | #94A3B8 | — | — |
| Win | #34D399 | — | — |
| Loss | #F87171 | — | — |
## Card Anatomy
```
┌─────────────────────────────────┐
│ GRADIENT HEADER (game color) │ 60-80px
│ ◉ Icon Game Name ● ● │
├─────────────────────────────────┤
│ CONTENT BODY (bg-1) │ flexible
├─────────────────────────────────┤
│ ACTION FOOTER (optional) │ CTA/chips
└─────────────────────────────────┘
```
- Radius: 20px
- Border: 1px solid rgba(255,255,255,0.08)
- Shadow: 0 8px 32px rgba(0,0,0,0.4)
- Touch: scale(0.97) + shadow compress + haptic
## Screen Map
### Home (vertical scroll)
1. Header (fixed): Brand + Level + Coins + Gems + Notifications
2. Streak card (daily reward, pulses if unclaimed)
3. Games 2x2 grid (equal, always visible above fold)
4. My ratings (horizontal scroll pills)
5. Recent matches (last 5)
6. Friends online (avatar row)
7. Nav bar (fixed): Home | Games | Rank | Social | Me
### Game Lobby (bottom sheet, 70% screen)
- Mode selection (chips)
- Time control / bot picker (per game)
- "Play" CTA (large, game-color gradient)
- Swipe down to dismiss
### In-Game (immersive)
- Header + Nav hide
- Full-screen game board
- Opponent bar (top) + Player bar (bottom)
- Action drawer (swipe up): resign/draw/settings
### Post-Game (results card, slides up)
- W/L/D icon + confetti/shake
- Rating change (animated counter)
- XP + coins earned
- Rematch + Home buttons
## Interaction Rules
- Button press: scale(0.95) + haptic(10ms) + audio(click)
- Card press: scale(0.97) + tilt-toward-finger + shadow compress
- Navigation: slide transitions (250ms, ease-out)
- Game enter: zoom + darken
- Game exit: zoom out + brighten
- Win: confetti burst + haptic([20,50,20]) + fanfare
- Lose: subtle shake + haptic([50,30,50]) + descending tone
- Coin earn: particles fly to header counter + tick animation
- Level up: full-screen overlay + confetti + badge reveal
## Typography
- Arabic: IBM Plex Sans Arabic (400, 500, 600, 700, 800)
- Numbers: system monospace, tabular-nums
- Brand: "EL3AB" — always gold, weight 800
## Spacing
- Card padding: 16px
- Card gap: 12px
- Section gap: 20px
- Touch target minimum: 44px
- Border radius: 20px (cards), 12px (buttons), 9999px (pills/badges)
## Responsive
- Mobile-first: 320px - 480px (primary)
- Tablet: 768px+ (2-column games grid becomes 4-column, sidebar appears)
- Desktop: 1024px+ (sidebar nav, wider content area)
- Game boards: always square, max-width: min(100vw - 32px, 400px)
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 { navigate, register, init as initRouter, getParams } 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 };
import { api } from '../core/api.js';
export default {
render() {
return `
<h2 style="font-size:22px;font-weight:700;text-align:center;margin-bottom:var(--sp-4);">🏅 الانجازات</h2>
<div id="ach-list"><div class="skeleton" style="height:80px;margin-bottom:var(--sp-2);"></div><div class="skeleton" style="height:80px;margin-bottom:var(--sp-2);"></div><div class="skeleton" style="height:80px;"></div></div>
`;
},
async mount(el) {
const data = await api('/api/achievements');
const container = el.querySelector('#ach-list');
if (!data?.achievements?.length) { container.innerHTML = '<div class="empty">لا يوجد انجازات</div>'; return; }
const total = data.achievements.length;
const unlocked = data.achievements.filter(a => a.unlocked).length;
container.innerHTML = `
<div style="text-align:center;margin-bottom:var(--sp-4);">
<span style="font-size:24px;font-weight:700;">${unlocked}</span>
<span style="color:var(--text-3);font-size:14px;"> / ${total}</span>
</div>
` + data.achievements.map(a => {
const done = a.unlocked;
return `<div class="card card-pad" style="margin-bottom:var(--sp-2);display:flex;align-items:center;gap:var(--sp-3);${done ? '' : 'opacity:0.5;'}">
<div style="width:40px;height:40px;border-radius:var(--r-sm);background:${done ? 'var(--gold-dim)' : 'var(--bg-3)'};display:flex;align-items:center;justify-content:center;font-size:20px;">${done ? '✅' : '🔒'}</div>
<div style="flex:1;">
<p style="font-size:14px;font-weight:600;">${a.name_ar || a.name}</p>
<p style="font-size:12px;color:var(--text-3);">${a.description_ar || a.description || ''}</p>
</div>
${a.xp_reward ? `<span class="badge badge-cyan">+${a.xp_reward} XP</span>` : ''}
</div>`;
}).join('');
},
destroy() {}
};
import { api } from '../core/api.js';
import { navigate } from '../core/router.js';
import { haptics } from '../core/haptics.js';
import { audio } from '../core/audio.js';
export default {
render() {
return `
<h2 style="font-size:20px;font-weight:700;text-align:center;margin-bottom:var(--sp-2);">🎯 طاولة</h2>
<p style="text-align:center;color:var(--text-3);font-size:13px;margin-bottom:var(--sp-5);">طاولة زهر كلاسيكية</p>
<div class="card card-pad" style="margin-bottom:var(--sp-4);">
<p style="font-size:16px;font-weight:700;margin-bottom:var(--sp-3);">مباراة جديدة</p>
<div style="margin-bottom:var(--sp-3);">
<label class="input-label">طول المباراة</label>
<div class="chip-row" id="match-length">
<button class="chip active" data-len="1">1 نقطة</button>
<button class="chip" data-len="3">3 نقاط</button>
<button class="chip" data-len="5">5 نقاط</button>
</div>
</div>
<button class="btn btn-primary btn-block btn-lg" id="btn-create">انشئ غرفة</button>
</div>
<div class="card card-pad" style="margin-bottom:var(--sp-4);">
<p style="font-size:16px;font-weight:700;margin-bottom:var(--sp-3);">انضم لغرفة</p>
<div style="display:flex;gap:var(--sp-2);">
<input class="input" id="room-code" placeholder="كود الغرفة" dir="ltr" style="flex:1;background:var(--bg-2);border:1px solid var(--border);border-radius:var(--r-md);padding:0 var(--sp-4);">
<button class="btn btn-secondary" id="btn-join">انضم</button>
</div>
</div>
<button class="btn btn-ghost btn-block" id="btn-match">⚔️ ابحث عن خصم</button>
`;
},
mount(el) {
const lenRow = el.querySelector('#match-length');
lenRow.querySelectorAll('.chip').forEach(c => {
c.addEventListener('click', () => {
haptics.selection();
lenRow.querySelectorAll('.chip').forEach(x => x.classList.remove('active'));
c.classList.add('active');
});
});
el.querySelector('#btn-create').addEventListener('click', async () => {
haptics.tap(); audio.tap();
const len = parseInt(el.querySelector('#match-length .chip.active').dataset.len);
const res = await api('/api/backgammon', { method: 'POST', body: { action: 'create', match_length: len } });
if (res?.ok && res.match) navigate(`/backgammon-game?match_id=${res.match.id}`);
});
el.querySelector('#btn-join').addEventListener('click', async () => {
haptics.tap();
const code = el.querySelector('#room-code').value.trim();
if (!code) return;
const res = await api('/api/backgammon', { method: 'POST', body: { action: 'join', room_code: code } });
if (res?.ok) navigate(`/backgammon-game?match_id=${res.match.id}`);
});
el.querySelector('#btn-match').addEventListener('click', async () => {
haptics.tap(); audio.tap();
await api('/api/backgammon', { method: 'POST', body: { action: 'matchmake', sub_action: 'join' } });
});
},
destroy() {}
};
import { api, cachedApi } from '../core/api.js';
import { navigate } from '../core/router.js';
import { haptics } from '../core/haptics.js';
import { audio } from '../core/audio.js';
const TC = {
bullet: [{ t: 60, i: 0, l: '1+0' }, { t: 60, i: 1, l: '1+1' }, { t: 120, i: 1, l: '2+1' }],
blitz: [{ t: 180, i: 0, l: '3+0' }, { t: 180, i: 2, l: '3+2' }, { t: 300, i: 0, l: '5+0' }, { t: 300, i: 3, l: '5+3' }],
rapid: [{ t: 600, i: 0, l: '10+0' }, { t: 600, i: 5, l: '10+5' }, { t: 900, i: 10, l: '15+10' }],
classical: [{ t: 1800, i: 0, l: '30+0' }, { t: 3600, i: 0, l: '60+0' }]
};
let bots = [];
export default {
render() {
return `
<h2 style="font-size:20px;font-weight:700;text-align:center;margin-bottom:var(--sp-2);">♟️ شطرنج</h2>
<p style="text-align:center;color:var(--text-3);font-size:13px;margin-bottom:var(--sp-5);">اختر نوع المباراة</p>
<!-- VS Human -->
<div class="card card-pad" style="border-color:rgba(245,183,49,0.15);margin-bottom:var(--sp-4);">
<p style="font-size:16px;font-weight:700;margin-bottom:var(--sp-3);">⚔️ ضد لاعب حقيقي</p>
<div class="chip-row" id="mp-cat" style="margin-bottom:var(--sp-2);">
<button class="chip active" data-cat="bullet">Bullet</button>
<button class="chip" data-cat="blitz">Blitz</button>
<button class="chip" data-cat="rapid">Rapid</button>
<button class="chip" data-cat="classical">Classical</button>
</div>
<div class="chip-row" id="mp-tc" style="margin-bottom:var(--sp-4);"></div>
<button class="btn btn-primary btn-block btn-lg" id="btn-mp">ابحث عن خصم</button>
</div>
<!-- VS Bot -->
<div class="card card-pad" style="margin-bottom:var(--sp-4);">
<p style="font-size:16px;font-weight:700;margin-bottom:var(--sp-3);">🤖 ضد البوت</p>
<div class="chip-row" id="bot-cat" style="margin-bottom:var(--sp-2);">
<button class="chip active" data-cat="blitz">Blitz</button>
<button class="chip" data-cat="rapid">Rapid</button>
</div>
<div class="chip-row" id="bot-tc" style="margin-bottom:var(--sp-3);"></div>
<div style="margin-bottom:var(--sp-3);">
<label class="input-label">الخصم</label>
<select class="input" id="bot-select" dir="ltr" style="background:var(--bg-2);border:1px solid var(--border);border-radius:var(--r-md);height:var(--touch);padding:0 var(--sp-4);">
<option>جاري التحميل...</option>
</select>
</div>
<div class="chip-row" id="color-select" style="margin-bottom:var(--sp-4);">
<button class="chip active" data-color="w">⬜ ابيض</button>
<button class="chip" data-color="b">⬛ اسود</button>
<button class="chip" data-color="random">🎲 عشوائي</button>
</div>
<button class="btn btn-secondary btn-block btn-lg" id="btn-bot">ابدأ المباراة</button>
</div>
<!-- Quick Match -->
<button class="btn btn-ghost btn-block" id="btn-quick" style="margin-bottom:var(--sp-4);">
⚡ مباراة سريعة ضد بوت عشوائي
</button>
`;
},
async mount(el) {
const data = await cachedApi('/api/bots.php', 60000);
if (data?.bots) {
bots = data.bots;
const select = el.querySelector('#bot-select');
select.innerHTML = bots.map(b => {
const elo = Math.round((b.elo_min + b.elo_max) / 2);
return `<option value="${b.id}">${b.name} (${elo})</option>`;
}).join('');
}
function renderTC(containerId, cat) {
const opts = TC[cat] || TC.blitz;
const container = el.querySelector(`#${containerId}`);
container.innerHTML = opts.map((o, i) =>
`<button class="chip${i === 0 ? ' active' : ''}" data-time="${o.t}" data-inc="${o.i}">${o.l}</button>`
).join('');
setupChips(container);
}
function setupChips(container) {
container.querySelectorAll('.chip').forEach(c => {
c.addEventListener('click', () => {
haptics.selection();
container.querySelectorAll('.chip').forEach(x => x.classList.remove('active'));
c.classList.add('active');
});
});
}
['mp-cat', 'bot-cat'].forEach(catId => {
const catEl = el.querySelector(`#${catId}`);
const tcId = catId === 'mp-cat' ? 'mp-tc' : 'bot-tc';
setupChips(catEl);
catEl.addEventListener('click', (e) => {
const chip = e.target.closest('.chip');
if (!chip) return;
renderTC(tcId, chip.dataset.cat);
});
renderTC(tcId, catId === 'mp-cat' ? 'bullet' : 'blitz');
});
setupChips(el.querySelector('#color-select'));
el.querySelector('#btn-mp').addEventListener('click', () => {
haptics.tap(); audio.tap();
const tc = el.querySelector('#mp-tc .chip.active');
navigate(`/matchmaking?tc=chess&time=${tc.dataset.time * 1000}&inc=${tc.dataset.inc * 1000}`);
});
el.querySelector('#btn-bot').addEventListener('click', () => {
haptics.tap(); audio.tap();
const tc = el.querySelector('#bot-tc .chip.active');
let color = el.querySelector('#color-select .chip.active').dataset.color;
if (color === 'random') color = Math.random() < 0.5 ? 'w' : 'b';
const bot = el.querySelector('#bot-select').value;
navigate(`/game?bot=${bot}&color=${color}&time=${tc.dataset.time}&inc=${tc.dataset.inc}&rated=true`);
});
el.querySelector('#btn-quick').addEventListener('click', () => {
haptics.tap(); audio.tap();
const available = bots.length ? bots.filter(b => b.id !== 'grandmaster').map(b => b.id) : ['nour'];
const bot = available[Math.floor(Math.random() * available.length)];
const color = Math.random() < 0.5 ? 'w' : 'b';
navigate(`/game?bot=${bot}&color=${color}&time=300&inc=0&rated=false`);
});
},
destroy() {}
};
import { api } from '../core/api.js';
import { navigate } from '../core/router.js';
import { haptics } from '../core/haptics.js';
import { audio } from '../core/audio.js';
export default {
render() {
return `
<h2 style="font-size:20px;font-weight:700;text-align:center;margin-bottom:var(--sp-2);">🁣 دومينو</h2>
<p style="text-align:center;color:var(--text-3);font-size:13px;margin-bottom:var(--sp-5);">فردي او فرق</p>
<div class="card card-pad" style="margin-bottom:var(--sp-4);">
<p style="font-size:16px;font-weight:700;margin-bottom:var(--sp-3);">مباراة سريعة</p>
<div style="margin-bottom:var(--sp-3);">
<label class="input-label">النوع</label>
<div class="chip-row" id="mode-select">
<button class="chip active" data-mode="2p">فردي (2 لاعبين)</button>
<button class="chip" data-mode="4p_teams">فرق (4 لاعبين)</button>
</div>
</div>
<div style="margin-bottom:var(--sp-3);">
<label class="input-label">الصعوبة</label>
<div class="chip-row" id="diff-select">
<button class="chip active" data-diff="easy">سهل</button>
<button class="chip" data-diff="medium">متوسط</button>
<button class="chip" data-diff="hard">صعب</button>
</div>
</div>
<button class="btn btn-primary btn-block btn-lg" id="btn-start">ابدأ</button>
</div>
<div class="card card-pad" style="margin-bottom:var(--sp-4);">
<p style="font-size:16px;font-weight:700;margin-bottom:var(--sp-3);">انضم لغرفة</p>
<div style="display:flex;gap:var(--sp-2);">
<input class="input" id="room-code" placeholder="كود الغرفة" dir="ltr" style="flex:1;background:var(--bg-2);border:1px solid var(--border);border-radius:var(--r-md);padding:0 var(--sp-4);">
<button class="btn btn-secondary" id="btn-join">انضم</button>
</div>
</div>
`;
},
mount(el) {
['mode-select', 'diff-select'].forEach(id => {
const container = el.querySelector(`#${id}`);
container.querySelectorAll('.chip').forEach(c => {
c.addEventListener('click', () => {
haptics.selection();
container.querySelectorAll('.chip').forEach(x => x.classList.remove('active'));
c.classList.add('active');
});
});
});
el.querySelector('#btn-start').addEventListener('click', async () => {
haptics.tap(); audio.tap();
const mode = el.querySelector('#mode-select .chip.active').dataset.mode;
const diff = el.querySelector('#diff-select .chip.active').dataset.diff;
const res = await api('/api/domino', { method: 'POST', body: { action: 'create', mode, difficulty: diff } });
if (res?.ok && res.match) {
const startRes = await api('/api/domino', { method: 'POST', body: { action: 'start', match_id: res.match.id } });
if (startRes?.ok) navigate(`/domino-game?match_id=${startRes.match.id}`);
}
});
el.querySelector('#btn-join').addEventListener('click', async () => {
haptics.tap();
const code = el.querySelector('#room-code').value.trim();
if (!code) return;
const res = await api('/api/domino', { method: 'POST', body: { action: 'join', room_code: code } });
if (res?.ok) navigate(`/domino-game?match_id=${res.match.id}`);
});
},
destroy() {}
};
import { api } from '../core/api.js';
import { haptics } from '../core/haptics.js';
import { audio } from '../core/audio.js';
export default {
render() {
return `
<h2 style="font-size:22px;font-weight:700;text-align:center;margin-bottom:var(--sp-4);">👥 الاصدقاء</h2>
<!-- Search -->
<div style="display:flex;gap:var(--sp-2);margin-bottom:var(--sp-4);">
<input class="input" id="friend-search" placeholder="ابحث عن لاعب..." style="flex:1;">
<button class="btn btn-secondary btn-sm" id="btn-search">بحث</button>
</div>
<!-- Requests -->
<section id="requests-sec" style="display:none;margin-bottom:var(--sp-4);">
<div class="sec-hdr"><h2 class="sec-title">طلبات صداقة</h2></div>
<div class="card" id="requests-list"></div>
</section>
<!-- Friends List -->
<section>
<div class="sec-hdr"><h2 class="sec-title">اصدقائي</h2></div>
<div class="card" id="friends-list"><div class="empty">جاري التحميل...</div></div>
</section>
<!-- Search Results -->
<section id="search-sec" style="display:none;margin-top:var(--sp-4);">
<div class="sec-hdr"><h2 class="sec-title">نتائج البحث</h2></div>
<div class="card" id="search-results"></div>
</section>
`;
},
async mount(el) {
// Load friends
const friendsData = await api('/api/friends?action=list');
const list = el.querySelector('#friends-list');
if (friendsData?.friends?.length) {
list.innerHTML = '<div class="activity-list">' + friendsData.friends.map(f => `
<div class="activity-item">
<div style="width:8px;height:8px;border-radius:50%;background:${f.is_online ? 'var(--online)' : 'var(--text-3)'}"></div>
<div class="activity-detail"><p class="activity-name">${f.display_name || f.username}</p></div>
<span style="font-size:11px;color:var(--text-3);">${f.is_online ? 'متصل' : 'غير متصل'}</span>
</div>
`).join('') + '</div>';
} else {
list.innerHTML = '<div class="empty">لا يوجد اصدقاء بعد</div>';
}
// Load requests
const reqData = await api('/api/friends?action=requests');
if (reqData?.requests?.length) {
el.querySelector('#requests-sec').style.display = '';
el.querySelector('#requests-list').innerHTML = '<div class="activity-list">' + reqData.requests.map(r => `
<div class="activity-item">
<div class="activity-detail"><p class="activity-name">${r.display_name || r.username}</p></div>
<button class="btn btn-sm btn-secondary" data-accept="${r.id}">قبول</button>
<button class="btn btn-sm btn-ghost" data-reject="${r.id}">رفض</button>
</div>
`).join('') + '</div>';
el.querySelector('#requests-list').addEventListener('click', async (e) => {
const acceptBtn = e.target.closest('[data-accept]');
const rejectBtn = e.target.closest('[data-reject]');
if (acceptBtn) {
haptics.success(); audio.coin();
await api('/api/friends', { method: 'POST', body: { action: 'accept', request_id: acceptBtn.dataset.accept } });
acceptBtn.closest('.activity-item').remove();
} else if (rejectBtn) {
haptics.tap();
await api('/api/friends', { method: 'POST', body: { action: 'reject', request_id: rejectBtn.dataset.reject } });
rejectBtn.closest('.activity-item').remove();
}
});
}
// Search
el.querySelector('#btn-search').addEventListener('click', async () => {
const q = el.querySelector('#friend-search').value.trim();
if (!q) return;
haptics.tap();
const data = await api(`/api/friends?action=search&q=${encodeURIComponent(q)}`);
const sec = el.querySelector('#search-sec');
const results = el.querySelector('#search-results');
if (data?.results?.length) {
sec.style.display = '';
results.innerHTML = '<div class="activity-list">' + data.results.map(u => `
<div class="activity-item">
<div class="activity-detail"><p class="activity-name">${u.display_name || u.username}</p></div>
<button class="btn btn-sm btn-secondary" data-add="${u.id}">اضف</button>
</div>
`).join('') + '</div>';
results.addEventListener('click', async (e) => {
const btn = e.target.closest('[data-add]');
if (!btn) return;
haptics.tap(); audio.tap();
await api('/api/friends', { method: 'POST', body: { action: 'add', user_id: btn.dataset.add } });
btn.textContent = 'تم ✓';
btn.disabled = true;
});
} else {
sec.style.display = '';
results.innerHTML = '<div class="empty">لا نتائج</div>';
}
});
},
destroy() {}
};
import { getParams } from '../core/router.js';
function loadScript(src) {
return new Promise((resolve, reject) => {
if (document.querySelector(`script[src="${src}"]`)) { resolve(); return; }
const s = document.createElement('script');
s.src = src;
s.onload = resolve;
s.onerror = reject;
document.body.appendChild(s);
});
}
function loadCSS(href) {
if (document.querySelector(`link[href="${href}"]`)) return;
const l = document.createElement('link');
l.rel = 'stylesheet';
l.href = href;
document.head.appendChild(l);
}
export default {
render() {
return `
<div id="chess-game-container" style="display:flex;flex-direction:column;align-items:center;gap:var(--sp-3);padding:var(--sp-3) 0;">
<div id="opponent-info" style="display:flex;align-items:center;gap:var(--sp-3);width:100%;max-width:400px;padding:0 var(--sp-2);">
<span style="font-size:13px;font-weight:600;" id="opponent-name">Bot</span>
<span style="flex:1"></span>
<span style="font-size:14px;font-weight:700;font-family:var(--font-mono);" id="clock-opponent">--:--</span>
</div>
<div id="board-wrapper" style="width:100%;max-width:400px;aspect-ratio:1;">
<div id="board"></div>
</div>
<div id="player-info" style="display:flex;align-items:center;gap:var(--sp-3);width:100%;max-width:400px;padding:0 var(--sp-2);">
<span style="font-size:13px;font-weight:600;" id="player-name">انت</span>
<span style="flex:1"></span>
<span style="font-size:14px;font-weight:700;font-family:var(--font-mono);" id="clock-player">--:--</span>
</div>
<div style="display:flex;gap:var(--sp-2);margin-top:var(--sp-2);">
<button class="btn btn-ghost btn-sm" id="btn-resign">استسلام</button>
<button class="btn btn-ghost btn-sm" id="btn-draw">تعادل</button>
</div>
</div>
`;
},
async mount(el) {
const params = getParams();
const bot = params.bot || 'nour';
const color = params.color || 'w';
const time = parseInt(params.time) || 300;
const inc = parseInt(params.inc) || 0;
const rated = params.rated !== 'false';
// Enter immersive mode
document.getElementById('app').classList.add('immersive');
// Load game engine
loadCSS('/public/css/chessboard.css');
await loadScript('/public/js/chess.min.js');
await loadScript('/public/js/board.js');
await loadScript('/public/js/game.js');
// Initialize
if (window.Board && window.Game) {
window.Board.init('board', {
flipped: color === 'b',
playerColor: color
});
window.Game.start({
color: color,
botId: bot,
time: time,
increment: inc,
rated: rated
});
}
},
destroy() {
document.getElementById('app').classList.remove('immersive');
if (window.Game && window.Game.cleanup) window.Game.cleanup();
}
};
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() {}
};
import { api } from '../core/api.js';
import { haptics } from '../core/haptics.js';
const GAMES = [
{ key: 'chess', name: 'شطرنج', modes: ['bullet', 'blitz', 'rapid', 'classical'] },
{ key: 'ludo', name: 'لودو', modes: ['default'] },
{ key: 'backgammon', name: 'طاولة', modes: ['default'] },
{ key: 'domino', name: 'دومينو', modes: ['default'] },
];
const MODE_NAMES = { bullet: 'بوليت', blitz: 'بليتز', rapid: 'رابيد', classical: 'كلاسيك', default: 'عام' };
let currentGame = 'chess';
let currentMode = 'blitz';
export default {
render() {
return `
<h2 style="font-size:22px;font-weight:700;text-align:center;margin-bottom:var(--sp-4);">🏆 المتصدرين</h2>
<div class="chip-row" id="lb-games" style="justify-content:center;margin-bottom:var(--sp-3);">
${GAMES.map(g => `<button class="chip${g.key === 'chess' ? ' active' : ''}" data-game="${g.key}">${g.name}</button>`).join('')}
</div>
<div class="chip-row" id="lb-modes" style="justify-content:center;margin-bottom:var(--sp-4);"></div>
<div class="card" id="lb-list"><div class="empty">جاري التحميل...</div></div>
`;
},
async mount(el) {
const gamesRow = el.querySelector('#lb-games');
const modesRow = el.querySelector('#lb-modes');
const list = el.querySelector('#lb-list');
function setupChips(container, onChange) {
container.querySelectorAll('.chip').forEach(c => {
c.addEventListener('click', () => {
haptics.selection();
container.querySelectorAll('.chip').forEach(x => x.classList.remove('active'));
c.classList.add('active');
onChange(c);
});
});
}
function renderModes() {
const game = GAMES.find(g => g.key === currentGame);
if (!game || game.modes.length <= 1) {
modesRow.innerHTML = '';
currentMode = game?.modes[0] || 'default';
} else {
modesRow.innerHTML = game.modes.map((m, i) =>
`<button class="chip${(m === 'blitz' || i === 0) ? ' active' : ''}" data-mode="${m}">${MODE_NAMES[m]}</button>`
).join('');
currentMode = game.modes.includes('blitz') ? 'blitz' : game.modes[0];
setupChips(modesRow, (c) => { currentMode = c.dataset.mode; load(); });
}
}
async function load() {
list.innerHTML = '<div class="empty">جاري التحميل...</div>';
let players = [];
try {
const data = await api(`/api/ratings.php?action=leaderboard&game=${currentGame}&mode=${currentMode}`);
if (data?.leaderboard?.length) players = data.leaderboard;
} catch {}
if (!players.length) {
try {
const legacy = await api(`/api/leaderboard?mode=${currentMode}`);
if (legacy?.players) players = legacy.players.map((p, i) => ({
rank: i + 1, display_name: p.display_name, username: p.username, rating: p[`elo_${currentMode}`] || 1200
}));
} catch {}
}
if (!players.length) {
list.innerHTML = '<div class="empty">لا يوجد لاعبين بعد</div>';
return;
}
list.innerHTML = '<div class="activity-list">' + players.map((p, i) => {
const rank = p.rank || i + 1;
const medal = rank === 1 ? '🥇' : rank === 2 ? '🥈' : rank === 3 ? '🥉' : rank;
return `<div class="activity-item">
<span style="min-width:28px;text-align:center;font-weight:700;font-size:${rank <= 3 ? '18px' : '13px'};color:var(--text-3);">${medal}</span>
<div class="activity-detail"><p class="activity-name">${p.display_name || p.username || '---'}</p></div>
<span style="font-weight:700;font-family:var(--font-mono);font-size:14px;">${p.rating}</span>
</div>`;
}).join('') + '</div>';
}
setupChips(gamesRow, (c) => { currentGame = c.dataset.game; renderModes(); load(); });
renderModes();
load();
},
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() {}
};
import { api } from '../core/api.js';
import { navigate } from '../core/router.js';
import { haptics } from '../core/haptics.js';
import { audio } from '../core/audio.js';
export default {
render() {
return `
<h2 style="font-size:20px;font-weight:700;text-align:center;margin-bottom:var(--sp-2);">🎲 لودو</h2>
<p style="text-align:center;color:var(--text-3);font-size:13px;margin-bottom:var(--sp-5);">العب مع اصدقائك او ضد البوت</p>
<!-- Quick Play vs Bots -->
<div class="card card-pad" style="margin-bottom:var(--sp-4);">
<p style="font-size:16px;font-weight:700;margin-bottom:var(--sp-3);">مباراة سريعة</p>
<div style="margin-bottom:var(--sp-3);">
<label class="input-label">عدد اللاعبين</label>
<div class="chip-row" id="player-count">
<button class="chip" data-count="2">2</button>
<button class="chip active" data-count="3">3</button>
<button class="chip" data-count="4">4</button>
</div>
</div>
<div style="margin-bottom:var(--sp-3);">
<label class="input-label">صعوبة البوت</label>
<div class="chip-row" id="difficulty">
<button class="chip active" data-diff="easy">سهل</button>
<button class="chip" data-diff="hard">صعب</button>
</div>
</div>
<button class="btn btn-primary btn-block btn-lg" id="btn-start">ابدأ اللعب</button>
</div>
<!-- Join Room -->
<div class="card card-pad" style="margin-bottom:var(--sp-4);">
<p style="font-size:16px;font-weight:700;margin-bottom:var(--sp-3);">انضم لغرفة</p>
<div style="display:flex;gap:var(--sp-2);">
<input class="input" id="room-code" placeholder="كود الغرفة" dir="ltr" style="flex:1;background:var(--bg-2);border:1px solid var(--border);border-radius:var(--r-md);padding:0 var(--sp-4);">
<button class="btn btn-secondary" id="btn-join">انضم</button>
</div>
</div>
<!-- Matchmaking -->
<button class="btn btn-ghost btn-block" id="btn-match">⚔️ ابحث عن خصم حقيقي</button>
`;
},
mount(el) {
function setupChips(id) {
const container = el.querySelector(`#${id}`);
container.querySelectorAll('.chip').forEach(c => {
c.addEventListener('click', () => {
haptics.selection();
container.querySelectorAll('.chip').forEach(x => x.classList.remove('active'));
c.classList.add('active');
});
});
}
setupChips('player-count');
setupChips('difficulty');
el.querySelector('#btn-start').addEventListener('click', async () => {
haptics.tap(); audio.tap();
const count = parseInt(el.querySelector('#player-count .chip.active').dataset.count);
const diff = el.querySelector('#difficulty .chip.active').dataset.diff;
const bots = [];
for (let i = 1; i < count; i++) bots.push({ name: `بوت ${i}`, difficulty: diff });
const res = await api('/api/ludo', { method: 'POST', body: { action: 'create', player_count: count, bots } });
if (res?.ok && res.match) {
const startRes = await api('/api/ludo', { method: 'POST', body: { action: 'start', match_id: res.match.id } });
if (startRes?.ok) {
navigate(`/ludo-game?match_id=${startRes.match.id}`);
}
}
});
el.querySelector('#btn-join').addEventListener('click', async () => {
haptics.tap();
const code = el.querySelector('#room-code').value.trim();
if (!code) return;
const res = await api('/api/ludo', { method: 'POST', body: { action: 'join', room_code: code } });
if (res?.ok) navigate(`/ludo-game?match_id=${res.match.id}`);
});
el.querySelector('#btn-match').addEventListener('click', async () => {
haptics.tap(); audio.tap();
await api('/api/ludo', { method: 'POST', body: { action: 'matchmake', sub_action: 'join' } });
// TODO: matchmaking polling screen
});
},
destroy() {}
};
import { state } from '../core/state.js';
import { api } from '../core/api.js';
import { navigate } from '../core/router.js';
const TITLES = { 1:'مبتدئ', 2:'مستجد', 3:'مستجد ٢', 4:'لاعب', 5:'لاعب ٢', 6:'ماهر', 7:'ماهر ٢', 8:'خبير', 9:'خبير ٢', 10:'استاذ', 11:'استاذ ٢', 12:'بطل', 13:'بطل ٢', 14:'اسطورة', 15:'غراند ماستر' };
const XP_REQ = [0, 0, 100, 300, 600, 1000, 1500, 2100, 2800, 3600, 4500, 5500, 6600, 7800, 9100, 10500];
export default {
render() {
return `
<div style="display:flex;flex-direction:column;align-items:center;gap:var(--sp-3);padding:var(--sp-6) 0 var(--sp-4);background:linear-gradient(180deg,rgba(0,212,255,0.03),transparent);border-radius:var(--r-xl);border:1px solid var(--border);margin-bottom:var(--sp-4);">
<div style="width:72px;height:72px;border-radius:50%;background:var(--bg-3);display:flex;align-items:center;justify-content:center;box-shadow:0 0 0 3px var(--gold);">
<svg class="icon-xl" style="color:var(--text-3)"><use href="/public/icons/sprite.svg#icon-profile"></use></svg>
</div>
<h2 style="font-size:20px;font-weight:700;" id="p-name">---</h2>
<p style="font-size:13px;color:var(--text-3);" id="p-username">@---</p>
<div style="display:flex;gap:var(--sp-2);">
<span class="badge badge-gold" id="p-level">Lv 1</span>
<span class="badge badge-cyan" id="p-title">مبتدئ</span>
</div>
</div>
<!-- XP Bar -->
<div class="card card-pad" style="margin-bottom:var(--sp-4);">
<div style="display:flex;justify-content:space-between;margin-bottom:var(--sp-2);">
<span style="font-size:12px;color:var(--text-3);">مستوى الحساب</span>
<span style="font-size:12px;color:var(--text-3);" id="p-xp">0 / 100 XP</span>
</div>
<div style="width:100%;height:6px;background:var(--bg-3);border-radius:var(--r-full);overflow:hidden;">
<div id="p-xp-bar" style="height:100%;width:0%;background:linear-gradient(90deg,var(--cyan),var(--gold));border-radius:var(--r-full);transition:width 0.8s var(--ease);"></div>
</div>
</div>
<!-- Ratings -->
<section style="margin-bottom:var(--sp-4);">
<div class="sec-hdr"><h2 class="sec-title">تصنيفات الالعاب</h2></div>
<div id="p-ratings"><div class="skeleton" style="height:80px"></div></div>
</section>
<!-- Stats -->
<section style="margin-bottom:var(--sp-4);">
<div class="sec-hdr"><h2 class="sec-title">احصائيات</h2></div>
<div class="stats-row" id="p-stats">
<div class="stat"><div class="stat-val" id="s-games">0</div><div class="stat-lbl">مباريات</div></div>
<div class="stat"><div class="stat-val" style="color:var(--win)" id="s-wins">0</div><div class="stat-lbl">فوز</div></div>
<div class="stat"><div class="stat-val" id="s-draws">0</div><div class="stat-lbl">تعادل</div></div>
<div class="stat"><div class="stat-val" style="color:var(--loss)" id="s-losses">0</div><div class="stat-lbl">خسارة</div></div>
</div>
</section>
<!-- Economy -->
<div class="card card-pad" style="margin-bottom:var(--sp-4);">
<div style="display:flex;justify-content:space-around;">
<div style="text-align:center;"><p style="font-size:18px;font-weight:700;" id="p-coins">0</p><p style="font-size:11px;color:var(--text-3);">عملات</p></div>
<div style="text-align:center;"><p style="font-size:18px;font-weight:700;" id="p-gems">0</p><p style="font-size:11px;color:var(--text-3);">جواهر</p></div>
<div style="text-align:center;"><p style="font-size:18px;font-weight:700;" id="p-streak">0</p><p style="font-size:11px;color:var(--text-3);">🔥 ايام</p></div>
</div>
</div>
<!-- Actions -->
<a href="/settings" class="btn btn-ghost btn-block" style="margin-bottom:var(--sp-2);">⚙️ الاعدادات</a>
<button class="btn btn-danger btn-block" id="btn-logout">تسجيل خروج</button>
`;
},
async mount(el) {
const data = await api('/api/profile');
if (!data?.profile) return;
const p = data.profile;
el.querySelector('#p-name').textContent = p.display_name || p.username || '---';
el.querySelector('#p-username').textContent = '@' + (p.username || '---');
el.querySelector('#p-level').textContent = 'Lv ' + (p.level || 1);
el.querySelector('#p-title').textContent = TITLES[p.level] || 'مبتدئ';
el.querySelector('#s-games').textContent = p.total_games_played || 0;
el.querySelector('#s-wins').textContent = p.total_wins || 0;
el.querySelector('#s-draws').textContent = p.total_draws || 0;
el.querySelector('#s-losses').textContent = p.total_losses || 0;
el.querySelector('#p-coins').textContent = (p.coins || 0).toLocaleString();
el.querySelector('#p-gems').textContent = p.gems || 0;
el.querySelector('#p-streak').textContent = p.daily_streak || 0;
const xp = p.xp || 0;
const lvl = p.level || 1;
const next = XP_REQ[lvl + 1] || (lvl * 1200);
const prev = XP_REQ[lvl] || 0;
const pct = Math.min(100, Math.round(((xp - prev) / (next - prev)) * 100));
el.querySelector('#p-xp').textContent = `${xp} / ${next} XP`;
setTimeout(() => { el.querySelector('#p-xp-bar').style.width = pct + '%'; }, 100);
// Ratings
try {
const rd = await api('/api/ratings.php?action=player');
if (rd?.ratings?.length) {
const names = { chess:'شطرنج', ludo:'لودو', backgammon:'طاولة', domino:'دومينو' };
const modes = { bullet:'بوليت', blitz:'بليتز', rapid:'رابيد', classical:'كلاسيك', default:'' };
el.querySelector('#p-ratings').innerHTML = '<div class="stats-row">' + rd.ratings.map(r => {
const lbl = (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">${lbl}</div></div>`;
}).join('') + '</div>';
} else {
el.querySelector('#p-ratings').innerHTML = '<p style="color:var(--text-3);font-size:13px;">العب مباريات لتظهر تصنيفاتك</p>';
}
} catch { el.querySelector('#p-ratings').innerHTML = ''; }
el.querySelector('#btn-logout').addEventListener('click', () => state.logout());
},
destroy() {}
};
import { audio } from '../core/audio.js';
import { haptics } from '../core/haptics.js';
import { state } from '../core/state.js';
export default {
render() {
const muted = audio.isMuted();
return `
<h2 style="font-size:22px;font-weight:700;text-align:center;margin-bottom:var(--sp-5);">⚙️ الاعدادات</h2>
<div class="card card-pad" style="margin-bottom:var(--sp-3);">
<div style="display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:14px;font-weight:500;">🔊 الصوت</span>
<button class="btn btn-sm ${muted ? 'btn-ghost' : 'btn-secondary'}" id="btn-sound">${muted ? 'مكتوم' : 'مفعل'}</button>
</div>
</div>
<div class="card card-pad" style="margin-bottom:var(--sp-3);">
<div style="display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:14px;font-weight:500;">📳 الاهتزاز</span>
<span style="font-size:12px;color:var(--text-3);">${'vibrate' in navigator ? 'مدعوم' : 'غير مدعوم'}</span>
</div>
</div>
<div class="card card-pad" style="margin-bottom:var(--sp-5);">
<div style="display:flex;align-items:center;justify-content:space-between;">
<span style="font-size:14px;font-weight:500;">🌐 اللغة</span>
<span style="font-size:12px;color:var(--text-3);">العربية</span>
</div>
</div>
<button class="btn btn-danger btn-block" id="btn-logout">تسجيل خروج</button>
`;
},
mount(el) {
el.querySelector('#btn-sound').addEventListener('click', (e) => {
haptics.tap();
const on = audio.toggle();
e.target.textContent = on ? 'مفعل' : 'مكتوم';
e.target.className = `btn btn-sm ${on ? 'btn-secondary' : 'btn-ghost'}`;
if (on) audio.tap();
});
el.querySelector('#btn-logout').addEventListener('click', () => {
haptics.tap();
state.logout();
});
},
destroy() {}
};
import { api } from '../core/api.js';
import { haptics } from '../core/haptics.js';
import { audio } from '../core/audio.js';
import { coinBurst } from '../core/particles.js';
const CATEGORIES = [
{ key: 'board_theme', name: 'رقعة اللعب' },
{ key: 'piece_set', name: 'طقم القطع' },
{ key: 'avatar_frame', name: 'إطار الصورة' },
{ key: 'trail_effect', name: 'تأثيرات' },
];
export default {
render() {
return `
<h2 style="font-size:22px;font-weight:700;text-align:center;margin-bottom:var(--sp-2);">🛍️ المتجر</h2>
<div style="display:flex;justify-content:center;gap:var(--sp-4);margin-bottom:var(--sp-4);">
<div style="text-align:center;"><span style="font-size:18px;font-weight:700;color:var(--gold);" id="shop-coins">0</span><p style="font-size:10px;color:var(--text-3);">عملات</p></div>
<div style="text-align:center;"><span style="font-size:18px;font-weight:700;color:var(--purple);" id="shop-gems">0</span><p style="font-size:10px;color:var(--text-3);">جواهر</p></div>
</div>
<div class="chip-row" id="shop-cats" style="justify-content:center;margin-bottom:var(--sp-4);">
${CATEGORIES.map((c, i) => `<button class="chip${i === 0 ? ' active' : ''}" data-cat="${c.key}">${c.name}</button>`).join('')}
</div>
<div id="shop-items"><div class="empty">جاري التحميل...</div></div>
`;
},
async mount(el) {
let currentCat = 'board_theme';
const profileData = await api('/api/profile');
if (profileData?.profile) {
el.querySelector('#shop-coins').textContent = (profileData.profile.coins || 0).toLocaleString();
el.querySelector('#shop-gems').textContent = profileData.profile.gems || 0;
}
const catsRow = el.querySelector('#shop-cats');
catsRow.querySelectorAll('.chip').forEach(c => {
c.addEventListener('click', () => {
haptics.selection();
catsRow.querySelectorAll('.chip').forEach(x => x.classList.remove('active'));
c.classList.add('active');
currentCat = c.dataset.cat;
loadItems();
});
});
async function loadItems() {
const container = el.querySelector('#shop-items');
container.innerHTML = '<div class="skeleton" style="height:80px;margin-bottom:var(--sp-2);"></div>'.repeat(3);
const data = await api(`/api/shop?type=${currentCat}`);
if (!data?.items?.length) { container.innerHTML = '<div class="empty">لا يوجد عناصر</div>'; return; }
container.innerHTML = data.items.map(item => {
const rarityColor = { common:'var(--text-3)', uncommon:'var(--win)', rare:'var(--cyan)', epic:'var(--purple)', legendary:'var(--gold)' }[item.rarity] || 'var(--text-3)';
return `<div class="card card-pad" style="margin-bottom:var(--sp-2);display:flex;align-items:center;gap:var(--sp-3);${item.owned ? 'opacity:0.6' : ''}">
<div style="width:44px;height:44px;border-radius:var(--r-sm);background:var(--bg-3);border:2px solid ${rarityColor};display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<svg class="icon-lg" style="color:${rarityColor}"><use href="/public/icons/sprite.svg#icon-star"></use></svg>
</div>
<div style="flex:1;min-width:0;">
<p style="font-size:14px;font-weight:600;">${item.name_ar || item.name}</p>
<p style="font-size:11px;color:${rarityColor};">${item.rarity || ''}</p>
</div>
${item.owned
? (item.equipped ? '<span class="badge badge-win">مُجهّز</span>' : `<button class="btn btn-sm btn-ghost" data-equip="${item.id}">تجهيز</button>`)
: `<button class="btn btn-sm btn-primary" data-buy="${item.id}">🪙 ${item.price_coins || 0}</button>`
}
</div>`;
}).join('');
container.addEventListener('click', async (e) => {
const buyBtn = e.target.closest('[data-buy]');
const equipBtn = e.target.closest('[data-equip]');
if (buyBtn) {
haptics.tap();
const res = await api('/api/shop', { method: 'POST', body: { action: 'buy', item_id: buyBtn.dataset.buy } });
if (res?.ok) {
audio.coin(); haptics.success();
const rect = buyBtn.getBoundingClientRect();
coinBurst(rect.left, rect.top);
loadItems();
const pd = await api('/api/profile');
if (pd?.profile) el.querySelector('#shop-coins').textContent = (pd.profile.coins || 0).toLocaleString();
} else {
audio.error(); haptics.error();
}
} else if (equipBtn) {
haptics.tap();
await api('/api/shop', { method: 'POST', body: { action: 'equip', item_id: equipBtn.dataset.equip, equip: true } });
audio.tap();
loadItems();
}
});
}
loadItems();
},
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; }
/* EL3AB v3 — Design System from DESIGN.md */
:root {
--bg-0: #030A12;
--bg-1: #081420;
--bg-2: #0F1F30;
--bg-3: #182B42;
--bg-glass: rgba(8, 20, 32, 0.88);
--gold: #F5B731;
--gold-dim: rgba(245, 183, 49, 0.12);
--gold-glow: rgba(245, 183, 49, 0.3);
--cyan: #00D4FF;
--purple: #8B5CF6;
--chess-1: #2563EB;
--chess-2: #F5B731;
--ludo-1: #8B5CF6;
--ludo-2: #EC4899;
--domino-1: #10B981;
--domino-2: #06B6D4;
--backgammon-1: #F59E0B;
--backgammon-2: #EF4444;
--win: #34D399;
--win-dim: rgba(52, 211, 153, 0.12);
--loss: #F87171;
--loss-dim: rgba(248, 113, 113, 0.12);
--online: #22C55E;
--text-1: #F8FAFC;
--text-2: #94A3B8;
--text-3: #64748B;
--text-inv: #0F172A;
--border: rgba(255, 255, 255, 0.08);
--radius: 20px;
--radius-sm: 12px;
--radius-pill: 9999px;
--shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
--touch: 44px;
--header-h: 56px;
--nav-h: 64px;
--font: 'IBM Plex Sans Arabic', system-ui, sans-serif;
--ease: cubic-bezier(0.2, 0, 0, 1);
--ease-bounce: cubic-bezier(0.34, 1.56, 0.64, 1);
}
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html { direction: rtl; font-family: var(--font); color: var(--text-1); background: var(--bg-0); -webkit-font-smoothing: antialiased; }
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 { font: inherit; color: inherit; background: none; border: none; outline: none; }
img { display: block; max-width: 100%; }
/* === SHELL === */
.shell { min-height: 100dvh; padding-top: var(--header-h); padding-bottom: calc(var(--nav-h) + env(safe-area-inset-bottom) + 16px); }
.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 16px; background: var(--bg-glass); backdrop-filter: blur(20px); border-bottom: 1px solid var(--border);
padding-top: env(safe-area-inset-top); transition: transform 0.3s var(--ease);
}
.hdr.hide { transform: translateY(-100%); }
.hdr-brand { font-weight: 800; font-size: 20px; color: var(--gold); letter-spacing: -0.5px; }
.hdr-mid { display: flex; align-items: center; gap: 12px; }
.hdr-stat { display: flex; align-items: center; gap: 4px; font-size: 13px; font-weight: 600; font-variant-numeric: tabular-nums; }
.hdr-stat svg { width: 14px; height: 14px; }
.hdr-end { display: flex; align-items: center; gap: 4px; }
.hdr-btn { width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; border-radius: var(--radius-sm); color: var(--text-2); }
.hdr-btn:active { background: var(--bg-2); }
.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); border-top: 1px solid var(--border);
transition: transform 0.3s var(--ease);
}
.nav.hide { 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 0.15s;
}
.nav-item.active { color: var(--gold); }
.nav-item svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.nav-item:active svg { transform: scale(0.85); transition: transform 0.1s var(--ease-bounce); }
.main { width: 100%; max-width: 500px; margin: 0 auto; padding: 16px; }
@media (min-width: 768px) {
.main { max-width: 600px; padding: 24px; }
}
@media (min-width: 1024px) {
.main { max-width: 680px; }
}
/* === CARDS === */
.card {
background: var(--bg-1); border: 1px solid var(--border); border-radius: var(--radius);
box-shadow: var(--shadow); overflow: hidden; transition: transform 0.15s var(--ease);
}
.card:active { transform: scale(0.97); }
.card-pad { padding: 16px; }
/* === GAME GRID === */
.game-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.game-card {
position: relative; border-radius: var(--radius); overflow: hidden;
border: 1px solid var(--border); cursor: pointer;
transition: transform 0.15s var(--ease-bounce); box-shadow: var(--shadow);
}
.game-card:active { transform: scale(0.95); }
.game-card-hero {
height: clamp(80px, 20vw, 110px); display: flex; align-items: center; justify-content: center;
}
.game-card-hero--chess { background: linear-gradient(135deg, var(--chess-1), var(--chess-2)); }
.game-card-hero--ludo { background: linear-gradient(135deg, var(--ludo-1), var(--ludo-2)); }
.game-card-hero--domino { background: linear-gradient(135deg, var(--domino-1), var(--domino-2)); }
.game-card-hero--backgammon { background: linear-gradient(135deg, var(--backgammon-1), var(--backgammon-2)); }
.game-card-hero-icon { font-size: 36px; filter: drop-shadow(0 2px 8px rgba(0,0,0,0.3)); }
.game-card-body {
padding: 12px; display: flex; align-items: center; justify-content: space-between;
background: var(--bg-1);
}
.game-card-name { font-size: 14px; font-weight: 700; }
.game-card-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--online); animation: pulse 2s infinite; }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
/* === STREAK === */
.streak {
display: flex; align-items: center; gap: 12px;
padding: 14px 16px; background: var(--bg-1); border: 1px solid var(--border);
border-radius: var(--radius); box-shadow: var(--shadow);
}
.streak-fire { font-size: 28px; animation: fire 1.5s ease-in-out infinite alternate; }
@keyframes fire { from { transform: scale(1) rotate(-3deg); } to { transform: scale(1.1) rotate(3deg); } }
.streak-info { flex: 1; }
.streak-day { font-size: 14px; font-weight: 600; }
.streak-reward { font-size: 12px; color: var(--text-2); }
.streak-btn {
min-height: 36px; padding: 8px 16px; background: var(--gold); color: var(--text-inv);
font-size: 13px; font-weight: 700; border-radius: var(--radius-sm);
box-shadow: 0 4px 16px var(--gold-glow); transition: transform 0.1s;
}
.streak-btn:active { transform: scale(0.93); }
.streak-btn:disabled { opacity: 0.4; box-shadow: none; }
/* === STATS === */
.stats-scroll { display: flex; gap: 8px; overflow-x: auto; padding: 4px 0; scrollbar-width: none; }
.stats-scroll::-webkit-scrollbar { display: none; }
.stat-pill {
flex-shrink: 0; padding: 8px 14px; background: var(--bg-2); border: 1px solid var(--border);
border-radius: var(--radius-pill); font-size: 12px; font-weight: 600; white-space: nowrap;
display: flex; align-items: center; gap: 6px;
}
.stat-pill-val { font-variant-numeric: tabular-nums; color: var(--text-1); }
.stat-pill-lbl { color: var(--text-2); }
/* === ACTIVITY === */
.activity { display: flex; flex-direction: column; }
.activity-item { display: flex; align-items: center; gap: 12px; padding: 12px 16px; border-bottom: 1px solid var(--border); }
.activity-item:last-child { border-bottom: none; }
.activity-icon { width: 32px; height: 32px; border-radius: var(--radius-sm); display: flex; align-items: center; justify-content: center; font-size: 14px; }
.activity-icon.win { background: var(--win-dim); color: var(--win); }
.activity-icon.loss { background: var(--loss-dim); color: var(--loss); }
.activity-text { flex: 1; font-size: 13px; font-weight: 500; }
.activity-result { font-size: 12px; font-weight: 700; }
/* === SECTION === */
.sec { margin-bottom: 20px; }
.sec-hdr { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.sec-title { font-size: 16px; font-weight: 700; }
.sec-link { font-size: 12px; color: var(--cyan); font-weight: 500; }
/* === BUTTONS === */
.btn {
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
min-height: var(--touch); padding: 10px 20px; font-weight: 600; font-size: 14px;
border-radius: var(--radius-sm); transition: transform 0.1s; white-space: nowrap;
}
.btn:active { transform: scale(0.95); }
.btn:disabled { opacity: 0.4; pointer-events: none; }
.btn-gold { background: var(--gold); color: var(--text-inv); box-shadow: 0 4px 16px var(--gold-glow); }
.btn-cyan { background: var(--cyan); color: var(--text-inv); }
.btn-ghost { background: transparent; color: var(--text-2); border: 1px solid var(--border); }
.btn-block { display: flex; width: 100%; }
.btn-lg { min-height: 52px; font-size: 16px; font-weight: 700; border-radius: var(--radius); }
/* === INPUTS === */
.input {
width: 100%; height: var(--touch); padding: 0 16px;
background: var(--bg-2); border: 1px solid var(--border); border-radius: var(--radius-sm);
color: var(--text-1); font-size: 14px; transition: border-color 0.15s;
}
.input:focus { border-color: var(--cyan); }
.input::placeholder { color: var(--text-3); }
/* === CHIPS === */
.chips { display: flex; gap: 8px; 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(--radius-sm);
color: var(--text-2); transition: all 0.15s;
}
.chip:active { transform: scale(0.95); }
.chip.on { background: var(--gold); border-color: var(--gold); color: var(--text-inv); }
/* === BOTTOM SHEET === */
.sheet-bg { position: fixed; inset: 0; z-index: 150; background: rgba(0,0,0,0.6); opacity: 0; pointer-events: none; transition: opacity 0.3s; }
.sheet-bg.open { opacity: 1; pointer-events: auto; }
.sheet {
position: fixed; bottom: 0; left: 0; right: 0; z-index: 151;
background: var(--bg-1); border-top-left-radius: var(--radius); border-top-right-radius: var(--radius);
padding: 16px 16px calc(16px + env(safe-area-inset-bottom));
transform: translateY(100%); transition: transform 0.35s var(--ease);
max-height: 80vh; overflow-y: auto;
}
.sheet.open { transform: translateY(0); }
.sheet-handle { width: 36px; height: 4px; background: var(--text-3); border-radius: var(--radius-pill); margin: 0 auto 16px; }
/* === LOGIN === */
.login-wrap { display: flex; align-items: center; justify-content: center; min-height: 100dvh; padding: 20px; }
.login-card { width: 100%; max-width: 380px; }
.login-brand { font-weight: 800; font-size: 44px; color: var(--gold); text-align: center; text-shadow: 0 0 40px var(--gold-glow); margin-bottom: 8px; }
.login-sub { text-align: center; color: var(--text-2); font-size: 14px; margin-bottom: 32px; }
.login-err { padding: 12px; background: var(--loss-dim); border: 1px solid rgba(248,113,113,0.2); border-radius: var(--radius-sm); color: var(--loss); font-size: 13px; text-align: center; margin-top: 16px; animation: shake 0.4s; }
@keyframes shake { 0%,100%{transform:translateX(0)} 20%{transform:translateX(-4px)} 40%{transform:translateX(4px)} 60%{transform:translateX(-2px)} 80%{transform:translateX(2px)} }
/* === EMPTY === */
.empty { padding: 48px 20px; text-align: center; color: var(--text-3); font-size: 14px; }
/* === SKELETON === */
.skel { background: linear-gradient(90deg, var(--bg-2) 25%, var(--bg-3) 50%, var(--bg-2) 75%); background-size: 200% 100%; animation: skel 1.5s infinite linear; border-radius: var(--radius-sm); }
@keyframes skel { 0%{background-position:-200% 0} 100%{background-position:200% 0} }
/* === IMMERSIVE (in-game) === */
.shell.immersive .hdr { transform: translateY(-100%); }
.shell.immersive .nav { transform: translateY(100%); }
.shell.immersive { padding-top: 0; padding-bottom: 0; }
/* === UTILITIES === */
.mb-3 { margin-bottom: 12px; }
.mb-4 { margin-bottom: 16px; }
.mb-5 { margin-bottom: 20px; }
.text-center { text-align: center; }
/* === RESPONSIVE GAME BOARD === */
.board-container {
width: min(100vw - 32px, 400px); aspect-ratio: 1; margin: 0 auto;
}
/* === REDUCED MOTION === */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
*, *::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;
}
...@@ -2,20 +2,13 @@ ...@@ -2,20 +2,13 @@
$route = $_GET['route'] ?? ''; $route = $_GET['route'] ?? '';
$route = trim($route, '/'); $route = trim($route, '/');
// API routes — pass through to PHP endpoints // API passthrough
if (str_starts_with($route, 'api/')) { if (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)) { require $apiPath; } else { http_response_code(404); echo json_encode(['error' => 'not found']); }
require $apiPath;
} else {
http_response_code(404);
echo json_encode(['error' => 'endpoint not found']);
}
exit; exit;
} }
// Everything else: serve the SPA shell
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="ar" dir="rtl"> <html lang="ar" dir="rtl">
...@@ -26,71 +19,453 @@ if (str_starts_with($route, 'api/')) { ...@@ -26,71 +19,453 @@ if (str_starts_with($route, 'api/')) {
<title>EL3AB</title> <title>EL3AB</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <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 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/el3ab.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> </head>
<body> <body>
<div id="app"> <div class="shell" id="shell">
<!-- Header -->
<header class="hdr" id="hdr"> <header class="hdr" id="hdr">
<a href="/" class="hdr-brand">EL3AB</a> <a href="/" class="hdr-brand" onclick="return route(event,'/')">EL3AB</a>
<div class="hdr-center"> <div class="hdr-mid">
<span class="hdr-level" id="hdr-level">Lv 1</span> <span class="hdr-stat" id="hdr-level" style="color:var(--gold)">Lv 1</span>
<div class="hdr-currency"> <span class="hdr-stat"><svg style="color:var(--gold)" viewBox="0 0 24 24" fill="currentColor"><circle cx="12" cy="12" r="10"/></svg><span id="hdr-coins">0</span></span>
<svg class="hdr-currency-icon icon-sm icon-fill" style="color:var(--gold)"><use href="/public/icons/sprite.svg#icon-coin"></use></svg> <span class="hdr-stat"><svg style="color:var(--purple)" viewBox="0 0 24 24" fill="currentColor"><polygon points="12,2 15,10 24,10 17,15 19,23 12,18 5,23 7,15 0,10 9,10"/></svg><span id="hdr-gems">0</span></span>
<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>
<div class="hdr-actions"> <div class="hdr-end">
<a href="/notifications" class="hdr-btn" aria-label="الاشعارات"> <button class="hdr-btn" onclick="route(event,'/notifications')">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-bell"></use></svg> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 01-3.46 0"/></svg>
</a> </button>
</div> </div>
</header> </header>
<!-- Main Content (SPA renders here) -->
<div class="main" id="main"></div> <div class="main" id="main"></div>
<!-- Bottom Navigation -->
<nav class="nav" id="nav"> <nav class="nav" id="nav">
<a href="/" class="nav-item active"> <a href="/" class="nav-item active" data-route="/" onclick="return route(event,'/')">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-home"></use></svg> <svg viewBox="0 0 24 24"><path d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-4 0v-6a1 1 0 011-1h2a1 1 0 011 1v6m-6 0h6"/></svg>
<span>الرئيسية</span> <span>الرئيسية</span>
</a> </a>
<a href="/games" class="nav-item"> <a href="/games" class="nav-item" data-route="/games" onclick="return route(event,'/games')">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-games"></use></svg> <svg viewBox="0 0 24 24"><rect x="2" y="2" width="20" height="20" rx="5"/><circle cx="8" cy="8" r="1.5" fill="currentColor"/><circle cx="16" cy="16" r="1.5" fill="currentColor"/></svg>
<span>العاب</span> <span>العاب</span>
</a> </a>
<a href="/leaderboard" class="nav-item"> <a href="/leaderboard" class="nav-item" data-route="/leaderboard" onclick="return route(event,'/leaderboard')">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-leaderboard"></use></svg> <svg viewBox="0 0 24 24"><path d="M8 21V11M12 21V3M16 21v-5"/></svg>
<span>ترتيب</span> <span>ترتيب</span>
</a> </a>
<a href="/friends" class="nav-item"> <a href="/friends" class="nav-item" data-route="/friends" onclick="return route(event,'/friends')">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-friends"></use></svg> <svg viewBox="0 0 24 24"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75"/></svg>
<span>اجتماعي</span> <span>اجتماعي</span>
</a> </a>
<a href="/profile" class="nav-item"> <a href="/profile" class="nav-item" data-route="/profile" onclick="return route(event,'/profile')">
<svg class="icon"><use href="/public/icons/sprite.svg#icon-profile"></use></svg> <svg viewBox="0 0 24 24"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
<span>حسابي</span> <span>حسابي</span>
</a> </a>
</nav> </nav>
<!-- Toast Layer --> </div>
<div class="toast-layer" id="toasts"></div>
<!-- Particle Canvas --> <!-- Compatibility: game engines need window.App -->
<canvas id="particles"></canvas> <script src="/public/js/app.js"></script>
</div> <!-- SPA Router -->
<script>
const main = document.getElementById('main');
const screens = {};
let current = null;
// Simple screen registry
function defineScreen(path, renderFn) { screens[path] = renderFn; }
function route(e, path) {
if (e) { e.preventDefault(); e.stopPropagation(); }
history.pushState({}, '', path);
render(path);
return false;
}
function render(path) {
// Update nav active state
document.querySelectorAll('.nav-item').forEach(item => {
const r = item.dataset.route;
item.classList.toggle('active', r === path || (r !== '/' && path.startsWith(r)));
});
// Find screen
const screen = screens[path] || screens['/'];
if (!screen) { main.innerHTML = '<div class="empty">الصفحة غير موجودة</div>'; return; }
// Render
main.innerHTML = '';
main.style.opacity = '0';
main.style.transform = 'translateY(8px)';
screen();
requestAnimationFrame(() => {
main.style.transition = 'opacity 0.2s, transform 0.2s';
main.style.opacity = '1';
main.style.transform = 'translateY(0)';
});
}
// Handle back/forward
window.addEventListener('popstate', () => render(location.pathname));
// Handle link clicks
document.addEventListener('click', (e) => {
const a = e.target.closest('a[href]');
if (!a) return;
const href = a.getAttribute('href');
if (!href || href.startsWith('http') || href.startsWith('#') || href.startsWith('/api/')) return;
e.preventDefault();
route(null, href);
});
// === SCREENS ===
defineScreen('/', async function homeScreen() {
if (!App.isLoggedIn()) { route(null, '/login'); return; }
main.innerHTML = `
<div class="sec">
<div class="streak" id="streak">
<span class="streak-fire">🔥</span>
<div class="streak-info">
<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>
</div>
<div class="sec">
<div class="sec-hdr"><h2 class="sec-title">العب</h2></div>
<div class="game-grid">
<a href="/play" class="game-card"><div class="game-card-hero game-card-hero--chess"><span class="game-card-hero-icon">♟️</span></div><div class="game-card-body"><span class="game-card-name">شطرنج</span><span class="game-card-dot"></span></div></a>
<a href="/ludo" class="game-card"><div class="game-card-hero game-card-hero--ludo"><span class="game-card-hero-icon">🎲</span></div><div class="game-card-body"><span class="game-card-name">لودو</span><span class="game-card-dot"></span></div></a>
<a href="/domino" class="game-card"><div class="game-card-hero game-card-hero--domino"><span class="game-card-hero-icon">🁣</span></div><div class="game-card-body"><span class="game-card-name">دومينو</span><span class="game-card-dot"></span></div></a>
<a href="/backgammon" class="game-card"><div class="game-card-hero game-card-hero--backgammon"><span class="game-card-hero-icon">🎯</span></div><div class="game-card-body"><span class="game-card-name">طاولة</span><span class="game-card-dot"></span></div></a>
</div>
</div>
<div class="sec" id="ratings-sec" style="display:none">
<div class="sec-hdr"><h2 class="sec-title">تصنيفاتي</h2></div>
<div class="stats-scroll" id="ratings-row"></div>
</div>
<div class="sec">
<div class="sec-hdr"><h2 class="sec-title">اخر المباريات</h2></div>
<div class="card" id="recent"><div class="empty">لم تلعب بعد</div></div>
</div>
`;
// Load profile
const data = await App.fetch('/api/profile');
if (data && data.profile) {
const p = data.profile;
document.getElementById('hdr-level').textContent = 'Lv ' + (p.level || 1);
document.getElementById('hdr-coins').textContent = (p.coins || 0).toLocaleString();
document.getElementById('hdr-gems').textContent = p.gems || 0;
const streak = p.daily_streak || 0;
document.getElementById('streak-day').textContent = 'اليوم ' + streak;
document.getElementById('streak-reward').textContent = '+' + (50 + streak * 10) + ' عملة';
const today = new Date().toISOString().slice(0, 10);
if (p.last_daily_reward === today) {
document.getElementById('streak-btn').textContent = 'تم ✓';
document.getElementById('streak-btn').disabled = true;
}
}
// Streak claim
document.getElementById('streak-btn').onclick = async function() {
this.disabled = true;
const res = await App.fetch('/api/daily-reward', { method: 'POST' });
if (res && res.ok) {
App.toast('+' + res.reward + ' عملة!', 'success');
this.textContent = 'تم ✓';
document.getElementById('streak-day').textContent = 'اليوم ' + res.streak;
document.getElementById('hdr-coins').textContent = res.coins;
} else {
this.disabled = false;
App.toast(res?.error === 'already_claimed' ? 'جمعتها اليوم' : 'خطأ', 'error');
}
};
// Ratings
try {
const rd = await App.fetch('/api/ratings.php?action=player');
if (rd && rd.ratings && rd.ratings.length) {
document.getElementById('ratings-sec').style.display = '';
const names = {chess:'شطرنج',ludo:'لودو',backgammon:'طاولة',domino:'دومينو'};
const modes = {bullet:'بوليت',blitz:'بليتز',rapid:'رابيد',classical:'كلاسيك',default:''};
document.getElementById('ratings-row').innerHTML = rd.ratings.map(r => {
const lbl = (modes[r.mode]||'') ? names[r.game_key]+' '+modes[r.mode] : names[r.game_key];
return '<div class="stat-pill"><span class="stat-pill-val">'+r.rating+'</span><span class="stat-pill-lbl">'+lbl+'</span></div>';
}).join('');
}
} catch(e) {}
// Recent games
const gd = await App.fetch('/api/game?action=recent');
if (gd && gd.games && gd.games.length) {
document.getElementById('recent').innerHTML = '<div class="activity">' + gd.games.slice(0,5).map(g => {
const cls = g.result==='win'?'win':'loss';
const icon = g.result==='win'?'✓':g.result==='loss'?'✗':'=';
const txt = g.result==='win'?'فوز':g.result==='loss'?'خسارة':'تعادل';
return '<div class="activity-item"><div class="activity-icon '+cls+'">'+icon+'</div><div class="activity-text">مباراة شطرنج</div><span class="activity-result" style="color:var(--'+cls+')">'+txt+'</span></div>';
}).join('') + '</div>';
}
});
defineScreen('/login', function loginScreen() {
document.getElementById('hdr').classList.add('hide');
document.getElementById('nav').classList.add('hide');
main.innerHTML = `
<div class="login-wrap">
<div class="login-card">
<h1 class="login-brand">EL3AB</h1>
<p class="login-sub">سجل دخولك وابدأ اللعب</p>
<form id="login-form">
<div class="mb-4"><label style="display:block;font-size:13px;color:var(--text-2);margin-bottom:6px;">البريد</label><input type="email" class="input" id="l-email" placeholder="email@example.com" required dir="ltr"></div>
<div class="mb-5"><label style="display:block;font-size:13px;color:var(--text-2);margin-bottom:6px;">كلمة المرور</label><input type="password" class="input" id="l-pass" placeholder="••••••••" required dir="ltr"></div>
<button type="submit" class="btn btn-gold btn-block btn-lg" id="l-btn">تسجيل الدخول</button>
</form>
<p style="text-align:center;margin-top:20px;font-size:13px;color:var(--text-3);">ما عندك حساب؟ <a href="/register" style="color:var(--cyan)">انشئ حساب</a></p>
<div id="l-err" style="display:none" class="login-err"></div>
</div>
</div>
`;
document.getElementById('login-form').onsubmit = async function(e) {
e.preventDefault();
const btn = document.getElementById('l-btn');
const err = document.getElementById('l-err');
err.style.display = 'none';
btn.disabled = true; btn.textContent = 'جاري...';
try {
const res = await fetch('/api/auth', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({action:'login', email:document.getElementById('l-email').value, password:document.getElementById('l-pass').value}) });
const data = await res.json();
if (data.error) { err.textContent = data.error; err.style.display = 'block'; }
else { App.setAuth(data.access_token, data.user); location.href = '/'; }
} catch { err.textContent = 'خطأ في الاتصال'; err.style.display = 'block'; }
btn.disabled = false; btn.textContent = 'تسجيل الدخول';
};
});
defineScreen('/play', function chessLobby() {
main.innerHTML = `
<h2 class="text-center mb-5" style="font-size:20px;font-weight:700;">♟️ شطرنج</h2>
<div class="card card-pad mb-4">
<p style="font-size:15px;font-weight:700;margin-bottom:12px;">🤖 ضد البوت</p>
<div class="chips mb-3" id="tc-chips">
<button class="chip on" data-t="300" data-i="0">5+0</button>
<button class="chip" data-t="180" data-i="0">3+0</button>
<button class="chip" data-t="600" data-i="0">10+0</button>
<button class="chip" data-t="60" data-i="1">1+1</button>
</div>
<div class="mb-3"><select class="input" id="bot-sel" dir="ltr" style="background:var(--bg-2);border:1px solid var(--border);border-radius:var(--radius-sm);height:44px;padding:0 16px;"><option>Loading...</option></select></div>
<div class="chips mb-4" id="color-chips">
<button class="chip on" data-c="w">⬜ ابيض</button>
<button class="chip" data-c="b">⬛ اسود</button>
<button class="chip" data-c="random">🎲 عشوائي</button>
</div>
<button class="btn btn-gold btn-block btn-lg" id="btn-play">ابدأ</button>
</div>
`;
// Chip logic
document.querySelectorAll('.chips').forEach(group => {
group.querySelectorAll('.chip').forEach(c => {
c.onclick = () => { group.querySelectorAll('.chip').forEach(x => x.classList.remove('on')); c.classList.add('on'); };
});
});
// Load bots
App.fetch('/api/bots.php').then(d => {
if (!d || !d.bots) return;
document.getElementById('bot-sel').innerHTML = d.bots.map(b => '<option value="'+b.id+'">'+b.name+' ('+Math.round((b.elo_min+b.elo_max)/2)+')</option>').join('');
});
// Start game
document.getElementById('btn-play').onclick = () => {
const tc = document.querySelector('#tc-chips .on');
let color = document.querySelector('#color-chips .on').dataset.c;
if (color === 'random') color = Math.random() < 0.5 ? 'w' : 'b';
const bot = document.getElementById('bot-sel').value;
location.href = '/game?bot='+bot+'&color='+color+'&time='+tc.dataset.t+'&inc='+tc.dataset.i+'&rated=true';
};
});
defineScreen('/game', function gameScreen() {
// Immersive mode
document.getElementById('shell').classList.add('immersive');
main.innerHTML = `
<div style="display:flex;flex-direction:column;align-items:center;min-height:100dvh;justify-content:center;padding:8px;">
<div id="opponent-bar" style="width:100%;max-width:400px;display:flex;align-items:center;justify-content:space-between;padding:8px 4px;font-size:13px;font-weight:600;">
<span id="opp-name">Bot</span><span id="clock-opp" style="font-variant-numeric:tabular-nums;">--:--</span>
</div>
<div class="board-container"><div id="board"></div></div>
<div id="player-bar" style="width:100%;max-width:400px;display:flex;align-items:center;justify-content:space-between;padding:8px 4px;font-size:13px;font-weight:600;">
<span id="player-name">انت</span><span id="clock-player" style="font-variant-numeric:tabular-nums;">--:--</span>
</div>
<div style="display:flex;gap:8px;margin-top:12px;">
<button class="btn btn-ghost" onclick="if(confirm('متأكد؟'))Game.resign&&Game.resign()">استسلام</button>
</div>
</div>
`;
// Load chess engine and start
const params = Object.fromEntries(new URLSearchParams(location.search));
function loadS(src) { return new Promise((r,j)=>{ if(document.querySelector('script[src="'+src+'"]')){r();return;} const s=document.createElement('script');s.src=src;s.onload=r;s.onerror=j;document.body.appendChild(s); }); }
const css = document.createElement('link'); css.rel='stylesheet'; css.href='/public/css/chessboard.css'; document.head.appendChild(css);
Promise.all([loadS('/public/js/chess.min.js'), loadS('/public/js/board.js'), loadS('/public/js/game.js')]).then(() => {
if (window.Board) Board.init('board', { flipped: params.color === 'b', playerColor: params.color || 'w' });
if (window.Game) Game.start({ color: params.color||'w', botId: params.bot||'nour', time: parseInt(params.time)||300, increment: parseInt(params.inc)||0, rated: params.rated !== 'false' });
});
});
defineScreen('/ludo', function ludoLobby() {
main.innerHTML = `
<h2 class="text-center mb-5" style="font-size:20px;font-weight:700;">🎲 لودو</h2>
<div class="card card-pad mb-4">
<p style="font-size:15px;font-weight:700;margin-bottom:12px;">مباراة سريعة</p>
<div class="chips mb-3" id="ludo-count"><button class="chip" data-n="2">2</button><button class="chip on" data-n="3">3</button><button class="chip" data-n="4">4</button></div>
<div class="chips mb-4" id="ludo-diff"><button class="chip on" data-d="easy">سهل</button><button class="chip" data-d="hard">صعب</button></div>
<button class="btn btn-gold btn-block btn-lg" id="btn-ludo">ابدأ</button>
</div>
`;
document.querySelectorAll('.chips').forEach(g => g.querySelectorAll('.chip').forEach(c => { c.onclick=()=>{g.querySelectorAll('.chip').forEach(x=>x.classList.remove('on'));c.classList.add('on');}; }));
document.getElementById('btn-ludo').onclick = async () => {
const count = parseInt(document.querySelector('#ludo-count .on').dataset.n);
const diff = document.querySelector('#ludo-diff .on').dataset.d;
const bots = []; for(let i=1;i<count;i++) bots.push({name:'بوت '+i, difficulty:diff});
const res = await App.fetch('/api/ludo', {method:'POST', body:JSON.stringify({action:'create', player_count:count, bots})});
if(res&&res.ok) { const sr = await App.fetch('/api/ludo',{method:'POST',body:JSON.stringify({action:'start',match_id:res.match.id})}); if(sr&&sr.ok) location.href='/ludo-game?match_id='+sr.match.id; }
};
});
defineScreen('/domino', function dominoLobby() {
main.innerHTML = `
<h2 class="text-center mb-5" style="font-size:20px;font-weight:700;">🁣 دومينو</h2>
<div class="card card-pad mb-4">
<div class="chips mb-3" id="dom-mode"><button class="chip on" data-m="2p">فردي</button><button class="chip" data-m="4p_teams">فرق</button></div>
<div class="chips mb-4" id="dom-diff"><button class="chip on" data-d="easy">سهل</button><button class="chip" data-d="medium">متوسط</button><button class="chip" data-d="hard">صعب</button></div>
<button class="btn btn-gold btn-block btn-lg" id="btn-dom">ابدأ</button>
</div>
`;
document.querySelectorAll('.chips').forEach(g => g.querySelectorAll('.chip').forEach(c => { c.onclick=()=>{g.querySelectorAll('.chip').forEach(x=>x.classList.remove('on'));c.classList.add('on');}; }));
document.getElementById('btn-dom').onclick = async () => {
const mode = document.querySelector('#dom-mode .on').dataset.m;
const diff = document.querySelector('#dom-diff .on').dataset.d;
const res = await App.fetch('/api/domino',{method:'POST',body:JSON.stringify({action:'create',mode,difficulty:diff})});
if(res&&res.ok&&res.match){const sr=await App.fetch('/api/domino',{method:'POST',body:JSON.stringify({action:'start',match_id:res.match.id})});if(sr&&sr.ok)location.href='/domino-game?match_id='+sr.match.id;}
};
});
defineScreen('/backgammon', function bgLobby() {
main.innerHTML = `
<h2 class="text-center mb-5" style="font-size:20px;font-weight:700;">🎯 طاولة</h2>
<div class="card card-pad mb-4">
<div class="chips mb-4" id="bg-len"><button class="chip on" data-l="1">1 نقطة</button><button class="chip" data-l="3">3</button><button class="chip" data-l="5">5</button></div>
<button class="btn btn-gold btn-block btn-lg" id="btn-bg">انشئ غرفة</button>
</div>
`;
document.querySelectorAll('.chips').forEach(g => g.querySelectorAll('.chip').forEach(c => { c.onclick=()=>{g.querySelectorAll('.chip').forEach(x=>x.classList.remove('on'));c.classList.add('on');}; }));
document.getElementById('btn-bg').onclick = async () => {
const len = parseInt(document.querySelector('#bg-len .on').dataset.l);
const res = await App.fetch('/api/backgammon',{method:'POST',body:JSON.stringify({action:'create',match_length:len})});
if(res&&res.ok&&res.match) location.href='/backgammon-game?match_id='+res.match.id;
};
});
defineScreen('/profile', async function profileScreen() {
main.innerHTML = '<div class="skel" style="height:200px;margin-bottom:16px;"></div><div class="skel" style="height:100px;"></div>';
const data = await App.fetch('/api/profile');
if(!data||!data.profile){main.innerHTML='<div class="empty">خطأ</div>';return;}
const p = data.profile;
main.innerHTML = `
<div class="card card-pad text-center mb-4" style="padding:24px;">
<div style="width:64px;height:64px;border-radius:50%;background:var(--bg-3);margin:0 auto 12px;display:flex;align-items:center;justify-content:center;box-shadow:0 0 0 3px var(--gold);">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="var(--text-2)" stroke-width="2"><circle cx="12" cy="7" r="4"/><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/></svg>
</div>
<h2 style="font-size:18px;font-weight:700;">${p.display_name||p.username}</h2>
<p style="font-size:13px;color:var(--text-2);">@${p.username}</p>
<div style="display:flex;gap:8px;justify-content:center;margin-top:12px;">
<span class="stat-pill"><span class="stat-pill-val" style="color:var(--gold)">Lv ${p.level||1}</span></span>
<span class="stat-pill"><span class="stat-pill-val">${p.total_games_played||0}</span><span class="stat-pill-lbl">مباراة</span></span>
<span class="stat-pill"><span class="stat-pill-val" style="color:var(--win)">${p.total_wins||0}</span><span class="stat-pill-lbl">فوز</span></span>
</div>
</div>
<div class="card card-pad mb-4" style="display:flex;justify-content:space-around;">
<div class="text-center"><p style="font-size:18px;font-weight:700;color:var(--gold)">${(p.coins||0).toLocaleString()}</p><p style="font-size:10px;color:var(--text-2)">عملات</p></div>
<div class="text-center"><p style="font-size:18px;font-weight:700;color:var(--purple)">${p.gems||0}</p><p style="font-size:10px;color:var(--text-2)">جواهر</p></div>
<div class="text-center"><p style="font-size:18px;font-weight:700;">🔥 ${p.daily_streak||0}</p><p style="font-size:10px;color:var(--text-2)">ايام</p></div>
</div>
<button class="btn btn-ghost btn-block mb-3" onclick="route(event,'/settings')">⚙️ الاعدادات</button>
<button class="btn btn-block" style="color:var(--loss);border:1px solid var(--loss-dim);" onclick="App.logout()">تسجيل خروج</button>
`;
});
defineScreen('/leaderboard', async function lbScreen() {
main.innerHTML = '<h2 class="text-center mb-4" style="font-size:20px;font-weight:700;">🏆 المتصدرين</h2><div class="card"><div class="skel" style="height:200px;"></div></div>';
const data = await App.fetch('/api/leaderboard?mode=blitz');
if(!data||!data.players||!data.players.length){main.querySelector('.card').innerHTML='<div class="empty">لا لاعبين بعد</div>';return;}
main.querySelector('.card').innerHTML = '<div class="activity">'+data.players.map((p,i)=>{
const medal = i===0?'🥇':i===1?'🥈':i===2?'🥉':(i+1);
return '<div class="activity-item"><span style="min-width:28px;text-align:center;font-weight:700;">'+medal+'</span><div class="activity-text">'+(p.display_name||p.username)+'</div><span style="font-weight:700;font-variant-numeric:tabular-nums;">'+(p.elo_blitz||1200)+'</span></div>';
}).join('')+'</div>';
});
defineScreen('/friends', async function friendsScreen() {
main.innerHTML = '<h2 class="text-center mb-4" style="font-size:20px;font-weight:700;">👥 الاصدقاء</h2><div class="card"><div class="skel" style="height:150px;"></div></div>';
const data = await App.fetch('/api/friends?action=list');
if(!data||!data.friends||!data.friends.length){main.querySelector('.card').innerHTML='<div class="empty">لا اصدقاء بعد</div>';return;}
main.querySelector('.card').innerHTML = '<div class="activity">'+data.friends.map(f=>'<div class="activity-item"><span style="width:8px;height:8px;border-radius:50%;background:'+(f.is_online?'var(--online)':'var(--text-3)')+'"></span><div class="activity-text">'+(f.display_name||f.username)+'</div><span style="font-size:11px;color:var(--text-2);">'+(f.is_online?'متصل':'غير متصل')+'</span></div>').join('')+'</div>';
});
defineScreen('/settings', function settingsScreen() {
main.innerHTML = `
<h2 class="text-center mb-5" style="font-size:20px;font-weight:700;">⚙️ الاعدادات</h2>
<div class="card card-pad mb-3" style="display:flex;align-items:center;justify-content:space-between;">
<span>🔊 الصوت</span>
<button class="btn btn-ghost" id="snd-btn" style="min-height:36px;padding:6px 14px;font-size:12px;">${App.isMuted&&App.isMuted()?'مكتوم':'مفعل'}</button>
</div>
<button class="btn btn-block" style="color:var(--loss);border:1px solid var(--loss-dim);" onclick="App.logout()">تسجيل خروج</button>
`;
const btn = document.getElementById('snd-btn');
if(btn) btn.onclick = () => { if(App.toggleMute) App.toggleMute(); btn.textContent = App.isMuted&&App.isMuted()?'مكتوم':'مفعل'; };
});
defineScreen('/games', function(){route(null,'/');});
defineScreen('/notifications', function(){main.innerHTML='<div class="empty">لا اشعارات جديدة</div>';});
defineScreen('/shop', async function(){main.innerHTML='<h2 class="text-center mb-4" style="font-size:20px;font-weight:700;">🛍️ المتجر</h2><div class="empty">قريبا</div>';});
defineScreen('/achievements', async function(){main.innerHTML='<h2 class="text-center mb-4" style="font-size:20px;font-weight:700;">🏅 الانجازات</h2><div class="empty">قريبا</div>';});
// === GAME PAGES (load via full page reload to ensure game engines get clean DOM) ===
defineScreen('/ludo-game', function(){ location.reload(); });
defineScreen('/domino-game', function(){ location.reload(); });
defineScreen('/backgammon-game', function(){ location.reload(); });
// === BOOT ===
(function boot() {
const path = location.pathname;
if (!App.isLoggedIn() && path !== '/login' && path !== '/register') {
render('/login');
return;
}
if (App.isLoggedIn() && path === '/login') { history.replaceState({},'','/'); render('/'); return; }
// Show/hide header+nav for login
if (path !== '/login') {
document.getElementById('hdr').classList.remove('hide');
document.getElementById('nav').classList.remove('hide');
}
// Exit immersive if not on /game
if (path !== '/game') document.getElementById('shell').classList.remove('immersive');
<script type="module" src="/app/main.js"></script> render(path);
})();
</script>
</body> </body>
</html> </html>
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