From 9ca6e957abb8b308a4871b8e418088024238c01c Mon Sep 17 00:00:00 2001 From: Vlad Possin Date: Fri, 25 Dec 2020 21:42:34 +0600 Subject: [PATCH] created second part gp-1 --- projects/geo-review/geoReview.js | 82 +++++++++++++++++++++++++++ projects/geo-review/index.html | 57 +++++++++++++++++++ projects/geo-review/index.js | 4 ++ projects/geo-review/interactiveMap.js | 67 ++++++++++++++++++++++ projects/geo-review/server/data.json | 1 + projects/geo-review/server/index.js | 53 +++++++++++++++++ projects/geo-review/server/storage.js | 64 +++++++++++++++++++++ projects/geo-review/settings.json | 22 +++++++ 8 files changed, 350 insertions(+) create mode 100644 projects/geo-review/geoReview.js create mode 100644 projects/geo-review/index.html create mode 100644 projects/geo-review/index.js create mode 100644 projects/geo-review/interactiveMap.js create mode 100644 projects/geo-review/server/data.json create mode 100644 projects/geo-review/server/index.js create mode 100644 projects/geo-review/server/storage.js create mode 100644 projects/geo-review/settings.json diff --git a/projects/geo-review/geoReview.js b/projects/geo-review/geoReview.js new file mode 100644 index 0000000..38e81d1 --- /dev/null +++ b/projects/geo-review/geoReview.js @@ -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 = ` +
+ ${item.name} [${item.place}] +
+
${item.text}
+ `; + 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; + } + } + } +} diff --git a/projects/geo-review/index.html b/projects/geo-review/index.html new file mode 100644 index 0000000..55d0539 --- /dev/null +++ b/projects/geo-review/index.html @@ -0,0 +1,57 @@ + + +
+ + diff --git a/projects/geo-review/index.js b/projects/geo-review/index.js new file mode 100644 index 0000000..a674ba9 --- /dev/null +++ b/projects/geo-review/index.js @@ -0,0 +1,4 @@ +import './index.html'; +import GeoReview from './geoReview'; + +new GeoReview(); diff --git a/projects/geo-review/interactiveMap.js b/projects/geo-review/interactiveMap.js new file mode 100644 index 0000000..8e58cb1 --- /dev/null +++ b/projects/geo-review/interactiveMap.js @@ -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); + } +} diff --git a/projects/geo-review/server/data.json b/projects/geo-review/server/data.json new file mode 100644 index 0000000..13ba858 --- /dev/null +++ b/projects/geo-review/server/data.json @@ -0,0 +1 @@ +{"55.80527279361752_37.52327026367186":[{"name":"Сергей","place":"Кофемания","text":"Очень вкусно"},{"name":"Андрей","place":"Кофемания","text":"Согласен с Сергеем"}]} \ No newline at end of file diff --git a/projects/geo-review/server/index.js b/projects/geo-review/server/index.js new file mode 100644 index 0000000..1e935cc --- /dev/null +++ b/projects/geo-review/server/index.js @@ -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); +} diff --git a/projects/geo-review/server/storage.js b/projects/geo-review/server/storage.js new file mode 100644 index 0000000..0898c33 --- /dev/null +++ b/projects/geo-review/server/storage.js @@ -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; diff --git a/projects/geo-review/settings.json b/projects/geo-review/settings.json new file mode 100644 index 0000000..8d95040 --- /dev/null +++ b/projects/geo-review/settings.json @@ -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": "" + } + } + } +}