Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions projects/geo-review/geoReview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import InteractiveMap from './interactiveMap';

export default class GeoReview {
constructor() {
this.formTemplate = document.querySelector('#addFormTemplate').innerHTML;
this.map = new InteractiveMap('map', this.onClick.bind(this));
this.map.init().then(this.onInit.bind(this));
}

async onInit() {
const coords = await this.callApi('coords');

for (const item of coords) {
for (let i = 0; i < item.total; i++) {
this.map.createPlacemark(item.coords);
}
}

document.body.addEventListener('click', this.onDocumentClick.bind(this));
}

async callApi(method, body = {}) {
const res = await fetch(`/geo-review-3/${method}`, {
method: 'post',
body: JSON.stringify(body),
});
return await res.json();
}

createForm(coords, reviews) {
const root = document.createElement('div');
root.innerHTML = this.formTemplate;
const reviewList = root.querySelector('.review-list');
const reviewForm = root.querySelector('[data-role=review-form]');
reviewForm.dataset.coords = JSON.stringify(coords);

for (const item of reviews) {
const div = document.createElement('div');
div.classList.add('review-item');
div.innerHTML = `
<div>
<b>${item.name}</b> [${item.place}]
</div>
<div>${item.text}</div>
`;
reviewList.appendChild(div);
}

return root;
}

async onClick(coords) {
this.map.openBalloon(coords, 'Загрузка...');
const list = await this.callApi('list', { coords });
const form = this.createForm(coords, list);
this.map.setBalloonContent(form.innerHTML);
}

async onDocumentClick(e) {
if (e.target.dataset.role === 'review-add') {
const reviewForm = document.querySelector('[data-role=review-form]');
const coords = JSON.parse(reviewForm.dataset.coords);
const data = {
coords,
review: {
name: document.querySelector('[data-role=review-name]').value,
place: document.querySelector('[data-role=review-place]').value,
text: document.querySelector('[data-role=review-text]').value,
},
};

try {
await this.callApi('add', data);
this.map.createPlacemark(coords);
this.map.closeBalloon();
} catch (e) {
const formError = document.querySelector('.form-error');
formError.innerText = e.message;
}
}
}
}
57 changes: 57 additions & 0 deletions projects/geo-review/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<style>
html, body, #map {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}

.form {
width: 300px;
}

.form .field {
width: 100%;
margin-bottom: 10px;
}

.form .field input,
.form .field textarea {
width: 100%;
box-sizing: border-box;
}

.form .field textarea {
resize: none;
}

.form .form-error {
color: red;
}

.form .review-list {
max-height: 500px;
overflow: scroll;
}
</style>

<div id="map"></div>

<script type="template" id="addFormTemplate">
<div class="review-list"></div>
<div class="form" data-role="review-form">
<h3>Отзыв:</h3>
<div class="field">
<input data-role="review-name" type="text" placeholder="Укажите ваше имя">
</div>
<div class="field">
<input data-role="review-place" type="text" placeholder="Укажите место">
</div>
<div class="field">
<textarea data-role="review-text" placeholder="Оставьте отзыв" rows="5"></textarea>
</div>

<button data-role="review-add">Добавить</button>
<span class="form-error"></span>
</div>
</script>
4 changes: 4 additions & 0 deletions projects/geo-review/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import './index.html';
import GeoReview from './geoReview';

new GeoReview();
67 changes: 67 additions & 0 deletions projects/geo-review/interactiveMap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/* global ymaps */

