diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..d42ef4a --- /dev/null +++ b/.babelrc @@ -0,0 +1,16 @@ +{ + "presets": [ + [ + "@babel/env", + { + "modules": false, + "targets": { + "ie": "11" + } + } + ] + ], + "plugins": [ + "@babel/transform-runtime" + ] +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a6690da --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*.js,*.json] +indent_style = space +indent_size = 4 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +end_of_line = lf +# editorconfig-tools is unable to ignore longs strings or urls +max_line_length = 120 diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..55c30a5 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,107 @@ +{ + "parserOptions": { + "sourceType": "module", + "ecmaVersion": 7 + }, + "env": { + "node": true, + "browser": true, + "mocha": true, + "es6": true + }, + "extends": [ + "eslint:recommended" + ], + "rules": { + "no-cond-assign": "off", + "space-before-blocks": "error", + "spaced-comment": "error", + "curly": "error", + "guard-for-in": "error", + "no-caller": "error", + "no-else-return": "error", + "no-empty-function": "error", + "no-new-wrappers": "error", + "no-with": "error", + "block-spacing": "error", + "brace-style": [ + "error", + "1tbs" + ], + "indent": [ + "error", + 4, + { + "SwitchCase": 1 + } + ], + "max-len": [ + "error", + 120, + { "ignoreComments": true } + ], + "newline-after-var": "error", + "newline-before-return": "error", + "no-multiple-empty-lines": [ + "error", + { + "max": 1, + "maxEOF": 1 + } + ], + "no-nested-ternary": "error", + "no-tabs": "error", + "one-var-declaration-per-line": "error", + "quotes": [ + "error", + "single" + ], + "max-statements-per-line": [ + "error", + { + "max": 1 + } + ], + "keyword-spacing": [ + "error", + { + "after": true + } + ], + "key-spacing": [ + "error", + { + "afterColon": true + } + ], + "object-curly-spacing": ["error", "always"], + "dot-notation": "error", + "no-eval": "error", + "no-multi-spaces": "error", + "yoda": "error", + "camelcase": [ + "error", + { + "properties": "never" + } + ], + "comma-spacing": [ + "error", + { + "after": true + } + ], + "consistent-this": [ + "error", + "that" + ], + "lines-around-directive": [ + "error", + "always" + ], + "max-nested-callbacks": [ + "error", + 4 + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a59f389 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.idea/ +.vscode/ +.DS_Store +*.lock +!yarn.lock +*.log +out/ +dist/ +node_modules/ +coverage/ diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..967912d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: node_js +node_js: + - "8" +script: "npm test -- --single-run --browsers Firefox" +before_script: + - export DISPLAY=:99.0 + - sh -e /etc/init.d/xvfb start +cache: yarn +addons: + firefox: latest diff --git a/README.md b/README.md new file mode 100644 index 0000000..9022d24 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +## LoftSchool example project + +### Доступные команды + +* `npm install` - установить зависимости +* `npm run prepare` - запустить тесты и проверить стиль кода +* `npm run test` - запустить тесты +* `npm run codestyle` - проверить стиль кода +* `npm run start` - запустить встроенный сервер и следить за изменениями файлов +* `npm run build` - собрать проект в папку `build` diff --git a/img/balloon.svg b/img/balloon.svg new file mode 100644 index 0000000..906180a --- /dev/null +++ b/img/balloon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/index.hbs b/index.hbs new file mode 100644 index 0000000..c358c70 --- /dev/null +++ b/index.hbs @@ -0,0 +1,12 @@ + + + + + {{htmlWebpackPlugin.options.title}} + + + +
+ + \ No newline at end of file diff --git a/index.html b/index.html deleted file mode 100644 index 789b489..0000000 --- a/index.html +++ /dev/null @@ -1 +0,0 @@ -"Hello World" diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 0000000..cdb8307 --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,37 @@ +process.env.CHROME_BIN = require('puppeteer').executablePath() + +module.exports = function(config) { + config.set({ + basePath: '', + frameworks: ['mocha', 'chai'], + files: [ + 'test/**/*.js' + ], + preprocessors: { + 'test/**/*.js': ['webpack', 'sourcemap'], + }, + webpack: require('./webpack.config.test'), + webpackMiddleware: { + stats: 'errors-only' + }, + reporters: ['mocha', 'coverage-istanbul'], + coverageIstanbulReporter: { + reports: ['html', 'lcovonly', 'text-summary'], + fixWebpackSourcePaths: true + }, + port: 9876, + browsers: ['ChromeHeadless'], // или Chrome или Firefox + captureTimeout: 20000, + singleRun: true, + plugins: [ + require('karma-mocha'), + require('karma-chai'), + require('karma-webpack'), + require('karma-mocha-reporter'), + require('karma-firefox-launcher'), + require('karma-chrome-launcher'), + require('karma-coverage-istanbul-reporter'), + require('karma-sourcemap-loader') + ] + }); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..a9dce67 --- /dev/null +++ b/package.json @@ -0,0 +1,55 @@ +{ + "name": "yandex-georeview", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "codestyle": "eslint src", + "travis": "npm run codestyle && npm test", + "build": "webpack --progress --colors", + "watch": "webpack-dev-server --progress --colors --open", + "start": "npm run watch", + "test": "karma start", + "cover": "istanbul cover _mocha -- test/*.js", + "prepare": "npm run travis" + }, + "author": "", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.1.2", + "@babel/plugin-transform-runtime": "^7.1.0", + "@babel/preset-env": "^7.1.0", + "@babel/runtime": "^7.1.2", + "@fortawesome/fontawesome-free": "^5.12.1", + "babel-loader": "^8.0.4", + "clean-webpack-plugin": "^0.1.19", + "css-loader": "^1.0.1", + "file-loader": "^2.0.0", + "handlebars": "^4.0.12", + "handlebars-loader": "^1.7.0", + "html-webpack-plugin": "^3.2.0", + "mini-css-extract-plugin": "^0.4.4", + "normalize.css": "^8.0.1", + "style-loader": "^0.23.1", + "webpack": "^4.25.1", + "webpack-dev-server": "^3.1.10" + }, + "devDependencies": { + "chai": "^4.2.0", + "eslint": "^5.8.0", + "istanbul": "^0.4.5", + "istanbul-instrumenter-loader": "^3.0.1", + "karma": "^3.1.1", + "karma-chai": "^0.1.0", + "karma-chrome-launcher": "^2.2.0", + "karma-coverage-istanbul-reporter": "^2.0.4", + "karma-firefox-launcher": "^1.1.0", + "karma-mocha": "^1.3.0", + "karma-mocha-reporter": "^2.2.5", + "karma-sourcemap-loader": "^0.3.7", + "karma-webpack": "^4.0.0-rc.2", + "mocha": "^5.2.0", + "puppeteer": "^1.10.0", + "webpack-cli": "^3.1.2" + } +} diff --git a/src/index.hbs b/src/index.hbs new file mode 100644 index 0000000..c358c70 --- /dev/null +++ b/src/index.hbs @@ -0,0 +1,12 @@ + + + + + {{htmlWebpackPlugin.options.title}} + + + +
+ + \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..729bf93 --- /dev/null +++ b/src/index.js @@ -0,0 +1,7 @@ +require('@fortawesome/fontawesome-free/css/all.min.css'); +import './styles/balloon.css'; +import init from './js/georeview'; +import '../img/balloon.svg'; +// import init from './cluster'; + +ymaps.ready(init); \ No newline at end of file diff --git a/src/js/georeview.js b/src/js/georeview.js new file mode 100644 index 0000000..2a1ef68 --- /dev/null +++ b/src/js/georeview.js @@ -0,0 +1,156 @@ +const balloonTpl = require('../templates/balloon.hbs'); +const reviewsTpl = require('../templates/reviews.hbs'); + +const init = () => { + + const getCoords = (point) => { + return point.coords ? point.coords : point.geometry.getCoordinates(); + }; + + const balloonLayout = ymaps.templateLayoutFactory.createClass( + '$[[options.contentLayout]]', { + build: function () { + balloonLayout.superclass.build.call(this); + const closeButton = document.querySelector('.btn-close'); + + closeButton.addEventListener('click', () => { + this.closeBalloon(); + }); + + const addReviewBtn = document.querySelector('.add-review-btn'); + + addReviewBtn.addEventListener('click', e => { + e.preventDefault(); + let geoObject = map.balloon.getData(); + let reviewList = document.querySelector(".reviews-list"); + let reviewForm = addReviewBtn.closest('form.add-review'); + let reviewDetails = getReviewDetails(reviewForm); + let placeWithReview = { + coords: getCoords(geoObject), + addr: geoObject.addr, + review: reviewDetails + }; + + addOrUpdateLocalStorageItem(placeWithReview); + updateReviewList(reviewList, geoObject.addr); + createPlacemark(placeWithReview); + + // map.balloon.close(); + }) + }, + clear: function () { + balloonLayout.superclass.clear.call(this); + }, + closeBalloon: function () { + this.events.fire('userclose'); + }, + getShape: function () { + let el = document.querySelector('.review-popup'), + result = null; + if (el) { + result = new ymaps.shape.Rectangle( + new ymaps.geometry.pixel.Rectangle([ + [0, 0], + [el.offsetWidth, el.offsetHeight] + ]) + ); + } + return result; + } + }); + + const getReviewDetails = (reviewForm) => { + return { + author: reviewForm['author'].value, + place: reviewForm['place'].value, + reviewText: reviewForm['review-text'].value, + date: new Date().toLocaleDateString() + }; + }; + + const getReviews = (address) => { + let currentAddressReviewsStr = localStorage.getItem(address); + return JSON.parse(currentAddressReviewsStr); + }; + + const addOrUpdateLocalStorageItem = (placeReview) => { + if (!localStorage.getItem(placeReview.addr)) { + let reviews = []; + + reviews.push(placeReview); + localStorage.setItem(placeReview.addr, JSON.stringify(reviews)); + } else { + let currentAddressReviews = getReviews(placeReview.addr); + + currentAddressReviews.push(placeReview); + localStorage.setItem(placeReview.addr, JSON.stringify(currentAddressReviews)); + } + }; + + const updateReviewList = (reviewElement, address) => { + let reviews = getReviews(address); + console.log(reviews); + let reviewHtml = reviewsTpl({reviews}); + console.log(reviewHtml); + reviewElement.innerHTML = reviewHtml; + }; + + const reverseGeoCode = (coords) => { + // Определяем адрес по координатам (обратное геокодирование). + return ymaps.geocode(coords).then(res => { + let firstGeoObject = res.geoObjects.get(0); + return firstGeoObject.getAddressLine(); + }); + }; + + const bcl = (address) => { + let balloonHTML = balloonTpl(address); + return ymaps.templateLayoutFactory.createClass(balloonHTML); + }; + + let map = new ymaps.Map('map', { + center: [55.650625, 37.62708], + zoom: 10, + controlls: ['zoomControl'] + + }, {balloonLayout}); + + const openBalloon = (geoObj) => { + map.balloon.open(geoObj.coords, geoObj, { + layout: balloonLayout, + contentLayout: bcl(geoObj) + }); + }; + + map.events.add('click', e => { + const pointCoords = e.get('coords'); + const pointAddress = reverseGeoCode(pointCoords); + + pointAddress.then(address => { + let geoObj = { + coords: pointCoords, + addr: address + }; + openBalloon(geoObj); + }); + }); + + const createPlacemark = (placemarkData) => { + let myPlacemark = new ymaps.Placemark(placemarkData.coords, { + geoObj: placemarkData + }, { + balloonShadow: false, + balloonLayout: balloonLayout, + balloonContentLayout: bcl(placemarkData), + iconLayout: 'default#image', + iconImageHref: './img/balloon.svg', + iconImageSize: [20, 30], + }); + + map.geoObjects.add(myPlacemark); + + return myPlacemark; + }; +}; + +export default init; \ No newline at end of file diff --git a/src/styles/balloon.css b/src/styles/balloon.css new file mode 100644 index 0000000..3e4cb32 --- /dev/null +++ b/src/styles/balloon.css @@ -0,0 +1,185 @@ +.review-popup { + background-color: #ffffff; + border-radius: 10px; + width: 300px; + position: relative; + z-index: 999; +} + +.map-balloon{ + font-size: 24px; + cursor: pointer; +} + +.popup-geo-point { + font-size: 14px; + margin-right: 7px; +} + +.popup-header { + background-color: #ff8663; + padding: 15px; + color: #ffffff; + font-size: 12px; + border-radius: 5px 5px 0 0; + position: relative; +} + +.reviews-list-container{ + height: 110px; + overflow-x: hidden; + overflow-y: scroll; + margin-bottom: 25px; +} + +.reviews-list { + display: flex; + flex-direction: column; +} + +.review-author { + font-weight: 700; + margin-right: 10px; +} + +.review-date { + margin-left: 20px; +} + +ul{ + margin: 0; + padding: 0; + list-style: none; +} + +::-webkit-scrollbar { + width: 5px; + background-color: #F5F5F5; +} + +::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); + border-radius: 10px; + background-color: #F5F5F5; +} + +::-webkit-scrollbar-thumb { + border-radius: 10px; + -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); + background-color: #ff8663; +} + + +.review-header { + display: flex; + align-items: center; + margin-bottom: 5px; +} + +.review-item { + margin-bottom: 10px; +} + +.review-item:last-child { + margin-bottom: 0; +} + +.review-text { + color: #4f4f4f; + font-size: 15px; + font-weight: 400; + line-height: 22px; +} + + +.btn-close { + background: transparent; + border: none; + outline: none; + position: absolute; + right: 10px; + top: 8px; + font-size: 16px; + color: #ffffff; + cursor: pointer; + padding: 5px; +} + +.reviews { + color: #9f9f9f; + padding: 15px 15px 0 15px; + margin-bottom: 15px; + height: 150px; +} + +.add-review-form::before { + content: ""; + margin-bottom: 10px; + display: block; + width: 100%; + height: 1px; + background-color: #f0f0ff; +} + +.delimiter{ + border-top: 1px solid #f0f0f0; +} + +.add-review-header{ + color: #ff8663; + font-size: 16px; + text-align: left; + font-weight: 500; + text-transform: uppercase; +} + +html, body, #map { + width: 100%; + height: 100%; + padding: 0; + margin: 0; +} + +.add-review-form{ + padding: 0 0px 15px 5px; +} + + + +form { + display: flex; + flex-direction: column; +} + +input, #review-textarea { + font-size: 15px; + margin-bottom: 16px; + margin-left: 5px; + margin-right: 10px; + padding: 10px 10px; + border: none; +} + +input:focus, #review-textarea:focus { + color: #9f9f9f; + outline: none; + border: 1px solid #29B0D9; + border-radius: 8px; +} + +#review-textarea{ + height: 100px; +} + +.add-review-btn { + float: right; + color: #ffffff; + right: 15px; + bottom: 10px; + padding: 10px; + font-size: 14px; + border: 1px solid #ff8663; + border-radius: 10px; + background-color: #ff8663; + outline: none; +} diff --git a/src/templates/balloon.hbs b/src/templates/balloon.hbs new file mode 100644 index 0000000..a4b038a --- /dev/null +++ b/src/templates/balloon.hbs @@ -0,0 +1,36 @@ +
+ +
+
+
    + {{> "reviews.hbs"}} +
+
+
+ +
+
+
+ Ваш отзыв +
+ + + + + +
+ +
+ +
+
+
\ No newline at end of file diff --git a/src/templates/reviews.hbs b/src/templates/reviews.hbs new file mode 100644 index 0000000..9e3f64b --- /dev/null +++ b/src/templates/reviews.hbs @@ -0,0 +1,12 @@ +{{#each reviews}} +
  • +
    +
    {{review.author}}:
    +
    {{review.place}}
    +
    {{review.date}}
    +
    +
    {{review.reviewText}}
    +
  • +{{else}} + Отзывов пока нет +{{/each}} \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..54a08ec --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,74 @@ +const HtmlPlugin = require('html-webpack-plugin'); +const CleanWebpackPlugin = require('clean-webpack-plugin'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const rules = require('./webpack.config.rules'); +const fs = require('fs'); +const path = require('path'); + +const root = path.resolve('src'); +const files = fs.readdirSync(root) + .reduce((all, current) => { + const ext = path.extname(current); + const name = path.basename(current, ext); + const absPath = path.join(root, current); + + if (!all.hasOwnProperty(ext)) { + all[ext] = []; + } + + all[ext].push({ name, absPath }); + + return all; + }, { '.js': [], '.hbs': [] }); +const entries = files['.js'].reduce((all, { name, absPath }) => { + all[name] = absPath; + + return all; +}, {}); +const html = files['.hbs'] + .filter(file => entries.hasOwnProperty(file.name)) + .map((file) => { + return new HtmlPlugin({ + title: file.name, + template: file.absPath, + filename: `${file.name}.html`, + chunks: [file.name] + }); + }); + +if (!html.length || !files['.hbs'].find(file => file.name === 'index')) { + html.push(new HtmlPlugin({ + title: 'index', + template: 'index.hbs', + chunks: ['index'] + })); +} + +module.exports = { + entry: entries, + output: { + filename: '[name].[hash].js', + path: path.resolve('dist') + }, + mode: 'development', + devtool: 'source-map', + module: { + rules: [ + ...rules, + { + test: /\.css$/, + use: [ + MiniCssExtractPlugin.loader, + 'css-loader' + ] + } + ] + }, + plugins: [ + new MiniCssExtractPlugin({ + filename: '[name].css', + }), + ...html, + new CleanWebpackPlugin(['dist']) + ] +}; \ No newline at end of file diff --git a/webpack.config.rules.js b/webpack.config.rules.js new file mode 100644 index 0000000..9a68e28 --- /dev/null +++ b/webpack.config.rules.js @@ -0,0 +1,20 @@ +module.exports = [ + { + test: /\.js$/, + exclude: /node_modules/, + loader: 'babel-loader', + options: { cacheDirectory: true } + }, + { + test: /\.hbs/, + loader: 'handlebars-loader' + }, + { + test: /\.(jpe?g|png|gif|svg|eot|ttf|woff|woff2)$/i, + loader: 'file-loader', + options: { + name: '[hash:8].[ext]', + outputPath: 'reosurces' + } + } +]; \ No newline at end of file diff --git a/webpack.config.test.js b/webpack.config.test.js new file mode 100644 index 0000000..9dd9124 --- /dev/null +++ b/webpack.config.test.js @@ -0,0 +1,24 @@ +const path = require('path'); +const rules = require('./webpack.config.rules'); + +module.exports = { + mode: 'development', + devtool: 'inline-source-map', + module: { + rules: [ + ...rules, + { + test: /\.js$/, + enforce: 'post', + include: [path.resolve('src/')], + loader: 'istanbul-instrumenter-loader', + options: { esModules: true } + }, + { + test: /\.css$/, + include: path.resolve('src/'), + use: ['style-loader', 'css-loader'] + } + ] + } +};