From 7a27b2cf3fb318b30f4221d5a708fbcbd9705015 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 20 Aug 2026 21:12:05 +0000 Subject: [PATCH] =?UTF-8?q?Map:=202GIS=20tiles=20+=20places=20(=D0=B1?= =?UTF-8?q?=D0=B0=D1=80=D1=8B/=D1=80=D0=B5=D1=81=D1=82=D0=BE=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D1=8B)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - L.tileLayer 2GIS - PlacesApi.nearby() в client - Кнопка 'Показать/Скрыть бары' - Маркеры баров с эмодзи (🍺🍽️☕🎉) - Список под картой (карточки) - Invite to place: отправка предложения в чат - 2GIS deeplink в сообщении --- public/index.html | 2 +- public/js/api.js | 12 ++- public/js/views/map.js | 167 +++++++++++++++++++++++++++++++++++++---- 3 files changed, 163 insertions(+), 18 deletions(-) diff --git a/public/index.html b/public/index.html index b580725..1a15e64 100644 --- a/public/index.html +++ b/public/index.html @@ -45,4 +45,4 @@ - \ No newline at end of file + diff --git a/public/js/api.js b/public/js/api.js index d8578a2..78d5321 100644 --- a/public/js/api.js +++ b/public/js/api.js @@ -173,4 +173,14 @@ const ConsentApi = { }, }; -window.API = { api, Auth, ChatApi, ReviewApi, ConsentApi, Store }; \ No newline at end of file +const PlacesApi = { + async nearby(lat, lng, radius, limit = 30) { + try { + return await api(`/api/v1/places/nearby?lat=${lat}&lng=${lng}&radius=${radius || 5}&limit=${limit}`); + } catch (e) { + return { places: [], count: 0, error: e.message }; + } + }, +}; + +window.API = { api, Auth, ChatApi, ReviewApi, ConsentApi, PlacesApi, Store }; \ No newline at end of file diff --git a/public/js/views/map.js b/public/js/views/map.js index abdc359..2651a41 100644 --- a/public/js/views/map.js +++ b/public/js/views/map.js @@ -2,9 +2,13 @@ const MapView = { map: null, markers: [], + placeMarkers: [], // маркеры баров/ресторанов + placeMarkersLayer: null, radius: 5, visible: true, nearby: [], + places: [], + showPlaces: true, async render(root) { root.innerHTML = ` @@ -26,25 +30,29 @@ const MapView = {
Нажми чтобы получить координаты
-
-
- - + -
-
+
+
`; if (!this.map) { this.map = L.map('map').setView([55.7558, 37.6173], 12); - L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { - attribution: '© OpenStreetMap', + // 2GIS tile layer + L.tileLayer('https://tile2.maps.2gis.com/tiles?x={x}&y={y}&z={z}', { + attribution: '© 2GIS', maxZoom: 19, }).addTo(this.map); } @@ -63,11 +71,10 @@ const MapView = { document.getElementById('radius-sel').addEventListener('change', async (e) => { this.radius = parseInt(e.target.value, 10); - document.getElementById('radius-label').textContent = this.radius + ' км'; - const c = document.getElementById('nearby-count'); - if (c) c.textContent = '⏳ поиск...'; + document.getElementById('radius-label') && (document.getElementById('radius-label').textContent = this.radius + ' км'); if (window._lastKnownPos) { await this.refreshNearbyFromPos(window._lastKnownPos.coords.latitude, window._lastKnownPos.coords.longitude); + await this.refreshPlacesFromPos(window._lastKnownPos.coords.latitude, window._lastKnownPos.coords.longitude); } }); @@ -76,12 +83,22 @@ const MapView = { this.requestLocation(); }; + document.getElementById('places-toggle').onclick = () => { + this.showPlaces = !this.showPlaces; + const txt = document.getElementById('places-toggle-text'); + if (txt) txt.textContent = this.showPlaces ? 'Скрыть бары' : 'Показать бары'; + if (this.placeMarkersLayer) { + if (this.showPlaces) this.map.addLayer(this.placeMarkersLayer); + else this.map.removeLayer(this.placeMarkersLayer); + } + const list = document.getElementById('places-list'); + if (list) list.style.display = this.showPlaces ? 'block' : 'none'; + }; + // Если уже есть сохранённые координаты — покажем на карте if (window._lastKnownPos) { this.applyPosition(window._lastKnownPos); } else { - // Авто-запрос геолокации при первом заходе на карту - // Не подвешиваем UI — requestLocation вернётся мгновенно, а браузер сам спросит у пользователя this.requestLocation(); } }, @@ -143,7 +160,6 @@ const MapView = { } if (this.map) { this.map.setView([lat, lng], 13); - // удалим старый круг this.map.eachLayer((layer) => { if (layer instanceof L.Circle) this.map.removeLayer(layer); }); @@ -152,6 +168,7 @@ const MapView = { color: '#FF6B35', fillColor: '#FF6B35', fillOpacity: 0.1, weight: 2, }).addTo(this.map); this.refreshNearbyFromPos(lat, lng); + this.refreshPlacesFromPos(lat, lng); } }, @@ -167,9 +184,8 @@ const MapView = { const r = await API.Auth.searchNearby(lat, lng, this.radius); this.nearby = r.results || []; const c = document.getElementById('nearby-count'); - if (c) c.textContent = this.nearby.length + ' чел. рядом'; + if (c) c.textContent = '👥 ' + this.nearby.length + ' чел. рядом'; - // удалить старые маркеры (кроме своего круга) this.markers.forEach(m => this.map.removeLayer(m)); this.markers = []; @@ -190,6 +206,125 @@ const MapView = { console.warn(e); } }, + + async refreshPlacesFromPos(lat, lng) { + try { + const r = await API.PlacesApi.nearby(lat, lng, this.radius, 50); + this.places = r.places || []; + // маркеры + if (this.placeMarkersLayer) this.map.removeLayer(this.placeMarkersLayer); + this.placeMarkersLayer = L.layerGroup(); + this.places.forEach((p) => { + const icon = L.divIcon({ + className: 'place-marker', + html: `
${placeEmoji(p.type)}
`, + iconSize: [32, 32], + }); + const m = L.marker([p.lat, p.lng], { icon }); + m.bindTooltip( + `${escapeHtml(p.name)}
${placeTypeName(p.type)} • ${(p.distance_m/1000).toFixed(1)} км${p.cuisine ? '
🍴 ' + escapeHtml(p.cuisine) : ''}`, + { direction: 'top' } + ); + m.on('click', () => this.openPlaceCard(p)); + this.placeMarkersLayer.addLayer(m); + }); + if (this.showPlaces) this.placeMarkersLayer.addTo(this.map); + + // список снизу + this.renderPlacesList(); + } catch (e) { + console.warn('refreshPlaces:', e); + } + }, + + renderPlacesList() { + const box = document.getElementById('places-list'); + if (!box) return; + if (!this.places.length) { + box.innerHTML = '
Бары рядом не найдены. Попробуй увеличить радиус.
'; + return; + } + box.innerHTML = '
🍻 Где выпить / поесть:
' + + this.places.map((p) => ` +
+
${placeEmoji(p.type)}
+
+
${escapeHtml(p.name)}
+
${placeTypeName(p.type)} • ${(p.distance_m/1000).toFixed(1)} км${p.cuisine ? ' • ' + escapeHtml(p.cuisine) : ''}
+
+ +
+ `).join(''); + // клики по карточкам + box.querySelectorAll('.card.clickable').forEach((el) => { + el.onclick = () => { + const id = parseInt(el.dataset.placeId, 10); + const p = this.places.find(x => x.osm_id === id); + if (p) this.openPlaceCard(p); + }; + }); + box.querySelectorAll('[data-invite-place]').forEach((btn) => { + btn.onclick = (e) => { + e.stopPropagation(); + const id = parseInt(btn.dataset.invitePlace, 10); + const p = this.places.find(x => x.osm_id === id); + if (p) this.inviteToPlace(p); + }; + }); + }, + + openPlaceCard(p) { + const msg = `${p.name} (${placeTypeName(p.type)})\n📍 ${p.lat.toFixed(4)}, ${p.lng.toFixed(4)}${p.cuisine ? '\n🍴 ' + p.cuisine : ''}${p.hours ? '\n🕐 ' + p.hours : ''}${p.phone ? '\n📞 ' + p.phone : ''}`; + TG_APP.showAlert(msg); + }, + + async inviteToPlace(p) { + // открываем список чатов для выбора кому предложить встречу + const chats = await API.ChatApi.listChats(); + if (!chats.chats || !chats.chats.length) { + TG_APP.showAlert('Сначала начни чат с кем-нибудь'); + return; + } + // простая реализация — показать список через confirm + const list = chats.chats.slice(0, 5).map((c, i) => `${i+1}. ${c.other_name || c.otherName || 'собеседник'}`).join('\n'); + TG_APP.showConfirm( + `Отправить предложение встретиться в "${p.name}"?\n\nТвои чаты:\n${list}\n\nПредложение будет отправлено в самый новый чат.` + ).then(async (ok) => { + if (!ok) return; + const target = chats.chats[0]; + const text = `🍻 Давай встретимся здесь: ${p.name}\n📍 https://2gis.ru/?m=${p.lng.toFixed(6)}%2C${p.lat.toFixed(6)}%2F16\n${p.address ? '🏠 ' + p.address : ''}`; + try { + await API.ChatApi.sendMessage(target.chat.id || target.chat.id, text); + TG_APP.showAlert('Приглашение отправлено в чат с ' + (target.other_name || 'собеседником')); + } catch (e) { + TG_APP.showAlert('Ошибка: ' + e.message); + } + }); + }, }; +function placeEmoji(type) { + switch (type) { + case 'bar': + case 'pub': + case 'biergarten': return '🍺'; + case 'restaurant': return '🍽️'; + case 'cafe': return '☕'; + case 'nightclub': return '🎉'; + default: return '🍴'; + } +} + +function placeTypeName(type) { + switch (type) { + case 'bar': return 'Бар'; + case 'pub': return 'Паб'; + case 'biergarten': return 'Пивная'; + case 'restaurant': return 'Ресторан'; + case 'cafe': return 'Кафе'; + case 'nightclub': return 'Ночной клуб'; + default: return 'Заведение'; + } +} + window.MapView = MapView;