export default class InteractiveMap {
constructor(mapId, onClick) {
this.mapId = mapId;
this.onClick = onClick;
}

async init() {
await this.injectYMapsScript();
await this.loadYMaps();
this.initMap();
}

injectYMapsScript() {
return new Promise((resolve) => {
const ymapsScript = document.createElement('script');
ymapsScript.src =
'https://api-maps.yandex.ru/2.1/?apikey=5a4c2cfe-31f1-4007-af4e-11db22b6954b&lang=ru_RU';
document.body.appendChild(ymapsScript);
ymapsScript.addEventListener('load', resolve);
});
}

loadYMaps() {
return new Promise((resolve) => ymaps.ready(resolve));
}

initMap() {
this.clusterer = new ymaps.Clusterer({
groupByCoordinates: true,
clusterDisableClickZoom: true,
clusterOpenBalloonOnClick: false,
});
this.clusterer.events.add('click', (e) => {
const coords = e.get('target').geometry.getCoordinates();
this.onClick(coords);
});
this.map = new ymaps.Map(this.mapId, {
center: [55.76, 37.64],
zoom: 10,
});
this.map.events.add('click', (e) => this.onClick(e.get('coords')));
this.map.geoObjects.add(this.clusterer);
}

openBalloon(coords, content) {
this.map.balloon.open(coords, content);
}

setBalloonContent(content) {
this.map.balloon.setData(content);
}

closeBalloon() {
this.map.balloon.close();
}

createPlacemark(coords) {
const placemark = new ymaps.Placemark(coords);
placemark.events.add('click', (e) => {
const coords = e.get('target').geometry.getCoordinates();
this.onClick(coords);
});
this.clusterer.add(placemark);
}
}
1 change: 1 addition & 0 deletions projects/geo-review/server/data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"55.80527279361752_37.52327026367186":[{"name":"Сергей","place":"Кофемания","text":"Очень вкусно"},{"name":"Андрей","place":"Кофемания","text":"Согласен с Сергеем"}]}
53 changes: 53 additions & 0 deletions projects/geo-review/server/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
const http = require('http');
const Storage = require('./storage');

createServer();

function readBody(req) {
return new Promise((resolve, reject) => {
let dataRaw = '';

req.on('data', (chunk) => (dataRaw += chunk));
req.on('error', reject);
req.on('end', () => resolve(JSON.parse(dataRaw)));
});
}

function end(res, data, statusCode = 200) {
res.statusCode = statusCode;
res.end(JSON.stringify(data));
}

function createServer() {
const storage = new Storage();

http
.createServer(async (req, res) => {
res.setHeader('content-type', 'application/json');

console.log('>', req.method, req.url);

if (req.method !== 'POST') {
end(res, {});
return;
}

try {
const body = await readBody(req);

if (req.url === '/coords') {
end(res, storage.getCoords());
} else if (req.url === '/add') {
storage.add(body);
end(res, { ok: true });
} else if (req.url === '/list') {
end(res, storage.getByCoords(body.coords));
} else {
end(res, {});
}
} catch (e) {
end(res, { error: { message: e.message } }, 500);
}
})
.listen(8181);
}
64 changes: 64 additions & 0 deletions projects/geo-review/server/storage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
const fs = require('fs');
const path = require('path');
const dataPath = path.join(__dirname, 'data.json');

class Storage {
constructor() {
if (!fs.existsSync(dataPath)) {
fs.writeFileSync(dataPath, '{}');
this.data = {};
} else {
this.data = JSON.parse(fs.readFileSync(dataPath, 'utf8'));
}
}

validateCoords(coords) {
if (!Array.isArray(coords) || coords.length !== 2) {
throw new Error('Invalid coords data');
}
}

validateReview(review) {
if (!review || !review.name || !review.place || !review.text) {
throw new Error('Invalid review data');
}
}

getIndex(coords) {
return `${coords[0]}_${coords[1]}`;
}

add(data) {
this.validateCoords(data.coords);
this.validateReview(data.review);
const index = this.getIndex(data.coords);
this.data[index] = this.data[index] || [];
this.data[index].push(data.review);
this.updateStorage();
}

getCoords() {
const coords = [];

for (const item in this.data) {
coords.push({
coords: item.split('_'),
total: this.data[item].length,
});
}

return coords;
}

getByCoords(coords) {
this.validateCoords(coords);
const index = this.getIndex(coords);
return this.data[index] || [];
}

updateStorage() {
fs.writeFile(dataPath, JSON.stringify(this.data), () => {});
}
}

module.exports = Storage;
22 changes: 22 additions & 0 deletions projects/geo-review/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"proxy": {
"/geo-review-3/list": {
"target": "http://localhost:8181",
"pathRewrite": {
"^/geo-review-3": ""
}
},
"/geo-review-3/add": {
"target": "http://localhost:8181",
"pathRewrite": {
"^/geo-review-3": ""
}
},
"/geo-review-3/coords": {
"target": "http://localhost:8181",
"pathRewrite": {
"^/geo-review-3": ""
}
}
}
}