Commit f39a8dcb authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: all screens built — chess lobby, profile, leaderboard, friends, shop,...

feat: all screens built — chess lobby, profile, leaderboard, friends, shop, achievements, settings, ludo/domino/backgammon lobbies

Phase 2+3 complete:
- Chess lobby: full time control picker, bot selector, multiplayer search
- Chess game: wraps Board + Game engines in immersive mode
- Ludo lobby: player count, bot difficulty, room codes
- Domino lobby: 2P/4P teams, difficulty, room codes
- Backgammon lobby: match length, room codes
- Profile: XP bar, per-game ratings, economy, stats
- Leaderboard: multi-game + multi-mode selector
- Friends: list, requests, search, add
- Shop: categories, buy with coin burst particles, equip
- Achievements: grid with progress tracking
- Settings: sound toggle, logout
- Fix: router import bug in main.js
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 9d4a6fa3
import { state } from './core/state.js'; import { state } from './core/state.js';
import { router, navigate, register, init as initRouter } from './core/router.js'; import { navigate, register, init as initRouter, getParams } from './core/router.js';
import { initCanvas } from './core/particles.js'; import { initCanvas } from './core/particles.js';
import { api } from './core/api.js'; import { api } from './core/api.js';
......
import { api } from '../core/api.js';
export default { export default {
render() { return '<div class="empty" style="padding:80px 20px;text-align:center;"><p style="font-size:48px;margin-bottom:16px;">🚧</p><p>قيد الانشاء</p></div>'; }, render() {
mount() {}, 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() {} 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 { export default {
render() { return '<div class="empty" style="padding:80px 20px;text-align:center;"><p style="font-size:48px;margin-bottom:16px;">🚧</p><p>قيد الانشاء</p></div>'; }, render() {
mount() {}, 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() {} 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 { export default {
render() { return '<div class="empty" style="padding:80px 20px;text-align:center;"><p style="font-size:48px;margin-bottom:16px;">🚧</p><p>قيد الانشاء</p></div>'; }, render() {
mount() {}, 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() {} 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 { export default {
render() { return '<div class="empty" style="padding:80px 20px;text-align:center;"><p style="font-size:48px;margin-bottom:16px;">🚧</p><p>قيد الانشاء</p></div>'; }, render() {
mount() {}, 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() {} destroy() {}
}; };
import { api } from '../core/api.js';
import { haptics } from '../core/haptics.js';
import { audio } from '../core/audio.js';
export default { export default {
render() { return '<div class="empty" style="padding:80px 20px;text-align:center;"><p style="font-size:48px;margin-bottom:16px;">🚧</p><p>قيد الانشاء</p></div>'; }, render() {
mount() {}, 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() {} 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 { export default {
render() { return '<div class="empty" style="padding:80px 20px;text-align:center;"><p style="font-size:48px;margin-bottom:16px;">🚧</p><p>قيد الانشاء</p></div>'; }, render() {
mount() {}, return `
destroy() {} <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 { 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 { export default {
render() { return '<div class="empty" style="padding:80px 20px;text-align:center;"><p style="font-size:48px;margin-bottom:16px;">🚧</p><p>قيد الانشاء</p></div>'; }, render() {
mount() {}, 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() {} 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 { export default {
render() { return '<div class="empty" style="padding:80px 20px;text-align:center;"><p style="font-size:48px;margin-bottom:16px;">🚧</p><p>قيد الانشاء</p></div>'; }, render() {
mount() {}, 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() {} 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 { export default {
render() { return '<div class="empty" style="padding:80px 20px;text-align:center;"><p style="font-size:48px;margin-bottom:16px;">🚧</p><p>قيد الانشاء</p></div>'; }, render() {
mount() {}, 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() {} destroy() {}
}; };
import { audio } from '../core/audio.js';
import { haptics } from '../core/haptics.js';
import { state } from '../core/state.js';
export default { export default {
render() { return '<div class="empty" style="padding:80px 20px;text-align:center;"><p style="font-size:48px;margin-bottom:16px;">🚧</p><p>قيد الانشاء</p></div>'; }, render() {
mount() {}, 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() {} 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 { export default {
render() { return '<div class="empty" style="padding:80px 20px;text-align:center;"><p style="font-size:48px;margin-bottom:16px;">🚧</p><p>قيد الانشاء</p></div>'; }, render() {
mount() {}, 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() {} destroy() {}
}; };
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