prox.

HORS LIGNE
Aucun utilisateur à proximité

👤 À proximité

0

prox.

Rencontres intimes de proximité

En continuant, tu acceptes que ta position soit partagée avec les autres utilisateurs disponibles uniquement.
Proximité !
Quelqu'un est à moins de 500m
// ===== UPDATE LIST (suite) ===== function updateNearbyList() { if (!state.registered) return; const sorted = state.users .filter(u => u.available) .map(u => ({...u, dist: getDistanceToUser(u)})) .sort((a,b) => a.dist - b.dist); panelCount.textContent = sorted.length; const near = sorted.filter(u => u.dist <= PROXIMITY_RADIUS); if (near.length > 0) { nearbyCount.textContent = `⚡ ${near.length} personne${near.length>1?'s':''} à moins de ${PROXIMITY_RADIUS}m`; } else if (sorted.length > 0) { nearbyCount.textContent = `${sorted.length} personne${sorted.length>1?'s':''} à proximité`; } else { nearbyCount.textContent = 'Aucun utilisateur à proximité'; } if (sorted.length === 0) { panelBody.innerHTML = '
Personne à proximité pour le moment
'; return; } panelBody.innerHTML = ''; for (const user of sorted) { const card = document.createElement('div'); card.className = 'user-card'; const now = Date.now(); const isNear = user.dist <= PROXIMITY_RADIUS; const justNotified = now - user.lastNotified < NOTIF_COOLDOWN_MS && isNear; const distText = user.dist < 100 ? '< 100 m' : `~ ${Math.round(user.dist)} m`; const ago = Math.round((now - user.lastSeen) / 60000); const agoText = ago < 1 ? 'à l\'instant' : `il y a ${ago} min`; const btnText = (user.messages && user.messages.length > 0) ? '💬' : '👋'; const btnClass = (user.messages && user.messages.length > 0) ? 'action-btn sent' : 'action-btn'; card.innerHTML = `
${user.pseudo} ${justNotified ? '' : ''}
${distText} · ${agoText}
`; panelBody.appendChild(card); // Event sur le bouton const btn = card.querySelector('.action-btn'); btn.addEventListener('click', (e) => { e.stopPropagation(); openChat(user.id); }); } } // ===== CHAT ===== function openChat(userId) { const user = state.users.find(u => u.id === userId); if (!user) return; state.chatWith = userId; chatPartnerName.textContent = user.pseudo; const dist = getDistanceToUser(user); chatPartnerDist.textContent = dist < 100 ? '< 100m' : `${Math.round(dist)}m`; chatOverlay.classList.add('show'); renderChatMessages(); chatInput.focus(); } function closeChat() { state.chatWith = null; chatOverlay.classList.remove('show'); } function renderChatMessages() { const user = state.users.find(u => u.id === state.chatWith); if (!user) return; chatMessages.innerHTML = ''; if (!user.messages || user.messages.length === 0) { const emptyDiv = document.createElement('div'); emptyDiv.style.cssText = 'text-align:center;padding:40px 0;color:rgba(255,255,255,0.3);font-size:0.85rem;'; emptyDiv.textContent = 'Envoie un message pour dire bonjour 👋'; chatMessages.appendChild(emptyDiv); return; } for (const msg of user.messages) { const div = document.createElement('div'); div.className = `msg ${msg.from === 'me' ? 'mine' : 'theirs'}`; div.innerHTML = `${msg.text}${msg.time}`; chatMessages.appendChild(div); } chatMessages.scrollTop = chatMessages.scrollHeight; } function sendChatMessage() { const text = chatInput.value.trim(); if (!text || !state.chatWith) return; const user = state.users.find(u => u.id === state.chatWith); if (!user) return; if (!user.messages) user.messages = []; const now = new Date(); const timeStr = now.toLocaleTimeString('fr-FR', {hour:'2-digit', minute:'2-digit'}); user.messages.push({from: 'me', text: text, time: timeStr}); chatInput.value = ''; renderChatMessages(); saveState(); // Simuler une réponse aléatoire après 1-3 secondes if (user.messages.filter(m => m.from === 'theirs').length < 3) { setTimeout(() => { const answers = [ 'Salut ! 😊', 'Hé toi !', 'T\'es où ?', 'Je suis pas loin 👀', 'Coucou !', 'Tu veux qu\'on se voit ?', 'J\'arrive !', 'Mmmh oui 😏', 'T\'es chaud ?', 'Viens me rejoindre...' ]; const ans = answers[Math.floor(Math.random() * answers.length)]; const t = new Date(); const ts = t.toLocaleTimeString('fr-FR', {hour:'2-digit', minute:'2-digit'}); user.messages.push({from: 'theirs', text: ans, time: ts}); renderChatMessages(); saveState(); // Notifier la réponse if (!chatOverlay.classList.contains('show') || state.chatWith !== user.id) { try { if (navigator.vibrate) navigator.vibrate(100); } catch(e) {} notifTitle.textContent = `💬 ${user.pseudo} a répondu`; notifBody.textContent = ans; notifDiv.classList.add('show'); if (state.notificationTimeout) clearTimeout(state.notificationTimeout); state.notificationTimeout = setTimeout(() => notifDiv.classList.remove('show'), 5000); } }, 1000 + Math.random() * 2000); } } // ===== NOTIFICATIONS ===== function requestNotificationPermission() { if ('Notification' in window && Notification.permission === 'default') { Notification.requestPermission(); } } // ===== TOGGLE AVAILABILITY ===== function toggleAvailability() { if (!state.registered) return; state.available = !state.available; if (state.available) { state.onlineSince = Date.now(); availabilityBtn.classList.remove('off'); availabilityBtn.innerHTML = '⚡ EN LIGNELes autres te voient à proximité'; userStatus.textContent = 'EN LIGNE'; userStatus.classList.add('online'); // Réinitialiser les cooldowns pour retester for (const u of state.users) { u.lastNotified = 0; } checkProximity(); renderMap(); updateNearbyList(); } else { state.onlineSince = null; availabilityBtn.classList.add('off'); availabilityBtn.innerHTML = '⚡ SE METTRE EN LIGNELes autres te verront à proximité'; userStatus.textContent = 'HORS LIGNE'; userStatus.classList.remove('online'); renderMap(); updateNearbyList(); } } // ===== REGISTRATION ===== function register() { const pseudo = pseudoInput.value.trim(); if (!pseudo) { pseudoInput.style.borderColor = '#ff6b9d'; pseudoInput.placeholder = 'Choisis un pseudo...'; setTimeout(() => { pseudoInput.style.borderColor = '#2a2a3a'; pseudoInput.placeholder = 'Ton pseudo'; }, 2000); return; } state.registered = true; state.pseudo = pseudo; registerOverlay.classList.remove('show'); requestNotificationPermission(); startWatching(); generateSimulatedUsers(); saveState(); renderMap(); updateNearbyList(); // Petite démo : rendre dispo directement state.available = true; state.onlineSince = Date.now(); availabilityBtn.classList.remove('off'); availabilityBtn.innerHTML = '⚡ EN LIGNELes autres te voient à proximité'; userStatus.textContent = 'EN LIGNE'; userStatus.classList.add('online'); checkProximity(); renderMap(); updateNearbyList(); } // ===== CANVAS TOUCH ===== let isDragging = false; let lastTouchX, lastTouchY; function setupCanvasInteraction() { // Touch events canvas.addEventListener('touchstart', (e) => { if (e.touches.length === 1) { isDragging = true; lastTouchX = e.touches[0].clientX; lastTouchY = e.touches[0].clientY; } }, {passive: true}); canvas.addEventListener('touchmove', (e) => { if (!isDragging || e.touches.length !== 1) return; const dx = e.touches[0].clientX - lastTouchX; const dy = e.touches[0].clientY - lastTouchY; state.mapOffsetX += dx; state.mapOffsetY += dy; lastTouchX = e.touches[0].clientX; lastTouchY = e.touches[0].clientY; renderMap(); }, {passive: true}); canvas.addEventListener('touchend', () => { isDragging = false; }, {passive: true}); // Mouse events (desktop fallback) canvas.addEventListener('mousedown', (e) => { isDragging = true; lastTouchX = e.clientX; lastTouchY = e.clientY; }); canvas.addEventListener('mousemove', (e) => { if (!isDragging) return; const dx = e.clientX - lastTouchX; const dy = e.clientY - lastTouchY; state.mapOffsetX += dx; state.mapOffsetY += dy; lastTouchX = e.clientX; lastTouchY = e.clientY; renderMap(); }); canvas.addEventListener('mouseup', () => { isDragging = false; }); canvas.addEventListener('mouseleave', () => { isDragging = false; }); // Zoom avec la molette canvas.addEventListener('wheel', (e) => { e.preventDefault(); const delta = e.deltaY > 0 ? 0.9 : 1.1; state.mapZoom = Math.max(0.3, Math.min(3, state.mapZoom * delta)); renderMap(); }, {passive: false}); // Pinch zoom touch let lastPinchDist = 0; canvas.addEventListener('touchstart', (e) => { if (e.touches.length === 2) { const dx = e.touches[0].clientX - e.touches[1].clientX; const dy = e.touches[0].clientY - e.touches[1].clientY; lastPinchDist = Math.sqrt(dx*dx + dy*dy); } }, {passive: true}); canvas.addEventListener('touchmove', (e) => { if (e.touches.length !== 2) return; e.preventDefault(); const dx = e.touches[0].clientX - e.touches[1].clientX; const dy = e.touches[0].clientY - e.touches[1].clientY; const dist = Math.sqrt(dx*dx + dy*dy); if (lastPinchDist > 0) { const scale = dist / lastPinchDist; state.mapZoom = Math.max(0.3, Math.min(3, state.mapZoom * scale)); renderMap(); } lastPinchDist = dist; }, {passive: false}); canvas.addEventListener('touchend', (e) => { if (e.touches.length < 2) lastPinchDist = 0; }, {passive: true}); } // ===== RESIZE ===== function resizeCanvas() { const container = canvas.parentElement; canvas.width = container.clientWidth * window.devicePixelRatio; canvas.height = container.clientHeight * window.devicePixelRatio; canvas.style.width = container.clientWidth + 'px'; canvas.style.height = container.clientHeight + 'px'; ctx.scale(window.devicePixelRatio, window.devicePixelRatio); renderMap(); } // ===== ANIMATION LOOP ===== function animate() { if (state.available) { // Re-animer le marqueur self (pulse) renderMap(); } requestAnimationFrame(animate); } // ===== INIT ===== function init() { loadState(); // Register button document.getElementById('registerBtn').addEventListener('click', register); pseudoInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') register(); }); // Availability toggle availabilityBtn.addEventListener('click', toggleAvailability); // Panel toggle document.getElementById('togglePanelBtn').addEventListener('click', () => { state.panelOpen = !state.panelOpen; panel.classList.toggle('open', state.panelOpen); }); // Panel handle drag (simple toggle) document.getElementById('panelHandle').addEventListener('click', () => { state.panelOpen = !state.panelOpen; panel.classList.toggle('open', state.panelOpen); }); // Satellite toggle (change map style) document.getElementById('toggleSatelliteBtn').addEventListener('click', () => { state.mapStyle = state.mapStyle === 'dark' ? 'satellite' : 'dark'; renderMap(); }); // Locate button document.getElementById('locateBtn').addEventListener('click', () => { state.mapOffsetX = 0; state.mapOffsetY = 0; state.mapZoom = 1; renderMap(); }); // Chat document.getElementById('chatBackBtn').addEventListener('click', closeChat); document.getElementById('chatSendBtn').addEventListener('click', sendChatMessage); chatInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') sendChatMessage(); }); // Notif dismiss document.getElementById('notifDismiss').addEventListener('click', () => { notifDiv.classList.remove('show'); if (state.notificationTimeout) clearTimeout(state.notificationTimeout); }); // Canvas resize window.addEventListener('resize', resizeCanvas); // Si déjà enregistré, reprendre if (state.registered) { requestNotificationPermission(); startWatching(); generateSimulatedUsers(); resizeCanvas(); setupCanvasInteraction(); renderMap(); updateNearbyList(); animate(); } else { // Montrer overlay inscription registerOverlay.classList.add('show'); resizeCanvas(); setupCanvasInteraction(); renderMap(); animate(); } } // Démarrer au chargement if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); }