Carregando... Clube de Jogadores de Cartas

Jogue com pessoas reais.

Jogue Truco online. Participe de Torneios

Sem compromisso. Cancele online quando quiser.

Por Que Escolher o Nipes?

A melhor plataforma brasileira de truco online, com milhares de jogadores cadastrados e partidas disputadas todos os dias. Tradição, qualidade e diversão garantidas.

Anos de Tradição

Conectando jogadores apaixonados por jogos de cartas. A comunidade mais confiável do Brasil para Truco online.

Milhares Jogando Agora

Nunca jogue sozinho! Sempre há jogadores online prontos para uma partida. Encontre adversários do seu nível a qualquer hora.

Torneios Diários

Participe de torneios emocionantes de Truco todos os dias. Suba no ranking e prove suas habilidades contra os melhores jogadores.

Comunidade Acolhedora

Faça amigos, converse pelo chat integrado e participe de uma comunidade apaixonada por jogos de cartas. Ambiente familiar e respeitoso.

Grátis para Jogar

Jogue gratuitamente ou faça parte da elite Nipes. Sem compromisso, cancele quando quiser. Divirta-se sem limites!

Jogue em Qualquer Lugar

Compatível com computador, celular e tablet. Sem downloads ou instalações - basta acessar pelo navegador e começar a jogar.

}); function updateAuthState() { const username = localStorage.getItem('username'); const authButtons = document.getElementById('auth-buttons'); const profileSection = document.getElementById('profileSection'); const avatar = localStorage.getItem('profileImage'); if (username) { if (authButtons) authButtons.classList.add('hidden'); if (profileSection) profileSection.classList.remove('hidden'); if (welcomeUsername) welcomeUsername.textContent = username; const headerUsernameEl = document.getElementById('headerUsername'); if (headerUsernameEl) headerUsernameEl.textContent = username; if (headerProfileImage) headerProfileImage.src = avatar || DEFAULT_AVATAR_SRC; // initializeChat(); // Inicializa o chat quando logado handleRoute(); } else { if (authButtons) authButtons.classList.remove('hidden'); if (profileSection) profileSection.classList.add('hidden'); if (headerProfileImage) headerProfileImage.src = DEFAULT_AVATAR_SRC; stopChatPolling(); // Para o chat quando deslogado navigateToHome(); } } function showMessage(element, message, type) { if (!element) return; element.textContent = message; element.className = `message message-${type}`; element.classList.remove('hidden'); } async function handleSignup(e) { e.preventDefault(); const form = e.target; const username = form.signupUsername.value; const email = form.signupEmail.value; const password = form.signupPassword.value; const signupMessage = document.getElementById('signupMessage'); try { const response = await fetch('server.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'register', username, email, password }) }); const result = await response.json(); if (result.status === 'success') { try{ startNotificationsPolling(); }catch(e){}; showMessage(signupMessage, result.message, 'success'); setTimeout(() => { closeModal('signupModal'); openModal('loginModal'); }, 2000); } else { showMessage(signupMessage, result.message, 'error'); } } catch (error) { showMessage(signupMessage, 'Erro ao conectar com o servidor.', 'error'); } } async function handleLogin(e) { e.preventDefault(); const form = e.target; const credential = form.loginCredential.value.trim(); const password = form.loginPassword.value; console.log('🔓 Tentando fazer login:', { credential: credential.substring(0, 3) + '***' }); if (!credential || !password) { showMessage(loginMessage, 'Preencha todos os campos', 'error'); return; } try { console.log('🧪 Testando conectividade...'); const result = await makeRequest('server.php', { action: 'login', credential, password }); console.log('🎉 Login bem-sucedido:', result); if (result.status === 'success') { // Limpa dados do usuário anterior try { stopNotificationsPolling(); } catch(e) {} try { resetTransientUI(); } catch(e) {} lastSearchRequestId++; try { if (currentSearchAbort) currentSearchAbort.abort(); } catch(e) {} currentSearchAbort = null; // Salva novo usuário if (result.username) localStorage.setItem('username', result.username); localStorage.setItem('email', result.email); if (result.profile_privacy !== undefined) { const isPublic = (result.profile_privacy === 1 || result.profile_privacy === '1' || result.profile_privacy === true || result.profile_privacy === 'true'); localStorage.setItem('profile_privacy', isPublic ? '1' : '0'); } if (result.profile_picture) { localStorage.setItem('profileImage', result.profile_picture); } // Atualiza interface updateAuthState(); closeModal('loginModal'); // NOVO: Atualiza lista de amigos e notificações imediatamente console.log('🔄 Atualizando dados do novo usuário...'); try { await fetchFriends(); console.log('✅ Lista de amigos atualizada'); } catch(e) { console.warn('⚠️ Erro ao buscar amigos:', e); } try { await fetchNotifications(true); console.log('✅ Notificações atualizadas'); } catch(e) { console.warn('⚠️ Erro ao buscar notificações:', e); } // Inicia polling try { startNotificationsPolling(); } catch(e) { console.warn('⚠️ Erro ao iniciar polling:', e); } } else { console.warn('❌ Login falhou:', result); showMessage(loginMessage, result.message || 'Credenciais inválidas', 'error'); } } catch (error) { console.error('💥 Erro no processo de login:', error); let errorMessage = 'Erro ao conectar com o servidor.'; if (error.message.includes('HTTP 404')) { errorMessage = 'Arquivo server.php não encontrado.'; } else if (error.message.includes('HTTP 500')) { errorMessage = 'Erro interno do servidor. Contate o administrador.'; } else if (error.message.includes('conexão')) { errorMessage = 'Erro de conexão. Verifique sua internet.'; } else if (error.message.includes('JSON')) { errorMessage = 'Resposta inválida do servidor.'; } showMessage(loginMessage, errorMessage, 'error'); } } async function testServerConnection() { console.log('🧪 Testando conexão com servidor...'); try { const response = await fetch('server.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'test' }) }); console.log('📡 Status da resposta:', response.status); const text = await response.text(); console.log('📄 Conteúdo da resposta:', text.substring(0, 200)); return { success: response.ok, status: response.status, content: text }; } catch (error) { console.error('❌ Erro no teste:', error); return { success: false, error: error.message }; } } function handleLogout() { try{ stopNotificationsPolling(); }catch(e){}; try{ resetTransientUI(); }catch(e){}; lastSearchRequestId++; try{ if (currentSearchAbort) { currentSearchAbort.abort(); } }catch(e){} currentSearchAbort=null; stopChatPolling(); // Para o chat no logout closeChatSystem(); // fecha/limpa DM chat localStorage.clear(); updateAuthState(); if (profileImage) profileImage.src = DEFAULT_AVATAR_SRC; if (userProfileImage) userProfileImage.src = DEFAULT_AVATAR_SRC; } function updateProfileForm() { const username = localStorage.getItem('username'); const email = localStorage.getItem('email'); // *** AJUSTE: aceitar '1'/'0' ou 'true'/'false' *** const privacyRaw = localStorage.getItem('profile_privacy'); const isPublic = (privacyRaw === '1' || privacyRaw === 'true' || privacyRaw === 1 || privacyRaw === true); const avatar = localStorage.getItem('profileImage'); const accountUsernameEl = document.getElementById('accountUsername'); const userProfileNameEl = document.getElementById('userProfileName'); const userProfileStatusEl = document.getElementById('userProfileStatus'); const profilePrivacySwitch = document.getElementById('profilePrivacySwitch'); if (accountUsernameEl) accountUsernameEl.textContent = username || ''; if (accountEmailInput) accountEmailInput.value = email || ''; if (profilePrivacySwitch) profilePrivacySwitch.checked = isPublic; if (userProfileNameEl) userProfileNameEl.textContent = username || 'Usuário Desconhecido'; if (userProfileStatusEl) userProfileStatusEl.textContent = `Status: ${isPublic ? 'Público' : 'Privado'}`; if (profileImage) profileImage.src = avatar || DEFAULT_AVATAR_SRC; if (userProfileImage) userProfileImage.src = avatar || DEFAULT_AVATAR_SRC; } async function saveProfile(e) { e.preventDefault(); const form = e.target; const currentEmail = localStorage.getItem('email'); const formData = new FormData(); formData.append('action', 'update_profile'); formData.append('current_email', currentEmail); formData.append('new_email', form.email.value); formData.append('new_password', form.password.value); const profilePrivacySwitch = document.getElementById('profilePrivacySwitch'); formData.append('profile_privacy', profilePrivacySwitch ? profilePrivacySwitch.checked : true); const profileImageFile = profileImageInput ? profileImageInput.files[0] : null; if (profileImageFile) { formData.append('profile_picture', profileImageFile); } try { const response = await fetch('server.php', { method: 'POST', body: formData }); const result = await response.json(); if (result.status === 'success') { try{ startNotificationsPolling(); }catch(e){}; if (result.username) localStorage.setItem('username', result.username); localStorage.setItem('email', result.email); // *** AJUSTE: normalizar e salvar como '1' ou '0' *** const isPublic = !!(profilePrivacySwitch && profilePrivacySwitch.checked); localStorage.setItem('profile_privacy', isPublic ? '1' : '0'); if (result.profile_picture) { localStorage.setItem('profileImage', result.profile_picture); } updateAuthState(); updateProfileForm(); showMessage(profileMessage, result.message, 'success'); } else { showMessage(profileMessage, result.message, 'error'); } } catch (error) { showMessage(profileMessage, 'Erro ao conectar com o servidor para salvar o perfil.', 'error'); } } // Upload de imagem (preview) if (profileImageInput) { profileImageInput.addEventListener('change', (e) => { const file = e.target.files[0]; if (file && profileImage) { const reader = new FileReader(); reader.onload = (event) => { profileImage.src = event.target.result; }; reader.readAsDataURL(file); } }); } async function fetchFriends() { if (window._fetchFriendsInFlight) return; window._fetchFriendsInFlight = true; const currentUsername = localStorage.getItem('username'); if (!currentUsername || !friendsCount || !friendsList) return; try { const response = await fetch('server.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'get_friends', current_username: currentUsername }) }); const result = await response.json(); if (result.status === 'success') { try{ startNotificationsPolling(); }catch(e){}; friendsCount.textContent = result.friends.length; friendsList.innerHTML = ''; if (result.friends.length > 0) { result.friends.forEach(friend => { const friendItem = document.createElement('div'); friendItem.className = 'friend-item'; // adiciona atributos para facilitar seleção do badge e ações friendItem.setAttribute('data-username', friend.username); if (friend.profile_picture) { friendItem.setAttribute('data-avatar', friend.profile_picture); } // === Atualização otimista da lista de amigos (sidebar) === function addFriendToSidebar(username, avatarUrl) { try { const list = document.getElementById('friendsList') || document.querySelector('#friendsList, .friends-list, [data-friends-list]'); const countEl = document.getElementById('friendsCount') || document.querySelector('#friendsCount, .friends-count, [data-friends-count]'); if (!list || !username) return; if (list.querySelector(`[data-username="${username}"]`)) return; const imgSrc = (avatarUrl && typeof avatarUrl === 'string' && avatarUrl.length) ? avatarUrl : (typeof DEFAULT_AVATAR_SRC !== 'undefined' ? DEFAULT_AVATAR_SRC : ''); const item = document.createElement('div'); item.className = 'friend-item'; item.setAttribute('data-username', username); item.innerHTML = ` Avatar de Amigo ${username} `; if (typeof openFriendOptions === 'function') { item.onclick = () => openFriendOptions(username, imgSrc); } else if (typeof openDmChat === 'function') { item.onclick = () => openDmChat(username, imgSrc); } list.prepend(item); if (countEl) { const n = parseInt((countEl.textContent || '0').replace(/\D+/g,'')) || 0; countEl.textContent = String(n + 1); } try { document.dispatchEvent(new CustomEvent('friends:updated', { detail: { added: username }})); } catch(e){} } catch(e) { console.warn('addFriendToSidebar falhou:', e); } } friendItem.innerHTML = ` Avatar de Amigo 4186 ${friend.username} ${friend.unread && Number(friend.unread) > 0 ? `${friend.unread}` : ''} `; friendItem.onclick = () => openFriendOptions(friend.username, friend.profile_picture || DEFAULT_AVATAR_SRC); friendsList.appendChild(friendItem); }); } else { friendsList.innerHTML = '

Adicione seus primeiros amigos!

'; } } } catch (error) { console.error('Erro ao carregar lista de amigos:', error); } } async function showPlayerProfile(username) { const currentUsername = localStorage.getItem('username'); try { const response = await fetch('server.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'get_player_profile', username: username, current_username: currentUsername }) }); const result = await response.json(); if (result.status === 'success') { try{ startNotificationsPolling(); }catch(e){}; showPage('otherUserProfilePage', result); } else if (result.status === 'private_profile') { if (result.profile) { openPrivateProfileModal(result.profile); } } else { console.warn('Falha ao abrir perfil:', result); } } catch (error) { console.error('Erro ao buscar perfil:', error); } } async function addFriend(friendUsername) { const currentUsername = localStorage.getItem('username'); try { const response = await fetch('server.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'add_friend', friend_username: friendUsername, current_username: currentUsername }) }); const result = await response.json(); alert(result.message); if (result.status === 'success') { try{ startNotificationsPolling(); }catch(e){}; try { closeModal('privateProfileModal'); } catch (e) {} showPlayerProfile(friendUsername); fetchFriends(); } } catch (error) { console.error('Erro ao adicionar amigo:', error); alert('Erro ao adicionar amigo.'); } } async function removeFriend(friendUsername) { const currentUsername = localStorage.getItem('username'); try { const response = await fetch('server.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'remove_friend', friend_username: friendUsername, current_username: currentUsername }) }); const result = await response.json(); alert(result.message); if (result.status === 'success') { try{ startNotificationsPolling(); }catch(e){}; showPlayerProfile(friendUsername); fetchFriends(); } } catch (error) { console.error('Erro ao remover amigo:', error); alert('Erro ao remover amigo.'); } } function inviteToGame(username) { alert(`Convite para jogar enviado para ${username}! (Funcionalidade em desenvolvimento)`); } // === BUSCA DE JOGADORES === let lastSearchRequestId = 0; let currentSearchAbort = null; async function searchPlayer(query) { if (query.length < 3 || !searchResults) { if (searchResults) searchResults.innerHTML = ''; return; } const currentUsername = localStorage.getItem('username'); const myReqId = ++lastSearchRequestId; try { try { if (currentSearchAbort) currentSearchAbort.abort(); } catch(e){} currentSearchAbort = new AbortController(); const response = await fetch('server.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'search_player', query: query, current_username: currentUsername }), signal: currentSearchAbort.signal }); const result = await response.json(); if (myReqId !== lastSearchRequestId) { return; } if (playerSearchInput && playerSearchInput.value.trim() !== query.trim()) { return; } searchResults.innerHTML = ''; if (result.status === 'success' && result.players && result.players.length > 0) { result.players.forEach(player => { const playerItem = document.createElement('div'); playerItem.className = 'flex items-center gap-4 p-3 bg-gray-200 rounded-md cursor-pointer hover:bg-gray-300 transition-colors text-black'; playerItem.innerHTML = ` Foto de Perfil ${player.username} `; playerItem.onclick = () => navigateToUser(player.username); searchResults.appendChild(playerItem); }); } else { searchResults.innerHTML = '

