Commit 1ae4c0ac authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix: friendship system + friend invite/challenge lobby

Friendship bugs fixed:
- Accept now uses service key (bypasses RLS that blocked updates)
- Duplicate request prevention (checks both directions)
- Reject uses proper POST action instead of broken DELETE call
- Cleaned up stale duplicate rows in DB

New features:
- Friend invite system: challenge dialog with game/time selection
- Match created as 'friendly' with status 'waiting'
- Opponent sees invite banner (polls every 5s), can accept/decline
- Accept starts the match, both players enter the game
- 2-minute invite expiry
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 432f8feb
...@@ -12,15 +12,17 @@ require_once __DIR__ . '/../includes/auth.php'; ...@@ -12,15 +12,17 @@ require_once __DIR__ . '/../includes/auth.php';
$token = requireAuth(); $token = requireAuth();
$userId = getUserId($token); $userId = getUserId($token);
$db = supabase($token);
$method = $_SERVER['REQUEST_METHOD']; $method = $_SERVER['REQUEST_METHOD'];
$action = $_GET['action'] ?? ($_POST['action'] ?? '');
// Use service key for all friendship operations to bypass RLS issues
$sdb = supabaseService();
if ($method === 'GET') { if ($method === 'GET') {
$action = $_GET['action'] ?? 'list'; $action = $_GET['action'] ?? 'list';
if ($action === 'list') { if ($action === 'list') {
$friends = $db->get('friendships', [ $friends = $sdb->get('friendships', [
'or' => "(requester_id.eq.{$userId},addressee_id.eq.{$userId})", 'or' => "(requester_id.eq.{$userId},addressee_id.eq.{$userId})",
'status' => 'eq.accepted', 'status' => 'eq.accepted',
'select' => 'id,requester_id,addressee_id' 'select' => 'id,requester_id,addressee_id'
...@@ -40,28 +42,37 @@ if ($method === 'GET') { ...@@ -40,28 +42,37 @@ if ($method === 'GET') {
} }
$idList = implode(',', $friendIds); $idList = implode(',', $friendIds);
$profiles = $db->get('profiles', [ $profiles = $sdb->get('profiles', [
'id' => "in.({$idList})", 'id' => "in.({$idList})",
'select' => 'id,username,display_name,avatar_url,is_online,level' 'select' => 'id,username,display_name,avatar_url,is_online,level'
]); ]);
jsonResponse(['friends' => $profiles ?: []]); jsonResponse(['friends' => is_array($profiles) && !isset($profiles['error']) ? $profiles : []]);
} }
if ($action === 'pending') { if ($action === 'pending') {
$pending = $db->get('friendships', [ $pending = $sdb->get('friendships', [
'addressee_id' => 'eq.' . $userId, 'addressee_id' => 'eq.' . $userId,
'status' => 'eq.pending', 'status' => 'eq.pending',
'select' => 'id,requester_id' 'select' => 'id,requester_id,created_at'
]);
jsonResponse(['pending' => is_array($pending) && !isset($pending['error']) ? $pending : []]);
}
if ($action === 'sent') {
$sent = $sdb->get('friendships', [
'requester_id' => 'eq.' . $userId,
'status' => 'eq.pending',
'select' => 'id,addressee_id,created_at'
]); ]);
jsonResponse(['pending' => $pending ?: []]); jsonResponse(['sent' => is_array($sent) && !isset($sent['error']) ? $sent : []]);
} }
if ($action === 'search') { if ($action === 'search') {
$query = $_GET['query'] ?? ''; $query = $_GET['query'] ?? '';
if (strlen($query) < 2) jsonError('Query too short'); if (strlen($query) < 2) jsonError('Query too short');
$results = $db->get('profiles', [ $results = $sdb->get('profiles', [
'or' => "(username.ilike.*{$query}*,display_name.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',
...@@ -75,12 +86,14 @@ if ($method === 'GET') { ...@@ -75,12 +86,14 @@ if ($method === 'GET') {
$ids = $_GET['ids'] ?? ''; $ids = $_GET['ids'] ?? '';
if (!$ids) jsonResponse(['profiles' => []]); if (!$ids) jsonResponse(['profiles' => []]);
$idList = implode(',', array_map('trim', explode(',', $ids))); $idList = implode(',', array_map('trim', explode(',', $ids)));
$profiles = $db->get('profiles', [ $profiles = $sdb->get('profiles', [
'id' => "in.({$idList})", 'id' => "in.({$idList})",
'select' => 'id,username,display_name,avatar_url,level,is_online' 'select' => 'id,username,display_name,avatar_url,level,is_online'
]); ]);
jsonResponse(['profiles' => is_array($profiles) && !isset($profiles['error']) ? $profiles : []]); jsonResponse(['profiles' => is_array($profiles) && !isset($profiles['error']) ? $profiles : []]);
} }
jsonError('Invalid action');
} }
if ($method === 'POST') { if ($method === 'POST') {
...@@ -90,8 +103,22 @@ if ($method === 'POST') { ...@@ -90,8 +103,22 @@ if ($method === 'POST') {
if ($action === 'request') { if ($action === 'request') {
$targetId = $input['target_id'] ?? ''; $targetId = $input['target_id'] ?? '';
if (!$targetId) jsonError('target_id required'); if (!$targetId) jsonError('target_id required');
if ($targetId === $userId) jsonError('Cannot add yourself');
// Check for existing friendship or pending request in EITHER direction
$existing = $sdb->get('friendships', [
'or' => "(and(requester_id.eq.{$userId},addressee_id.eq.{$targetId}),and(requester_id.eq.{$targetId},addressee_id.eq.{$userId}))",
'select' => 'id,status',
'limit' => 1
]);
if (is_array($existing) && !isset($existing['error']) && !empty($existing)) {
$status = $existing[0]['status'] ?? '';
if ($status === 'accepted') jsonError('Already friends');
if ($status === 'pending') jsonError('Request already pending');
}
$result = $db->insert('friendships', [ $result = $sdb->insert('friendships', [
'requester_id' => $userId, 'requester_id' => $userId,
'addressee_id' => $targetId, 'addressee_id' => $targetId,
'status' => 'pending' 'status' => 'pending'
...@@ -104,12 +131,196 @@ if ($method === 'POST') { ...@@ -104,12 +131,196 @@ if ($method === 'POST') {
$friendshipId = $input['friendship_id'] ?? ''; $friendshipId = $input['friendship_id'] ?? '';
if (!$friendshipId) jsonError('friendship_id required'); if (!$friendshipId) jsonError('friendship_id required');
$result = $db->update('friendships', ['status' => 'accepted'], [ // Verify this request is addressed to the current user
$friendship = $sdb->get('friendships', [
'id' => 'eq.' . $friendshipId, 'id' => 'eq.' . $friendshipId,
'addressee_id' => 'eq.' . $userId 'addressee_id' => 'eq.' . $userId,
'status' => 'eq.pending',
'select' => 'id,requester_id',
'limit' => 1
]);
if (!is_array($friendship) || isset($friendship['error']) || empty($friendship)) {
jsonError('Friend request not found or already handled');
}
$result = $sdb->update('friendships', [
'status' => 'accepted',
'updated_at' => gmdate('c')
], ['id' => 'eq.' . $friendshipId]);
if (isset($result['error'])) jsonError($result['error']);
jsonResponse(['success' => true, 'friend_id' => $friendship[0]['requester_id']]);
}
if ($action === 'reject') {
$friendshipId = $input['friendship_id'] ?? '';
if (!$friendshipId) jsonError('friendship_id required');
// Verify this request is addressed to the current user
$friendship = $sdb->get('friendships', [
'id' => 'eq.' . $friendshipId,
'addressee_id' => 'eq.' . $userId,
'status' => 'eq.pending',
'limit' => 1
]);
if (!is_array($friendship) || isset($friendship['error']) || empty($friendship)) {
jsonError('Friend request not found');
}
$sdb->delete('friendships', ['id' => 'eq.' . $friendshipId]);
jsonResponse(['success' => true]);
}
if ($action === 'remove') {
$targetId = $input['target_id'] ?? '';
if (!$targetId) jsonError('target_id required');
$sdb->delete('friendships', [
'or' => "(and(requester_id.eq.{$userId},addressee_id.eq.{$targetId}),and(requester_id.eq.{$targetId},addressee_id.eq.{$userId}))"
]);
jsonResponse(['success' => true]);
}
if ($action === 'report') {
$targetId = $input['target_id'] ?? '';
$reason = $input['reason'] ?? 'other';
if (!$targetId) jsonError('target_id required');
// Just acknowledge — in future, store reports in a table
jsonResponse(['success' => true]);
}
if ($action === 'invite') {
$targetId = $input['target_id'] ?? '';
$gameKey = $input['game_key'] ?? 'chess';
$timeControl = $input['time_control'] ?? 'rapid_10_0';
if (!$targetId) jsonError('target_id required');
// Create a private lobby match
$initialTime = 600000;
if (strpos($timeControl, 'bullet') !== false) $initialTime = 60000;
elseif (strpos($timeControl, 'blitz') !== false) $initialTime = 300000;
elseif (strpos($timeControl, 'classical') !== false) $initialTime = 3600000;
// Randomly assign colors
$isWhite = rand(0, 1) === 0;
$whiteId = $isWhite ? $userId : $targetId;
$blackId = $isWhite ? $targetId : $userId;
$match = $sdb->insert('matches', [
'game_key' => $gameKey,
'match_type' => 'friendly',
'white_player_id' => $whiteId,
'black_player_id' => $blackId,
'status' => 'waiting',
'time_control' => $timeControl,
'initial_time_ms' => $initialTime,
'white_time_remaining_ms' => $initialTime,
'black_time_remaining_ms' => $initialTime,
'current_fen' => 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1',
'moves' => '[]',
'move_count' => 0,
'game_state' => json_encode(['invite_from' => $userId, 'invite_to' => $targetId, 'invite_t' => time()]),
'is_rated' => false,
'created_at' => gmdate('c')
]);
if (isset($match['error'])) jsonError($match['error']);
$matchId = $match[0]['id'] ?? null;
jsonResponse([
'match_id' => $matchId,
'color' => $isWhite ? 'w' : 'b',
'game_key' => $gameKey,
'time_control' => $timeControl
]); ]);
}
if ($action === 'check-invites') {
// Check if there are pending invites for this user
$invites = $sdb->get('matches', [
'or' => "(white_player_id.eq.{$userId},black_player_id.eq.{$userId})",
'status' => 'eq.waiting',
'match_type' => 'eq.friendly',
'select' => 'id,game_key,time_control,white_player_id,black_player_id,game_state,created_at',
'order' => 'created_at.desc',
'limit' => 5
]);
if (!is_array($invites) || isset($invites['error'])) {
jsonResponse(['invites' => []]);
}
// Filter: only show invites TO this user (not ones I sent) and recent (< 2 min)
$result = [];
$now = time();
foreach ($invites as $inv) {
$gs = is_array($inv['game_state']) ? $inv['game_state'] : json_decode($inv['game_state'] ?? '{}', true);
$inviteTo = $gs['invite_to'] ?? null;
$inviteT = $gs['invite_t'] ?? 0;
if ($inviteTo !== $userId) continue;
if ($now - $inviteT > 120) continue; // Expired after 2 min
$inviteFrom = $gs['invite_from'] ?? null;
$result[] = [
'match_id' => $inv['id'],
'game_key' => $inv['game_key'],
'time_control' => $inv['time_control'],
'from_id' => $inviteFrom,
'created_at' => $inv['created_at']
];
}
jsonResponse(['invites' => $result]);
}
if ($action === 'accept-invite') {
$matchId = $input['match_id'] ?? '';
if (!$matchId) jsonError('match_id required');
// Verify the match is for this user and is still waiting
$matches = $sdb->get('matches', [
'id' => 'eq.' . $matchId,
'status' => 'eq.waiting',
'or' => "(white_player_id.eq.{$userId},black_player_id.eq.{$userId})",
'select' => 'id,white_player_id,black_player_id',
'limit' => 1
]);
if (!is_array($matches) || isset($matches['error']) || empty($matches)) {
jsonError('Invite not found or expired');
}
// Start the match
$sdb->update('matches', [
'status' => 'in_progress',
'started_at' => gmdate('c')
], ['id' => 'eq.' . $matchId]);
$match = $matches[0];
$color = ($match['white_player_id'] === $userId) ? 'w' : 'b';
jsonResponse([
'match_id' => $matchId,
'color' => $color,
'started' => true
]);
}
if ($action === 'decline-invite') {
$matchId = $input['match_id'] ?? '';
if (!$matchId) jsonError('match_id required');
$sdb->update('matches', [
'status' => 'completed',
'result' => 'declined'
], ['id' => 'eq.' . $matchId, 'status' => 'eq.waiting']);
jsonResponse(['success' => true]); jsonResponse(['success' => true]);
} }
jsonError('Invalid action');
} }
if ($method === 'DELETE') { if ($method === 'DELETE') {
...@@ -117,7 +328,7 @@ if ($method === 'DELETE') { ...@@ -117,7 +328,7 @@ if ($method === 'DELETE') {
$targetId = $input['target_id'] ?? ''; $targetId = $input['target_id'] ?? '';
if (!$targetId) jsonError('target_id required'); if (!$targetId) jsonError('target_id required');
$db->delete('friendships', [ $sdb->delete('friendships', [
'or' => "(and(requester_id.eq.{$userId},addressee_id.eq.{$targetId}),and(requester_id.eq.{$targetId},addressee_id.eq.{$userId}))" 'or' => "(and(requester_id.eq.{$userId},addressee_id.eq.{$targetId}),and(requester_id.eq.{$targetId},addressee_id.eq.{$userId}))"
]); ]);
jsonResponse(['success' => true]); jsonResponse(['success' => true]);
......
...@@ -9,10 +9,12 @@ import { emoji } from '../../../core/theme.js'; ...@@ -9,10 +9,12 @@ import { emoji } from '../../../core/theme.js';
let activeTab = 'friends'; let activeTab = 'friends';
let refreshTimer = null; let refreshTimer = null;
let inviteCheckTimer = null;
export function mountFriends(el) { export function mountFriends(el) {
activeTab = 'friends'; activeTab = 'friends';
if (refreshTimer) clearInterval(refreshTimer); if (refreshTimer) clearInterval(refreshTimer);
if (inviteCheckTimer) clearInterval(inviteCheckTimer);
el.innerHTML = ` el.innerHTML = `
<div style="display:flex;flex-direction:column;height:100%;"> <div style="display:flex;flex-direction:column;height:100%;">
...@@ -28,6 +30,8 @@ export function mountFriends(el) { ...@@ -28,6 +30,8 @@ export function mountFriends(el) {
<button class="social-tab" data-tab="activity">${emoji('news', '📰', 12)} أخبار</button> <button class="social-tab" data-tab="activity">${emoji('news', '📰', 12)} أخبار</button>
</div> </div>
</div> </div>
<!-- Invite banner -->
<div id="invite-banner" style="display:none;"></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>
</div> </div>
<style> <style>
...@@ -62,10 +66,89 @@ export function mountFriends(el) { ...@@ -62,10 +66,89 @@ export function mountFriends(el) {
loadTab(el, 'friends'); loadTab(el, 'friends');
loadPendingCount(el); loadPendingCount(el);
checkInvites(el);
refreshTimer = setInterval(() => { refreshTimer = setInterval(() => {
if (activeTab === 'online') loadTab(el, 'online'); if (activeTab === 'online') loadTab(el, 'online');
}, 15000); }, 15000);
inviteCheckTimer = setInterval(() => checkInvites(el), 5000);
}
async function checkInvites(el) {
try {
const data = await net.post('friends.php', { action: 'check-invites' });
const invites = data.invites || [];
const banner = el.querySelector('#invite-banner');
if (!banner) return;
if (invites.length === 0) {
banner.style.display = 'none';
return;
}
// Get inviter profiles
const fromIds = [...new Set(invites.map(i => i.from_id).filter(Boolean))];
let profiles = {};
if (fromIds.length > 0) {
try {
const pData = await net.get('friends.php', { action: 'profiles', ids: fromIds.join(',') });
(pData.profiles || []).forEach(p => { profiles[p.id] = p; });
} catch (e) {}
}
banner.style.display = 'block';
banner.innerHTML = invites.map(inv => {
const from = profiles[inv.from_id] || {};
const name = from.display_name || from.username || 'صديق';
const gameLabel = inv.game_key === 'ludo' ? 'لودو' : inv.game_key === 'domino' ? 'دومينو' : 'شطرنج';
return `
<div style="display:flex;align-items:center;gap:10px;padding:10px 16px;background:linear-gradient(135deg,#1a2a1a,#0f1f0f);border-bottom:1px solid rgba(52,211,153,0.2);animation:slideDown 0.3s;">
<span style="font-size:20px;">⚔️</span>
<div style="flex:1;">
<div style="font-size:13px;font-weight:600;color:#34D399;">${name} يتحداك!</div>
<div style="font-size:11px;color:#64748b;">${gameLabel}</div>
</div>
<button class="btn btn-primary" data-accept-invite="${inv.match_id}" style="min-height:30px;padding:5px 14px;font-size:11px;">قبول</button>
<button class="btn btn-secondary" data-decline-invite="${inv.match_id}" style="min-height:30px;padding:5px 10px;font-size:11px;">✕</button>
</div>
`;
}).join('');
banner.querySelectorAll('[data-accept-invite]').forEach(btn => {
btn.addEventListener('click', async () => {
btn.disabled = true;
btn.textContent = '...';
try {
const res = await net.post('friends.php', { action: 'accept-invite', match_id: btn.dataset.acceptInvite });
if (res.error) { btn.textContent = 'خطأ'; return; }
audio.play('reward');
juice.hapticSuccess();
// Navigate to game
const inv = invites.find(i => i.match_id === btn.dataset.acceptInvite);
scene.push('chess-game', {
mode: 'live',
matchId: res.match_id,
color: res.color,
timeControl: inv?.time_control || 'rapid_10_0',
isFriendly: true
});
} catch (e) {
btn.textContent = 'فشل';
}
});
});
banner.querySelectorAll('[data-decline-invite]').forEach(btn => {
btn.addEventListener('click', async () => {
try {
await net.post('friends.php', { action: 'decline-invite', match_id: btn.dataset.declineInvite });
btn.closest('div[style*="display:flex"]').remove();
if (banner.children.length === 0) banner.style.display = 'none';
} catch (e) {}
});
});
} catch (e) {}
} }
async function loadPendingCount(el) { async function loadPendingCount(el) {
...@@ -185,7 +268,12 @@ async function loadPending(content, rootEl) { ...@@ -185,7 +268,12 @@ async function loadPending(content, rootEl) {
btn.style.opacity = '0.5'; btn.style.opacity = '0.5';
btn.style.pointerEvents = 'none'; btn.style.pointerEvents = 'none';
try { try {
await net.post('friends.php', { action: 'accept', friendship_id: friendshipId }); const res = await net.post('friends.php', { action: 'accept', friendship_id: friendshipId });
if (res.error) {
btn.style.opacity = '1';
btn.style.pointerEvents = 'auto';
return;
}
audio.play('reward'); audio.play('reward');
juice.hapticLight(); juice.hapticLight();
card.querySelector('.friend-actions').innerHTML = '<span style="font-size:11px;color:#34D399;font-weight:600;">✓ تم القبول</span>'; card.querySelector('.friend-actions').innerHTML = '<span style="font-size:11px;color:#34D399;font-weight:600;">✓ تم القبول</span>';
...@@ -203,7 +291,7 @@ async function loadPending(content, rootEl) { ...@@ -203,7 +291,7 @@ async function loadPending(content, rootEl) {
const card = btn.closest('.friend-card'); const card = btn.closest('.friend-card');
const friendshipId = btn.dataset.reject; const friendshipId = btn.dataset.reject;
try { try {
await net.del('friends.php', { target_id: friendshipId }); await net.post('friends.php', { action: 'reject', friendship_id: friendshipId });
audio.play('click'); audio.play('click');
card.style.transition = 'opacity 0.3s, transform 0.3s'; card.style.transition = 'opacity 0.3s, transform 0.3s';
card.style.opacity = '0'; card.style.opacity = '0';
...@@ -244,7 +332,6 @@ async function loadOnline(content) { ...@@ -244,7 +332,6 @@ async function loadOnline(content) {
} }
function renderFriendCard(f) { 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">
...@@ -257,7 +344,6 @@ function renderFriendCard(f) { ...@@ -257,7 +344,6 @@ function renderFriendCard(f) {
</div> </div>
<div class="friend-actions"> <div class="friend-actions">
${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>` : ''} ${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', '👤', 14)}</div>
<div class="friend-action" data-remove="${f.id}" title="إزالة" style="font-size:11px;color:#64748b;">✕</div> <div class="friend-action" data-remove="${f.id}" title="إزالة" style="font-size:11px;color:#64748b;">✕</div>
</div> </div>
</div> </div>
...@@ -272,15 +358,7 @@ function bindFriendActions(content) { ...@@ -272,15 +358,7 @@ function bindFriendActions(content) {
const uid = btn.dataset.invite; const uid = btn.dataset.invite;
const card = btn.closest('.friend-card'); const card = btn.closest('.friend-card');
const name = card?.querySelector('[style*="font-weight:600"]')?.textContent || 'صديق'; const name = card?.querySelector('[style*="font-weight:600"]')?.textContent || 'صديق';
bus.emit('challenge:send', { targetId: uid, targetName: name }); showInviteDialog(content, uid, name);
scene.switchWorld('play');
});
});
content.querySelectorAll('[data-profile]').forEach(btn => {
btn.addEventListener('click', () => {
audio.play('click');
scene.push('player-profile', { playerId: btn.dataset.profile });
}); });
}); });
...@@ -291,7 +369,7 @@ function bindFriendActions(content) { ...@@ -291,7 +369,7 @@ function bindFriendActions(content) {
const name = card?.querySelector('[style*="font-weight:600"]')?.textContent || 'هذا الصديق'; const name = card?.querySelector('[style*="font-weight:600"]')?.textContent || 'هذا الصديق';
if (!confirm(`إزالة ${name} من الأصدقاء؟`)) return; if (!confirm(`إزالة ${name} من الأصدقاء؟`)) return;
try { try {
await net.del('friends.php', { target_id: btn.dataset.remove }); await net.post('friends.php', { action: 'remove', target_id: btn.dataset.remove });
card.style.transition = 'opacity 0.3s, transform 0.3s'; card.style.transition = 'opacity 0.3s, transform 0.3s';
card.style.opacity = '0'; card.style.opacity = '0';
card.style.transform = 'translateX(20px)'; card.style.transform = 'translateX(20px)';
...@@ -301,6 +379,122 @@ function bindFriendActions(content) { ...@@ -301,6 +379,122 @@ function bindFriendActions(content) {
}); });
} }
function showInviteDialog(content, targetId, targetName) {
const existing = document.getElementById('invite-dialog');
if (existing) existing.remove();
const dialog = document.createElement('div');
dialog.id = 'invite-dialog';
dialog.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:999;display:flex;align-items:center;justify-content:center;padding:24px;';
dialog.innerHTML = `
<div style="background:#1a1a2e;border-radius:16px;padding:24px;width:100%;max-width:320px;text-align:center;">
<div style="font-size:24px;margin-bottom:8px;">⚔️</div>
<div style="font-size:16px;font-weight:700;color:#f8fafc;margin-bottom:4px;">تحدّي ${targetName}</div>
<div style="font-size:12px;color:#64748b;margin-bottom:16px;">اختر اللعبة ونوع الوقت</div>
<div style="display:flex;gap:8px;justify-content:center;margin-bottom:12px;">
<button class="inv-game active" data-game="chess" style="flex:1;padding:10px;border-radius:10px;background:#2563EB;border:2px solid #2563EB;color:#fff;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;">♟ شطرنج</button>
<button class="inv-game" data-game="ludo" style="flex:1;padding:10px;border-radius:10px;background:#1a1a2e;border:2px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:13px;font-weight:600;cursor:pointer;font-family:inherit;">🎲 لودو</button>
</div>
<div id="time-options" style="display:flex;gap:6px;justify-content:center;margin-bottom:16px;flex-wrap:wrap;">
<button class="inv-time active" data-tc="bullet_1_0" style="padding:6px 12px;border-radius:8px;background:#E4AC38;border:none;color:#1a1a1a;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;">1 دقيقة</button>
<button class="inv-time" data-tc="blitz_3_0" style="padding:6px 12px;border-radius:8px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;">3 دقائق</button>
<button class="inv-time" data-tc="blitz_5_0" style="padding:6px 12px;border-radius:8px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;">5 دقائق</button>
<button class="inv-time" data-tc="rapid_10_0" style="padding:6px 12px;border-radius:8px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.08);color:#94a3b8;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;">10 دقائق</button>
</div>
<button class="btn btn-primary" id="send-invite" style="width:100%;font-size:14px;padding:12px;">أرسل التحدي</button>
<button id="cancel-invite" style="margin-top:8px;background:none;border:none;color:#64748b;font-size:12px;cursor:pointer;font-family:inherit;">إلغاء</button>
</div>
`;
document.body.appendChild(dialog);
let selectedGame = 'chess';
let selectedTc = 'bullet_1_0';
dialog.querySelectorAll('.inv-game').forEach(btn => {
btn.addEventListener('click', () => {
dialog.querySelectorAll('.inv-game').forEach(b => {
b.style.background = '#1a1a2e';
b.style.borderColor = 'rgba(255,255,255,0.08)';
b.style.color = '#94a3b8';
b.classList.remove('active');
});
btn.style.background = '#2563EB';
btn.style.borderColor = '#2563EB';
btn.style.color = '#fff';
btn.classList.add('active');
selectedGame = btn.dataset.game;
// Hide time options for ludo
const timeDiv = dialog.querySelector('#time-options');
timeDiv.style.display = selectedGame === 'ludo' ? 'none' : 'flex';
});
});
dialog.querySelectorAll('.inv-time').forEach(btn => {
btn.addEventListener('click', () => {
dialog.querySelectorAll('.inv-time').forEach(b => {
b.style.background = 'rgba(255,255,255,0.05)';
b.style.borderColor = 'rgba(255,255,255,0.08)';
b.style.color = '#94a3b8';
b.classList.remove('active');
});
btn.style.background = '#E4AC38';
btn.style.border = 'none';
btn.style.color = '#1a1a1a';
btn.classList.add('active');
selectedTc = btn.dataset.tc;
});
});
dialog.querySelector('#send-invite').addEventListener('click', async () => {
const sendBtn = dialog.querySelector('#send-invite');
sendBtn.disabled = true;
sendBtn.textContent = 'جاري الإرسال...';
try {
const res = await net.post('friends.php', {
action: 'invite',
target_id: targetId,
game_key: selectedGame,
time_control: selectedTc
});
if (res.error) {
sendBtn.textContent = res.error;
sendBtn.disabled = false;
return;
}
audio.play('reward');
juice.hapticSuccess();
sendBtn.textContent = '✓ تم إرسال التحدي!';
sendBtn.style.background = '#34D399';
// Wait for opponent to accept — navigate to game
setTimeout(() => {
dialog.remove();
scene.push('chess-game', {
mode: 'live',
matchId: res.match_id,
color: res.color,
timeControl: selectedTc,
isFriendly: true,
waitingForOpponent: true
});
}, 1000);
} catch (e) {
sendBtn.textContent = 'فشل — حاول مرة أخرى';
sendBtn.disabled = false;
}
});
dialog.querySelector('#cancel-invite').addEventListener('click', () => dialog.remove());
dialog.addEventListener('click', (e) => { if (e.target === dialog) dialog.remove(); });
}
function showSearch(el) { function showSearch(el) {
const content = el.querySelector('#social-content'); const content = el.querySelector('#social-content');
content.innerHTML = ` content.innerHTML = `
...@@ -354,7 +548,12 @@ function showSearch(el) { ...@@ -354,7 +548,12 @@ function showSearch(el) {
addBtn.disabled = true; addBtn.disabled = true;
addBtn.textContent = '...'; addBtn.textContent = '...';
try { try {
await net.post('friends.php', { action: 'request', target_id: addBtn.dataset.add }); const res = await net.post('friends.php', { action: 'request', target_id: addBtn.dataset.add });
if (res.error) {
addBtn.textContent = res.error === 'Already friends' ? 'صديق بالفعل' : res.error === 'Request already pending' ? 'مرسل سابقاً' : res.error;
addBtn.style.background = '#64748b';
return;
}
audio.play('reward'); audio.play('reward');
juice.hapticLight(); juice.hapticLight();
addBtn.textContent = '✓ تم الإرسال'; addBtn.textContent = '✓ تم الإرسال';
...@@ -362,7 +561,7 @@ function showSearch(el) { ...@@ -362,7 +561,7 @@ function showSearch(el) {
addBtn.style.borderColor = '#34D399'; addBtn.style.borderColor = '#34D399';
} catch (e) { } catch (e) {
const msg = e.message || ''; const msg = e.message || '';
if (msg.includes('duplicate') || msg.includes('already')) { if (msg.includes('duplicate') || msg.includes('already') || msg.includes('Already')) {
addBtn.textContent = 'مرسل سابقاً'; addBtn.textContent = 'مرسل سابقاً';
addBtn.style.background = '#64748b'; addBtn.style.background = '#64748b';
} else { } else {
......
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