diff --git a/.babelrc b/.babelrc index d258e70..014a6dd 100644 --- a/.babelrc +++ b/.babelrc @@ -1,6 +1,9 @@ { - "presets": ["react", "es2015", "stage-0"], + "presets": [ + "react", "env", "stage-0" + ], "plugins": [ - "transform-async-to-generator" + "transform-async-to-generator", + "transform-custom-element-classes" ] } diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..ea74684 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +app/tests +app/vendor diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..1dfd1c6 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,23 @@ +{ + "parser": "babel-eslint", + "extends": "eslint:recommended", + "env": { + "browser": true, + "node": true, + "es6": true, + "jquery": true + }, + "globals": { + "app": true + }, + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module" + }, + "rules": { + "no-console": "error", + "curly": "error", + "semi": ["error", "always"], + "no-empty": ["error", { "allowEmptyCatch": true }] + } +} diff --git a/.gitignore b/.gitignore index 5fba84b..7c29102 100644 --- a/.gitignore +++ b/.gitignore @@ -14,5 +14,4 @@ app/tests/__tests__/coverage/* .DS_Store build/* - /index.html diff --git a/.travis.yml b/.travis.yml index 4bef046..7aa7f87 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,11 @@ language: node_js node_js: - - stable + - 8.11.2 + +branches: + only: + - master install: - npm install diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2e13383 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,27 @@ +# Contributing + +✨ Thank you for contributing ✨ + +There are just guideliness. + +Please feel free to contribute by submitting PR's for improvements to code snippets, explanations, etc. + +## Submitting an issue + +Found a problem? Have an enhancement? + +First of all see if your issue or idea has already been [reported](https://github.com/shystruk/create-react-redux-app-structure/issues). + +If do not, open a [new one](https://github.com/shystruk/create-react-redux-app-structure/issues/new). + +## Submitting a pull request + +- Fork this repository +- Clone fork `git clone ...` +- Navigate to the cloned directory +- Install all dependencies `npm install` +- Crate a new branch for the feature `git checkout -b new-feature` +- Make changes +- Commit changes `git commit -am 'What is feature about? :)'` +- Push to the branch `git push origin new-feature` +- Submit a PR diff --git a/Gruntfile.js b/Gruntfile.js index 648e9f9..98782b9 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -14,6 +14,18 @@ module.exports = function(grunt) { file: './build/libs.js', cleanup: true }, + vendorJs: { + replace: ['./index.html'], + replacement: 'vendor.js', + file: './build/vendor.js', + cleanup: true + }, + web_components_vendorJs: { + replace: ['./index.html'], + replacement: 'web_components_vendor.js', + file: './build/web_components_vendor.js', + cleanup: true + }, indexJs: { replace: ['./index.html'], replacement: 'index.js', diff --git a/LICENSE b/LICENSE index 5e45736..00ebed1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017 Vasyl Stokolosa +Copyright (c) Vasyl Stokolosa Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 8e1c067..ba1b222 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,66 @@ # Create React Redux App Structure [![Twitter URL](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?hashtags=reactjs%20%23redux%20%23javascript&original_referer=https%3A%2F%2Fpublish.twitter.com%2F&ref_src=twsrc%5Etfw&text=Start%20your%20project%20fast%20with%20Create%20React%20Redux%20App%20Structure&tw_p=tweetbutton&url=https%3A%2F%2Fgithub.com%2Fshystruk%2Fcreate-react-redux-app-structure&via=shystrukk) # -[![MIT Licence](https://badges.frapsoft.com/os/mit/mit.svg?v=103)](https://opensource.org/licenses/mit-license.php) [![Build Status](https://travis-ci.org/shystruk/create-react-redux-app-structure.svg?branch=master)](https://travis-ci.org/shystruk/create-react-redux-app-structure) [![npm version](https://badge.fury.io/js/create-react-redux-app-structure.svg)](https://badge.fury.io/js/create-react-redux-app-structure) -Create React + Redux app structure with build configurations. +[![MIT Licence](https://badges.frapsoft.com/os/mit/mit.svg?v=103)](https://opensource.org/licenses/mit-license.php) [![codecov](https://codecov.io/gh/shystruk/create-react-redux-app-structure/branch/master/graph/badge.svg)](https://codecov.io/gh/shystruk/create-react-redux-app-structure) [![Build Status](https://travis-ci.org/shystruk/create-react-redux-app-structure.svg?branch=master)](https://travis-ci.org/shystruk/create-react-redux-app-structure) [![npm version](https://badge.fury.io/js/create-react-redux-app-structure.svg)](https://badge.fury.io/js/create-react-redux-app-structure) -https://medium.com/@shystruk/how-create-react-redux-app-structure-helps-you-to-start-a-project-faster-cf564c64689c +Create React + Redux app structure with build configurations. ## What can I find here? ## -- Express, Cors -- React + Redux, ES6, async/await +- [Express](https://expressjs.com/), Cors +- [React](https://reactjs.org/) + [Redux](https://redux.js.org/), ES6, async/await +- [Web Components](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements) (Custom Elements) integration - React Router +- Internationalization - SASS - PostCSS (autoprefixer), so you do not need -webkit, -moz or other prefixes -- Build script configuration **Development, Staging, Production** with CDN, [cache-busting](https://www.keycdn.com/support/what-is-cache-busting/) support, +- Build script configuration **Development, Staging, Production** with CDN, [cache-busting](https://www.keycdn.com/support/what-is-cache-busting/) support - Build script to bundle JS, CSS, with sourcemaps -- Unit tests Jest, Enzyme -- E2E Cypress tests -- Ghooks (pre-commit) +- Unit tests [Jest](https://jestjs.io/), [Enzyme](http://airbnb.io/enzyme/) +- E2E [Cypress](https://www.cypress.io/) tests +- [ESLint](https://eslint.org/) +- Ghooks (pre-commit with unit tests and eslint validation) - Code Coverage (https://codecov.io) - Travis CI runs Unit and E2E tests and report to codecov +## Quick Start ## +Create React + Redux app structure works on macOS, Windows, and Linux. +If something doesn’t work, please file an [issue](https://github.com/shystruk/create-react-redux-app-structure/issues/new). + +#### npm +`npm i -g create-react-redux-app-structure` + +#### yarn +`yarn add global create-react-redux-app-structure` + +``` +create-react-redux-app-structure my-app +cd my-app/ +npm run fast-start +``` +http://localhost:8080/ will be opened automatically. + +When you are ready to deploy to staging/production please see [Build Scripts](#build-scripts) section. + ## Getting Started ## -You can download the repository or install through npm `npm i create-react-redux-app-structure`. +**You will need to have Node >= 6 on your local development machine and [Yarn](https://yarnpkg.com/en/docs/install#mac-stable) installed.** -As CLI is not supported yet and you have installed the package through npm, go to `node_modules/create-react-redux-app-structure` to get app structure. +Install it once globally: -### Installation ### -**`npm install`** or **`yarn install`** +#### npm +`npm i -g create-react-redux-app-structure` -### Run build script ### -Have a look at [Build Scripts](#build-scripts) section +#### yarn +`yarn add global create-react-redux-app-structure` -### Run server ### -**`node index.js`** +> Patience, please. It takes time, most of it is spent installing npm packages. -Then open http://localhost:8080/ to see test weather app :) +### Creating an App ### +To create a new app, run: +``` +create-react-redux-app-structure my-app +cd my-app/ +``` +It will create a directory called my-app inside the current folder. ### Prepare config.json for build configurations ### For running builds you need to have **config.json** in app/ folder. @@ -45,6 +71,21 @@ Inside that file: - **assetHost** CDN path for each build - **serverHost** is used for running e2e Cypress tests +### Installation ### +**`npm install`** or **`yarn install`** + +>You can run **npm run fast-start** script, it will install all npm packages, run dev build, server and open http://localhost:8080/ + +![](images/demo.gif) + +### Run build script ### +Have a look at [Build Scripts](#build-scripts) section + +### Run server ### +**`node index.js`** or **npm run server** + +Then open http://localhost:8080/ to see test weather app :) + ## Build scripts ## Development - **`npm run dev`** or **`yarn run dev`** @@ -64,7 +105,7 @@ Coverage is here - *app/tests/__tests__/coverage/Icon-report/index.html* ## Automation tests ## -Let's images that for automation tests we need to get access to the Redux store. +Let's imagine that for automation tests we need to get access to the Redux store. We can do that by adding to the `window` object property with reference to the store. For e.g. in `app.jsx` file. Automation tests run only in **staging**, so for production build we remove them out by Grunt task `strip_code` @@ -74,6 +115,25 @@ window.store = store; /* end-staging-code */ ``` +## Tips ## +Kill all node processes: +- MacOS `sudo killall -9 node` +- Windows (cmd) `taskkill /f /im node.exe` + +## Detailed description about features and approaches ## +- [How create-react-redux-app-structure helps you to start a project faster](https://medium.com/@shystruk/how-create-react-redux-app-structure-helps-you-to-start-a-project-faster-cf564c64689c) +- [clearIntervals() when user has a nap](https://codeburst.io/clearintervals-when-user-has-a-nap-3bf8010c986b) +- [Do you still register window event listeners in each component?](https://medium.com/@shystruk/do-you-still-register-window-event-listeners-in-each-component-react-in-example-31a4b1f6f1c8) +- [v4 Create React + Redux app structure with build configurations. What’s new?](https://medium.com/@shystruk/v4-create-react-redux-app-structure-with-build-configurations-whats-new-523bdec328c6) +- [Integrate Custom Elements into React app](https://medium.com/@shystruk/integrate-custom-elements-into-react-app-ef38eba12905) + ## Contributing ## -I would love to have your help. If you have an idea how to improve, change the app structure please submit a pull request or create an issue. + +I would love to have your help. + +If you have an idea how to improve or found an issue please read the [Contributions Guidelines](CONTRIBUTING.md) before submitting a PR. Thanks! + +## License + +MIT © [Vasyl Stokolosa](https://about.me/shystruk) diff --git a/app/actions/alert.js b/app/actions/alert.js index 2d33414..ab257a6 100644 --- a/app/actions/alert.js +++ b/app/actions/alert.js @@ -1,5 +1,3 @@ -'use strict'; - export const SHOW_ALERT = 'SHOW_ALERT'; export const HIDE_ALERT = 'HIDE_ALERT'; @@ -11,7 +9,7 @@ export function showAlert(message) { return { type: SHOW_ALERT, message - } + }; } /** @@ -20,5 +18,5 @@ export function showAlert(message) { export function hideAlert() { return { type: HIDE_ALERT - } + }; } diff --git a/app/actions/notification.js b/app/actions/notification.js new file mode 100644 index 0000000..fbc6d2b --- /dev/null +++ b/app/actions/notification.js @@ -0,0 +1,22 @@ +export const SHOW_NOTIFICATION = 'SHOW_NOTIFICATION'; +export const REMOVE_NOTIFICATION = 'REMOVE_NOTIFICATION'; + +/** + * @param {String} message + * @return {Object} + */ +export function pushNotification(message) { + return { + type: SHOW_NOTIFICATION, + message + }; +} + +/** + * @return {Object} + */ +export function removeNotification() { + return { + type: REMOVE_NOTIFICATION + }; +} diff --git a/app/actions/weather.js b/app/actions/weather.js index 0a585d3..53143ff 100644 --- a/app/actions/weather.js +++ b/app/actions/weather.js @@ -1,8 +1,11 @@ -'use strict'; +import { getWeatherData } from './../resources/Open_Weather.resource'; +import { parseWeatherResponseForUI } from './../components/Open_Weather_Search/Open_Weather_Search.utils'; export const PUSH_WEATHER = 'PUSH_WEATHER'; export const PUSH_WEATHER_LIST = 'PUSH_WEATHER_LIST'; export const REMOVE_WEATHER_FROM_LIST = 'REMOVE_WEATHER_FROM_LIST'; +export const REQUEST_WEATHER = 'REQUEST_WEATHER'; +export const RECEIVE_WEATHER = 'RECEIVE_WEATHER'; /** * @param {Object} weather @@ -12,5 +15,39 @@ export function pushWeather(weather) { return { type: PUSH_WEATHER, weather - } + }; } + +/** + * @return {Object} + */ +export function requestWeather() { + return { + type: REQUEST_WEATHER + }; +} + +/** + * @return {Object} + */ +export function receiveWeather() { + return { + type: RECEIVE_WEATHER + }; +} + +/** + * @param {String} cityName + * @return {Promise} + */ +export const fetchWeather = (cityName) => (dispatch) => { + dispatch(requestWeather()); + + return getWeatherData(cityName) + .then((weather) => { + dispatch(pushWeather(parseWeatherResponseForUI(weather))); + }) + .finally(() => { + dispatch(receiveWeather()); + }); +}; diff --git a/app/actions/weather_list.js b/app/actions/weather_list.js index 53dd306..cf52bb8 100644 --- a/app/actions/weather_list.js +++ b/app/actions/weather_list.js @@ -1,5 +1,3 @@ -'use strict'; - export const PUSH_WEATHER_LIST = 'PUSH_WEATHER_LIST'; export const REMOVE_WEATHER_FROM_LIST = 'REMOVE_WEATHER_FROM_LIST'; @@ -11,7 +9,7 @@ export function pushWeatherList(weather) { return { type: PUSH_WEATHER_LIST, weather - } + }; } /** @@ -22,7 +20,5 @@ export function removeWeatherFromList(index) { return { type: REMOVE_WEATHER_FROM_LIST, index - } + }; } - - diff --git a/app/app.jsx b/app/app.jsx index c8d7fdd..4ed7843 100644 --- a/app/app.jsx +++ b/app/app.jsx @@ -1,22 +1,34 @@ -'use strict'; - import React from 'react'; import ReactDom from 'react-dom'; -import { BrowserRouter as Router } from 'react-router-dom' +import { BrowserRouter as Router } from 'react-router-dom'; import { Provider } from 'react-redux'; import store from './store'; +import _find from 'lodash/find'; +import DOMReady from './helpers/domReady'; +DOMReady(); import App from './pages/App'; -/* staging-code */ -window.store = store; -/* end-staging-code */ +// Localization +import messages from './i18n/messages'; +import { addLocaleData, IntlProvider } from 'react-intl'; +import en from 'react-intl/locale-data/en'; +import es from 'react-intl/locale-data/es'; +import fr from 'react-intl/locale-data/fr'; +addLocaleData([...en, ...es, ...fr]); + +let locale = _find(['en', 'es', 'fr'], (locale) => { + return app.locale.indexOf(locale) !== -1; +}); + ReactDom.render( - - - + + + + + , document.getElementById('app') diff --git a/app/components/CSV_Validator/CSV_Validator.jsx b/app/components/CSV_Validator/CSV_Validator.jsx new file mode 100644 index 0000000..5d02600 --- /dev/null +++ b/app/components/CSV_Validator/CSV_Validator.jsx @@ -0,0 +1,44 @@ +import React from 'react'; +import CSVFileValidator from 'csv-file-validator'; + +const CSV_Config = { + headers: [] +} + +export default class CSV_Validator extends React.Component { + constructor() { + super(); + } + + /** + * @param {Object} event + */ + handleChange = (event) => { + this.validateFile(event.target.files[0]); + + // allow to upload the same file twice in a row + event.target.value = ''; + } + + /** + * @param {File} file + */ + validateFile = (file) => { + CSVFileValidator(file, CSV_Config) + .then(csvData => { + console.log('DATA: ', csvData.data) + console.log('MESSAGES: ', csvData.inValidMessages) + }) + .catch(err => { + console.log('ERROR: ', err) + }) + } + + render() { + return ( +
+ +
+ ); + } +} diff --git a/app/components/Open_Weather_Search/Open_Weather_Search.jsx b/app/components/Open_Weather_Search/Open_Weather_Search.jsx index a6eb02a..66e9c82 100644 --- a/app/components/Open_Weather_Search/Open_Weather_Search.jsx +++ b/app/components/Open_Weather_Search/Open_Weather_Search.jsx @@ -1,46 +1,63 @@ -'use strict'; - -import React, { Component } from 'react'; +import React from 'react'; import store from './../../store'; -import { pushWeather } from './../../actions/weather'; +import { pushWeather, fetchWeather } from './../../actions/weather'; import { pushWeatherList } from './../../actions/weather_list'; import { showAlert } from './../../actions/alert'; import { getWeatherData } from './../../resources/Open_Weather.resource'; +import { getGeoLocation } from './../../resources/Geolocation.resource'; import { Open_Weather_Interface } from './../../helpers/interfaces'; +import { getLocation, getCityNameFromGeocode } from './../../helpers/geolocation'; import { parseWeatherResponseForUI } from './Open_Weather_Search.utils'; import { KEY_CODES } from './../../constants/keyCodes.constant'; +import { ERROR_MESSAGES } from './../../constants/request.constant'; -export default class Open_Weather extends Component { +export default class Open_Weather extends React.Component { constructor() { super(); this.state = Open_Weather_Interface; + } - this.handleChange = this.handleChange.bind(this); - this.handleKeyUp = this.handleKeyUp.bind(this); - this.handleSearch = this.handleSearch.bind(this); + componentDidMount() { + if (!this.props.weather.loaded) { + this.setState(() => ({preload: true})); + + getLocation() + .then(position => { + return getGeoLocation(`${position.latitude},${position.longitude}`); + }) + .then(location => { + return store.dispatch(fetchWeather(getCityNameFromGeocode(location))); + }) + .catch(error => { + store.dispatch(showAlert((error && error.message) || ERROR_MESSAGES.GEO_LOCATION_UNAVAILABLE)); + }) + .finally(() => { + this.setState(() => ({preload: false})); + }); + } } /** * @param {Object} event */ - handleKeyUp(event) { + handleKeyUp = (event) => { if (event.keyCode === KEY_CODES.ENTER) { this.handleSearch(); } - } + }; /** * @param {Object} event */ - handleChange(event) { + handleChange = (event) => { const target = event.target; const value = target.type === 'checkbox' ? target.checked : target.value; this.setState(() => ({[target.name]: value})); - } + }; - async handleSearch() { + handleSearch = async () => { let weather = {}; this.setState(() => ({preload: true})); @@ -58,7 +75,7 @@ export default class Open_Weather extends Component { } finally { this.setState(() => ({preload: false})); } - } + }; /** * @param {Object} weather @@ -74,22 +91,24 @@ export default class Open_Weather extends Component { } render() { - return
+ return ( +
-

Current weather in your city

+

Current weather in your city

- - + + - + - + -
+
+ ); } } diff --git a/app/components/Open_Weather_Search/Open_Weather_Search.utils.js b/app/components/Open_Weather_Search/Open_Weather_Search.utils.js index 61583ce..a39f0a6 100644 --- a/app/components/Open_Weather_Search/Open_Weather_Search.utils.js +++ b/app/components/Open_Weather_Search/Open_Weather_Search.utils.js @@ -1,5 +1,3 @@ -'use strict'; - import _lowerCase from 'lodash/lowerCase'; /** @@ -14,5 +12,5 @@ export const parseWeatherResponseForUI = function (weather) { temperature: `${weather.main.temp}°С`, wind: `wind ${weather.wind.speed} m/s`, clouds: `clouds ${weather.clouds.all} %` - } + }; }; diff --git a/app/components/dumb/Resize_SubPub_Action.jsx b/app/components/dumb/Resize_SubPub_Action.jsx new file mode 100644 index 0000000..e165b90 --- /dev/null +++ b/app/components/dumb/Resize_SubPub_Action.jsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { FormattedHTMLMessage } from 'react-intl'; +import PublishSubscribe from 'publish-subscribe-js'; +import { PUB_SUB } from './../../constants/events.constant'; +import { Resize_SubPub_Action_Interface } from './../../helpers/interfaces'; + +export default class Resize_SubPub_Action extends React.Component { + constructor() { + super(); + + this.state = Resize_SubPub_Action_Interface; + + this.resizeSubKey = 0; + } + + updateSizes = (data = {}) => { + this.setState({ + width: data.width || window.innerWidth, + height: data.height || window.innerHeight + }); + }; + + componentDidMount() { + this.updateSizes(); + this.resizeSubKey = PublishSubscribe.subscribe(PUB_SUB.RESIZE, this.updateSizes); + } + + componentWillUnmount() { + PublishSubscribe.unsubscribe(PUB_SUB.RESIZE, this.resizeSubKey); + } + + render() { + return ( +
+
+
+
+
+ ); + } +} diff --git a/app/components/dumb/Show_Page_Visibility_API.jsx b/app/components/dumb/Show_Page_Visibility_API.jsx new file mode 100644 index 0000000..db352fb --- /dev/null +++ b/app/components/dumb/Show_Page_Visibility_API.jsx @@ -0,0 +1,59 @@ +import React from 'react'; +import PublishSubscribe from 'publish-subscribe-js'; +import { Show_Page_Visibility_API_Interface } from './../../helpers/interfaces'; +import { PUB_SUB } from './../../constants/events.constant'; +import SetInterval from 'set-interval'; +import store from './../../store'; +import { showAlert } from './../../actions/alert'; +import { FormattedMessage } from 'react-intl'; + +export default class Show_Page_Visibility_API extends React.Component { + constructor() { + super(); + + this.state = Show_Page_Visibility_API_Interface; + this.visibilityChangeSubKey = 0; + } + + simulateHTTPRequest = () => { + setTimeout(() => { + if (!this.unmount) { + this.setState({amountOfNewUsers: ++this.state.amountOfNewUsers}); + } + }, 500); + }; + + /** + * @param {Object} data + */ + handleVisibilityChange = (data) => { + if (data.action === 'continue') { + SetInterval.start(this.simulateHTTPRequest, 1000, 'page_visibility'); + store.dispatch(showAlert('Seems like the page was not visible. Do not worry, we keep working :)')); + } else { + SetInterval.clear('page_visibility'); + } + }; + + componentDidMount() { + SetInterval.start(this.simulateHTTPRequest, 1000, 'page_visibility'); + this.visibilityChangeSubKey = PublishSubscribe.subscribe(PUB_SUB.PAGE_VISIBILITY, this.handleVisibilityChange); + } + + componentWillUnmount() { + this.unmount = true; + SetInterval.clear('page_visibility'); + PublishSubscribe.unsubscribe(PUB_SUB.PAGE_VISIBILITY, this.visibilityChangeSubKey); + } + + render() { + return ( +
+
+

+ +

+
+ ); + } +} diff --git a/app/components/dumb/Weather_View.jsx b/app/components/dumb/Weather_View.jsx index 7352f1b..8f2d516 100644 --- a/app/components/dumb/Weather_View.jsx +++ b/app/components/dumb/Weather_View.jsx @@ -1,11 +1,9 @@ -'use strict'; - -import React, { Component } from 'react'; +import React from 'react'; import _isEmpty from 'lodash/isEmpty'; import { FLAG_IMAGE_URL } from '../../constants/components/Weather_View.constant'; import { scrollTo } from '../../helpers/uiActions'; -export default class Weather_View extends Component { +export default class Weather_View extends React.Component { /** * @param {Object} weather @@ -31,26 +29,28 @@ export default class Weather_View extends Component { let weather = this.props.weather; let weatherList = this.props.weatherList; - if (!_isEmpty(weather)) { - oneCity =
  • {Weather_View.weatherTemplate(weather)}
  • ; + if (!_isEmpty(weather.data)) { + oneCity =
  • {Weather_View.weatherTemplate(weather.data)}
  • ; } - return
    - +
    + ); } } diff --git a/app/constants/components/Open_Weather_Search.constant.js b/app/constants/components/Open_Weather_Search.constant.js index 39c8604..4f60303 100644 --- a/app/constants/components/Open_Weather_Search.constant.js +++ b/app/constants/components/Open_Weather_Search.constant.js @@ -1,5 +1,3 @@ -'use strict'; - import { URL } from '../request.constant'; export const API_KEY = '21f7dd457b739d8e583a82c904c09054'; diff --git a/app/constants/components/Weather_View.constant.js b/app/constants/components/Weather_View.constant.js index e3ac1f5..0008c19 100644 --- a/app/constants/components/Weather_View.constant.js +++ b/app/constants/components/Weather_View.constant.js @@ -1,3 +1 @@ -'use strict'; - export const FLAG_IMAGE_URL = 'http://openweathermap.org/images/flags/'; diff --git a/app/constants/events.constant.js b/app/constants/events.constant.js new file mode 100644 index 0000000..5a7b625 --- /dev/null +++ b/app/constants/events.constant.js @@ -0,0 +1,4 @@ +export const PUB_SUB = { + RESIZE: 'resizeEvent', + PAGE_VISIBILITY: 'visibilityChangeEvent' +}; diff --git a/app/constants/request.constant.js b/app/constants/request.constant.js index ad92fa5..9d1dfc4 100644 --- a/app/constants/request.constant.js +++ b/app/constants/request.constant.js @@ -1,6 +1,9 @@ -'use strict'; - export const URL = { WEATHER: 'weather/', - WEATHER_LIST: 'weather_list/' + WEATHER_LIST: 'weather_list/', + LOCATION: 'location/' +}; + +export const ERROR_MESSAGES = { + GEO_LOCATION_UNAVAILABLE: 'Location information is unavailable' }; diff --git a/app/helpers/appGlobal.js b/app/helpers/appGlobal.js index 4e6e4fb..be32084 100644 --- a/app/helpers/appGlobal.js +++ b/app/helpers/appGlobal.js @@ -6,29 +6,15 @@ var app = app || {}; (function () { -'use strict'; - - /** - * Publish/Subscribe for Window Resize Listener - */ - app.resizeEvent = Object.freeze({ - subscribers: [], - subscribe: function(subscriber) { - this.subscribers.push(subscriber); - }, - publish: function(args) { - this.subscribers.forEach(function (subscriber) { - try { - subscriber(args); - } catch(ignore) {} - }) - } - }); - - /** * A function that performs no operations. */ app.noop = Object.freeze(function(){}); + app.locale = + (navigator.languages && navigator.languages[0]) + || navigator.language + || navigator.userLanguage + || 'en'; + }()); diff --git a/app/helpers/domReady.js b/app/helpers/domReady.js index ec7ffc7..2618c83 100644 --- a/app/helpers/domReady.js +++ b/app/helpers/domReady.js @@ -3,35 +3,71 @@ * Listen when the DOM is fully loaded and have window event listeners in one place. */ -(function ($) { -'use strict'; +import PublishSubscribe from 'publish-subscribe-js'; +import jQuery from 'jquery'; +import { PUB_SUB } from './../constants/events.constant'; - $(document).ready(function() { - var toTopBtn = $('#toTop'); +export default function DOMReady() { + jQuery(document).ready(function() { + const toTopBtn = jQuery('#toTop'); + // Window Visibilitychange Listener + initPageVisibilityAPI(); + // Window Scroll Listener - $(window).scroll(function() { + jQuery(window).scroll(function() { showHideToTopBtn(); }); // Window Resize Listener - $(window).resize(function() { - window.app.resizeEvent.publish(); + jQuery(window).resize(function() { + PublishSubscribe.publish(PUB_SUB.RESIZE, {width: window.innerWidth, height: window.innerHeight}); }); toTopBtn.click(function () { - $('html, body').animate({scrollTop:0}, 'slow'); + jQuery('html, body').animate({scrollTop:0}, 'slow'); }); function showHideToTopBtn() { - if ($(window).scrollTop() < 200) { + if (jQuery(window).scrollTop() < 200) { toTopBtn.fadeOut(); - } else if ($(window).scrollTop() > 200) { + } else if (jQuery(window).scrollTop() > 200) { toTopBtn.fadeIn(); } } - }); -}(jQuery)); + function initPageVisibilityAPI() { + let hidden, visibilityChange; + + // Opera 12.10 and Firefox 18 and later support + if (typeof document.hidden !== 'undefined') { + hidden = 'hidden'; + visibilityChange = 'visibilitychange'; + } else if (typeof document.msHidden !== 'undefined') { + hidden = 'msHidden'; + visibilityChange = 'msvisibilitychange'; + } else if (typeof document.webkitHidden !== 'undefined') { + hidden = 'webkitHidden'; + visibilityChange = 'webkitvisibilitychange'; + } + + // Warn if the browser doesn't support addEventListener or the Page Visibility API + if (typeof document.addEventListener === 'undefined' || typeof document.hidden === 'undefined') { + alert('This demo requires a browser, such as Google Chrome or Firefox, that supports the Page Visibility API.'); + } else { + document.addEventListener(visibilityChange, _handleVisibilityChange, false); + } + + function _handleVisibilityChange(event) { + if (document[hidden]) { + PublishSubscribe.publish(PUB_SUB.PAGE_VISIBILITY, {event}); + } else { + // page is visible you may continue doing what was stopped + PublishSubscribe.publish(PUB_SUB.PAGE_VISIBILITY, {event, action: 'continue'}); + } + } + } + }); +} diff --git a/app/helpers/geolocation.js b/app/helpers/geolocation.js new file mode 100644 index 0000000..0ddedb3 --- /dev/null +++ b/app/helpers/geolocation.js @@ -0,0 +1,52 @@ +import _find from 'lodash/find'; +import _includes from 'lodash/includes'; + +/** + * @return {Promise} + */ +export const getLocation = () => { + return new Promise((resolve, reject) => { + if (navigator.geolocation) { + return void navigator.geolocation.getCurrentPosition((position) => { + resolve({ + latitude: position.coords.latitude, + longitude: position.coords.longitude + }); + }, (error) => { + switch(error.code) { + case error.PERMISSION_DENIED: + return reject({message: 'User denied the request for Geolocation'}); + case error.POSITION_UNAVAILABLE: + return reject({message: 'Location information is unavailable'}); + case error.TIMEOUT: + return reject({message: 'The request to get user location timed out'}); + case error.UNKNOWN_ERROR: + return reject({message: 'An unknown error occurred'}); + } + }); + } + + reject({message: 'Geolocation is not supported by this browser'}); + }); +}; + +/** + * @param {Object} location + * @return {String} + */ +export const getCityNameFromGeocode = (location) => { + const address = _find((location.results[0] && location.results[0].address_components) || [], (address) => { + return _includes(address.types, 'locality'); + }); + + if (address) { + switch (address.long_name) { + case 'Kyiv': + return 'Kiev'; + default: + return address.long_name; + } + } + + return 'Kiev'; +}; diff --git a/app/helpers/interfaces.js b/app/helpers/interfaces.js index 02d345f..9278ef5 100644 --- a/app/helpers/interfaces.js +++ b/app/helpers/interfaces.js @@ -3,10 +3,17 @@ * Interfaces that describes component state object */ -'use strict'; - export const Open_Weather_Interface = { cityName: '', isCityList: false, preload: false }; + +export const Resize_SubPub_Action_Interface = { + width: 0, + height: 0 +}; + +export const Show_Page_Visibility_API_Interface = { + amountOfNewUsers: 0 +}; diff --git a/app/helpers/uiActions.js b/app/helpers/uiActions.js index 7e776a5..50e6aff 100644 --- a/app/helpers/uiActions.js +++ b/app/helpers/uiActions.js @@ -1,5 +1,3 @@ -'use strict'; - import jQuery from 'jquery'; /** diff --git a/app/helpers/validation.js b/app/helpers/validation.js index 2d86213..e3f35b8 100644 --- a/app/helpers/validation.js +++ b/app/helpers/validation.js @@ -3,12 +3,10 @@ * Validation helpers */ -'use strict'; - /** * @param {String} email */ export const isEmailValid = email => { - const reqExp = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i; + const reqExp = /[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,3}$/; return reqExp.test(email); }; diff --git a/app/i18n/en/messages.js b/app/i18n/en/messages.js new file mode 100644 index 0000000..6614c7f --- /dev/null +++ b/app/i18n/en/messages.js @@ -0,0 +1,14 @@ +export default { + // PAGES + 'pages.home': 'Home', + 'pages.about': 'About', + 'pages.resize_subpub': 'Resize SubPub', + 'pages.page_visibility_api': 'Page Visibility API', + 'pages.components_communication': 'Components Communication', + 'pages.csv_check': 'CSV Validator', + + // COMPONENTS + 'dumb.amount_of_new_users': 'Amount of new users: {amountOfUsers}', + 'dumb.display_width': 'Width: {width}', + 'dumb.display_height': 'Height: {height}' +}; diff --git a/app/i18n/es/messages.js b/app/i18n/es/messages.js new file mode 100644 index 0000000..6305f4a --- /dev/null +++ b/app/i18n/es/messages.js @@ -0,0 +1,14 @@ +export default { + // PAGES + 'pages.home': 'Casa', + 'pages.about': 'Acerca de', + 'pages.resize_subpub': 'Cambiar el tamaño SubPub', + 'pages.page_visibility_api': 'API de visibilidad de la página', + 'pages.components_communication': 'Comunicación de componentes', + 'pages.csv_check': 'CSV Validator', + + // COMPONENTS + 'dumb.amount_of_new_users': 'Amount of new users: {amountOfUsers}', + 'dumb.display_width': 'Anchura: {width}', + 'dumb.display_height': 'Altura: {height}' +}; diff --git a/app/i18n/fr/messages.js b/app/i18n/fr/messages.js new file mode 100644 index 0000000..0206360 --- /dev/null +++ b/app/i18n/fr/messages.js @@ -0,0 +1,14 @@ +export default { + // PAGES + 'pages.home': 'Maison', + 'pages.about': 'Sur', + 'pages.resize_subpub': 'Redimensionner SubPub', + 'pages.page_visibility_api': 'L’API Page Visibility', + 'pages.components_communication': 'Composants Communication', + 'pages.csv_check': 'CSV Validator', + + // COMPONENTS + 'dumb.amount_of_new_users': 'Amount of new users: {amountOfUsers}', + 'dumb.display_width': 'Largeur: {width}', + 'dumb.display_height': 'La taille: {height}' +}; diff --git a/app/i18n/messages.js b/app/i18n/messages.js new file mode 100644 index 0000000..affcf25 --- /dev/null +++ b/app/i18n/messages.js @@ -0,0 +1,9 @@ +import en from './en/messages'; +import es from './es/messages'; +import fr from './fr/messages'; + +export default { + 'en': en, + 'es': es, + 'fr': fr +}; diff --git a/app/index.html b/app/index.html index d964d39..4063c1b 100644 --- a/app/index.html +++ b/app/index.html @@ -3,6 +3,7 @@ Create React + Redux App Structure + @@ -17,6 +18,8 @@
    + + diff --git a/app/pages/About/About.jsx b/app/pages/About/About.jsx index 7a54596..3854fe1 100644 --- a/app/pages/About/About.jsx +++ b/app/pages/About/About.jsx @@ -1,31 +1,37 @@ -'use strict'; - -import React, { Component } from 'react'; +import React from 'react'; +import { FormattedMessage } from 'react-intl'; import Open_Weather_Search from './../../components/Open_Weather_Search/Open_Weather_Search'; import Weather_View from './../../components/dumb/Weather_View'; -import Alert from './../../services/Alert'; +import CustomEvent from "custom-event-js"; -export default class About extends Component { +export default class About extends React.Component { constructor() { super(); } + componentDidMount() { + CustomEvent.DISPATCH('WEB_COMP_SHOW_NOTIFICATION', { + type: 'warning', + message: 'Hello again! ;) \n I\'m notification-service based on Custom Element' + }); + } + render() { - let alertStore = this.props.alert; let weatherStore = this.props.weather; let weatherListStore = this.props.weatherCities; - return
    - -

    About

    + return ( +
    - +

    - +

    You are not able to remove weather from the list on this page :)

    - + -
    + +
    + ); } } diff --git a/app/pages/App.jsx b/app/pages/App.jsx index 2194226..bf06479 100644 --- a/app/pages/App.jsx +++ b/app/pages/App.jsx @@ -1,37 +1,86 @@ -'use strict'; - -import React, { Component } from 'react'; +import React from 'react'; import { Route, Link, withRouter } from 'react-router-dom'; import { connect } from 'react-redux'; +import { FormattedMessage } from 'react-intl'; +import store from './../store'; +import 'notification-service-js'; +import CustomEvent from 'custom-event-js'; + +import noInternet from 'no-internet'; +import isEmpty from 'lodash/isEmpty'; + +import { pushNotification, removeNotification } from './../actions/notification'; +// Services +import Alert from './../services/Alert'; +import Notification from './../services/Notification'; + +// Pages import Home from './Home/Home'; import About from './About/About'; +import Resize_SubPub from './Resize_SubPub/Resize_SubPub'; +import Page_Visibility_API from './Page_Visibility_API/Page_Visibility_API'; +import Components_Communication from './Components_Communication/Components_Communication'; +import CSV_Check from './CSV_Check/CSV_Check'; function mapStateToProps(store, props) { return { alert: store.alert, weather: store.weather, - weatherCities: store.weatherCities - } + weatherCities: store.weatherCities, + notification: store.notification + }; } -class App extends Component { +class App extends React.Component { constructor() { super(); } + componentDidMount() { + noInternet({callback: (offline) => { + if (offline && isEmpty(this.props.notification.message)) { + store.dispatch(pushNotification('You are offline 😎')); + } else if (!offline && !isEmpty(this.props.notification.message)) { + store.dispatch(removeNotification()); + } + }}); + + CustomEvent.DISPATCH('WEB_COMP_SHOW_NOTIFICATION', { + type: 'success', + message: 'Welcome! I\'m notification-service based on Custom Element' + }); + } + render() { - return
    + let alertStore = this.props.alert; + let notificationStore = this.props.notification; + + return ( +
    + +
      +
    • +
    • +
    • +
    • +
    • +
    • +
    -
      -
    • Home
    • -
    • About
    • -
    + } /> + } /> + } /> + + + + - } /> - } /> -
    + + +
    + ); } } diff --git a/app/pages/CSV_Check/CSV_Check.jsx b/app/pages/CSV_Check/CSV_Check.jsx new file mode 100644 index 0000000..25dbc12 --- /dev/null +++ b/app/pages/CSV_Check/CSV_Check.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import CSV_Validator from '../../components/CSV_Validator/CSV_Validator'; + +export default class CSV_Check extends React.Component { + constructor() { + super(); + } + + render() { + return ( +
    +

    + + +
    + ); + } +} diff --git a/app/pages/Components_Communication/Components_Communication.jsx b/app/pages/Components_Communication/Components_Communication.jsx new file mode 100644 index 0000000..f2ed5a7 --- /dev/null +++ b/app/pages/Components_Communication/Components_Communication.jsx @@ -0,0 +1,70 @@ +import React from 'react'; +import jQuery from 'jquery'; +import { FormattedMessage } from 'react-intl'; +import { oneWay, twoWay, oneTime, disconnect, disconnectAll } from 'bind-dom'; + +export default class Components_Communication extends React.Component { + componentDidMount() { + oneWay('inputObserver', document.querySelector('#observer_1'), document.querySelector('#observer_2')); + oneTime('inputObserver_2', document.querySelector('#observer_3'), document.querySelector('#observer_4')); + twoWay('inputObserver_3', document.querySelector('#observer_5'), document.querySelector('#observer_6')); + + setTimeout(() => { + document.querySelector('#observer_3').innerHTML = 'I\'m here :)'; + }, 5000); + } + + componentWillUnmount() { + disconnect('inputObserver'); + disconnectAll(); + } + + onChange = (event) => { + jQuery('#observer_1').attr('value', event.target.value); + } + + onChangeArea = (event) => { + jQuery(event.target).attr('data-bind-dom', event.target.value.length); + } + + render() { + return ( +
    +

    + +

    Synchronization between two DOM elements (One-Time, One-Way & Two-Way binding) bind-dom

    + +
    +

    OneWay

    +
    +
    Input Observer
    + +
    +
    +
    Apply To
    + +
    +
    + +
    + +
    +

    OneTime

    + Loading... + Waiting... +
    + +
    +

    TwoWay

    +
    +