Map: 2GIS tiles + places (бары/рестораны)
- L.tileLayer 2GIS - PlacesApi.nearby() в client - Кнопка 'Показать/Скрыть бары' - Маркеры баров с эмодзи (🍺🍽️☕🎉) - Список под картой (карточки) - Invite to place: отправка предложения в чат - 2GIS deeplink в сообщении
This commit is contained in:
parent
4cc8858a25
commit
7a27b2cf3f
@ -173,4 +173,14 @@ const ConsentApi = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
window.API = { api, Auth, ChatApi, ReviewApi, ConsentApi, Store };
|
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 };
|
||||||
@ -2,9 +2,13 @@
|
|||||||
const MapView = {
|
const MapView = {
|
||||||
map: null,
|
map: null,
|
||||||
markers: [],
|
markers: [],
|
||||||
|
placeMarkers: [], // маркеры баров/ресторанов
|
||||||
|
placeMarkersLayer: null,
|
||||||
radius: 5,
|
radius: 5,
|
||||||
visible: true,
|
visible: true,
|
||||||
nearby: [],
|
nearby: [],
|
||||||
|
places: [],
|
||||||
|
showPlaces: true,
|
||||||
|
|
||||||
async render(root) {
|
async render(root) {
|
||||||
root.innerHTML = `
|
root.innerHTML = `
|
||||||
@ -26,25 +30,29 @@ const MapView = {
|
|||||||
</button>
|
</button>
|
||||||
<div class="privacy-note" id="loc-status">Нажми чтобы получить координаты</div>
|
<div class="privacy-note" id="loc-status">Нажми чтобы получить координаты</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="map" style="height:400px;border-radius:12px;overflow:hidden;margin-bottom:12px;"></div>
|
<div id="map" style="height:340px;border-radius:12px;overflow:hidden;margin-bottom:12px;"></div>
|
||||||
<div class="row">
|
<div class="row" style="gap:8px;flex-wrap:wrap;">
|
||||||
<label>Радиус: <strong id="radius-label">${this.radius} км</strong></label>
|
<button class="btn" id="places-toggle" style="width:auto;flex:1;margin:0;font-size:14px;padding:10px;">
|
||||||
<select id="radius-sel" style="background:var(--bg-elev);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:6px;">
|
🍻 <span id="places-toggle-text">Скрыть бары</span>
|
||||||
|
</button>
|
||||||
|
<select id="radius-sel" style="background:var(--bg-elev);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:10px;font-size:14px;">
|
||||||
<option value="1">1 км</option>
|
<option value="1">1 км</option>
|
||||||
|
<option value="3">3 км</option>
|
||||||
<option value="5" selected>5 км</option>
|
<option value="5" selected>5 км</option>
|
||||||
<option value="10">10 км</option>
|
<option value="10">10 км</option>
|
||||||
<option value="25">25 км</option>
|
<option value="25">25 км</option>
|
||||||
<option value="50">50 км</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div id="nearby-count" class="sub">—</div>
|
<div id="nearby-count" class="sub" style="margin-top:8px;">—</div>
|
||||||
|
<div id="places-list" style="margin-top:8px;"></div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (!this.map) {
|
if (!this.map) {
|
||||||
this.map = L.map('map').setView([55.7558, 37.6173], 12);
|
this.map = L.map('map').setView([55.7558, 37.6173], 12);
|
||||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
// 2GIS tile layer
|
||||||
attribution: '© OpenStreetMap',
|
L.tileLayer('https://tile2.maps.2gis.com/tiles?x={x}&y={y}&z={z}', {
|
||||||
|
attribution: '© 2GIS',
|
||||||
maxZoom: 19,
|
maxZoom: 19,
|
||||||
}).addTo(this.map);
|
}).addTo(this.map);
|
||||||
}
|
}
|
||||||
@ -63,11 +71,10 @@ const MapView = {
|
|||||||
|
|
||||||
document.getElementById('radius-sel').addEventListener('change', async (e) => {
|
document.getElementById('radius-sel').addEventListener('change', async (e) => {
|
||||||
this.radius = parseInt(e.target.value, 10);
|
this.radius = parseInt(e.target.value, 10);
|
||||||
document.getElementById('radius-label').textContent = this.radius + ' км';
|
document.getElementById('radius-label') && (document.getElementById('radius-label').textContent = this.radius + ' км');
|
||||||
const c = document.getElementById('nearby-count');
|
|
||||||
if (c) c.textContent = '⏳ поиск...';
|
|
||||||
if (window._lastKnownPos) {
|
if (window._lastKnownPos) {
|
||||||
await this.refreshNearbyFromPos(window._lastKnownPos.coords.latitude, window._lastKnownPos.coords.longitude);
|
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();
|
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) {
|
if (window._lastKnownPos) {
|
||||||
this.applyPosition(window._lastKnownPos);
|
this.applyPosition(window._lastKnownPos);
|
||||||
} else {
|
} else {
|
||||||
// Авто-запрос геолокации при первом заходе на карту
|
|
||||||
// Не подвешиваем UI — requestLocation вернётся мгновенно, а браузер сам спросит у пользователя
|
|
||||||
this.requestLocation();
|
this.requestLocation();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -143,7 +160,6 @@ const MapView = {
|
|||||||
}
|
}
|
||||||
if (this.map) {
|
if (this.map) {
|
||||||
this.map.setView([lat, lng], 13);
|
this.map.setView([lat, lng], 13);
|
||||||
// удалим старый круг
|
|
||||||
this.map.eachLayer((layer) => {
|
this.map.eachLayer((layer) => {
|
||||||
if (layer instanceof L.Circle) this.map.removeLayer(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,
|
color: '#FF6B35', fillColor: '#FF6B35', fillOpacity: 0.1, weight: 2,
|
||||||
}).addTo(this.map);
|
}).addTo(this.map);
|
||||||
this.refreshNearbyFromPos(lat, lng);
|
this.refreshNearbyFromPos(lat, lng);
|
||||||
|
this.refreshPlacesFromPos(lat, lng);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -167,9 +184,8 @@ const MapView = {
|
|||||||
const r = await API.Auth.searchNearby(lat, lng, this.radius);
|
const r = await API.Auth.searchNearby(lat, lng, this.radius);
|
||||||
this.nearby = r.results || [];
|
this.nearby = r.results || [];
|
||||||
const c = document.getElementById('nearby-count');
|
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.forEach(m => this.map.removeLayer(m));
|
||||||
this.markers = [];
|
this.markers = [];
|
||||||
|
|
||||||
@ -190,6 +206,125 @@ const MapView = {
|
|||||||
console.warn(e);
|
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: `<div style="width:32px;height:32px;border-radius:8px;background:#FFD8C2;color:#FF6B35;display:flex;align-items:center;justify-content:center;font-weight:700;border:2px solid #FF6B35;font-size:18px;">${placeEmoji(p.type)}</div>`,
|
||||||
|
iconSize: [32, 32],
|
||||||
|
});
|
||||||
|
const m = L.marker([p.lat, p.lng], { icon });
|
||||||
|
m.bindTooltip(
|
||||||
|
`<b>${escapeHtml(p.name)}</b><br>${placeTypeName(p.type)} • ${(p.distance_m/1000).toFixed(1)} км${p.cuisine ? '<br>🍴 ' + 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 = '<div class="empty" style="padding:12px;">Бары рядом не найдены. Попробуй увеличить радиус.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
box.innerHTML = '<div class="sub" style="margin-bottom:8px;">🍻 Где выпить / поесть:</div>' +
|
||||||
|
this.places.map((p) => `
|
||||||
|
<div class="card clickable" data-place-id="${p.osm_id}" style="padding:10px;display:flex;align-items:center;gap:10px;">
|
||||||
|
<div style="width:40px;height:40px;border-radius:10px;background:#FFD8C2;display:flex;align-items:center;justify-content:center;font-size:22px;flex-shrink:0;">${placeEmoji(p.type)}</div>
|
||||||
|
<div style="flex:1;min-width:0;">
|
||||||
|
<div style="font-weight:600;font-size:14px;">${escapeHtml(p.name)}</div>
|
||||||
|
<div class="privacy-note">${placeTypeName(p.type)} • ${(p.distance_m/1000).toFixed(1)} км${p.cuisine ? ' • ' + escapeHtml(p.cuisine) : ''}</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn ghost" data-invite-place="${p.osm_id}" style="width:auto;margin:0;padding:6px 10px;font-size:12px;">📨</button>
|
||||||
|
</div>
|
||||||
|
`).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;
|
window.MapView = MapView;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user