Nenhum jogador encontrado.

'; } } catch (error) { if (error && (error.name === 'AbortError' || error.message === 'The operation was aborted.')) { return; } console.error('Erro na busca:', error); if (searchResults) { searchResults.innerHTML = '

Erro na busca.

'; } } } // === ROUTER === function handleRoute() { try{ closeNotifDropdown(); }catch(e){} const raw = window.location.pathname || "/"; const path = decodeURIComponent(raw.replace(/^\/+|\/+$/g, "")); if (!path) { showPage('homePage'); return; } if (path.toLowerCase() === 'meuperfil') { showPage('profilePage'); return; } if (path.toLowerCase() === 'minhaconta') { showPage('accountPage'); return; } // Qualquer outro segmento: tratar como username showPlayerProfile(path); } function navigateToHome() { try{ closeNotifDropdown(); }catch(e){} const target = '/'; if (window.location.pathname !== target) { history.pushState({type:'home'}, '', target); } showPage('homePage'); } function navigateToUser(username) { try{ closeNotifDropdown(); }catch(e){} if (!username) return navigateToHome(); const target = '/' + encodeURIComponent(username); if (window.location.pathname !== target) { history.pushState({type:'user', username}, '', target); } showPlayerProfile(username); } function navigateToMyProfile() { try{ closeNotifDropdown(); }catch(e){} const target = '/meuperfil'; if (window.location.pathname !== target) { history.pushState({type:'me'}, '', target); } showPage('profilePage'); } function navigateToAccount() { try{ closeNotifDropdown(); }catch(e){} const target = '/minhaconta'; if (window.location.pathname !== target) { history.pushState({type:'account'}, '', target); } showPage('accountPage'); } window.addEventListener('popstate', () => handleRoute()); document.addEventListener('DOMContentLoaded', () => handleRoute()); // Event Listeners window.addEventListener('load', updateAuthState); if (logoutBtn) logoutBtn.addEventListener('click', handleLogout); if (homeBtn) homeBtn.addEventListener('click', () => navigateToHome()); if (homeLogoLink) { homeLogoLink.addEventListener('click', (e) => { e.preventDefault(); navigateToHome(); }); } if (accountBtn) accountBtn.addEventListener('click', () => navigateToAccount()); if (profileBtn) profileBtn.addEventListener('click', () => navigateToMyProfile()); const signupForm = document.getElementById('signupForm'); const loginForm = document.getElementById('loginForm'); if (signupForm) signupForm.addEventListener('submit', handleSignup); if (loginForm) loginForm.addEventListener('submit', handleLogin); if (profileForm) profileForm.addEventListener('submit', saveProfile); if (playerSearchInput) { playerSearchInput.addEventListener('keyup', (e) => { searchPlayer(e.target.value); }); } // === SISTEMA DE NOTIFICAÇÕES === let _notifPollHandle = null; let _notifCache = []; if (typeof window !== 'undefined') { window._notifBackoff = 0; window._fetchNotifInFlight = false; } function updateNotifBadge(count){ const badge = document.getElementById('notifBadge'); if(!badge) return; if(typeof count !== 'number'){ const root = document.getElementById('notifList') || document.querySelector('.notif-list'); count = root ? root.querySelectorAll('.notif-item').length : 0; } if(count > 0){ badge.style.display='flex'; badge.textContent=String(count); } else { badge.style.display='none'; badge.textContent='0'; } } async function fetchNotifications(force){ if (window._fetchNotifInFlight && !force) return; window._fetchNotifInFlight = true; try { const username = localStorage.getItem('username') || window.CURRENT_USER || ''; if (!username) { window._fetchNotifInFlight = false; return; } const res = await fetch('server.php', { method:'POST', headers:{'Content-Type':'application/json','Accept':'application/json'}, body: JSON.stringify({action:'get_notifications', username}) }); const data = await res.json().catch(()=>({status:'error', notifications:[]})); const list = Array.isArray(data.notifications) ? data.notifications : []; window._notifCache = list.slice(); updateNotifBadge(list.length); const dd = document.getElementById('notifDropdown'); if (dd && dd.style.display === 'block') { renderNotifications(list); } } catch(e){ console.warn('fetchNotifications erro:', e); } finally { window._fetchNotifInFlight = false; } } function renderNotifications(items){ const list = document.getElementById('notifList'); if (!list) return; const arr = Array.isArray(items) ? items : []; if (arr.length === 0){ list.innerHTML = '
Sem notificações.
'; window._notifCache = []; updateNotifBadge(0); return; } list.innerHTML = ''; window._notifCache = arr.slice(); for (const n of arr){ const id = Number(n.id || 0); const sender = (n.sender && String(n.sender)) || (n.message && String(n.message).split(' ')[0]) || 'Usuário'; const div = document.createElement('div'); div.className = 'notif-item'; div.dataset.id = id; div.dataset.requester = sender; const body = document.createElement('div'); body.className = 'notif-body'; body.innerHTML = `${sender} quer te adicionar como amigo.`; const actions = document.createElement('div'); actions.className = 'notif-actions'; const bA = Object.assign(document.createElement('button'), {className:'btn-accept', textContent:'Aceitar'}); const bR = Object.assign(document.createElement('button'), {className:'btn-reject', textContent:'Recusar'}); const bX = Object.assign(document.createElement('button'), {className:'btn-dismiss', textContent:'✕'}); actions.append(bA,bR,bX); div.append(body, actions); list.append(div); } updateNotifBadge(arr.length); } // Delegação única (uma vez) (function initNotifDelegation(){ const root = document.getElementById('notifList') || document.querySelector('.notif-list'); if(!root || root.__delegated) return; root.__delegated = true; root.addEventListener('click', (ev)=>{ const btn = ev.target.closest('button'); if(!btn) return; const item = ev.target.closest('.notif-item'); if(!item) return; const notifId = Number(item.dataset.id || 0); const requester = item.dataset.requester || (item.querySelector('strong')?.textContent.trim() || ''); if(btn.classList.contains('btn-accept')){ respondFriendRequest(requester,'accept',notifId); }else if(btn.classList.contains('btn-reject')){ respondFriendRequest(requester,'reject',notifId); }else if(btn.classList.contains('btn-dismiss')){ dismissNotification(notifId); } }, {passive:true}); })(); function removeNotificationById(notifId){ try { const list = document.getElementById('notifList') || document.querySelector('.notif-list'); if (!list) return; const el = list.querySelector(`.notif-item[data-id="${notifId}"]`); if (el) { el.classList.add('notif-fade-out'); setTimeout(()=>{ el.remove(); }, 140); } updateNotifBadge(); } catch(e){ console.warn('removeNotificationById falhou:', e); } } async function markNotificationsRead() { const username = localStorage.getItem('username'); if (!username) return; try{ await fetch('server.php',{method:'POST',headers:{'Content-Type':'application/json'},body: JSON.stringify({ action:'mark_notifications_read', username })}); _notifCache = _notifCache.map(n => ({...n, is_read: 1})); updateNotifBadge(); }catch(e){} } function toggleNotificationsDropdown(){ const dd = document.getElementById('notifDropdown'); if(!dd) return; const show = dd.style.display !== 'block'; dd.style.display = show ? 'block' : 'none'; if (show) { fetchNotifications(true); } } // === UI session guards and helpers (anti-stale) === function closeNotifDropdown() { const dd = document.getElementById('notifDropdown'); if (dd) dd.style.display = 'none'; } function resetTransientUI() { try { closeNotifDropdown(); const inp = document.getElementById('playerSearchInput'); const list = document.getElementById('searchResults'); if (inp) inp.value = ''; if (list) list.innerHTML = ''; } catch(e){} } // Fecha notificações se clicar fora, rolar, apertar Esc ou ao sair da página document.addEventListener('click', (e) => { const area = document.getElementById('notifArea'); const dd = document.getElementById('notifDropdown'); if (!area || !dd) return; if (dd.style.display === 'block' && !area.contains(e.target)) { dd.style.display = 'none'; } }, true); window.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeNotifDropdown(); }); window.addEventListener('scroll', () => closeNotifDropdown(), true); window.addEventListener('beforeunload', () => closeNotifDropdown()); async function respondFriendRequest(requester, decision, notifId) { // Disable buttons to prevent duplicate submits try { const item = document.querySelector('.notif-item[data-id="'+Number(notifId)+'"]'); if (item) item.querySelectorAll('.btn-accept,.btn-reject').forEach(b=>b.disabled=true); } catch(e){} const responder = localStorage.getItem('username'); try { const res = await fetch('server.php', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ action:'respond_friend_request', responder_username: responder, requester_username: requester, decision, notif_id: notifId }) }); const data = await res.json(); if (data.status === 'success') { // Remove notificação da UI imediatamente removeNotificationById(notifId); // Mostra toast de sucesso if (typeof toastSuccess === 'function') { toastSuccess(decision === 'accept' ? 'Pedido aceito!' : 'Pedido recusado'); } // Se aceitou, adiciona amigo na lista e atualiza if (decision === 'accept') { console.log('✅ Pedido aceito, atualizando lista de amigos...'); // Adiciona imediatamente na sidebar (otimista) if (typeof addFriendToSidebar === 'function' && requester) { try { const avatarUrl = data.friend_data?.profile_picture || null; addFriendToSidebar(requester, avatarUrl); } catch(e) { console.warn('Erro ao adicionar amigo na sidebar:', e); } } // Atualiza lista completa do servidor try { await fetchFriends(); console.log('✅ Lista de amigos atualizada'); } catch(e) { console.warn('Erro ao atualizar lista de amigos:', e); } } } else { // Se falhou, recarrega notificações console.warn('Falha ao responder pedido:', data); if (typeof fetchNotifications === 'function') { fetchNotifications(true); } } } catch(e) { console.error('Erro ao responder pedido:', e); if (typeof fetchNotifications === 'function') { fetchNotifications(true); } } } catch(e){} // Remove imediatamente (cache + DOM) removeNotificationById(notifId); if (decision === 'accept' && typeof addFriendToSidebar === 'function' && requester) { try { addFriendToSidebar(requester); } catch(e){} } // Remove imediatamente (cache + DOM) removeNotificationById(notifId); // Se aceitou, adiciona na lista de amigos imediatamente if (decision === 'accept' && typeof addFriendToSidebar === 'function' && requester) { try { addFriendToSidebar(requester); } catch(e){} } if (typeof notifId !== 'undefined' && notifId !== null) { _notifCache = _notifCache.filter(n => n.id !== notifId); renderNotifications(); updateNotifBadge(); if (typeof sanitizeNotificationButtons==='function') sanitizeNotificationButtons(); if (typeof ensureNotifDelegation==='function') ensureNotifDelegation(); } const responder = localStorage.getItem('username'); if (typeof notifId !== 'undefined' && notifId !== null) { _notifCache = _notifCache.filter(n => n.id !== notifId); renderNotifications(); updateNotifBadge(); if (typeof sanitizeNotificationButtons==='function') sanitizeNotificationButtons(); if (typeof ensureNotifDelegation==='function') ensureNotifDelegation(); } try { const res = await fetch('server.php', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ action:'respond_friend_request', responder_username: responder, requester_username: requester, decision, notif_id: notifId }) }); const data = await res.json(); if (data.status !== 'success') { fetchNotifications(); } else { try { scheduleUIRefresh(200); } catch(e){} if (typeof fetchFriends==='function') fetchFriends(); } } catch(e){ fetchNotifications(); } } function scheduleAutoDismiss() { const dd = document.getElementById('notifDropdown'); if (!dd || dd.style.display !== 'block') return; const items = dd.querySelectorAll('.notif-item[data-type="friend_accept"], .notif-item[data-type="friend_reject"]'); items.forEach((el)=>{ if (el.dataset.autod==='1') return; el.dataset.autod='1'; const id = parseInt(el.dataset.id,10); setTimeout(() => { (async () => { try{ el.classList.add('notif-fade-out'); setTimeout(()=>{ _notifCache = _notifCache.filter(n => n.id !== id); renderNotifications(); updateNotifBadge(); if (typeof sanitizeNotificationButtons==='function') sanitizeNotificationButtons(); if (typeof ensureNotifDelegation==='function') ensureNotifDelegation(); }, 400); const username = localStorage.getItem('username'); await fetch('server.php',{method:'POST',headers:{'Content-Type':'application/json'}, body: JSON.stringify({ action:'dismiss_notification', username, notif_id:id })}); }catch(e){} })().catch(()=>{}); }, 12000); }); } function startNotificationsPolling() { const area = document.getElementById('notifArea'); if (area) area.style.display='block'; fetchNotifications(); if (_notifPollHandle) clearInterval(_notifPollHandle); _notifPollHandle = setInterval(()=>{ if (!window._fetchNotifInFlight) fetchNotifications(); }, 30000); } function stopNotificationsPolling() { if (_notifPollHandle) clearInterval(_notifPollHandle); _notifPollHandle = null; _notifCache = []; const area = document.getElementById('notifArea'); if (area) area.style.display='none'; const dd = document.getElementById('notifDropdown'); if (dd) dd.style.display='none'; const badge = document.getElementById('notifBadge'); if (badge) badge.style.display='none'; } async function dismissNotification(id, ev) { try { if (ev) ev.stopPropagation(); } catch(e){} if (!id || isNaN(id)) return; // Remove from UI with fade const item = document.querySelector('.notif-item[data-id="'+id+'"]'); try { if (item) { item.classList.add('notif-fade-out'); setTimeout(()=>{ if (item && item.parentNode) item.parentNode.removeChild(item); }, 200); } } catch(e){} // Update cache & badge try { _notifCache = _notifCache.filter(n => n.id !== id); renderNotifications(); updateNotifBadge(); if (typeof sanitizeNotificationButtons==='function') sanitizeNotificationButtons(); if (typeof ensureNotifDelegation==='function') ensureNotifDelegation(); } catch(e){} // Backend delete try { const username = localStorage.getItem('username'); await fetch('server.php', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ action:'dismiss_notification', username, notif_id:id }) }); } catch(e){} } function syncNotifThemeWithHeader() { try { const hdr = document.querySelector('header, .site-header, #header'); if (!hdr) return; const cs = getComputedStyle(hdr); let baseColor = null; const bgImg = cs.backgroundImage; if (bgImg && bgImg.includes('gradient')) { const m = bgImg.match(/rgba?\([^\)]+\)|#(?:[0-9a-fA-F]{3,8})/g); if (m && m.length) baseColor = m[0]; } if (!baseColor || baseColor === 'rgba(0, 0, 0, 0)') { const bg = cs.backgroundColor; if (bg && bg !== 'rgba(0, 0, 0, 0)') baseColor = bg; } if (!baseColor) return; function parseColor(str){ if (!str) return null; str = str.trim(); if (str.startsWith('#')) { let hex = str.slice(1); if (hex.length===3) hex = hex.split('').map(x=>x+x).join(''); if (hex.length>=6) { return {r:parseInt(hex.slice(0,2),16), g:parseInt(hex.slice(2,4),16), b:parseInt(hex.slice(4,6),16)}; } } const m = str.match(/rgba?\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i); if (m) return {r:parseInt(m[1]), g:parseInt(m[2]), b:parseInt(m[3])}; return null; } function lighten(rgb, p){ return { r: Math.min(255, Math.round(rgb.r + (255 - rgb.r) * p)), g: Math.min(255, Math.round(rgb.g + (255 - rgb.g) * p)), b: Math.min(255, Math.round(rgb.b + (255 - rgb.b) * p)) }; } function toCss(rgb){ return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`; } const rgb = parseColor(baseColor); if (!rgb) return; // Make the notif header a bit lighter than the site header const primaryLight = lighten(rgb, 0.12); // ~12% lighter const primarySlight = lighten(rgb, 0.05); // ~5% lighter (for second gradient stop) document.documentElement.style.setProperty('--notif-primary', toCss(primaryLight)); document.documentElement.style.setProperty('--notif-primary-600', toCss(primarySlight)); } catch(e){} } // === SISTEMA DE VALIDAÇÃO DE USERNAME === function validateUsernameClient(username) { const errors = []; username = username.trim(); if (username.length < 3) { errors.push('O nome de usuário deve ter pelo menos 3 caracteres.'); return errors; } if (username.length > 20) { errors.push('O nome de usuário não pode ter mais de 20 caracteres.'); } if (!/^[a-zA-Z0-9_]+$/.test(username)) { errors.push('Apenas letras, números e underscore (_) são permitidos.'); } if (/^_|_$/.test(username)) { errors.push('Não pode começar ou terminar com underscore.'); } if (username.includes('__')) { errors.push('Não pode ter underscores consecutivos.'); } return errors; } async function validateUsernameServer(username) { try { const result = await makeRequest('server.php', { action: 'validate_username', username: username }); return { valid: result.status === 'success', message: result.message, errors: result.errors || [] }; } catch (error) { console.error('Erro na validação:', error); return { valid: false, message: 'Erro de conexão ao validar nome de usuário.', errors: ['Erro de conexão'] }; } } function showUsernameValidation(inputElement, messageElement, isValid, message) { if (!inputElement || !messageElement) return; inputElement.classList.remove('border-green-500', 'border-red-500', 'border-yellow-500'); messageElement.classList.remove('text-green-600', 'text-red-600', 'text-yellow-600', 'hidden'); if (isValid === true) { inputElement.classList.add('border-green-500'); messageElement.classList.add('text-green-600'); messageElement.textContent = message; } else if (isValid === false) { inputElement.classList.add('border-red-500'); messageElement.classList.add('text-red-600'); messageElement.textContent = message; } else { inputElement.classList.add('border-yellow-500'); messageElement.classList.add('text-yellow-600'); messageElement.textContent = 'Verificando disponibilidade...'; } } function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } const validateUsernameWithDebounce = debounce(async function(username, inputElement, messageElement) { const clientErrors = validateUsernameClient(username); if (clientErrors.length > 0) { showUsernameValidation(inputElement, messageElement, false, clientErrors[0]); return false; } showUsernameValidation(inputElement, messageElement, null, 'Verificando disponibilidade...'); const serverResult = await validateUsernameServer(username); showUsernameValidation(inputElement, messageElement, serverResult.valid, serverResult.message); return serverResult.valid; }, 800); function setupUsernameValidation() { const usernameInput = document.getElementById('signupUsername'); const usernameMessage = document.getElementById('usernameValidationMessage'); if (!usernameInput) return; if (!usernameMessage) { const messageElement = document.createElement('div'); messageElement.id = 'usernameValidationMessage'; messageElement.className = 'text-sm mt-1 hidden'; usernameInput.parentNode.insertBefore(messageElement, usernameInput.nextSibling); } usernameInput.addEventListener('input', function(e) { const username = e.target.value.trim(); const messageEl = document.getElementById('usernameValidationMessage'); if (username.length === 0) { messageEl.classList.add('hidden'); usernameInput.classList.remove('border-green-500', 'border-red-500', 'border-yellow-500'); return; } validateUsernameWithDebounce(username, usernameInput, messageEl); }); usernameInput.addEventListener('blur', function(e) { const username = e.target.value.trim(); if (username.length > 0) { const messageEl = document.getElementById('usernameValidationMessage'); validateUsernameWithDebounce(username, usernameInput, messageEl); } }); } async function handleSignupWithValidation(e) { e.preventDefault(); const form = e.target; const username = form.signupUsername.value.trim(); const email = form.signupEmail.value.trim(); const password = form.signupPassword.value; const signupMessage = document.getElementById('signupMessage'); if (signupMessage) { signupMessage.classList.add('hidden'); } if (!username || !email || !password) { showMessage(signupMessage, 'Todos os campos são obrigatórios.', 'error'); return; } const usernameValid = await validateUsernameServer(username); if (!usernameValid.valid) { showMessage(signupMessage, usernameValid.message, 'error'); return; } if (!isValidEmail(email)) { showMessage(signupMessage, 'Por favor, insira um email válido.', 'error'); return; } if (password.length < 6) { showMessage(signupMessage, 'A senha deve ter pelo menos 6 caracteres.', 'error'); return; } try { const result = await makeRequest('server.php', { action: 'register', username, email, password }); if (result.status === 'success') { try{ startNotificationsPolling(); }catch(e){}; showMessage(signupMessage, result.message, 'success'); setTimeout(() => { if (typeof closeModal === 'function') closeModal('signupModal'); if (typeof openModal === 'function') openModal('loginModal'); }, 2000); } else { showMessage(signupMessage, result.message || 'Erro no cadastro', 'error'); } } catch (error) { console.error('Erro no cadastro:', error); showMessage(signupMessage, 'Erro de conexão. Tente novamente.', 'error'); } } function isValidEmail(email) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } function togglePasswordVisibility(inputId, buttonId) { const input = document.getElementById(inputId); const button = document.getElementById(buttonId); if (!input || !button) return; if (input.type === 'password') { input.type = 'text'; button.innerHTML = '🙈'; } else { input.type = 'password'; button.innerHTML = '👁️'; } } function clearForm(formId) { const form = document.getElementById(formId); if (form) { form.reset(); const inputs = form.querySelectorAll('input'); inputs.forEach(input => { input.classList.remove('border-green-500', 'border-red-500', 'border-yellow-500'); }); const messages = form.querySelectorAll('[id*="Message"], [id*="Validation"]'); messages.forEach(msg => { msg.classList.add('hidden'); }); } } function getPasswordStrength(password) { let score = 0; if (password.length >= 8) score++; if (/[a-z]/.test(password)) score++; if (/[A-Z]/.test(password)) score++; if (/[0-9]/.test(password)) score++; if (/[^A-Za-z0-9]/.test(password)) score++; switch (score) { case 0: case 1: return { text: 'muito fraca', class: 'text-red-600' }; case 2: return { text: 'fraca', class: 'text-red-600' }; case 3: return { text: 'média', class: 'text-yellow-600' }; case 4: return { text: 'boa', class: 'text-green-600' }; case 5: return { text: 'excelente', class: 'text-green-600' }; default: return { text: 'válida', class: 'text-green-600' }; } } // === FUNÇÕES UTILITÁRIAS === async function makeRequest(url, data) { console.log('🔄 Enviando requisição:', { url, data }); try { // Adiciona timestamp para evitar cache const timestamp = new Date().getTime(); const requestUrl = `${url}?_t=${timestamp}`; const response = await fetch(requestUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(data), // Adiciona configurações extras credentials: 'same-origin', cache: 'no-cache' }); console.log('📡 Resposta do servidor:', { status: response.status, statusText: response.statusText, headers: Object.fromEntries(response.headers.entries()) }); // Verifica se a resposta HTTP foi bem-sucedida if (!response.ok) { const errorText = await response.text(); console.error('❌ Erro HTTP:', errorText); throw new Error(`HTTP ${response.status}: ${response.statusText} - ${errorText.substring(0, 200)}`); } // Tenta fazer parse do JSON const text = await response.text(); console.log('📄 Texto da resposta:', text.substring(0, 500)); if (!text.trim()) { throw new Error('Resposta vazia do servidor'); } try { const jsonData = JSON.parse(text); console.log('✅ JSON parseado com sucesso:', jsonData); return jsonData; } catch (parseError) { console.error('❌ Erro no parse JSON:', parseError); console.error('📄 Resposta completa:', text); throw new Error('Resposta inválida do servidor - não é JSON válido'); } } catch (error) { console.error('🚫 Erro na requisição completa:', error); // Verifica se é erro de rede if (error instanceof TypeError && error.message.includes('fetch')) { throw new Error('Erro de conexão - verifique sua internet'); } throw error; } } function openPrivateProfileModal(profile) { try { var usernameEl = document.getElementById('privateProfileUsername'); var avatarEl = document.getElementById('privateProfileAvatar'); var actionsEl = document.getElementById('privateProfileActions'); if (usernameEl) usernameEl.textContent = '@' + (profile.username || ''); if (avatarEl) { var src = (profile.profile_picture && String(profile.profile_picture).trim()) ? profile.profile_picture : (typeof DEFAULT_AVATAR_SRC !== 'undefined' ? DEFAULT_AVATAR_SRC : ''); if (src) avatarEl.src = src; } var me = localStorage.getItem('username'); if (actionsEl) { if (me) { actionsEl.innerHTML = ''; } else { actionsEl.innerHTML = ''; } } if (typeof openModal === 'function') { openModal('privateProfileModal'); } else { document.getElementById('privateProfileModal').style.display = 'block'; } } catch (e) { console.error('Erro ao abrir modal de perfil privado:', e); } } // === INICIALIZAÇÃO === async function testServerConnection() { console.log('🧪 Testando conexão com servidor...'); try { const response = await fetch('server.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'test' }) }); console.log('📡 Status da resposta:', response.status); const text = await response.text(); console.log('📄 Conteúdo da resposta:', text.substring(0, 200)); return { success: response.ok, status: response.status, content: text }; } catch (error) { console.error('❌ Erro no teste:', error); return { success: false, error: error.message }; } } window.addEventListener('DOMContentLoaded', function() { // Configura validação de username setupUsernameValidation(); syncNotifThemeWithHeader(); // Substitui o handler de signup padrão const signupForm = document.getElementById('signupForm'); if (signupForm) { try { signupForm.removeEventListener('submit', handleSignup); } catch (_) {} signupForm.addEventListener('submit', handleSignupWithValidation); } // Adiciona limpeza de formulários quando modais são fechados const modals = ['loginModal', 'signupModal']; modals.forEach(modalId => { const modal = document.getElementById(modalId); if (modal) { modal.addEventListener('click', function(e) {/* if (e.target === modal || e.target.classList.contains('close-btn')) { clearForm(modalId.replace('Modal', 'Form')); if (modalId === 'loginModal' && window.loginModalInstance) window.loginModalInstance.hide(); if (modalId === 'signupModal' && window.signupModalInstance) window.signupModalInstance.hide(); } }); */ } }); // Validação em tempo real para cadastro const emailInput = document.getElementById('signupEmail'); const passwordInput = document.getElementById('signupPassword'); const termsCheckbox = document.getElementById('termsAccept'); const submitBtn = document.getElementById('signupSubmitBtn'); function checkFormValidity() { const usernameInput = document.getElementById('signupUsername'); const usernameValid = usernameInput && usernameInput.classList.contains('border-green-500'); const emailValid = emailInput && emailInput.value && isValidEmail(emailInput.value); const passwordValid = passwordInput && passwordInput.value.length >= 6; const termsAccepted = termsCheckbox && termsCheckbox.checked; const allValid = usernameValid && emailValid && passwordValid && termsAccepted; if (submitBtn) { submitBtn.disabled = !allValid; } return allValid; } if (emailInput) { emailInput.addEventListener('input', function() { const email = this.value; const messageEl = document.getElementById('emailValidationMessage'); if (email && !isValidEmail(email)) { this.classList.add('border-red-500'); this.classList.remove('border-green-500'); if (messageEl) { messageEl.textContent = 'Por favor, insira um email válido.'; messageEl.className = 'validation-message text-red-600'; messageEl.classList.remove('hidden'); } } else if (email && isValidEmail(email)) { this.classList.add('border-green-500'); this.classList.remove('border-red-500'); if (messageEl) { messageEl.textContent = 'Email válido!'; messageEl.className = 'validation-message text-green-600'; messageEl.classList.remove('hidden'); } } else { this.classList.remove('border-green-500', 'border-red-500'); if (messageEl) { messageEl.classList.add('hidden'); } } checkFormValidity(); }); } if (passwordInput) { passwordInput.addEventListener('input', function() { const password = this.value; const messageEl = document.getElementById('passwordValidationMessage'); if (password.length < 6) { this.classList.add('border-red-500'); this.classList.remove('border-green-500'); if (messageEl) { messageEl.textContent = 'A senha deve ter pelo menos 6 caracteres.'; messageEl.className = 'validation-message text-red-600'; messageEl.classList.remove('hidden'); } } else { this.classList.add('border-green-500'); this.classList.remove('border-red-500'); if (messageEl) { const strength = getPasswordStrength(password); messageEl.textContent = `Senha ${strength.text}`; messageEl.className = `validation-message ${strength.class}`; messageEl.classList.remove('hidden'); } } checkFormValidity(); }); } if (termsCheckbox) { termsCheckbox.addEventListener('change', checkFormValidity); } // Inicializa o sistema se o usuário já está logado if (localStorage.getItem('username')) { startNotificationsPolling(); // initializeChat(); } }); // === LEGAL-AWARE ROUTER & CLICK INTERCEPTOR === (function(){ function isLegalPathname(p) { const t = (p || '').replace(/^\/+|\/+$/g, '').toLowerCase(); return t === 'termos' || t === 'privacidade' || t === 'termos.php' || t === 'privacidade.php'; } // Keep references to existing functions if present const _showPage = (typeof showPage === 'function') ? showPage : function(){}; const _showPlayerProfile = (typeof showPlayerProfile === 'function') ? showPlayerProfile : function(){}; // Override/define handleRoute with legal awareness window.handleRoute = function handleRoute(){ const rawPath = window.location.pathname || '/'; const trimmed = decodeURIComponent(rawPath.replace(/^\/+|\/+$/g, '')); const lower = trimmed.toLowerCase(); // Legal pages -> force real navigation (served by PHP) if (isLegalPathname(rawPath)) { if (lower.indexOf('termos') !== -1) { if (location.pathname !== '/termos') location.replace('/termos'); } else if (lower.indexOf('privacidade') !== -1) { if (location.pathname !== '/privacidade') location.replace('/privacidade'); } return; } if (!trimmed) { _showPage('homePage'); return; } if (lower === 'meuperfil') { _showPage('profilePage'); return; } if (lower === 'minhaconta') { _showPage('accountPage'); return; } // Treat as other user's profile _showPlayerProfile(trimmed); }; // Intercept internal link clicks but NEVER legal pages document.addEventListener('click', function(e){ const a = e.target && e.target.closest ? e.target.closest('a[href]') : null; if (!a) return; // Only left-click, no modifier keys const isMainClick = e.button === 0 && !e.metaKey && !e.ctrlKey && !e.shiftKey && !e.altKey; if (!isMainClick) return; // Respect target="_blank" if (a.target && a.target.toLowerCase() === '_blank') return; // Build absolute URL let href = a.getAttribute('href'); if (!href) return; try { const url = new URL(href, window.location.origin); // Different origin or hash navigation -> do not intercept if (url.origin !== window.location.origin || url.hash) return; // Do NOT intercept legal pages const p = url.pathname.toLowerCase(); if (p === '/termos' || p === '/privacidade' || p === '/termos.php' || p === '/privacidade.php') { return; // allow default browser navigation } // Intercept SPA routes e.preventDefault(); window.history.pushState({}, '', url.pathname + url.search); if (typeof window.handleRoute === 'function') window.handleRoute(); } catch (err) { // If URL parsing fails, let browser handle it return; } }, true); // Bind route handlers window.addEventListener('popstate', function(){ if (typeof window.handleRoute === 'function') window.handleRoute(); }); document.addEventListener('DOMContentLoaded', function(){ if (typeof window.handleRoute === 'function') window.handleRoute(); }); // Optional: kick once in case this script loads after content if (document.readyState === 'complete' || document.readyState === 'interactive') { try { window.handleRoute(); } catch(_) {} } })(); // === CAPTURE-PHASE LEGAL GUARD === (function(){ function isLegalUrl(u){ try{ var url = new URL(u, window.location.origin); var p = (url.pathname || '').toLowerCase(); return p === '/termos' || p === '/privacidade' || p === '/termos.php' || p === '/privacidade.php'; }catch(_){ return false; } } // Highest priority: capture phase and stopImmediatePropagation for legal links document.addEventListener('click', function(e){ var a = e.target && e.target.closest ? e.target.closest('a[href]') : null; if(!a) return; // Ignore modified clicks or non-left clicks var isMainClick = e.button === 0 && !e.metaKey && !e.ctrlKey && !e.shiftKey && !e.altKey; if(!isMainClick) return; // Respect target _blank if (a.target && a.target.toLowerCase() === '_blank') return; var href = a.getAttribute('href'); if(!href) return; if(isLegalUrl(href)){ // IMPORTANTE: impedimos que outros listeners (SPA) capturem e chamem preventDefault() // Deixamos o navegador fazer a navegação normal (sem SPA) e.stopImmediatePropagation(); // NÃO chamar e.preventDefault() return; } }, true); // capture = true // Se o documento já abriu com /termos ou /privacidade mas caiu no index.php // (ex: por causa de regra antiga de reescrita), forçamos navegação direta. (function forceLegalIfNeeded(){ var p = (location.pathname || '').toLowerCase(); if (p === '/termos' || p === '/termos.php') { if (location.pathname !== '/termos') location.replace('/termos'); } else if (p === '/privacidade' || p === '/privacidade.php') { if (location.pathname !== '/privacidade') location.replace('/privacidade'); } })(); })();
console.log('✅ Todos os event listeners configurados');