Commit 40356211 authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix: friends tab navigation + full friends system upgrade

- Fix: clicking friends tab now always shows friends scene (not
  notifications). Tab click resets to root when already active.
- Friends list: sorted online-first, shows count, retry on error
- Pending requests: fetches requester profiles (name, avatar, level)
  instead of showing raw UUIDs
- Search: searches both username and display_name, debounced input
- Online tab: auto-refreshes every 15s
- Activity feed: timeAgo formatting, action icons
- API: added 'profiles' action for batch profile lookups, search
  now matches display_name too
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent d2ec1d78
...@@ -62,14 +62,25 @@ if ($method === 'GET') { ...@@ -62,14 +62,25 @@ if ($method === 'GET') {
if (strlen($query) < 2) jsonError('Query too short'); if (strlen($query) < 2) jsonError('Query too short');
$results = $db->get('profiles', [ $results = $db->get('profiles', [
'username' => 'ilike.*' . $query . '*', 'or' => "(username.ilike.*{$query}*,display_name.ilike.*{$query}*)",
'id' => 'neq.' . $userId, 'id' => 'neq.' . $userId,
'select' => 'id,username,display_name,avatar_url,level,is_online', 'select' => 'id,username,display_name,avatar_url,level,is_online',
'limit' => 10 'limit' => 15
]); ]);
jsonResponse(['players' => is_array($results) && !isset($results['error']) ? $results : []]); jsonResponse(['players' => is_array($results) && !isset($results['error']) ? $results : []]);
} }
if ($action === 'profiles') {
$ids = $_GET['ids'] ?? '';
if (!$ids) jsonResponse(['profiles' => []]);
$idList = implode(',', array_map('trim', explode(',', $ids)));
$profiles = $db->get('profiles', [
'id' => "in.({$idList})",
'select' => 'id,username,display_name,avatar_url,level,is_online'
]);
jsonResponse(['profiles' => is_array($profiles) && !isset($profiles['error']) ? $profiles : []]);
}
} }
if ($method === 'POST') { if ($method === 'POST') {
......
...@@ -98,7 +98,8 @@ function renderTabBar() { ...@@ -98,7 +98,8 @@ function renderTabBar() {
const item = e.target.closest('.tab-item'); const item = e.target.closest('.tab-item');
if (!item) return; if (!item) return;
const world = item.dataset.world; const world = item.dataset.world;
scene.switchWorld(world); const isAlreadyActive = item.classList.contains('active');
scene.switchWorld(world, isAlreadyActive);
}); });
document.body.appendChild(tabBar); document.body.appendChild(tabBar);
......
...@@ -26,15 +26,19 @@ export function register(id, mountFn, unmountFn) { ...@@ -26,15 +26,19 @@ export function register(id, mountFn, unmountFn) {
sceneRegistry[id] = { mount: mountFn, unmount: unmountFn }; sceneRegistry[id] = { mount: mountFn, unmount: unmountFn };
} }
export function switchWorld(world) { export function switchWorld(world, resetToRoot = false) {
if (!worlds.includes(world) || world === currentWorld) return; if (!worlds.includes(world)) return;
if (world === currentWorld && !resetToRoot) return;
const prev = currentWorld; const prev = currentWorld;
currentWorld = world; currentWorld = world;
store.set('activeWorld', world); store.set('activeWorld', world);
bus.emit('world:changed', { from: prev, to: world }); bus.emit('world:changed', { from: prev, to: world });
const stack = sceneStacks[world]; const stack = sceneStacks[world];
if (stack.length > 0) { if (resetToRoot && stack.length > 1) {
sceneStacks[world] = [stack[0]];
renderScene(stack[0]);
} else if (stack.length > 0) {
renderScene(stack[stack.length - 1]); renderScene(stack[stack.length - 1]);
} }
} }
......
...@@ -2,27 +2,30 @@ import * as net from '../../../core/net.js'; ...@@ -2,27 +2,30 @@ import * as net from '../../../core/net.js';
import * as scene from '../../../core/scene.js'; import * as scene from '../../../core/scene.js';
import * as audio from '../../../core/audio.js'; import * as audio from '../../../core/audio.js';
import * as bus from '../../../core/bus.js'; import * as bus from '../../../core/bus.js';
import * as store from '../../../core/store.js';
import * as juice from '../../../core/juice.js'; import * as juice from '../../../core/juice.js';
import { t } from '../../../core/i18n.js'; import { t } from '../../../core/i18n.js';
import { emoji } from '../../../core/theme.js'; import { emoji } from '../../../core/theme.js';
let activeTab = 'friends'; let activeTab = 'friends';
let refreshTimer = null;
export function mountFriends(el) {
activeTab = 'friends';
if (refreshTimer) clearInterval(refreshTimer);
export async function mountFriends(el) {
el.innerHTML = ` el.innerHTML = `
<div style="display:flex;flex-direction:column;height:100%;"> <div style="display:flex;flex-direction:column;height:100%;">
<!-- Header with search -->
<div style="padding:12px 16px;background:#0f0f1e;"> <div style="padding:12px 16px;background:#0f0f1e;">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;"> <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;">
<h2 style="font-size:18px;font-weight:700;color:#f8fafc;">${t('social.friends')}</h2> <h2 style="font-size:18px;font-weight:700;color:#f8fafc;">${t('social.friends')}</h2>
<button class="btn btn-secondary" id="btn-search" style="min-height:34px;padding:6px 14px;font-size:12px;">🔍 بحث</button> <button class="btn btn-secondary" id="btn-search" style="min-height:34px;padding:6px 14px;font-size:12px;">${emoji('search_icon', '🔍', 13)} بحث</button>
</div> </div>
<!-- Tabs -->
<div style="display:flex;gap:6px;flex-wrap:wrap;"> <div style="display:flex;gap:6px;flex-wrap:wrap;">
<button class="social-tab active" data-tab="friends">الأصدقاء</button> <button class="social-tab active" data-tab="friends">الأصدقاء</button>
<button class="social-tab" data-tab="pending">الطلبات</button> <button class="social-tab" data-tab="pending">الطلبات <span id="pending-count" style="display:none;font-size:10px;background:#EF4444;color:#fff;border-radius:50%;padding:1px 5px;margin-right:2px;"></span></button>
<button class="social-tab" data-tab="online">متصلين</button> <button class="social-tab" data-tab="online">متصلين</button>
<button class="social-tab" data-tab="activity">📰 أخبار</button> <button class="social-tab" data-tab="activity">${emoji('news', '📰', 12)} أخبار</button>
</div> </div>
</div> </div>
<div id="social-content" style="flex:1;overflow-y:auto;padding:12px 16px;"></div> <div id="social-content" style="flex:1;overflow-y:auto;padding:12px 16px;"></div>
...@@ -33,22 +36,20 @@ export async function mountFriends(el) { ...@@ -33,22 +36,20 @@ export async function mountFriends(el) {
.social-tab:active{transform:scale(0.95);} .social-tab:active{transform:scale(0.95);}
.friend-card{display:flex;align-items:center;gap:12px;padding:12px;background:#1a1a2e;border-radius:12px;margin-bottom:8px;transition:transform 0.1s;} .friend-card{display:flex;align-items:center;gap:12px;padding:12px;background:#1a1a2e;border-radius:12px;margin-bottom:8px;transition:transform 0.1s;}
.friend-card:active{transform:scale(0.98);} .friend-card:active{transform:scale(0.98);}
.friend-avatar{width:44px;height:44px;border-radius:50%;background:#2a2a4a;display:flex;align-items:center;justify-content:center;font-size:18px;overflow:hidden;position:relative;} .friend-avatar{width:44px;height:44px;border-radius:50%;background:#2a2a4a;display:flex;align-items:center;justify-content:center;font-size:18px;overflow:hidden;position:relative;flex-shrink:0;}
.friend-avatar img{width:100%;height:100%;object-fit:cover;border-radius:50%;} .friend-avatar img{width:100%;height:100%;object-fit:cover;border-radius:50%;}
.online-dot{position:absolute;bottom:0;right:0;width:12px;height:12px;border-radius:50%;background:#34D399;border:2px solid #1a1a2e;} .online-dot{position:absolute;bottom:0;right:0;width:12px;height:12px;border-radius:50%;background:#34D399;border:2px solid #1a1a2e;}
.friend-actions{display:flex;gap:6px;} .friend-actions{display:flex;gap:6px;flex-shrink:0;}
.friend-action{width:34px;height:34px;border-radius:8px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.08);display:flex;align-items:center;justify-content:center;font-size:14px;cursor:pointer;transition:transform 0.1s,background 0.15s;} .friend-action{width:34px;height:34px;border-radius:8px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.08);display:flex;align-items:center;justify-content:center;font-size:14px;cursor:pointer;transition:transform 0.1s,background 0.15s;}
.friend-action:active{transform:scale(0.85);background:rgba(255,255,255,0.1);} .friend-action:active{transform:scale(0.85);background:rgba(255,255,255,0.1);}
</style> </style>
`; `;
// Search button
el.querySelector('#btn-search').addEventListener('click', () => { el.querySelector('#btn-search').addEventListener('click', () => {
audio.play('click'); audio.play('click');
showSearch(el); showSearch(el);
}); });
// Tab switching
el.querySelectorAll('.social-tab').forEach(tab => { el.querySelectorAll('.social-tab').forEach(tab => {
tab.addEventListener('click', () => { tab.addEventListener('click', () => {
audio.play('click'); audio.play('click');
...@@ -60,6 +61,27 @@ export async function mountFriends(el) { ...@@ -60,6 +61,27 @@ export async function mountFriends(el) {
}); });
loadTab(el, 'friends'); loadTab(el, 'friends');
loadPendingCount(el);
refreshTimer = setInterval(() => {
if (activeTab === 'online') loadTab(el, 'online');
}, 15000);
}
async function loadPendingCount(el) {
try {
const data = await net.get('friends.php', { action: 'pending' });
const count = (data.pending || []).length;
const badge = el.querySelector('#pending-count');
if (badge) {
if (count > 0) {
badge.textContent = count;
badge.style.display = 'inline';
} else {
badge.style.display = 'none';
}
}
} catch (e) {}
} }
async function loadTab(el, tab) { async function loadTab(el, tab) {
...@@ -68,7 +90,7 @@ async function loadTab(el, tab) { ...@@ -68,7 +90,7 @@ async function loadTab(el, tab) {
switch (tab) { switch (tab) {
case 'friends': await loadFriends(content); break; case 'friends': await loadFriends(content); break;
case 'pending': await loadPending(content); break; case 'pending': await loadPending(content, el); break;
case 'online': await loadOnline(content); break; case 'online': await loadOnline(content); break;
case 'activity': await loadActivity(content); break; case 'activity': await loadActivity(content); break;
} }
...@@ -87,56 +109,108 @@ async function loadFriends(content) { ...@@ -87,56 +109,108 @@ async function loadFriends(content) {
<div style="font-size:12px;color:#64748b;margin-bottom:16px;">ابحث عن لاعبين وأرسل لهم طلب صداقة</div> <div style="font-size:12px;color:#64748b;margin-bottom:16px;">ابحث عن لاعبين وأرسل لهم طلب صداقة</div>
<button class="btn btn-primary" id="empty-search" style="font-size:13px;padding:10px 24px;">${emoji('search_icon', '🔍', 13)} ابحث عن لاعبين</button> <button class="btn btn-primary" id="empty-search" style="font-size:13px;padding:10px 24px;">${emoji('search_icon', '🔍', 13)} ابحث عن لاعبين</button>
</div>`; </div>`;
content.querySelector('#empty-search')?.addEventListener('click', () => showSearch(content.closest('[style*="height:100%"]') || content.parentElement)); content.querySelector('#empty-search')?.addEventListener('click', () => {
showSearch(content.closest('[style*="height:100%"]') || content.parentElement);
});
return; return;
} }
content.innerHTML = friends.map(f => renderFriendCard(f, 'friend')).join(''); const online = friends.filter(f => f.is_online);
const offline = friends.filter(f => !f.is_online);
const sorted = [...online, ...offline];
content.innerHTML = `
<div style="font-size:11px;color:#64748b;margin-bottom:8px;">${friends.length} أصدقاء${online.length > 0 ? ` — ${online.length} متصل` : ''}</div>
${sorted.map(f => renderFriendCard(f)).join('')}
`;
bindFriendActions(content); bindFriendActions(content);
} catch (e) { } catch (e) {
content.innerHTML = `<div style="text-align:center;color:#ef4444;">فشل التحميل</div>`; content.innerHTML = `<div style="text-align:center;color:#ef4444;padding:24px;">فشل التحميل — <span style="text-decoration:underline;cursor:pointer;" id="retry-friends">حاول مرة أخرى</span></div>`;
content.querySelector('#retry-friends')?.addEventListener('click', () => loadFriends(content));
} }
} }
async function loadPending(content) { async function loadPending(content, rootEl) {
try { try {
const data = await net.get('friends.php', { action: 'pending' }); const data = await net.get('friends.php', { action: 'pending' });
const pending = data.pending || []; const pending = data.pending || [];
if (pending.length === 0) { if (pending.length === 0) {
content.innerHTML = `<div style="text-align:center;padding:40px;color:#64748b;">لا توجد طلبات معلقة</div>`; content.innerHTML = `
<div style="text-align:center;padding:40px;color:#64748b;">
<div style="font-size:32px;margin-bottom:8px;opacity:0.5;">${emoji('inbox', '📥', 32)}</div>
<div>لا توجد طلبات معلقة</div>
</div>`;
return; return;
} }
content.innerHTML = pending.map(p => ` const requesterIds = pending.map(p => p.requester_id).filter(Boolean);
<div class="friend-card"> let profiles = {};
<div class="friend-avatar">${emoji('person', '👤', 16)}</div> if (requesterIds.length > 0) {
<div style="flex:1;"> try {
<div style="font-size:13px;font-weight:600;color:#f8fafc;">${p.requester_id?.substring(0, 8) || 'Player'}</div> const profileData = await net.get('friends.php', { action: 'profiles', ids: requesterIds.join(',') });
<div style="font-size:11px;color:#64748b;">أرسل لك طلب صداقة</div> (profileData.profiles || []).forEach(p => { profiles[p.id] = p; });
</div> } catch (e) {}
<div class="friend-actions"> }
<div class="friend-action" data-accept="${p.id}" title="قبول" style="color:#34D399;"></div>
<div class="friend-action" data-reject="${p.id}" title="رفض" style="color:#EF4444;"></div> content.innerHTML = `
</div> <div style="font-size:11px;color:#64748b;margin-bottom:10px;">${pending.length} طلب معلق</div>
</div> ${pending.map(p => {
`).join(''); const profile = profiles[p.requester_id] || {};
const name = profile.display_name || profile.username || p.requester_id?.substring(0, 8) || 'لاعب';
const avatar = profile.avatar_url;
const level = profile.level || 1;
return `
<div class="friend-card" data-friendship="${p.id}">
<div class="friend-avatar">
${avatar ? `<img src="${avatar}">` : emoji('person', '👤', 18)}
</div>
<div style="flex:1;min-width:0;">
<div style="font-size:13px;font-weight:600;color:#f8fafc;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${name}</div>
<div style="font-size:11px;color:#64748b;">مستوى ${level} — أرسل لك طلب صداقة</div>
</div>
<div class="friend-actions">
<div class="friend-action" data-accept="${p.id}" title="قبول" style="background:rgba(52,211,153,0.15);border-color:rgba(52,211,153,0.3);color:#34D399;font-weight:700;">✓</div>
<div class="friend-action" data-reject="${p.id}" title="رفض" style="background:rgba(239,68,68,0.1);border-color:rgba(239,68,68,0.2);color:#EF4444;">✕</div>
</div>
</div>
`;
}).join('')}
`;
content.querySelectorAll('[data-accept]').forEach(btn => { content.querySelectorAll('[data-accept]').forEach(btn => {
btn.addEventListener('click', async () => { btn.addEventListener('click', async () => {
audio.play('coin', 'reward'); const card = btn.closest('.friend-card');
juice.hapticLight(); const friendshipId = btn.dataset.accept;
await net.post('friends.php', { action: 'accept', friendship_id: btn.dataset.accept }); btn.style.opacity = '0.5';
btn.closest('.friend-card').style.opacity = '0.5'; btn.style.pointerEvents = 'none';
btn.closest('.friend-card').querySelector('.friend-actions').innerHTML = '<span style="font-size:11px;color:#34D399;">✓ تم القبول</span>'; try {
await net.post('friends.php', { action: 'accept', friendship_id: friendshipId });
audio.play('reward');
juice.hapticLight();
card.querySelector('.friend-actions').innerHTML = '<span style="font-size:11px;color:#34D399;font-weight:600;">✓ تم القبول</span>';
card.style.borderRight = '3px solid #34D399';
loadPendingCount(rootEl);
} catch (e) {
btn.style.opacity = '1';
btn.style.pointerEvents = 'auto';
}
}); });
}); });
content.querySelectorAll('[data-reject]').forEach(btn => { content.querySelectorAll('[data-reject]').forEach(btn => {
btn.addEventListener('click', async () => { btn.addEventListener('click', async () => {
audio.play('click'); const card = btn.closest('.friend-card');
await net.del('friends.php', { target_id: btn.dataset.reject }); const friendshipId = btn.dataset.reject;
btn.closest('.friend-card').remove(); try {
await net.del('friends.php', { target_id: friendshipId });
audio.play('click');
card.style.transition = 'opacity 0.3s, transform 0.3s';
card.style.opacity = '0';
card.style.transform = 'translateX(20px)';
setTimeout(() => card.remove(), 300);
loadPendingCount(rootEl);
} catch (e) {}
}); });
}); });
} catch (e) { } catch (e) {
...@@ -150,32 +224,41 @@ async function loadOnline(content) { ...@@ -150,32 +224,41 @@ async function loadOnline(content) {
const friends = (data.friends || []).filter(f => f.is_online); const friends = (data.friends || []).filter(f => f.is_online);
if (friends.length === 0) { if (friends.length === 0) {
content.innerHTML = `<div style="text-align:center;padding:40px;color:#64748b;">لا يوجد أصدقاء متصلين الآن</div>`; content.innerHTML = `
<div style="text-align:center;padding:40px;color:#64748b;">
<div style="font-size:32px;margin-bottom:8px;opacity:0.5;">${emoji('sleeping', '😴', 32)}</div>
<div style="font-size:14px;margin-bottom:4px;">لا يوجد أصدقاء متصلين الآن</div>
<div style="font-size:11px;color:#475569;">سيظهرون هنا عند دخولهم</div>
</div>`;
return; return;
} }
content.innerHTML = friends.map(f => renderFriendCard(f, 'online')).join(''); content.innerHTML = `
<div style="font-size:11px;color:#34D399;margin-bottom:10px;">${friends.length} متصل الآن</div>
${friends.map(f => renderFriendCard(f)).join('')}
`;
bindFriendActions(content); bindFriendActions(content);
} catch (e) { } catch (e) {
content.innerHTML = `<div style="text-align:center;color:#ef4444;">فشل التحميل</div>`; content.innerHTML = `<div style="text-align:center;color:#ef4444;">فشل التحميل</div>`;
} }
} }
function renderFriendCard(f, context) { function renderFriendCard(f) {
const userId = store.get('auth.userId');
return ` return `
<div class="friend-card" data-uid="${f.id}"> <div class="friend-card" data-uid="${f.id}">
<div class="friend-avatar"> <div class="friend-avatar">
${f.avatar_url ? `<img src="${f.avatar_url}">` : emoji('person', '👤', 16)} ${f.avatar_url ? `<img src="${f.avatar_url}">` : emoji('person', '👤', 18)}
${f.is_online ? '<div class="online-dot"></div>' : ''} ${f.is_online ? '<div class="online-dot"></div>' : ''}
</div> </div>
<div style="flex:1;"> <div style="flex:1;min-width:0;">
<div style="font-size:13px;font-weight:600;color:#f8fafc;">${f.display_name || f.username}</div> <div style="font-size:13px;font-weight:600;color:#f8fafc;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${f.display_name || f.username || 'لاعب'}</div>
<div style="font-size:11px;color:${f.is_online ? '#34D399' : '#64748b'};">${f.is_online ? 'متصل الآن' : 'غير متصل'}</div> <div style="font-size:11px;color:${f.is_online ? '#34D399' : '#64748b'};">${f.is_online ? 'متصل الآن' : 'غير متصل'}${f.level ? ` — مستوى ${f.level}` : ''}</div>
</div> </div>
<div class="friend-actions"> <div class="friend-actions">
${f.is_online ? `<div class="friend-action" data-invite="${f.id}" title="دعوة للعب">${emoji('challenge_swords', '⚔️', 16)}</div>` : ''} ${f.is_online ? `<div class="friend-action" data-invite="${f.id}" title="تحدّي" style="background:rgba(228,172,56,0.15);border-color:rgba(228,172,56,0.3);color:#E4AC38;">${emoji('challenge_swords', '⚔️', 14)}</div>` : ''}
<div class="friend-action" data-profile="${f.id}" title="الملف الشخصي">${emoji('person', '👤', 16)}</div> <div class="friend-action" data-profile="${f.id}" title="الملف الشخصي">${emoji('person', '👤', 14)}</div>
<div class="friend-action" data-remove="${f.id}" title="إزالة" style="font-size:12px;">✕</div> <div class="friend-action" data-remove="${f.id}" title="إزالة" style="font-size:11px;color:#64748b;">✕</div>
</div> </div>
</div> </div>
`; `;
...@@ -186,20 +269,34 @@ function bindFriendActions(content) { ...@@ -186,20 +269,34 @@ function bindFriendActions(content) {
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
audio.play('click'); audio.play('click');
juice.hapticLight(); juice.hapticLight();
bus.emit('navigate', { world: 'play' }); const uid = btn.dataset.invite;
setTimeout(() => { const card = btn.closest('.friend-card');
const tile = document.querySelector('[data-game="chess"]'); const name = card?.querySelector('[style*="font-weight:600"]')?.textContent || 'صديق';
if (tile) tile.click(); bus.emit('challenge:send', { targetId: uid, targetName: name });
}, 500); scene.switchWorld('play');
});
});
content.querySelectorAll('[data-profile]').forEach(btn => {
btn.addEventListener('click', () => {
audio.play('click');
scene.push('player-profile', { playerId: btn.dataset.profile });
}); });
}); });
content.querySelectorAll('[data-remove]').forEach(btn => { content.querySelectorAll('[data-remove]').forEach(btn => {
btn.addEventListener('click', async () => { btn.addEventListener('click', async () => {
if (!confirm('إزالة هذا الصديق؟')) return;
audio.play('click'); audio.play('click');
await net.del('friends.php', { target_id: btn.dataset.remove }); const card = btn.closest('.friend-card');
btn.closest('.friend-card').remove(); const name = card?.querySelector('[style*="font-weight:600"]')?.textContent || 'هذا الصديق';
if (!confirm(`إزالة ${name} من الأصدقاء؟`)) return;
try {
await net.del('friends.php', { target_id: btn.dataset.remove });
card.style.transition = 'opacity 0.3s, transform 0.3s';
card.style.opacity = '0';
card.style.transform = 'translateX(20px)';
setTimeout(() => card.remove(), 300);
} catch (e) {}
}); });
}); });
} }
...@@ -209,9 +306,10 @@ function showSearch(el) { ...@@ -209,9 +306,10 @@ function showSearch(el) {
content.innerHTML = ` content.innerHTML = `
<div style="margin-bottom:16px;"> <div style="margin-bottom:16px;">
<div style="display:flex;gap:8px;"> <div style="display:flex;gap:8px;">
<input class="input" id="search-input" type="text" placeholder="اسم المستخدم..." style="flex:1;min-height:40px;font-size:14px;"> <input class="input" id="search-input" type="text" placeholder="اسم المستخدم أو الاسم..." style="flex:1;min-height:40px;font-size:14px;" autocomplete="off">
<button class="btn btn-primary" id="search-go" style="min-height:40px;padding:8px 16px;font-size:13px;">بحث</button> <button class="btn btn-primary" id="search-go" style="min-height:40px;padding:8px 16px;font-size:13px;">بحث</button>
</div> </div>
<div style="font-size:11px;color:#475569;margin-top:4px;">ابحث بأي جزء من الاسم (حرفين على الأقل)</div>
</div> </div>
<div id="search-results"></div> <div id="search-results"></div>
`; `;
...@@ -220,9 +318,13 @@ function showSearch(el) { ...@@ -220,9 +318,13 @@ function showSearch(el) {
const btn = content.querySelector('#search-go'); const btn = content.querySelector('#search-go');
input.focus(); input.focus();
let debounceTimer = null;
const doSearch = async () => { const doSearch = async () => {
const query = input.value.trim(); const query = input.value.trim();
if (query.length < 2) return; if (query.length < 2) {
content.querySelector('#search-results').innerHTML = '<div style="text-align:center;color:#64748b;padding:12px;font-size:12px;">أدخل حرفين على الأقل</div>';
return;
}
const results = content.querySelector('#search-results'); const results = content.querySelector('#search-results');
results.innerHTML = '<div style="text-align:center;color:#64748b;padding:12px;">جاري البحث...</div>'; results.innerHTML = '<div style="text-align:center;color:#64748b;padding:12px;">جاري البحث...</div>';
...@@ -230,28 +332,45 @@ function showSearch(el) { ...@@ -230,28 +332,45 @@ function showSearch(el) {
const data = await net.get('friends.php', { action: 'search', query }); const data = await net.get('friends.php', { action: 'search', query });
const players = data.players || []; const players = data.players || [];
if (players.length === 0) { if (players.length === 0) {
results.innerHTML = '<div style="text-align:center;color:#64748b;padding:12px;">لم يتم العثور على نتائج</div>'; results.innerHTML = '<div style="text-align:center;color:#64748b;padding:24px;font-size:13px;">لم يتم العثور على نتائج — جرب اسم آخر</div>';
return; return;
} }
results.innerHTML = players.map(p => ` results.innerHTML = players.map(p => `
<div class="friend-card"> <div class="friend-card">
<div class="friend-avatar">${p.avatar_url ? `<img src="${p.avatar_url}">` : emoji('person', '👤', 16)}</div> <div class="friend-avatar">
<div style="flex:1;"> ${p.avatar_url ? `<img src="${p.avatar_url}">` : emoji('person', '👤', 18)}
<div style="font-size:13px;font-weight:600;color:#f8fafc;">${p.display_name || p.username}</div> ${p.is_online ? '<div class="online-dot"></div>' : ''}
<div style="font-size:11px;color:#64748b;">Level ${p.level || 1}</div>
</div> </div>
<button class="btn btn-primary" data-add="${p.id}" style="min-height:32px;padding:6px 12px;font-size:11px;">+ أضف</button> <div style="flex:1;min-width:0;">
<div style="font-size:13px;font-weight:600;color:#f8fafc;">${p.display_name || p.username || 'لاعب'}</div>
<div style="font-size:11px;color:#64748b;">${p.is_online ? 'متصل' : 'غير متصل'} — مستوى ${p.level || 1}</div>
</div>
<button class="btn btn-primary" data-add="${p.id}" style="min-height:32px;padding:6px 12px;font-size:11px;flex-shrink:0;">+ أضف</button>
</div> </div>
`).join(''); `).join('');
results.querySelectorAll('[data-add]').forEach(addBtn => { results.querySelectorAll('[data-add]').forEach(addBtn => {
addBtn.addEventListener('click', async () => { addBtn.addEventListener('click', async () => {
audio.play('coin', 'reward');
juice.hapticLight();
await net.post('friends.php', { action: 'request', target_id: addBtn.dataset.add });
addBtn.textContent = '✓ تم الإرسال';
addBtn.disabled = true; addBtn.disabled = true;
addBtn.style.background = '#34D399'; addBtn.textContent = '...';
try {
await net.post('friends.php', { action: 'request', target_id: addBtn.dataset.add });
audio.play('reward');
juice.hapticLight();
addBtn.textContent = '✓ تم الإرسال';
addBtn.style.background = '#34D399';
addBtn.style.borderColor = '#34D399';
} catch (e) {
const msg = e.message || '';
if (msg.includes('duplicate') || msg.includes('already')) {
addBtn.textContent = 'مرسل سابقاً';
addBtn.style.background = '#64748b';
} else {
addBtn.textContent = 'فشل';
addBtn.disabled = false;
setTimeout(() => { addBtn.textContent = '+ أضف'; }, 2000);
}
}
}); });
}); });
} catch (e) { } catch (e) {
...@@ -261,26 +380,66 @@ function showSearch(el) { ...@@ -261,26 +380,66 @@ function showSearch(el) {
btn.addEventListener('click', doSearch); btn.addEventListener('click', doSearch);
input.addEventListener('keydown', (e) => { if (e.key === 'Enter') doSearch(); }); input.addEventListener('keydown', (e) => { if (e.key === 'Enter') doSearch(); });
input.addEventListener('input', () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(doSearch, 500);
});
} }
async function loadActivity(content) { async function loadActivity(content) {
try { try {
const data = await net.get("activity.php"); const data = await net.get('activity.php');
const activities = data.activity || []; const activities = data.activity || [];
if (activities.length === 0) { if (activities.length === 0) {
content.innerHTML = `<div style="text-align:center;padding:40px;color:#64748b;">لا توجد أخبار — العب لتظهر أخبارك هنا</div>`; content.innerHTML = `
<div style="text-align:center;padding:40px;color:#64748b;">
<div style="font-size:32px;margin-bottom:8px;opacity:0.5;">${emoji('news', '📰', 32)}</div>
<div>لا توجد أخبار — العب لتظهر أخبارك هنا</div>
</div>`;
return; return;
} }
const labels = { game_win: "فاز بمباراة", game_loss: "خسر مباراة", achievement_unlock: "حصل على إنجاز", level_up: "ارتقى لمستوى جديد", tournament_join: "انضم لبطولة" }; const labels = {
game_win: 'فاز بمباراة',
game_loss: 'خسر مباراة',
game_draw: 'تعادل',
achievement_unlock: 'حصل على إنجاز',
level_up: 'ارتقى لمستوى جديد',
tournament_join: 'انضم لبطولة',
tournament_win: 'فاز ببطولة',
friend_add: 'أضاف صديقاً جديداً'
};
const icons = {
game_win: '🏆', game_loss: '💔', game_draw: '🤝',
achievement_unlock: '🎖️', level_up: '⬆️',
tournament_join: '🏅', tournament_win: '🥇', friend_add: '👋'
};
content.innerHTML = activities.map(a => { content.innerHTML = activities.map(a => {
const actor = a.actor || {}; const actor = a.actor || {};
const label = labels[a.action] || a.action; const label = labels[a.action] || a.action;
const icon = icons[a.action] || '📌';
return `<div style="display:flex;gap:10px;padding:10px;background:#1a1a2e;border-radius:10px;margin-bottom:6px;"> return `<div style="display:flex;gap:10px;padding:10px;background:#1a1a2e;border-radius:10px;margin-bottom:6px;">
<div style="width:36px;height:36px;border-radius:50%;background:#2a2a4a;display:flex;align-items:center;justify-content:center;font-size:14px;">${emoji('person', '👤', 14)}</div> <div style="width:36px;height:36px;border-radius:50%;background:#2a2a4a;display:flex;align-items:center;justify-content:center;font-size:14px;flex-shrink:0;">
<div style="flex:1;"><div style="font-size:13px;color:#f8fafc;"><strong>${actor.display_name || actor.username || "?"}</strong> ${label}</div></div> ${actor.avatar_url ? `<img src="${actor.avatar_url}" style="width:100%;height:100%;object-fit:cover;border-radius:50%;">` : icon}
</div>
<div style="flex:1;min-width:0;">
<div style="font-size:13px;color:#f8fafc;"><strong>${actor.display_name || actor.username || '?'}</strong> ${label}</div>
${a.created_at ? `<div style="font-size:10px;color:#475569;margin-top:2px;">${timeAgo(a.created_at)}</div>` : ''}
</div>
</div>`; </div>`;
}).join(""); }).join('');
} catch (e) { } catch (e) {
content.innerHTML = `<div style="text-align:center;color:#64748b;padding:32px;">لا توجد أخبار</div>`; content.innerHTML = `<div style="text-align:center;color:#64748b;padding:32px;">لا توجد أخبار</div>`;
} }
} }
function timeAgo(dateStr) {
const diff = Date.now() - new Date(dateStr).getTime();
const mins = Math.floor(diff / 60000);
if (mins < 1) return 'الآن';
if (mins < 60) return `منذ ${mins} دقيقة`;
const hours = Math.floor(mins / 60);
if (hours < 24) return `منذ ${hours} ساعة`;
const days = Math.floor(hours / 24);
if (days < 7) return `منذ ${days} يوم`;
return new Date(dateStr).toLocaleDateString('ar');
}
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