├── .babelrc ├── .coveralls.yml ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmignore ├── .nycrc ├── .stylelintrc ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── appveyor.yml ├── package.json ├── preprocessor.js ├── server ├── api │ └── mocks │ │ ├── forecast.json │ │ ├── logo.json │ │ ├── weatherStation.json │ │ └── weatherStations.json ├── enviroment │ ├── development.js │ ├── index.js │ └── production.js ├── httpServer.js ├── index.js ├── lib │ ├── configureStore.js │ ├── files.js │ ├── renderContainer.js │ └── renderPage.js ├── middleware │ ├── dev-middleware.js │ ├── index.js │ ├── prod-middleware.js │ └── routing-middleware.js ├── routing.js ├── server.js ├── statics │ └── index.js └── templates │ ├── calculator.js │ ├── index.js │ └── main.js ├── src ├── app │ ├── assets │ │ └── images │ │ │ ├── favicon.ico │ │ │ ├── github.svg │ │ │ └── react-base-logo.png │ ├── components │ │ ├── LinkButton │ │ │ ├── index.jsx │ │ │ ├── spec │ │ │ │ └── LinkButton.spec.js │ │ │ └── styles.css │ │ └── Logo │ │ │ ├── index.jsx │ │ │ └── styles.css │ ├── containers │ │ ├── App │ │ │ ├── index.jsx │ │ │ └── spec │ │ │ │ └── App.spec.js │ │ ├── Calculator │ │ │ ├── actionTypes │ │ │ │ └── index.js │ │ │ ├── actions │ │ │ │ ├── index.js │ │ │ │ └── spec │ │ │ │ │ └── Calculator.actions.spec.js │ │ │ ├── components │ │ │ │ ├── Button │ │ │ │ │ ├── index.jsx │ │ │ │ │ ├── spec │ │ │ │ │ │ └── Button.spec.js │ │ │ │ │ └── styles.css │ │ │ │ ├── ButtonPannel │ │ │ │ │ ├── index.jsx │ │ │ │ │ ├── spec │ │ │ │ │ │ └── ButtonPannel.spec.js │ │ │ │ │ └── styles.css │ │ │ │ └── Display │ │ │ │ │ ├── index.jsx │ │ │ │ │ ├── spec │ │ │ │ │ └── Display.spec.js │ │ │ │ │ └── styles.css │ │ │ ├── index.jsx │ │ │ ├── models │ │ │ │ └── index.js │ │ │ ├── reducers │ │ │ │ ├── index.js │ │ │ │ └── spec │ │ │ │ │ └── Calculator.reducer.spec.js │ │ │ ├── spec │ │ │ │ └── Calculator.container.spec.js │ │ │ └── styles.css │ │ ├── Main │ │ │ ├── actionTypes │ │ │ │ └── index.js │ │ │ ├── actions │ │ │ │ ├── index.js │ │ │ │ └── spec │ │ │ │ │ └── Logo.action.spec.js │ │ │ ├── api │ │ │ │ └── index.js │ │ │ ├── index.jsx │ │ │ ├── models │ │ │ │ └── index.js │ │ │ ├── reducers │ │ │ │ ├── index.js │ │ │ │ └── spec │ │ │ │ │ └── Logo.reducers.spec.js │ │ │ ├── spec │ │ │ │ └── Main.spec.js │ │ │ └── styles.css │ │ └── WeatherStations │ │ │ ├── actionTypes │ │ │ └── index.js │ │ │ ├── actions │ │ │ ├── index.js │ │ │ └── spec │ │ │ │ └── WeatherStations.action.spec.js │ │ │ ├── api │ │ │ ├── index.js │ │ │ └── spec │ │ │ │ └── weatherStations.api.spec.js │ │ │ ├── components │ │ │ ├── ForecastDetail │ │ │ │ ├── index.jsx │ │ │ │ ├── spec │ │ │ │ │ └── ForecastDetail.component.spec.js │ │ │ │ └── styles.css │ │ │ ├── ForecastDetailItem │ │ │ │ ├── index.jsx │ │ │ │ ├── spec │ │ │ │ │ └── forecastDetailItem.component.spec.js │ │ │ │ └── styles.css │ │ │ ├── MapBox │ │ │ │ ├── index.jsx │ │ │ │ ├── spec │ │ │ │ │ └── MapBox.component.spec.js │ │ │ │ └── styles.css │ │ │ ├── MapInfoWindow │ │ │ │ ├── index.jsx │ │ │ │ ├── spec │ │ │ │ │ └── MapInfoWindow.component.spec.js │ │ │ │ └── styles.css │ │ │ ├── MapMarker │ │ │ │ ├── index.jsx │ │ │ │ ├── spec │ │ │ │ │ └── MapMarker.component.spec.js │ │ │ │ ├── styles.css │ │ │ │ └── stylesMarker.js │ │ │ └── WeatherStationDetails │ │ │ │ ├── index.jsx │ │ │ │ ├── spec │ │ │ │ └── WeatherStationDetails.component.spec.js │ │ │ │ └── styles.css │ │ │ ├── config.js │ │ │ ├── helpers │ │ │ ├── index.jsx │ │ │ └── spec │ │ │ │ └── WeatherStations.helpers.spec.js │ │ │ ├── index.jsx │ │ │ ├── models │ │ │ ├── ForecastModel.js │ │ │ ├── WeatherStationDetailsModel.js │ │ │ ├── WeatherStationsModel.js │ │ │ └── index.js │ │ │ ├── reducers │ │ │ ├── index.js │ │ │ └── spec │ │ │ │ └── WeatherStations.reducer.spec.js │ │ │ ├── spec │ │ │ └── WeatherStations.container.spec.js │ │ │ └── styles.css │ └── styles │ │ ├── base.css │ │ └── index.js └── base │ ├── client │ └── index.js │ ├── conf │ └── .react-base │ │ ├── logo.js │ │ ├── logo.txt │ │ └── templates │ │ └── generator-react-base │ │ ├── generators │ │ ├── index.js │ │ └── templates │ │ │ ├── _action.js │ │ │ ├── _action.spec.js │ │ │ ├── _actionTypes.js │ │ │ ├── _api.js │ │ │ ├── _component.js │ │ │ ├── _component.spec.js │ │ │ ├── _container.js │ │ │ ├── _container.spec.js │ │ │ ├── _models.js │ │ │ ├── _reducer.js │ │ │ ├── _reducer.spec.js │ │ │ └── _styles.css │ │ └── package.json │ ├── index.js │ ├── models │ └── index.js │ ├── reducers │ └── index.js │ ├── routes │ └── index.js │ ├── shared │ ├── Context.js │ ├── CreateActionType.js │ ├── CreateReducer.js │ ├── Env.js │ ├── Errors.js │ ├── FetchData.js │ ├── FileSystem.js │ ├── GetActionPrefix.js │ ├── ModelHelper.js │ ├── Regenerate.js │ ├── ResolveRequestAction.js │ ├── TestSetup.js │ ├── console.js │ ├── index.js │ ├── regenerators │ │ ├── RegenerateModelIndex.js │ │ ├── RegenerateReducerIndex.js │ │ ├── RegenerateRoutes.js │ │ └── spec │ │ │ ├── RegenerateModelIndex.spec.js │ │ │ └── RegenerateReducerIndex.spec.js │ └── spec │ │ ├── Errors.spec.js │ │ ├── FetchData.shared.spec.js │ │ ├── FileSystem.shared.spec.js │ │ ├── ModelHelper.shared.spec.js │ │ └── TypeHelper.shared.spec.js │ ├── store │ ├── ConfigureStore.js │ ├── GlobalState.js │ ├── InitialState.js │ └── spec │ │ └── ConfigureStore.store.spec.js │ ├── types │ └── FetchTypes.js │ └── wp-plugins │ ├── compileInfoPlugin.js │ ├── fileHashPlugin.js │ └── index.js ├── webpack ├── index.babel.js ├── webpack.common.config.js ├── webpack.dev.config.js ├── webpack.dll.config.babel.js └── webpack.prod.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-0"], 3 | "plugins": ["transform-react-jsx","transform-object-rest-spread","transform-class-properties"], 4 | "env": { 5 | "development": { 6 | "plugins": [ 7 | [ "babel-plugin-webpack-alias", { "config": "./webpack/index.babel.js" } ] 8 | ] 9 | }, 10 | "production": { 11 | "plugins": [ 12 | [ "babel-plugin-webpack-alias", { "config": "./webpack/index.babel.js" } ] 13 | ] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: 7323SLwgvPvYa3An9lKsN7gD3WYdwtFQA 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | 17 | [*.md] 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": [ 4 | "react" 5 | ], 6 | "extends": ["airbnb-base", "eslint:recommended", "plugin:react/recommended"], 7 | "globals": { 8 | "__DEV__": true, 9 | "describe" : false, 10 | "it" : false 11 | }, 12 | "env": { 13 | "browser": true, 14 | "node": true 15 | }, 16 | "parserOptions": { 17 | "ecmaVersion": 6 18 | }, 19 | "rules": { 20 | "react/require-extension": "off", 21 | "react/jsx-filename-extension": 0, 22 | "react/jsx-pascal-case": 2, 23 | "react/prefer-stateless-function": 0, 24 | "react/prefer-es6-class": 2, 25 | "react/no-multi-comp": 2, 26 | "react/jsx-boolean-value": [2, "always"], 27 | "react/require-render-return": 2, 28 | "react/no-is-mounted": 2, 29 | "react/jsx-wrap-multilines": 1, 30 | "react/jsx-no-bind": 0, 31 | "react/forbid-prop-types": 0, 32 | "jsx-quotes": [2, "prefer-double"], 33 | "react/jsx-curly-spacing": [2, "always"], 34 | "import/default": 0, 35 | "import/no-duplicates": 0, 36 | "import/no-extraneous-dependencies": 0, 37 | "import/extensions": 0, 38 | "import/named": 0, 39 | "import/namespace": 0, 40 | "import/no-unresolved": 0, 41 | "import/no-named-as-default": 0, 42 | "import/prefer-default-export": 0, 43 | "comma-dangle": 0, 44 | "indent": [2, 2, {"SwitchCase": 1}], 45 | "no-console": 0, 46 | "no-alert": 0, 47 | "semi": 2, 48 | "no-var": 2, 49 | "space-before-blocks": 2, 50 | "keyword-spacing": 2, 51 | "no-global-assign": 0, 52 | "no-unsafe-negation": 0, 53 | "space-in-parens": ["error", "never"], 54 | "eol-last": 0, 55 | "no-trailing-spaces": 0, 56 | "padded-blocks": 0, 57 | "arrow-parens": 0, 58 | "linebreak-style": 0, 59 | "no-use-before-define": 0 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .idea 3 | coverage/ 4 | dist/ 5 | build/ 6 | *.vimrc 7 | .DS_STORE 8 | node_modules/ 9 | bower_components/ 10 | .nyc_output/ 11 | .vscode/ 12 | package-lock.json 13 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | spec 3 | coverage 4 | examples 5 | .babelrc 6 | .eslintrc.js 7 | .nyc_output 8 | .travis.yml 9 | .appveyor.yml 10 | .nycrc 11 | mocha.opts 12 | tsconfig.json 13 | tslint.json 14 | *.swp 15 | *.log 16 | .coveralls.yml 17 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "webpack/**", 4 | "**/*.spec.js", 5 | "src/base/wp-plugins/**" 6 | ], 7 | "extension": [ 8 | ".jsx", 9 | ".es6", 10 | "js" 11 | ] 12 | } -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "block-no-empty": null, 4 | "color-no-invalid-hex": true, 5 | "declaration-colon-space-after": "always", 6 | "indentation": [2, { 7 | "except": ["value"] 8 | }], 9 | "max-empty-lines": 2, 10 | "unit-whitelist": ["em", "rem", "%", "s", "px"] 11 | } 12 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6.2.0" 4 | before_script: 5 | - npm install 6 | env: 7 | - NODE_ENV=development 8 | script: 9 | - npm run build:prod 10 | - npm run generate:dll 11 | - npm run regenerate 12 | - npm run test:coverage 13 | - npm run coveralls 14 | - npm run lint:all 15 | notifications: 16 | slack: 17 | on_pull_requests: false 18 | rooms: 19 | - cejs-atsistemas:XxetFLj7MuJALpcVhx2PKEE6#react-base 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 1.0.1 / 2017-06-05 2 | ================== 3 | 4 | * Add Webpack2 support 5 | * Add Changelog.md 6 | * Updated to React 15.5.4 7 | * Updated React Test Addons Utils 8 | * Updated webpack assets file 9 | * Updated contributors in package.json -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to React-Base 2 | 3 | Love [React-Base](https://html5boilerplate.com) and want to get involved? 4 | Awesome! There are plenty of ways you can help! 5 | 6 | Please take a moment to review this document in order to make the contribution 7 | process easy and effective for everyone involved. 8 | 9 | Following these guidelines helps to communicate that you respect the time of 10 | the developers managing and developing this open source project. In return, 11 | they should reciprocate that respect in addressing your issue or assessing 12 | patches and features. 13 | 14 | 15 | ## Using the issue tracker 16 | 17 | The [issue tracker](https://github.com/atsistemas/react-base/issues) is 18 | the preferred channel for [bug reports](#bugs), [features requests](#features) 19 | and [submitting pull requests](#pull-requests), but please respect the following restrictions: 20 | 21 | * Please **do not** use the issue tracker for personal support requests. 22 | 23 | * Please **do not** derail or troll issues. Keep the discussion on topic and 24 | respect the opinions of others. 25 | 26 | * Please **do not** open issues or pull requests regarding the code in React-Base third party modules. 27 | 28 | 29 | 30 | ## Bug reports 31 | 32 | A bug is a _demonstrable problem_ that is caused by the code in the repository. 33 | Good bug reports are extremely helpful - thank you! 34 | 35 | Guidelines for bug reports: 36 | 37 | 1. **Use the GitHub issue search** — check if the issue has already been 38 | reported. 39 | 40 | 2. **Check if the issue has been fixed** — try to reproduce it using the 41 | latest `master` branch in the repository. 42 | 43 | 3. **Isolate the problem** — ideally create a [reduced test 44 | case](https://css-tricks.com/reduced-test-cases/) and a live example. 45 | 46 | A good bug report shouldn't leave others needing to chase you up for more 47 | information. Please try to be as detailed as possible in your report. What is 48 | your environment? What steps will reproduce the issue? What browser(s) and OS 49 | experience the problem? What would you expect to be the outcome? All these 50 | details will help people to fix any potential bugs. 51 | 52 | Example: 53 | 54 | > Short and descriptive example bug report title 55 | > 56 | > A summary of the issue and the browser/OS environment in which it occurs. If 57 | > suitable, include the steps required to reproduce the bug. 58 | > 59 | > 1. This is the first step 60 | > 2. This is the second step 61 | > 3. Further steps, etc. 62 | > 63 | > `` - a link to the reduced test case 64 | > 65 | > Any other information you want to share that is relevant to the issue being 66 | > reported. This might include the lines of code that you have identified as 67 | > causing the bug, and potential solutions (and your opinions on their 68 | > merits). 69 | 70 | 71 | 72 | ## Feature requests 73 | 74 | Feature requests are welcome. But take a moment to find out whether your idea 75 | fits with the scope and aims of the project. It's up to *you* to make a strong 76 | case to convince the project's developers of the merits of this feature. Please 77 | provide as much detail and context as possible. 78 | 79 | 80 | 81 | ## Pull requests 82 | 83 | Good pull requests - patches, improvements, new features - are a fantastic 84 | help. They should remain focused in scope and avoid containing unrelated 85 | commits. 86 | 87 | **Please ask first** before embarking on any significant pull request (e.g. 88 | implementing features, refactoring code, porting to a different language), 89 | otherwise you risk spending a lot of time working on something that the 90 | project's developers might not want to merge into the project. 91 | 92 | Please adhere to the coding conventions used throughout a project (indentation, 93 | accurate comments, etc.) and any other requirements (such as test coverage). 94 | 95 | Adhering to the following process is the best way to get your work 96 | included in the project: 97 | 98 | 1. [Fork](https://help.github.com/articles/fork-a-repo/) the project, clone your 99 | fork, and configure the remotes: 100 | 101 | ```bash 102 | # Clone your fork of the repo into the current directory 103 | git clone https://github.com//react-base.git 104 | # Navigate to the newly cloned directory 105 | cd react-base 106 | # Assign the original repo to a remote called "upstream" 107 | git remote add upstream https://github.com/atsistemas/react-base.git 108 | ``` 109 | 110 | 2. If you cloned a while ago, get the latest changes from upstream: 111 | 112 | ```bash 113 | git checkout master 114 | git pull upstream master 115 | ``` 116 | 117 | 3. Create a new topic branch (off the main project development branch) to 118 | contain your feature, change, or fix: 119 | 120 | ```bash 121 | git checkout -b [feat|fix]/ 122 | ``` 123 | Using *feat* for new features, or *fix* for bug fixing. 124 | 125 | 4. Commit your changes in logical chunks. Please adhere to these [git commit 126 | message guidelines](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 127 | or your code is unlikely be merged into the main project. Use Git's 128 | [interactive rebase](https://help.github.com/articles/about-git-rebase/) 129 | feature to tidy up your commits before making them public. 130 | 131 | 5. Locally merge (or rebase) the upstream development branch into your topic branch: 132 | 133 | ```bash 134 | git pull [--rebase] upstream master 135 | ``` 136 | 137 | 6. Push your topic branch up to your fork: 138 | 139 | ```bash 140 | git push origin [feat|fix]/ 141 | ``` 142 | 143 | 7. [Open a Pull Request](https://help.github.com/articles/using-pull-requests/) 144 | with a clear title and description. 145 | 146 | **IMPORTANT**: By submitting a patch, you agree to allow the project 147 | owners to license your work under the terms of the [MIT License](LICENSE). 148 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Ce Js @ atSistemas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |  2 | 3 | # React-Base 4 | 5 | 6 |  7 |  8 | [](https://coveralls.io/github/atSistemas/react-base?branch=master) 9 |  10 |  11 | 12 | **A modular platform for Redux Isomorphic applications** 13 | 14 | This repository is a modular abstraction to build a [ReactJS](https://facebook.github.io/react/) web application based on [Redux](http://redux.js.org/) paradigm. 15 | You can use it to quickly scaffold your React web application projects and development environments for these projects. 16 | 17 | This seed should clarify how to wire up all the modules of your application, even when we understand that in some cases 18 | there must be some changes needed by the structure to fit your needs correctly 19 | 20 | ## Overview 21 | 22 | **React-Base** makes use of the latest tools to improve your workflow, and enables you to create future ready applications: 23 | 24 | - [Redux](http://redux.js.org/) based architecture 25 | - Isomorphic / Universal Javascript Apps 26 | - [Webpack 3](https://webpack.github.io/) build configuration depending on enviroment 27 | - Immutable data modeling using [ImmutableJS](https://facebook.github.io/immutable-js/) 28 | - Store middleware to handle request actions. 29 | - Development & Production server using [express](https://github.com/expressjs/express) and [webpack-dev-server](https://webpack.github.io/) 30 | - Hot Reload/Live Reload support for Js & Css using [Webpack HMR](https://webpack.github.io/docs/hot-module-replacement.html) 31 | - Container and component generators using [Yeoman](https://github.com/yeoman/yo) 32 | - JSX and ES6 transpilation using [Babel](https://babeljs.io/) 33 | - [Mocha](https://mochajs.org/) as testing framework 34 | - [Enzyme/JsDom](https://github.com/airbnb/enzyme) for unit/ui testing 35 | - [Nyc](https://github.com/bcoe/nyc) for code coverage 36 | - [PostCSS](http://postcss.org/) processing with isomorphic support. 37 | - [CssModules](https://github.com/css-modules/css-modules) based 38 | - Code Linting using [Eslint](https://github.com/eslint/eslint) 39 | - Css Linting using [CssLint](https://github.com/stylelint/stylelint) 40 | - [Airbnb](https://github.com/airbnb/javascript/tree/master/react) React Style Guide 41 | 42 | 43 | ## Getting Started 44 | 45 | To get you started, you need to meet the prerequisites, and then follow the installation instructions. 46 | 47 | ### Prerequisites 48 | 49 | React-Base makes use a number of NodeJS tools to initialize and test React-Base. You must have node.js 6.2.0 at least, and its package manager (npm) installed. You can get it from [nodejs.org](node). 50 | 51 | ### Installing 52 | 53 | You can clone our Git repository: 54 | 55 | `$ git clone https://github.com/atSistemas/react-base.git` 56 | 57 | This method requires Git to be installed on your computer. You can get it from 58 | [here](http://git-scm.com). 59 | 60 | ### Wiring up your development environment 61 | 62 | Setting up **React-Base** is as easy as running: 63 | 64 | `$ npm install` 65 | 66 | This command will install all the required dependencies and start your development server, which takes care of all the changes you make to your code and runs all the awesome stuff that ends with your code automagically transpiled and running on your browser. 67 | 68 | Please note that `npm install` is only required on your first start, or in case of updated dependencies. 69 | 70 | 71 | ### Initializing development server 72 | 73 | Once all the dependencies are installed, you can run `$ npm run start` to initialize your development server using [webpack-dev-server](https://webpack.github.io/) express middleware. 74 | 75 | The dev server uses [HMR](https://webpack.github.io/docs/hot-module-replacement.html) (Hot module replacement) that injects updated modules into the bundle in runtime. It's like LiveReload 76 | 77 | 78 | ## Architecture 79 | 80 | React-base is based on [Redux](http://redux.js.org/) paradigm so you can find all the typical entities of an Redux project like [reducers](http://redux.js.org/docs/basics/Reducers.html) , [store](http://redux.js.org/docs/basics/Store.html), [actions](http://redux.js.org/docs/basics/Actions.html) , etc. 81 | 82 | There are four main folders: 83 | 84 | * `server` contains React-Base development & production server based in express with Universal/Isomorphic support and custom middlewares like Gzip. 85 | 86 | ```javascript 87 | server 88 | api/ //Api mocks 89 | lib/ //Universal rendering files 90 | middleware/ //enviroment middleware 91 | statics/ //definition of statics path 92 | templates/ //universal templates 93 | server //Server 94 | routing //Routing middleware 95 | ``` 96 | 97 | * `webpack` contains React-Base Webpack configuration separated by enviroment that allows to use different plugins and loaders in each target enviroment. 98 | 99 | ```javascript 100 | webpack 101 | webpack.common.config/ //Common config 102 | webpack.dev.config/ //Development config 103 | webpack.prod.config/ //Production config 104 | webpack.test.config/ //Testing config 105 | webpack.dll.config/ //Dll config 106 | ``` 107 | 108 | * `src/base/` contains React-Base platform bootstrapping code. 109 | 110 | ```javascript 111 | base 112 | client/ //client bootstrap 113 | conf/ //Configuration files and Yeoman templates 114 | middleware/ //Redux Store middleware 115 | components/ //base components 116 | models/ //model index 117 | reducers/ //reducer index 118 | routes/ //routes index 119 | shared/ // shared base folder 120 | regenerators/ //index regenerators 121 | CreateActionType //Custom action type creator 122 | CreateReducer //Custom reducer creator 123 | ENV //Env handler 124 | Errors //Errors handler 125 | FetchData //Isomorfic data handler 126 | FileSystem //Filesystem manager 127 | JsDomSetup //JsDom Configuration FileSystem 128 | ModelHelper //Inmutable deserializators 129 | Regenerate // Regenerate indexes 130 | store/ //Store configuration and AppState definition 131 | types/ //Action request Types 132 | wp-plugins/ //Custom webpack plugins 133 | ... 134 | ``` 135 | 136 | * `src/app/` is the place where to put your application source code. 137 | 138 | React-Base uses a "featured based" distribution, so all the necessary code for each page/features is located in its own folder inside containers folder as in `src/app/containers/myContainer` 139 | 140 | A container is a React component who contains other components, Redux entities, functions and store subscriptions. Each container is self-contained and represents a feature like "clients" or "products" and it contains all the necessary stuff. 141 | ```javascript 142 | app/ 143 | containers/ 144 | myContainer/ 145 | api/ //api calls 146 | actionTypes/ //action types definition 147 | actions/ //action creators 148 | components/ //container components 149 | models/ //containers models using immutable 150 | reducers/ //container reducers 151 | index.ts //container component 152 | ... 153 | ``` 154 | 155 | ## Action Types 156 | ActionTypes it's a representation using constants of your possible actions: 157 | 158 | ```javascript 159 | import { createActionType } from 'base'; 160 | 161 | export const ActionTypes = createActionType([ 162 | 'CLICK', 163 | 'MAIN_CONTAINER', 164 | 'MAIN_ERROR', 165 | 'MAIN_REQUEST', 166 | 'MAIN_SUCCESS', 167 | 'LAZY_CONTAINER', 168 | 'LOGIN', 169 | ]); 170 | 171 | ``` 172 | 173 | ## Actions 174 | Actions are payloads of information witch represent that something happend in your application and send data from your application to your store: 175 | 176 | ```javascript 177 | clickHandler(id) { 178 | return { 179 | type: ActionTypes.USER_CLICK, 180 | payload: { 181 | id: id 182 | } 183 | }; 184 | } 185 | 186 | ``` 187 | 188 | React-Base include a Redux Store middleware to handle actions with service calls more easyly. You can define in the api folder of your container, an api call based in a fetch call: 189 | 190 | ```javascript 191 | 192 | fetchUsers() { 193 | return fetch(url) 194 | .then(req => req.json()) 195 | .then(data => data) 196 | .catch(err => err) 197 | }, 198 | 199 | ``` 200 | 201 | Then, in your action you can attach this service call in your action using the request param: 202 | 203 | ```javascript 204 | export function getPosts() { 205 | return { 206 | type: ActionTypes.USERS_REQUEST, 207 | request: api.fetchUsers() 208 | }; 209 | } 210 | ``` 211 | 212 | The request middleware will resolve the request param 213 | and dispatch a new action with "ACTION_SUCCESS" or "ACTION_ERROR" with the response of the request in the payload. 214 | 215 | ## Reducers 216 | Reducers describe how the state of your application changes in response to a new Action. React-Base uses a custom CreateReducer that allows to use separated reducers functions instead of "switch based" reducers. 217 | 218 | ```javascript 219 | import { createReducer } from 'base'; 220 | 221 | const click = (state, action) => { 222 | return state.update('mainData', (value) => action.payload); 223 | }; 224 | 225 | const request = (state, action) => { 226 | return state; 227 | }; 228 | 229 | const actionHandlers = { 230 | [ActionTypes.CLICK]: click, 231 | [ActionTypes.LOGIN]: login, 232 | [ActionTypes.MAIN_REQUEST]: request, 233 | [ActionTypes.MAIN_SUCCESS]: success, 234 | }; 235 | 236 | export default CreateReducer(actionHandlers, new MainModel()); 237 | 238 | ``` 239 | 240 | ## Models 241 | Represents your model data using ImmutableJS Data Types and sets its initial state using setInitialState() function. 242 | 243 | 244 | ```javascript 245 | import { Record } from 'immutable'; 246 | 247 | const MainModel = new Record({ 248 | display:0, 249 | operator:'', 250 | operation:'', 251 | prevValue: 0, 252 | nextValue: 0, 253 | newValue: false, 254 | resetDisplay: false, 255 | }); 256 | 257 | function setInitialState(initialState) { 258 | return initialState.Maiin = new MainModel(); 259 | } 260 | 261 | export { MainModel, setInitialState }; 262 | 263 | 264 | ``` 265 | ### Generating a new container 266 | 267 | React-base uses Yeoman to generate new application containers or components. 268 | 269 | Fist of all you need to link yo: 270 | 271 | `$ npm run yo` 272 | 273 | Then, you can generate a new container run: 274 | 275 | `$ npm run generate:container` 276 | 277 | You'll be asked to provide a name for your container. After that, React-base will create all the necessary folder and file template structures you, and will rebuild the file indexes (routes, reducers, models, etc), so you don't have to worry about including all the required imports. 278 | 279 | After that, you can access to your container from http://localhost:8000/myContainer 280 | 281 | ### Regenerating indexes 282 | 283 | You can rebuild the file indexes (reducers, models and routes) running `$ npm run regenerate` 284 | 285 | ### Generating a new component 286 | 287 | As with containers, React-base can automate components creation for you. To create a new component, just type: 288 | 289 | `$ npm run generate:component` 290 | 291 | Same as before, you will be asked for a component name, and after that React-base will do the rest, placing a component template under `app/components`, and rebuilding all the indexes. 292 | 293 | ## Distribution 294 | 295 | You can generate a complete distribution source ready for production enviroments. 296 | 297 | ### Building your production application 298 | 299 | `$ npm run build:prod` will create a minified version for your application, ready for production. 300 | 301 | ### Running production server 302 | 303 | `$ npm run start:prod` will run production enviroment of your application serving content from dist directory. 304 | 305 | 306 | ## Testing your application 307 | 308 | React base uses - [Enzyme](https://github.com/airbnb/enzyme) a testing utillity created by [Airbnb](https://github.com/airbnb/) for unit testing and Ui testing using [Airbnb](https://github.com/tmpvar/jsdom) so you can run your ui testing without a browser. 309 | 310 | You can write your tests normally using Mocha and Chai for assertions. 311 | 312 | ### Running your tests 313 | 314 | `$ npm run test` will perform your unit testing, or npm test:coverage to run your tests and display a code coverage report. 315 | 316 | ### Generating code coverage 317 | 318 | React base uses [Nyc](https://github.com/bcoe/nyc) for code coverage and you can generate reports in console or icov/html format. 319 | 320 | `$ npm run test` will perform your code coverage, generating an html report located in coverage/ folder. 321 | 322 | ## Contributing 323 | 324 | Anyone and everyone is welcome to contribute, however, if you decide to get involved, please take a moment to review the [guidelines](CONTRIBUTING.md): 325 | 326 | * [Bug reports](CONTRIBUTING.md#bugs) 327 | * [Feature requests](CONTRIBUTING.md#features) 328 | * [Pull requests](CONTRIBUTING.md#pull-requests) 329 | 330 | ## License 331 | 332 | **React-Base** is available under the [MIT license](LICENSE). 333 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | 2 | environment: 3 | matrix: 4 | - nodejs_version: 8.9.1 5 | 6 | version: "{build}" 7 | build: off 8 | deploy: off 9 | matrix: 10 | fast_finish: true 11 | 12 | install: 13 | - ps: Install-Product node $env:nodejs_version 14 | - npm install 15 | 16 | test_script: 17 | - npm run build:prod 18 | - npm run generate:dll 19 | - npm run test:coverage 20 | - npm run lint:all 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-base-starter", 3 | "version": "1.1.2", 4 | "description": "React Base React/Redux Isomorphic Starter", 5 | "author": "Pablo Magaz", 6 | "contributors": [ 7 | { 8 | "name": "Luis Martinez Marina", 9 | "email": "lmartinez.marina@atsistemas.com" 10 | }, 11 | { 12 | "name": "Santiago Fernandez Blanco", 13 | "email": "sfernandez.blanco@atsistemas.com" 14 | }, 15 | { 16 | "name": "Gastón Messeri", 17 | "email": "gemesseri@atsistemas.com" 18 | }, 19 | { 20 | "name": "Fernando Ramos González", 21 | "email": "framos@atsistemas.com" 22 | } 23 | ], 24 | "main": "client/client.js", 25 | "scripts": { 26 | "logo": "node ./src/base/conf/.react-base/logo.js", 27 | "webpack": "node_modules/webpack/bin/webpack.js", 28 | "clean": "rimraf dist/ && npm run generate:dll", 29 | "clean:files": "rimraf dist/app.js", 30 | "generate": "npm run logo && yo react-base", 31 | "generate:component": "npm run logo && yo react-base --option=2", 32 | "generate:container": "npm run logo && yo react-base --option=1", 33 | "regenerate:modelIndex": "node ./src/base/shared/Regenerate.js --model", 34 | "regenerate:routes": "node ./src/base/shared/Regenerate.js --routes", 35 | "regenerate:reducerIndex": "node ./src/base/shared/Regenerate.js --reducer", 36 | "regenerate": "npm run logo && node ./src/base/shared/Regenerate.js --all", 37 | "generate:dll": "cross-env NODE_ENV=dll webpack --config webpack/webpack.dll.config.babel.js", 38 | "build": "cross-env NODE_ENV=development webpack --config webpack/index.babel.js", 39 | "build:prod": "npm run clean && cross-env NODE_ENV=production webpack --config webpack/index.babel.js", 40 | "lint": "npm run logo && node node_modules/eslint/bin/eslint.js src --ignore-pattern **/*.spec.js --ext .js --ext .jsx", 41 | "lint:css": "npm run logo && stylelint \"src/**/*.css\" --config .stylelintrc", 42 | "lint:all": "npm run lint && npm run lint:css", 43 | "test": "npm run logo && mocha --require babel-core/register --recursive \"./src/**/*.spec.js\" --require ignore-styles \"./src/base/shared/TestSetup.js\"", 44 | "test:coverage": "nyc npm test && nyc report --reporter=lcov", 45 | "start": "npm run logo && cross-env NODE_ENV=development node server/", 46 | "start:prod": "npm run build:prod && cross-env NODE_ENV=production node server/", 47 | "yo": "npm link src/base/conf/.react-base/templates/generator-react-base", 48 | "postYeomanGenerator": "npm run regenerate && npm run start", 49 | "coveralls": "cat ./coverage/lcov.info | ./node_modules/.bin/coveralls", 50 | "postinstall": "npm run generate:dll", 51 | "prepublishOnly": "npm run lint && npm run test:coverage && npm run build" 52 | 53 | }, 54 | "dependencies": { 55 | "babel-core": "6.26.0", 56 | "compression": "1.7.1", 57 | "express": "4.16.2", 58 | "google-map-react": "0.25.0", 59 | "helmet": "3.9.0", 60 | "react": "16.1.1", 61 | "react-dom": "16.1.1", 62 | "react-redux": "5.0.6", 63 | "react-router": "3.2.0", 64 | "recompose": "0.26.0", 65 | "redux": "3.7.2", 66 | "redux-req-middleware": "1.0.4" 67 | }, 68 | "devDependencies": { 69 | "assets-webpack-plugin": "3.5.1", 70 | "babel-cli": "6.26.0", 71 | "babel-eslint": "8.0.2", 72 | "babel-loader": "7.1.2", 73 | "babel-plugin-transform-class-properties": "6.24.1", 74 | "babel-plugin-transform-object-rest-spread": "6.26.0", 75 | "babel-plugin-webpack-alias": "2.1.2", 76 | "babel-preset-es2015": "6.24.1", 77 | "babel-preset-react": "6.24.1", 78 | "babel-preset-react-hmre": "1.1.1", 79 | "babel-preset-stage-0": "6.24.1", 80 | "babel-register": "6.26.0", 81 | "chai": "4.1.2", 82 | "classnames": "2.2.5", 83 | "copy-webpack-plugin": "4.2.1", 84 | "coveralls": "3.0.0", 85 | "create-reduxreducer": "1.0.0", 86 | "cross-env": "5.1.1", 87 | "css-loader": "0.28.7", 88 | "css-modules-require-hook": "4.2.2", 89 | "enzyme": "3.2.0", 90 | "enzyme-adapter-react-16": "1.1.0", 91 | "eslint": "4.11.0", 92 | "eslint-config-airbnb-base": "12.1.0", 93 | "eslint-loader": "1.9.0", 94 | "eslint-plugin-import": "2.8.0", 95 | "eslint-plugin-jsx-a11y": "6.0.2", 96 | "eslint-plugin-react": "7.5.1", 97 | "extract-text-webpack-plugin": "3.0.2", 98 | "generator-webapp": "3.0.1", 99 | "ignore-styles": "5.0.1", 100 | "immutable": "3.8.2", 101 | "isomorphic-fetch": "2.2.1", 102 | "istanbul": "0.4.5", 103 | "jsdom": "11.4.0", 104 | "mocha": "4.0.1", 105 | "nyc": "11.3.0", 106 | "postcss-browser-reporter": "0.5.0", 107 | "postcss-clean": "1.1.0", 108 | "postcss-import": "11.0.0", 109 | "postcss-loader": "2.0.8", 110 | "postcss-mixins": "6.2.0", 111 | "postcss-modules-extract-imports": "1.1.0", 112 | "postcss-nested": "2.1.2", 113 | "postcss-reporter": "5.0.0", 114 | "postcss-url": "7.2.1", 115 | "precss": "2.0.0", 116 | "progress-bar-webpack-plugin": "1.10.0", 117 | "react-addons-linked-state-mixin": "15.6.2", 118 | "react-controllables": "0.6.0", 119 | "react-ink": "6.2.0", 120 | "react-pure-render": "1.0.2", 121 | "react-test-renderer": "16.1.1", 122 | "redux-logger": "3.0.6", 123 | "rimraf": "2.6.2", 124 | "sinon": "4.1.2", 125 | "style-loader": "0.19.0", 126 | "stylelint": "8.2.0", 127 | "supports-color": "5.0.0", 128 | "webpack": "3.8.1", 129 | "webpack-dev-middleware": "1.12.1", 130 | "webpack-dev-server": "2.9.4", 131 | "webpack-hot-middleware": "2.20.0", 132 | "yeoman-option-or-prompt": "1.0.2", 133 | "yo": "1.8.5" 134 | }, 135 | "repository": { 136 | "type": "git", 137 | "url": "https://github.com/atsistemas/react-base" 138 | }, 139 | "bugs": { 140 | "url": "https://github.com/atsistemas/react-base/issues" 141 | }, 142 | "license": "MIT", 143 | "keywords": [ 144 | "react", 145 | "redux", 146 | "starter", 147 | "starterKit", 148 | "seed", 149 | "isomorphic", 150 | "universal", 151 | "postcss", 152 | "css-modules", 153 | "webpack", 154 | "enzyme", 155 | "testing", 156 | "yeoman", 157 | "istambul", 158 | "immutablejs", 159 | "mocha", 160 | "chai", 161 | "postcss" 162 | ] 163 | } 164 | -------------------------------------------------------------------------------- /preprocessor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const babel = require('babel-core'); 4 | const jestPreset = require('babel-preset-jest'); 5 | 6 | module.exports = { 7 | process(src, filename) { 8 | if (babel.util.canCompile(filename)) { 9 | return babel.transform(src, { 10 | filename, 11 | presets: [jestPreset], 12 | retainLines: true, 13 | }).code; 14 | } 15 | return src; 16 | }, 17 | }; -------------------------------------------------------------------------------- /server/api/mocks/forecast.json: -------------------------------------------------------------------------------- 1 | { 2 | "city": { 3 | "coord": { 4 | "lat": 38.98333, 5 | "lon": -3.93333 6 | }, 7 | "country": "ES", 8 | "id": 2519402, 9 | "name": "Ciudad Real", 10 | "population": 0 11 | }, 12 | "cnt": 8, 13 | "cod": "200", 14 | "list": [ 15 | { 16 | "id": 1, 17 | "clouds": 0, 18 | "deg": 204, 19 | "dt": 1469793600, 20 | "humidity": 24, 21 | "pressure": 951.51, 22 | "speed": 2.36, 23 | "temp": { 24 | "day": 43.94, 25 | "eve": 37.62, 26 | "max": 43.94, 27 | "min": 23.04, 28 | "morn": 34.44, 29 | "night": 23.04 30 | }, 31 | "weather": [ 32 | { 33 | "description": "cielo claro", 34 | "icon": "01d", 35 | "id": 800, 36 | "main": "Clear" 37 | } 38 | ] 39 | }, 40 | { 41 | "id": 2, 42 | "clouds": 0, 43 | "deg": 226, 44 | "dt": 1469880000, 45 | "humidity": 23, 46 | "pressure": 952.13, 47 | "rain": 0.9, 48 | "speed": 5.86, 49 | "temp": { 50 | "day": 36.32, 51 | "eve": 32.56, 52 | "max": 37.29, 53 | "min": 20.61, 54 | "morn": 20.61, 55 | "night": 22.14 56 | }, 57 | "weather": [ 58 | { 59 | "description": "lluvia ligera", 60 | "icon": "10d", 61 | "id": 500, 62 | "main": "Rain" 63 | } 64 | ] 65 | }, 66 | { 67 | "id": 3, 68 | "clouds": 92, 69 | "deg": 215, 70 | "dt": 1469966400, 71 | "humidity": 26, 72 | "pressure": 952.84, 73 | "rain": 0.31, 74 | "speed": 6.37, 75 | "temp": { 76 | "day": 33.29, 77 | "eve": 30.28, 78 | "max": 33.29, 79 | "min": 17.2, 80 | "morn": 20.85, 81 | "night": 17.2 82 | }, 83 | "weather": [ 84 | { 85 | "description": "lluvia ligera", 86 | "icon": "10d", 87 | "id": 500, 88 | "main": "Rain" 89 | } 90 | ] 91 | }, 92 | { 93 | "id": 4, 94 | "clouds": 0, 95 | "deg": 243, 96 | "dt": 1470052800, 97 | "humidity": 0, 98 | "pressure": 949.3, 99 | "speed": 3.26, 100 | "temp": { 101 | "day": 32.92, 102 | "eve": 32.75, 103 | "max": 32.92, 104 | "min": 15.45, 105 | "morn": 15.45, 106 | "night": 20.82 107 | }, 108 | "weather": [ 109 | { 110 | "description": "cielo claro", 111 | "icon": "01d", 112 | "id": 800, 113 | "main": "Clear" 114 | } 115 | ] 116 | }, 117 | { 118 | "id": 5, 119 | "clouds": 54, 120 | "deg": 254, 121 | "dt": 1470139200, 122 | "humidity": 0, 123 | "pressure": 947.83, 124 | "speed": 4.62, 125 | "temp": { 126 | "day": 30.16, 127 | "eve": 30.96, 128 | "max": 30.96, 129 | "min": 16.63, 130 | "morn": 16.63, 131 | "night": 21.67 132 | }, 133 | "weather": [ 134 | { 135 | "description": "lluvia ligera", 136 | "icon": "10d", 137 | "id": 500, 138 | "main": "Rain" 139 | } 140 | ] 141 | }, 142 | { 143 | "id": 6, 144 | "clouds": 0, 145 | "deg": 54, 146 | "dt": 1470225600, 147 | "humidity": 0, 148 | "pressure": 947.95, 149 | "speed": 2.3, 150 | "temp": { 151 | "day": 29.76, 152 | "eve": 30.55, 153 | "max": 30.55, 154 | "min": 17.54, 155 | "morn": 17.57, 156 | "night": 17.54 157 | }, 158 | "weather": [ 159 | { 160 | "description": "cielo claro", 161 | "icon": "01d", 162 | "id": 800, 163 | "main": "Clear" 164 | } 165 | ] 166 | }, 167 | { 168 | "id": 7, 169 | "clouds": 0, 170 | "deg": 52, 171 | "dt": 1470312000, 172 | "humidity": 0, 173 | "pressure": 946.94, 174 | "speed": 0.28, 175 | "temp": { 176 | "day": 30.53, 177 | "eve": 32.43, 178 | "max": 32.43, 179 | "min": 13.42, 180 | "morn": 13.42, 181 | "night": 18.48 182 | }, 183 | "weather": [ 184 | { 185 | "description": "cielo claro", 186 | "icon": "01d", 187 | "id": 800, 188 | "main": "Clear" 189 | } 190 | ] 191 | }, 192 | { 193 | "id": 8, 194 | "clouds": 9, 195 | "deg": 223, 196 | "dt": 1470398400, 197 | "humidity": 0, 198 | "pressure": 948.14, 199 | "speed": 1.39, 200 | "temp": { 201 | "day": 33.86, 202 | "eve": 34.73, 203 | "max": 34.73, 204 | "min": 16.1, 205 | "morn": 16.1, 206 | "night": 22.82 207 | }, 208 | "weather": [ 209 | { 210 | "description": "cielo claro", 211 | "icon": "01d", 212 | "id": 800, 213 | "main": "Clear" 214 | } 215 | ] 216 | } 217 | ], 218 | "message": 0.0517 219 | } -------------------------------------------------------------------------------- /server/api/mocks/logo.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "alt": "React Base!", 4 | "name": "ReactBaseLogo", 5 | "width": 500, 6 | "src": "/assets/images/react-base-logo.png" 7 | } 8 | -------------------------------------------------------------------------------- /server/api/mocks/weatherStation.json: -------------------------------------------------------------------------------- 1 | { 2 | "last": { 3 | "dt": 1452416940, 4 | "main": { 5 | "humidity": 79, 6 | "pressure": 1000, 7 | "temp": 284.82 8 | }, 9 | "rain": { 10 | "today": 0 11 | }, 12 | "wind": { 13 | "deg": 14, 14 | "speed": 14 15 | }, 16 | "visibility": { 17 | 18 | } 19 | }, 20 | "params": [ 21 | "temp", 22 | "pressure", 23 | "humidity", 24 | "wind", 25 | "rain", 26 | "visibility" 27 | ], 28 | "station": { 29 | "coord": { 30 | "lat": 42.8668, 31 | "lon": -2.6793 32 | }, 33 | "id": 120139, 34 | "name": "EA2CQ-13", 35 | "status": 20, 36 | "type": 2 37 | } 38 | } -------------------------------------------------------------------------------- /server/api/mocks/weatherStations.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "stationId": 1442829648, 5 | "distance": 9.168, 6 | "calc": { 7 | "dewpoint": 11.1833 8 | }, 9 | "dt": 1470034503, 10 | "main": { 11 | "humidity": 37.5, 12 | "pressure": 1019.49, 13 | "temp": 300.05 14 | }, 15 | "coord": { 16 | "Lat": 40.495, 17 | "Lon": -3.655 18 | }, 19 | "station": { 20 | "id": 1442829648, 21 | "name": "pirates", 22 | "status": 20, 23 | "type": 5, 24 | "user_id": 0 25 | } 26 | }, 27 | { 28 | "id": 2, 29 | "stationId": 42622, 30 | "distance": 9.839, 31 | "dt": 1469991990, 32 | "main": { 33 | "humidity": 37.5, 34 | "pressure": 1019.49, 35 | "temp": 300.05 36 | }, 37 | "rain": { 38 | "1h": 0, 39 | "24h": 0, 40 | "today": 0 41 | }, 42 | "wind": { 43 | "deg": 11, 44 | "gust": 1.54, 45 | "speed": 0.51 46 | }, 47 | "coord": { 48 | "lat": 40.5048, 49 | "lng": -3.6668 50 | }, 51 | "station": { 52 | "id": 42622, 53 | "name": "EA4ENR", 54 | "status": 20, 55 | "type": 2 56 | } 57 | }, 58 | { 59 | "id": 3, 60 | "stationId": 5488, 61 | "distance": 13.928, 62 | "dt": 1470033000, 63 | "main": { 64 | "humidity": 29, 65 | "pressure": 1014, 66 | "temp": 307.59 67 | }, 68 | "visibility": { 69 | "distance": 10000, 70 | "prefix": 1 71 | }, 72 | "wind": { 73 | "deg": 10, 74 | "speed": 3.1, 75 | "var_beg": 340, 76 | "var_end": 40 77 | }, 78 | "coord": { 79 | "lat": 40.4936, 80 | "lon": -3.5668 81 | }, 82 | "station": { 83 | "id": 5488, 84 | "name": "LEMD", 85 | "status": 50, 86 | "type": 1 87 | } 88 | }, 89 | { 90 | "id": 4, 91 | "stationId": 187956, 92 | "distance": 20.445, 93 | "dt": 1469793737, 94 | "main": { 95 | "humidity": 29, 96 | "pressure": 1014, 97 | "temp": 307.59 98 | }, 99 | "rain": { 100 | "1h": 0, 101 | "24h": 0, 102 | "today": 0 103 | }, 104 | "wind": { 105 | "deg": 172, 106 | "gust": 0, 107 | "speed": 0 108 | }, 109 | "coord": { 110 | "lat": 40.4467, 111 | "lon": -3.461 112 | }, 113 | "station": { 114 | "id": 187956, 115 | "name": "EA4CQL-11", 116 | "status": 20, 117 | "type": 2 118 | } 119 | }, 120 | { 121 | "id": 5, 122 | "stationId": 120400, 123 | "distance": 20.445, 124 | "dt": 1452255851, 125 | "main": { 126 | "humidity": 66, 127 | "pressure": 1003, 128 | "temp": 288.15 129 | }, 130 | "rain": { 131 | "1h": 0, 132 | "24h": 0.508, 133 | "today": 7.62 134 | }, 135 | "wind": { 136 | "deg": 154, 137 | "gust": 0.51, 138 | "speed": 0 139 | }, 140 | "coord": { 141 | "lat": 40.4467, 142 | "lon": -3.461 143 | }, 144 | "station": { 145 | "id": 120400, 146 | "name": "EA4CQL-11", 147 | "status": 20, 148 | "type": 2 149 | } 150 | } 151 | ] 152 | -------------------------------------------------------------------------------- /server/enviroment/development.js: -------------------------------------------------------------------------------- 1 | export const port = 8000; 2 | export const sslPort = 443; -------------------------------------------------------------------------------- /server/enviroment/index.js: -------------------------------------------------------------------------------- 1 | import { env } from 'base'; 2 | import * as development from './development'; 3 | import * as production from './production'; 4 | 5 | const envConf = (env === 'production') ? production : development; 6 | 7 | export default envConf; 8 | -------------------------------------------------------------------------------- /server/enviroment/production.js: -------------------------------------------------------------------------------- 1 | export const port = 8000; 2 | export const sslPort = 443; -------------------------------------------------------------------------------- /server/httpServer.js: -------------------------------------------------------------------------------- 1 | import base from 'base/'; 2 | import envConf from './enviroment'; 3 | 4 | const httpServer = app => { 5 | app.listen(envConf.port, (err) => { 6 | if (err) return base.console.error(`${err}`); 7 | base.console.success(`HTTP Server up on http://localhost:${envConf.port}`); 8 | }); 9 | }; 10 | 11 | export default httpServer; -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | require('babel-core/register'); 2 | const base = require('../src/base').default; 3 | 4 | require('css-modules-require-hook')({ 5 | rootDir: './', 6 | mode: 'local', 7 | generateScopedName: (base.env === 'development') ? '[local]-[hash:base64:4]' : '[hash:base64:4]', 8 | }); 9 | 10 | base.console.info(`Starting ${base.env} enviroment...`); 11 | require("./server"); 12 | -------------------------------------------------------------------------------- /server/lib/configureStore.js: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, createStore } from 'redux'; 2 | import reduxReqMiddleware from 'redux-req-middleware'; 3 | import rootReducer from '../../src/base/reducers/'; 4 | 5 | export default function configureServerStore() { 6 | const configureStore = applyMiddleware(reduxReqMiddleware())(createStore); 7 | return configureStore(rootReducer); 8 | } 9 | -------------------------------------------------------------------------------- /server/lib/files.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import { env } from '../../src/base/'; 4 | import * as FileSystem from '../../src/base/shared/FileSystem'; 5 | 6 | let files; 7 | 8 | if (env === 'development') { 9 | files = { 10 | app: { js: '/app.js' }, 11 | vendor: { js: '/dlls/vendor.dll.js' }, 12 | }; 13 | } else { 14 | const assetsManifest = path.resolve(__dirname, '../../dist/webpack-assets.json'); 15 | files = JSON.parse(FileSystem.readFile(assetsManifest, 'utf8')); 16 | } 17 | 18 | export function getScripts(file) { 19 | const scriptPath = `${files[file].js}`; 20 | return (env === 'production') ? `` : ``; 21 | } 22 | 23 | export function getStyles(file) { 24 | const cssPath = files[file].css; 25 | return (env === 'production') ? `` : ''; 26 | } 27 | -------------------------------------------------------------------------------- /server/lib/renderContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { RouterContext } from 'react-router'; 4 | import { renderToString } from 'react-dom/server'; 5 | 6 | export default function renderContainer(store, renderProps) { 7 | 8 | return renderToString( 9 | 10 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /server/lib/renderPage.js: -------------------------------------------------------------------------------- 1 | import * as templates from '../templates/'; 2 | import { getScripts, getStyles } from '../lib/files'; 3 | 4 | export default function renderPage(routeMatch, container, store) { 5 | 6 | const params = { 7 | title: params, 8 | container: container, 9 | routeMatch: routeMatch, 10 | state: store.getState(), 11 | style: getStyles('app'), 12 | appScript: getScripts('app'), 13 | vendorScript: getScripts('vendor') 14 | }; 15 | 16 | let template; 17 | let route = routeMatch.substring(1).split('/'); 18 | if (route.length === 1) template = templates[route]; 19 | else { 20 | if (route[0] === 'post') template = templates.post; 21 | else template = templates.tag; 22 | } 23 | 24 | if (routeMatch === '/' || !template) return templates.main(params); 25 | else return template(params); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /server/middleware/dev-middleware.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import webpackDevMiddleware from 'webpack-dev-middleware'; 3 | import webpackHotMiddleware from 'webpack-hot-middleware'; 4 | 5 | import base from '../../src/base/'; 6 | 7 | const config = require('../../webpack/index.babel.js'); 8 | 9 | const compiler = webpack(config); 10 | 11 | const serverOptions = { 12 | hot: true, 13 | lazy: false, 14 | quiet: true, 15 | watch: true, 16 | noInfo: true, 17 | inline: true, 18 | progress: false, 19 | stats: { colors: true }, 20 | publicPath: config.output.publicPath, 21 | headers: { 22 | 'Access-Control-Allow-Origin': '*', 23 | 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', 24 | 'Access-Control-Allow-Headers': 'X-Requested-With , Accept-Language, Content-type, Authorization' 25 | } 26 | }; 27 | 28 | let bundleStart = Date.now(); 29 | 30 | compiler.plugin("compile", function() { 31 | bundleStart = Date.now(); 32 | }); 33 | 34 | compiler.plugin('done', function() { 35 | base.console.success(`Bundled project in ${Date.now() - bundleStart} ms!`); 36 | }); 37 | 38 | const applyDevMiddleware = function() { 39 | return [ 40 | webpackHotMiddleware(compiler, { log:false, noInfo: true, quiet: true}), 41 | webpackDevMiddleware(compiler, serverOptions), 42 | ]; 43 | }; 44 | 45 | module.exports = applyDevMiddleware; 46 | -------------------------------------------------------------------------------- /server/middleware/index.js: -------------------------------------------------------------------------------- 1 | import base from 'base'; 2 | 3 | const envMiddleware = (base.env === 'development') ? require('./dev-middleware') : require('./prod-middleware'); 4 | 5 | export default function applyEnvMiddleWare(app) { 6 | 7 | base.console.info(`Checking Env middlewares...`); 8 | 9 | return new Promise((resolve) => { 10 | let serverUp = false; 11 | 12 | envMiddleware().forEach(function(middleware) { 13 | const middlewareName = middleware.name || 'middleware'; 14 | app.use(middleware); 15 | 16 | if (base.env == 'production' && !serverUp) { 17 | serverUp = true; 18 | base.console.success(`Applied ${middlewareName} middleware`); 19 | resolve(true); 20 | } else { 21 | if (middleware.waitUntilValid) { 22 | middleware.waitUntilValid(function() { 23 | base.console.success(`Applied ${middlewareName} middleware`); 24 | resolve(true); 25 | }); 26 | } 27 | } 28 | }); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /server/middleware/prod-middleware.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import helmet from 'helmet'; 3 | import compression from 'compression'; 4 | 5 | import base from 'base'; 6 | 7 | const config = require('../../webpack/index.babel.js'); 8 | 9 | const compiler = webpack(config); 10 | let bundleStart = Date.now(); 11 | 12 | compiler.plugin("compile", function() { 13 | base.console.info(`Bundling project...`); 14 | bundleStart = Date.now(); 15 | }); 16 | 17 | compiler.plugin('done', function() { 18 | base.console.success(`Bundled project in ${Date.now() - bundleStart} ms!`); 19 | }); 20 | 21 | const allowCrossDomain = function(req, res, next) { 22 | res.header('Access-Control-Allow-Origin', '*'); 23 | res.header('Access-Control-Allow-Methods', 'GET,OPTIONS'); 24 | res.header('Access-Control-Allow-Headers', 'Content-Type'); 25 | next(); 26 | }; 27 | 28 | const applyProdMiddleware = () => { 29 | const middleware = [ 30 | helmet(), 31 | compression(), 32 | allowCrossDomain 33 | ]; 34 | return middleware; 35 | }; 36 | 37 | module.exports = applyProdMiddleware; -------------------------------------------------------------------------------- /server/middleware/routing-middleware.js: -------------------------------------------------------------------------------- 1 | import { match } from 'react-router'; 2 | 3 | import { fetchRequiredActions, context } from 'base'; 4 | import routes from '../../src/base/routes'; 5 | import renderPage from '../lib/renderPage'; 6 | import renderContainer from '../lib/renderContainer'; 7 | import configureServerStore from '../lib/configureStore'; 8 | 9 | export default function routingMiddleware(req, res) { 10 | 11 | const store = configureServerStore(); 12 | 13 | match({ routes , location: req.url }, (error, redirectLocation, renderProps) => { 14 | if (error) return res.status(500).send(error.message); 15 | if (redirectLocation) return res.redirect(302, redirectLocation.pathname + redirectLocation.search); 16 | if (renderProps == null) return res.status(404).send('Not found'); 17 | fetchRequiredActions(store.dispatch, renderProps.components, renderProps.params, context.context) 18 | .then(() => { 19 | const routeMatch = renderProps.location.pathname; 20 | const renderedContainer = renderContainer(store, renderProps); 21 | const page = renderPage(routeMatch, renderedContainer, store); 22 | return res.status(200).send(page); 23 | }) 24 | .catch(err => res.end(err.message)); 25 | 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /server/routing.js: -------------------------------------------------------------------------------- 1 | import base from '../src/base/'; 2 | import routingMiddleware from './middleware/routing-middleware'; 3 | 4 | const applyServerRouting = (app) => { 5 | 6 | app.use(routingMiddleware); 7 | base.console.success(`Routing up`); 8 | }; 9 | 10 | export default applyServerRouting; 11 | 12 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | import base from 'base'; 4 | import httpServer from './httpServer'; 5 | import applyStaticsPaths from './statics'; 6 | import applyServerRouting from './routing'; 7 | import applyEnvMiddleWare from './middleware'; 8 | 9 | const app = express(); 10 | 11 | const launchServer = () => { 12 | applyEnvMiddleWare(app) 13 | .then(() => { 14 | base.console.info(`Checking static paths...`); 15 | applyStaticsPaths(app); 16 | }) 17 | .then(() => { 18 | base.console.info(`Checking server routing...`); 19 | applyServerRouting(app); 20 | }) 21 | .then(() => { 22 | base.console.info(`Setting up server...`); 23 | httpServer(app); 24 | }) 25 | .catch((e) => { 26 | base.console.error(`Server Error ${e}...`); 27 | }); 28 | }; 29 | 30 | launchServer(); -------------------------------------------------------------------------------- /server/statics/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import express from 'express'; 3 | 4 | import base from 'base'; 5 | 6 | const commonStatics = [ 7 | {route: '/mocks', dir: path.join(__dirname, '../../server/api/mocks')} 8 | ]; 9 | 10 | const devStatics = [ 11 | {route: '/dlls', dir: path.join(__dirname, '../../dist/')}, 12 | {route: '/', dir: path.join(__dirname, '../../src/app')}, 13 | ]; 14 | 15 | const prodStatics = [ 16 | {route: '/', dir: path.join(__dirname, '../../dist')}, 17 | {route: '/assets', dir: path.join(__dirname, '../../dist/assets')}, 18 | ]; 19 | 20 | const envStatics = (base.env === 'development') ? commonStatics.concat(devStatics) : commonStatics.concat(prodStatics); 21 | const statics = envStatics; 22 | 23 | export default function applyStaticsPaths(app) { 24 | statics.map(function(staticPath) { 25 | app.use(staticPath.route, express.static(staticPath.dir)); 26 | base.console.success(`Applied static path ${staticPath.route}`); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /server/templates/calculator.js: -------------------------------------------------------------------------------- 1 | 2 | export default function calculator(params) { 3 | 4 | const state = JSON.stringify(params.state); 5 | 6 | return ` 7 | 8 | 9 | 10 | 11 | Simple Redux Calculator 12 | ${ params.vendorScript } 13 | ${ params.style } 14 | 15 | 16 | ${ params.container } 17 | 18 | ${ params.appScript } 19 | 20 | 21 | `; 22 | } 23 | -------------------------------------------------------------------------------- /server/templates/index.js: -------------------------------------------------------------------------------- 1 | import main from './main'; 2 | import calculator from './calculator'; 3 | 4 | export { main, calculator }; 5 | -------------------------------------------------------------------------------- /server/templates/main.js: -------------------------------------------------------------------------------- 1 | 2 | export default function main(params) { 3 | 4 | const state = JSON.stringify(params.state); 5 | 6 | return ` 7 | 8 | 9 | 10 | 11 | React Base 12 | ${ params.vendorScript } 13 | ${ params.style } 14 | 15 | 16 | ${ params.container } 17 | 18 | ${ params.appScript } 19 | 20 | 21 | `; 22 | } 23 | -------------------------------------------------------------------------------- /src/app/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atSistemas/react-base/d594a3538695f0ac9c45179dbd5d69f728e40b52/src/app/assets/images/favicon.ico -------------------------------------------------------------------------------- /src/app/assets/images/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /src/app/assets/images/react-base-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atSistemas/react-base/d594a3538695f0ac9c45179dbd5d69f728e40b52/src/app/assets/images/react-base-logo.png -------------------------------------------------------------------------------- /src/app/components/LinkButton/index.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router'; 2 | import React from 'react'; 3 | import { PropTypes } from 'prop-types'; 4 | import styles from './styles.css'; 5 | 6 | const propTypes = { 7 | value: PropTypes.string.isRequired, 8 | location: PropTypes.string.isRequired, 9 | }; 10 | 11 | const LinkButton = props => { 12 | return ( 13 | 14 | 15 | { props.value } 16 | 17 | 18 | ); 19 | }; 20 | 21 | LinkButton.propTypes = propTypes; 22 | 23 | export default LinkButton; 24 | -------------------------------------------------------------------------------- /src/app/components/LinkButton/spec/LinkButton.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { expect } from 'chai'; 3 | import { mount, shallow } from 'enzyme'; 4 | 5 | import LinkButton from '../'; 6 | 7 | describe('Components / LinkButton', () => { 8 | describe('', () => { 9 | 10 | it('Should has properties', () => { 11 | 12 | const component = mount(); 13 | expect(component.props().location).to.equal("/calculator"); 14 | expect(component.props().value).to.equal("Simple calculator"); 15 | 16 | }); 17 | 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/app/components/LinkButton/styles.css: -------------------------------------------------------------------------------- 1 | .LinkButton { 2 | &:last-child { 3 | margin-left: 30px; 4 | } 5 | 6 | button { 7 | background: #47BBEA; 8 | border-radius: 4px; 9 | border: 1px solid transparent; 10 | color: #ffffff; 11 | cursor: pointer; 12 | font-family: Arial; 13 | font-size: 14px; 14 | font-weight: 500; 15 | padding: .5rem 1rem; 16 | text-decoration: none; 17 | width: 100%; 18 | 19 | &:hover { 20 | background: #3cb0fd; 21 | border: 1px solid transparent; 22 | text-decoration: none; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/app/components/Logo/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PropTypes } from 'prop-types'; 3 | import { Link } from 'react-router'; 4 | 5 | const propTypes= { 6 | alt: PropTypes.string.isRequired, 7 | src: PropTypes.string.isRequired, 8 | width: PropTypes.number.isRequired 9 | }; 10 | 11 | const ReactBaseLogo = props => ( 12 | 13 | 14 | 15 | 16 | ); 17 | 18 | ReactBaseLogo.propTypes= propTypes; 19 | 20 | export default ReactBaseLogo; 21 | -------------------------------------------------------------------------------- /src/app/components/Logo/styles.css: -------------------------------------------------------------------------------- 1 | .main { 2 | height: 100%; 3 | display: table-cell; 4 | vertical-align: middle; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/containers/App/index.jsx: -------------------------------------------------------------------------------- 1 | import { PropTypes } from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | 4 | class App extends Component { 5 | 6 | static propTypes = { 7 | children: PropTypes.object.isRequired 8 | } 9 | 10 | render() { 11 | const children = this.props.children; 12 | return ( 13 | 14 | 15 | { children } 16 | 17 | 18 | ); 19 | 20 | } 21 | } 22 | 23 | export default App; 24 | -------------------------------------------------------------------------------- /src/app/containers/App/spec/App.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import React from 'react'; 3 | import ReactShallowRenderer from 'react-test-renderer/shallow'; 4 | import App from '../'; 5 | 6 | function setup() { 7 | let props = { 8 | children: {} 9 | }; 10 | 11 | let renderer = new ReactShallowRenderer(); 12 | renderer.render(); 13 | let output = renderer.getRenderOutput(); 14 | 15 | return { 16 | props, 17 | output, 18 | renderer 19 | }; 20 | } 21 | 22 | describe('comtainers', () => { 23 | describe('App comtainer', () => { 24 | it('should render correctly', () => { 25 | const { output } = setup(); 26 | expect(output.type).to.equal('div'); 27 | 28 | const main = output.props.children; 29 | expect(main.type).to.equal('main'); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/app/containers/Calculator/actionTypes/index.js: -------------------------------------------------------------------------------- 1 | import { createActionType } from 'base'; 2 | 3 | export default createActionType([ 4 | 'SUM', 5 | 'CLEAN', 6 | 'RESULT', 7 | 'DIVIDE', 8 | 'PERCENT', 9 | 'MULTIPLY', 10 | 'SUBSTRACT', 11 | 'CHANGE_SIGN', 12 | 'INPUT_NUMBER', 13 | 'INPUT_DECIMAL', 14 | 'INPUT_OPERATOR', 15 | 'INPUT_OPERATION' 16 | ]); 17 | -------------------------------------------------------------------------------- /src/app/containers/Calculator/actions/index.js: -------------------------------------------------------------------------------- 1 | import ActionTypes from '../actionTypes'; 2 | 3 | export const inputNumber = value => ({ 4 | type: ActionTypes.INPUT_NUMBER, 5 | payload: { value } 6 | }); 7 | 8 | export const inputOperation = value => ({ 9 | type: ActionTypes.INPUT_OPERATION, 10 | payload: { value } 11 | }); 12 | 13 | export const inputDecimal = () => ({ 14 | type: ActionTypes.INPUT_DECIMAL 15 | }); 16 | 17 | export const inputOperator = operator => ({ 18 | type: ActionTypes.INPUT_OPERATOR, 19 | payload: { operator } 20 | }); 21 | 22 | export const result = operator => ({ 23 | type: ActionTypes.RESULT, 24 | payload: { operator } 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/containers/Calculator/actions/spec/Calculator.actions.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import * as Actions from '../'; 3 | import ActionTypes from '../../actionTypes'; 4 | 5 | describe('Calculator', () => { 6 | 7 | describe('Actions', () => { 8 | 9 | it('Should get all Actions', () => { 10 | 11 | let expected; 12 | 13 | const inputNumber = Actions.inputNumber(3); 14 | expected = {type: ActionTypes.INPUT_NUMBER, payload: {value: 3 }}; 15 | expect(expected).to.deep.equal(inputNumber); 16 | 17 | const inputOperation = Actions.inputOperation(ActionTypes.CLEAN); 18 | expected = {type: ActionTypes.INPUT_OPERATION, payload: {value: ActionTypes.CLEAN}}; 19 | expect(expected).to.deep.equal(inputOperation); 20 | 21 | const inputDecimal = Actions.inputDecimal(); 22 | expected = {type: ActionTypes.INPUT_DECIMAL}; 23 | expect(expected).to.deep.equal(inputDecimal); 24 | 25 | const inputOperator = Actions.inputOperator(ActionTypes.SUM); 26 | expected = {type: ActionTypes.INPUT_OPERATOR, payload: {operator: ActionTypes.SUM}}; 27 | expect(expected).to.deep.equal(inputOperator); 28 | 29 | }); 30 | 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/app/containers/Calculator/components/Button/index.jsx: -------------------------------------------------------------------------------- 1 | import Ink from 'react-ink'; 2 | import React from 'react'; 3 | import { PropTypes } from 'prop-types'; 4 | 5 | import styles from './styles.css'; 6 | 7 | const propTypes = { 8 | type: PropTypes.string.isRequired, 9 | value: PropTypes.string.isRequired, 10 | onClick: PropTypes.func.isRequired 11 | }; 12 | 13 | const Button = props => { 14 | let style; 15 | 16 | if (props.type === 'operator') style = styles.ButtonOperate; 17 | else if (props.type === 'zero') style = styles.ButtonZero; 18 | else style = styles.Button; 19 | 20 | return ( 21 | 25 | { props.value } 26 | 31 | 32 | ); 33 | }; 34 | 35 | Button.propTypes= propTypes; 36 | 37 | export default Button; 38 | -------------------------------------------------------------------------------- /src/app/containers/Calculator/components/Button/spec/Button.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { expect } from 'chai'; 3 | import { mount, shallow } from 'enzyme'; 4 | 5 | import Button from '../'; 6 | 7 | describe('Calculator / Components', () => { 8 | describe('', () => { 9 | 10 | it('Should has properties', () => { 11 | 12 | const component = mount({}} />); 13 | expect(component.props().type).to.equal("number"); 14 | expect(component.props().value).to.equal("C"); 15 | 16 | }); 17 | 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/app/containers/Calculator/components/Button/styles.css: -------------------------------------------------------------------------------- 1 | .ButtonCommon { 2 | cursor: pointer; 3 | position: relative; 4 | margin: 0; 5 | padding: 0; 6 | border: none; 7 | font-size: 30px; 8 | text-align: center; 9 | overflow: hidden; 10 | flex-grow: 1; 11 | flex-shrink: 0; 12 | flex-basis: 25%; 13 | height: 70px; 14 | font-family: Arial; 15 | outline: none; 16 | } 17 | 18 | .CommonAnimate { 19 | animation: doClick .5s ease-out 0s 1 alternate forwards; 20 | } 21 | 22 | .Button { 23 | composes: ButtonCommon; 24 | box-shadow: inset 1px 1px 0 0 #e3e7e9; 25 | background-color: #fafafa; 26 | color: #979ca4; 27 | } 28 | 29 | .ButtonClicked { 30 | composes: ButtonCommon; 31 | background-color: #fafafa; 32 | color: #979ca4; 33 | } 34 | 35 | .ButtonOperate { 36 | composes: ButtonCommon; 37 | flex-basis: calc(25% - 1px); 38 | box-sizing: border-box; 39 | margin: 1px 0 0 1px; 40 | background-color: #fa722e; 41 | border-top: 1px solid #fa722e; 42 | border-left: 1px solid #fa722e; 43 | color: #fff; 44 | } 45 | 46 | .ButtonOperateClicked { 47 | composes: ButtonCommon; 48 | background-color: #fa722e; 49 | color: #fff; 50 | } 51 | 52 | .ButtonZero { 53 | composes: ButtonCommon; 54 | flex-grow: 2; 55 | flex-basis: 50%; 56 | box-shadow: inset 1px 1px 0 0 #e3e7e9; 57 | background-color: #fafafa; 58 | color: #979ca4; 59 | } 60 | -------------------------------------------------------------------------------- /src/app/containers/Calculator/components/ButtonPannel/index.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { PropTypes } from 'prop-types'; 3 | import React, { Component } from 'react'; 4 | import { bindActionCreators } from 'redux'; 5 | 6 | import Button from '../Button'; 7 | import styles from './styles.css'; 8 | import * as Actions from '../../actions'; 9 | import ActionTypes from '../../actionTypes'; 10 | 11 | export class ButtonPannel extends Component { 12 | 13 | static propTypes = { 14 | dispatch: PropTypes.func.isRequired 15 | } 16 | 17 | constructor (props) { 18 | super(props); 19 | this.actions = bindActionCreators(Actions, props.dispatch); 20 | } 21 | 22 | render () { 23 | return ( 24 | 25 | 26 | 27 | this.actions.inputOperation(ActionTypes.CLEAN) } /> 28 | this.actions.inputOperation(ActionTypes.CHANGE_SIGN) } /> 29 | this.actions.inputOperation(ActionTypes.PERCENT) } /> 30 | this.actions.inputOperator(ActionTypes.DIVIDE) } /> 31 | 32 | 33 | 34 | this.actions.inputNumber(7) } /> 35 | this.actions.inputNumber(8) } /> 36 | this.actions.inputNumber(9) } /> 37 | this.actions.inputOperator(ActionTypes.MULTIPLY) } /> 38 | 39 | 40 | this.actions.inputNumber(4) } /> 41 | this.actions.inputNumber(5) } /> 42 | this.actions.inputNumber(6) } /> 43 | this.actions.inputOperator(ActionTypes.SUBSTRACT) } /> 44 | 45 | 46 | this.actions.inputNumber(1) } /> 47 | this.actions.inputNumber(2) } /> 48 | this.actions.inputNumber(3) } /> 49 | this.actions.inputOperator(ActionTypes.SUM) } /> 50 | 51 | 52 | this.actions.inputNumber(0) } /> 53 | this.actions.inputDecimal() } /> 54 | this.actions.result() } /> 55 | 56 | 57 | 58 | 59 | ); 60 | } 61 | } 62 | 63 | export default connect(null)(ButtonPannel); 64 | -------------------------------------------------------------------------------- /src/app/containers/Calculator/components/ButtonPannel/spec/ButtonPannel.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { expect } from 'chai'; 3 | import { Provider } from 'react-redux'; 4 | import { mount, shallow } from 'enzyme'; 5 | 6 | import ButtonPannel from '../'; 7 | import configureStore from 'store/ConfigureStore'; 8 | 9 | describe('Calculator / Components', () => { 10 | 11 | describe('', () => { 12 | 13 | it('Should has 19 Button', () => { 14 | const store = configureStore([]); 15 | const component = mount( 16 | 17 | 18 | ); 19 | 20 | expect(component.find('Button')).to.have.lengthOf(19); 21 | }); 22 | 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/containers/Calculator/components/ButtonPannel/styles.css: -------------------------------------------------------------------------------- 1 | .ButtonPanel { 2 | flex: 5; 3 | display: flex; 4 | flex-direction: row; 5 | } 6 | 7 | .Row { 8 | flex: 1; 9 | display: flex; 10 | flex-direction: row; 11 | } 12 | 13 | .Column { 14 | flex: 1; 15 | display: flex; 16 | flex-direction: column; 17 | } 18 | 19 | .clicked { 20 | animation: doClick .5s ease-out 0s 1 alternate forwards; 21 | } 22 | 23 | @keyframes doClick { 24 | 0% { 25 | transform: scale(1); 26 | opacity: .7; 27 | } 28 | 29 | 100% { 30 | transform: scale(3); 31 | opacity: 0; 32 | } 33 | } -------------------------------------------------------------------------------- /src/app/containers/Calculator/components/Display/index.jsx: -------------------------------------------------------------------------------- 1 | import { Record } from 'immutable'; 2 | import { connect } from 'react-redux'; 3 | import { PropTypes } from 'prop-types'; 4 | import React, { Component } from 'react'; 5 | import classNames from 'classnames/bind'; 6 | 7 | import styles from './styles.css'; 8 | 9 | export class Display extends Component { 10 | 11 | static propTypes = { 12 | Calculator: PropTypes.instanceOf(Record).isRequired 13 | }; 14 | 15 | render () { 16 | /* eslint react/jsx-no-bind: 0 */ 17 | const cx = classNames.bind(styles); 18 | const display = this.props.Calculator.display; 19 | 20 | const classDisplay = cx({ 21 | Fade : this.props.Calculator.resetDisplay 22 | }); 23 | 24 | return ( 25 | 26 | { display } 27 | 28 | ); 29 | }} 30 | 31 | export default connect( 32 | (state) => ({ Calculator: state.Calculator }) 33 | )(Display); 34 | -------------------------------------------------------------------------------- /src/app/containers/Calculator/components/Display/spec/Display.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { expect } from 'chai'; 3 | import { Record } from 'immutable'; 4 | import { Provider } from 'react-redux'; 5 | import { mount, shallow } from 'enzyme'; 6 | 7 | import Display from '../'; 8 | import configureStore from 'store/ConfigureStore'; 9 | import { CalculatorModel, setInitialState } from '../../../models'; 10 | 11 | describe('Calculator / Components', () => { 12 | 13 | describe('', () => { 14 | 15 | it('Should Display data', () => { 16 | 17 | setInitialState({}); 18 | const fakeStore = {}; 19 | fakeStore.Calculator = new CalculatorModel({ display: 2222 }); 20 | const store = configureStore([], fakeStore); 21 | const component = mount( 22 | 23 | 24 | 25 | ); 26 | 27 | expect(component.find('span').text()).to.equal('2222'); 28 | }); 29 | 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/app/containers/Calculator/components/Display/styles.css: -------------------------------------------------------------------------------- 1 | .Display { 2 | display: flex; 3 | padding-top: 40px; 4 | flex: 2; 5 | flex-direction: column; 6 | background-color: #8C8787; 7 | font-size: 42px; 8 | color: white; 9 | box-shadow: inset 1px 1px 0 0 #e3e7e9; 10 | text-align: right; 11 | padding: 0px 30px; 12 | line-height: 100px; 13 | height: 100px; 14 | font-family: Arial; 15 | } 16 | 17 | .Fade { 18 | -webkit-animation: fadein 0.3s; 19 | -moz-animation: fadein 0.3s; 20 | -ms-animation: fadein 0.3s; 21 | -o-animation: fadein 0.3s; 22 | animation: fadein 0.3s; 23 | } 24 | 25 | @keyframes fadein { 26 | from { opacity: 0; } 27 | to { opacity: 1; } 28 | } 29 | 30 | @-moz-keyframes fadein { 31 | from { opacity: 0; } 32 | to { opacity: 1; } 33 | } 34 | 35 | @-webkit-keyframes fadein { 36 | from { opacity: 0; } 37 | to { opacity: 1; } 38 | } 39 | 40 | @-ms-keyframes fadein { 41 | from { opacity: 0; } 42 | to { opacity: 1; } 43 | } 44 | 45 | @-o-keyframes fadein { 46 | from { opacity: 0; } 47 | to { opacity: 1; } 48 | } 49 | -------------------------------------------------------------------------------- /src/app/containers/Calculator/index.jsx: -------------------------------------------------------------------------------- 1 | import { PropTypes } from 'prop-types'; 2 | import React, { Component } from 'react'; 3 | 4 | import { Record } from 'immutable'; 5 | import Display from './components/Display'; 6 | import ButtonPannel from './components/ButtonPannel'; 7 | import styles from './styles.css'; 8 | 9 | class Calculator extends Component { 10 | 11 | static proptypes = { 12 | Calculator: PropTypes.instanceOf(Record).isRequired 13 | } 14 | 15 | render () { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | } 25 | } 26 | 27 | export default Calculator; 28 | -------------------------------------------------------------------------------- /src/app/containers/Calculator/models/index.js: -------------------------------------------------------------------------------- 1 | import { Record } from 'immutable'; 2 | 3 | export const CalculatorModel = Record({ 4 | display:0, 5 | operator:'', 6 | operation:'', 7 | prevValue: 0, 8 | nextValue: 0, 9 | newValue: false, 10 | resetDisplay: false, 11 | }); 12 | 13 | export const setInitialState = initialState => ( 14 | initialState.Calculator = new CalculatorModel(initialState.Calculator) 15 | ); 16 | -------------------------------------------------------------------------------- /src/app/containers/Calculator/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { createReducer } from 'base'; 2 | import { CalculatorModel } from '../models'; 3 | import ActionTypes from '../actionTypes'; 4 | 5 | const inputOperator = (state, action) => { 6 | const currentOperator = action.payload.operator; 7 | const prevOperator = state.get('operator'); 8 | const prevValue = state.get('prevValue'); 9 | const nextValue = state.get('nextValue'); 10 | const newValue = state.get('newValue'); 11 | 12 | const result = (newValue) ? 13 | calculate(prevOperator, prevValue, nextValue) : prevValue; 14 | 15 | return state 16 | .set('nextValue', 0) 17 | .set('newValue', true) 18 | .set('display', result) 19 | .set('prevValue', result) 20 | .set('resetDisplay', true) 21 | .set('operator', currentOperator); 22 | }; 23 | 24 | const inputNumber = (state, action) => { 25 | const selectedValue = action.payload.value; 26 | const newValue = state.get('newValue'); 27 | const prevValue = (newValue) ? state.get('nextValue') : state.get('prevValue'); 28 | const value = parseFloat(`${prevValue}${selectedValue}`); 29 | 30 | if (newValue) { 31 | return state 32 | .set('display', value) 33 | .set('nextValue', value) 34 | .set('resetDisplay', false); 35 | } else { 36 | return state 37 | .set('display', value) 38 | .set('prevValue', value) 39 | .set('resetDisplay', false); 40 | } 41 | }; 42 | 43 | const inputDecimal = (state) => { 44 | const value = `${state.get('prevValue')}.`; 45 | return state 46 | .set('display', value) 47 | .set('newValue', false) 48 | .set('prevValue', value); 49 | }; 50 | 51 | const inputOperation = (state, action) => { 52 | let value = 0; 53 | const operation = action.payload.value; 54 | const prevValue = state.get('prevValue'); 55 | 56 | switch (operation) { 57 | case ActionTypes.PERCENT: 58 | value = prevValue / 100; 59 | return state 60 | .set('display', value) 61 | .set('prevValue', value); 62 | 63 | case ActionTypes.CLEAN: 64 | value = 0; 65 | return state 66 | .set('display', value) 67 | .set('prevValue', value) 68 | .set('nextValue', value) 69 | .set('resetDisplay', true); 70 | 71 | case ActionTypes.CHANGE_SIGN: 72 | value = (Math.sign(prevValue) === 1) ? 73 | -Math.abs(prevValue) : Math.abs(prevValue); 74 | return state 75 | .set('display', value) 76 | .set('prevValue', value); 77 | } 78 | }; 79 | 80 | const calculate = (operator, prevValue, nextValue) => { 81 | const result = { 82 | [ActionTypes.SUM]: () => prevValue + nextValue, 83 | [ActionTypes.DIVIDE]: () => prevValue / nextValue, 84 | [ActionTypes.MULTIPLY]: () => prevValue * nextValue, 85 | [ActionTypes.SUBSTRACT]: () => prevValue - nextValue 86 | }; 87 | return operator ? result[operator]() : prevValue; 88 | }; 89 | 90 | const result = (state) => { 91 | const operator = state.get('operator'); 92 | const prevValue = state.get('prevValue'); 93 | const nextValue = state.get('nextValue'); 94 | const result = calculate(operator, prevValue, nextValue); 95 | 96 | return state 97 | .set('newValue', false) 98 | .set('display', result) 99 | .set('prevValue', result) 100 | .set('resetDisplay', true); 101 | }; 102 | 103 | const actionHandlers = { 104 | [ActionTypes.RESULT]: result, 105 | [ActionTypes.INPUT_NUMBER]: inputNumber, 106 | [ActionTypes.INPUT_DECIMAL]: inputDecimal, 107 | [ActionTypes.INPUT_OPERATOR]: inputOperator, 108 | [ActionTypes.INPUT_OPERATION]: inputOperation 109 | }; 110 | 111 | export default createReducer(actionHandlers, new CalculatorModel()); 112 | -------------------------------------------------------------------------------- /src/app/containers/Calculator/reducers/spec/Calculator.reducer.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import calculatorReducer from '../'; 3 | import ActionTypes from '../../actionTypes'; 4 | import { CalculatorModel } from '../../models/'; 5 | 6 | describe('Calculator', () => { 7 | describe('Reducer', () => { 8 | 9 | it('Should input an Operator', () => { 10 | 11 | const model = new CalculatorModel(); 12 | const operator = 'MULTIPLY'; 13 | const action = {type:ActionTypes.INPUT_OPERATOR, payload:{operator: operator}}; 14 | const reducer = calculatorReducer(model, action); 15 | const expectedData = reducer.get('operator'); 16 | 17 | expect(expectedData).to.equal(operator); 18 | 19 | }); 20 | 21 | it('Should input a Number', () => { 22 | 23 | const number = 2222; 24 | const model = new CalculatorModel(); 25 | const action = { type: ActionTypes.INPUT_NUMBER, payload: {value: number }}; 26 | const reducer = calculatorReducer(model, action); 27 | const expectedData = reducer.get('display'); 28 | 29 | expect(expectedData).to.equal(number); 30 | 31 | }); 32 | 33 | it('Should input a Decimal', () => { 34 | 35 | const number = 2222; 36 | const decimal = number + '.'; 37 | const model = new CalculatorModel({ prevValue: number }); 38 | const action = {type:ActionTypes.INPUT_DECIMAL}; 39 | const reducer = calculatorReducer(model, action); 40 | const expectedData = reducer.get('display'); 41 | 42 | expect(expectedData).to.equal(decimal); 43 | 44 | }); 45 | 46 | it('Should Sum two numbers', () => { 47 | 48 | const numberA = 222; 49 | const numberB = 333; 50 | const model = new CalculatorModel({ prevValue: numberA, nextValue: numberB, operator: ActionTypes.SUM }); 51 | const action = {type: ActionTypes.RESULT }; 52 | const reducer = calculatorReducer(model, action); 53 | const expectedData = reducer.get('display'); 54 | 55 | expect(expectedData).to.equal(555); 56 | 57 | }); 58 | 59 | it('Should Substract numbers', () => { 60 | 61 | const numberA = 100; 62 | const numberB = 2; 63 | const model = new CalculatorModel({ prevValue: numberA, nextValue: numberB, operator: ActionTypes.SUBSTRACT }); 64 | const action = {type: ActionTypes.RESULT }; 65 | const reducer = calculatorReducer(model, action); 66 | const expectedData = reducer.get('display'); 67 | 68 | expect(expectedData).to.equal(98); 69 | 70 | }); 71 | 72 | it('Should Multiply numbers', () => { 73 | 74 | const numberA = 60; 75 | const numberB = 53; 76 | const model = new CalculatorModel({ prevValue: numberA, nextValue: numberB, operator: ActionTypes.MULTIPLY }); 77 | const action = {type: ActionTypes.RESULT }; 78 | const reducer = calculatorReducer(model, action); 79 | const expectedData = reducer.get('display'); 80 | 81 | expect(expectedData).to.equal(3180); 82 | 83 | }); 84 | 85 | it('Should Multiply numbers', () => { 86 | 87 | const numberA = 200; 88 | const numberB = 50; 89 | const model = new CalculatorModel({ prevValue: numberA, nextValue: numberB, operator: ActionTypes.DIVIDE }); 90 | const action = {type: ActionTypes.RESULT }; 91 | const reducer = calculatorReducer(model, action); 92 | const expectedData = reducer.get('display'); 93 | 94 | expect(expectedData).to.equal(4); 95 | 96 | }); 97 | 98 | it('Should reset Display', () => { 99 | 100 | const numberA = 200; 101 | const numberB = 50; 102 | const model = new CalculatorModel({ prevValue: numberA, nextValue: numberB, operator: ActionTypes.DIVIDE }); 103 | const action = {type: ActionTypes.INPUT_OPERATION, payload:{value:ActionTypes.CLEAN }}; 104 | const reducer = calculatorReducer(model, action); 105 | const expectedData = reducer.get('display'); 106 | 107 | expect(expectedData).to.equal(0); 108 | 109 | }); 110 | 111 | it('Should convert to negative', () => { 112 | 113 | const numberA = 200; 114 | const model = new CalculatorModel({ prevValue: numberA, operator: ActionTypes.DIVIDE }); 115 | const action = {type: ActionTypes.INPUT_OPERATION, payload:{value:ActionTypes.CHANGE_SIGN }}; 116 | const reducer = calculatorReducer(model, action); 117 | const expectedData = reducer.get('display'); 118 | 119 | expect(expectedData).to.equal(-200); 120 | 121 | }); 122 | 123 | }); 124 | }); -------------------------------------------------------------------------------- /src/app/containers/Calculator/spec/Calculator.container.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { expect } from 'chai'; 3 | import { Provider } from 'react-redux'; 4 | import { mount } from 'enzyme'; 5 | 6 | import configureStore from 'store/ConfigureStore'; 7 | import Calculator from '../'; 8 | 9 | describe('Containers', () => { 10 | describe('', () => { 11 | 12 | it('Should have display & buttonpannel', () => { 13 | const store = configureStore([]); 14 | const component = mount( 15 | 16 | 17 | ); 18 | 19 | expect(component.find('Display')).to.have.lengthOf(1); 20 | expect(component.find('ButtonPannel')).to.have.lengthOf(1); 21 | }); 22 | 23 | 24 | it('Should display sum result with result button', () => { 25 | const store = configureStore([]); 26 | const component = mount( 27 | 28 | 29 | ); 30 | 31 | component.find('Button') 32 | .findWhere(n => n.props().value === '1') 33 | .simulate('click'); 34 | 35 | component.find('Button') 36 | .findWhere(n => n.props().value === '2') 37 | .simulate('click'); 38 | 39 | component.find('Button') 40 | .findWhere(n => n.props().value === '+') 41 | .simulate('click'); 42 | 43 | component.find('Button') 44 | .findWhere(n => n.props().value === '3') 45 | .simulate('click'); 46 | 47 | component.find('Button') 48 | .findWhere(n => n.props().value === '=') 49 | .simulate('click'); 50 | 51 | const Display = component.find('Display'); 52 | expect(Display.text()).to.equal('15'); 53 | }); 54 | 55 | 56 | it('Should display substract result with result button', () => { 57 | const store = configureStore([]); 58 | const component = mount( 59 | 60 | 61 | ); 62 | 63 | component.find('Button') 64 | .findWhere(n => n.props().value === '4') 65 | .simulate('click'); 66 | 67 | component.find('Button') 68 | .findWhere(n => n.props().value === '0') 69 | .simulate('click'); 70 | 71 | component.find('Button') 72 | .findWhere(n => n.props().value === '-') 73 | .simulate('click'); 74 | 75 | component.find('Button') 76 | .findWhere(n => n.props().value === '5') 77 | .simulate('click'); 78 | 79 | component.find('Button') 80 | .findWhere(n => n.props().value === '=') 81 | .simulate('click'); 82 | 83 | const Display = component.find('Display'); 84 | expect(Display.text()).to.equal('35'); 85 | }); 86 | 87 | it('Should display multiply result without click result button', () => { 88 | const store = configureStore([]); 89 | const component = mount( 90 | 91 | 92 | ); 93 | 94 | component.find('Button') 95 | .findWhere(n => n.props().value === '6') 96 | .simulate('click'); 97 | 98 | component.find('Button') 99 | .findWhere(n => n.props().value === 'x') 100 | .simulate('click'); 101 | 102 | component.find('Button') 103 | .findWhere(n => n.props().value === '7') 104 | .simulate('click'); 105 | 106 | component.find('Button') 107 | .findWhere(n => n.props().value === 'x') 108 | .simulate('click'); 109 | 110 | const Display = component.find('Display'); 111 | expect(Display.text()).to.equal('42'); 112 | }); 113 | 114 | 115 | it('Should display divide and multiply result with result button', () => { 116 | const store = configureStore([]); 117 | const component = mount( 118 | 119 | 120 | ); 121 | 122 | component.find('Button') 123 | .findWhere(n => n.props().value === '9') 124 | .simulate('click'); 125 | 126 | component.find('Button') 127 | .findWhere(n => n.props().value === '÷') 128 | .simulate('click'); 129 | 130 | component.find('Button') 131 | .findWhere(n => n.props().value === '3') 132 | .simulate('click'); 133 | 134 | component.find('Button') 135 | .findWhere(n => n.props().value === 'x') 136 | .simulate('click'); 137 | 138 | component.find('Button') 139 | .findWhere(n => n.props().value === '2') 140 | .simulate('click'); 141 | 142 | component.find('Button') 143 | .findWhere(n => n.props().value === '=') 144 | .simulate('click'); 145 | 146 | const Display = component.find('Display'); 147 | expect(Display.text()).to.equal('6'); 148 | }); 149 | 150 | 151 | it('Should display a negative number', () => { 152 | const store = configureStore([]); 153 | const component = mount( 154 | 155 | 156 | ); 157 | 158 | component.find('Button') 159 | .findWhere(n => n.props().value === '9') 160 | .simulate('click'); 161 | 162 | 163 | component.find('Button') 164 | .findWhere(n => n.props().value === '4') 165 | .simulate('click'); 166 | 167 | component.find('Button') 168 | .findWhere(n => n.props().value === '+/-') 169 | .simulate('click'); 170 | 171 | const Display = component.find('Display'); 172 | expect(Display.text()).to.equal('-94'); 173 | }); 174 | 175 | 176 | 177 | it('Should reset display', () => { 178 | const store = configureStore([]); 179 | const component = mount( 180 | 181 | 182 | ); 183 | 184 | component.find('Button') 185 | .findWhere(n => n.props().value === '1') 186 | .simulate('click'); 187 | 188 | component.find('Button') 189 | .findWhere(n => n.props().value === 'C') 190 | .simulate('click'); 191 | 192 | const Display = component.find('Display'); 193 | expect(Display.text()).to.equal('0'); 194 | }); 195 | 196 | 197 | }); 198 | }); 199 | -------------------------------------------------------------------------------- /src/app/containers/Calculator/styles.css: -------------------------------------------------------------------------------- 1 | @import '../../styles/base.css'; 2 | 3 | .Calculator { 4 | align-items: center; 5 | display: flex; 6 | height: 100%; 7 | justify-content: center; 8 | position: absolute; 9 | width: 100%; 10 | 11 | > div { 12 | width: 350px; 13 | } 14 | } -------------------------------------------------------------------------------- /src/app/containers/Main/actionTypes/index.js: -------------------------------------------------------------------------------- 1 | import { createActionType } from 'base'; 2 | 3 | export default createActionType([ 4 | 'LOGO_REQUEST','LOGO_SUCCESS','LOGO_ERROR', 5 | ]); 6 | -------------------------------------------------------------------------------- /src/app/containers/Main/actions/index.js: -------------------------------------------------------------------------------- 1 | import LogoAPI from '../api'; 2 | import ActionTypes from '../actionTypes'; 3 | 4 | const getLogo = () => ({ 5 | type: ActionTypes.LOGO_REQUEST, 6 | request: LogoAPI.fetchLogo() 7 | }); 8 | 9 | export default { 10 | getLogo 11 | }; 12 | -------------------------------------------------------------------------------- /src/app/containers/Main/actions/spec/Logo.action.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import action from '../'; 3 | import ActionTypes from '../../actionTypes'; 4 | 5 | import LogoAPI from '../../api'; 6 | 7 | describe('Actions', () => { 8 | describe('Actions logo', () => { 9 | it('Should create action fetchLogo', () => { 10 | 11 | const expectedAction = { 12 | type: ActionTypes.LOGO_REQUEST, 13 | request: LogoAPI.fetchLogo() 14 | }; 15 | 16 | expect(JSON.parse(JSON.stringify(action.getLogo()))) 17 | .to.deep.equal(JSON.parse(JSON.stringify(expectedAction))); 18 | 19 | }); 20 | 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/app/containers/Main/api/index.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch'; 2 | import { MainModel } from '../models'; 3 | 4 | const url = 'http://localhost:8000/mocks/logo.json'; 5 | 6 | const fetchLogo = () => ( 7 | fetch(url) 8 | .then(req => req.json()) 9 | .then(data => new MainModel(data)) 10 | .catch(err => console.log(err)) 11 | ); 12 | 13 | export default { 14 | fetchLogo 15 | }; 16 | -------------------------------------------------------------------------------- /src/app/containers/Main/index.jsx: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | import { connect } from 'react-redux'; 3 | import { PropTypes } from 'prop-types'; 4 | import React, { Component } from 'react'; 5 | import { bindActionCreators } from 'redux'; 6 | import { fetchRequiredActions } from 'base'; 7 | 8 | import Actions from './actions'; 9 | import Logo from '../../components/Logo'; 10 | import LinkButton from '../../components/LinkButton'; 11 | import styles from './styles.css'; 12 | 13 | const propTypes = { 14 | dispatch: PropTypes.func.isRequired, 15 | MainModel: PropTypes.instanceOf(Immutable.Record).isRequired 16 | }; 17 | 18 | export class Main extends Component { 19 | 20 | static requiredActions = [Actions.getLogo]; 21 | 22 | constructor (props) { 23 | super(props); 24 | this.actions = bindActionCreators(Actions, props.dispatch); 25 | } 26 | 27 | componentDidMount() { 28 | fetchRequiredActions( 29 | Main.requiredActions, 30 | this.props, 31 | !this.props.MainModel.name 32 | ); 33 | } 34 | 35 | render () { 36 | const LogoData = this.props.MainModel; 37 | 38 | return ( 39 | 40 | 41 | 46 | 47 | 48 | Examples 49 | 50 | 51 | 55 | 56 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | ); 73 | } 74 | } 75 | 76 | Main.propTypes = propTypes; 77 | 78 | export default connect( 79 | (state) => ({ MainModel: state.Main }) 80 | )(Main); 81 | -------------------------------------------------------------------------------- /src/app/containers/Main/models/index.js: -------------------------------------------------------------------------------- 1 | import { Record } from 'immutable'; 2 | 3 | const MainModel = Record({ 4 | id: 0, 5 | src: '', 6 | alt: '', 7 | name: '', 8 | width: 0, 9 | }); 10 | 11 | function setInitialState(initialState) { 12 | return initialState.Main = new MainModel(initialState.Main); 13 | } 14 | 15 | export { MainModel, setInitialState }; 16 | -------------------------------------------------------------------------------- /src/app/containers/Main/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { createReducer } from 'base'; 2 | import { MainModel } from '../models'; 3 | import ActionTypes from '../actionTypes'; 4 | 5 | const logoRequest = state => state; 6 | 7 | const logoError = (state) => state; 8 | 9 | const logoSuccess = (state, action) => { 10 | const data = action.payload; 11 | return state 12 | .set('id', data.id) 13 | .set('alt', data.alt) 14 | .set('src', data.src) 15 | .set('name', data.name) 16 | .set('width', data.width); 17 | }; 18 | 19 | const actionHandlers = { 20 | [ActionTypes.LOGO_REQUEST]: logoRequest, 21 | [ActionTypes.LOGO_SUCCESS]: logoSuccess, 22 | [ActionTypes.LOGO_ERROR]: logoError, 23 | }; 24 | 25 | export default createReducer(actionHandlers, new MainModel()); 26 | -------------------------------------------------------------------------------- /src/app/containers/Main/reducers/spec/Logo.reducers.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import mainReducer from '../'; 4 | import ActionTypes from '../../actionTypes'; 5 | import { MainModel } from '../../models/'; 6 | 7 | describe('Reducers', () => { 8 | describe('Logo reducer', () => { 9 | 10 | it('Should perform a request', () => { 11 | const action = { type: ActionTypes.LOGO_REQUEST }; 12 | expect(mainReducer([], action)).to.deep.equal([]); 13 | 14 | }); 15 | 16 | it('Should return the response data', () => { 17 | 18 | const data = { 19 | id: 1, 20 | alt: "React Base!", 21 | name: "ReactBaseLogo", 22 | width: 500, 23 | src: "/assets/images/react-base-logo.png" 24 | }; 25 | const action = { type: ActionTypes.LOGO_SUCESS, payload: data }; 26 | const model = new MainModel(data); 27 | const reducer = mainReducer(model, action); 28 | const src = reducer.get('src'); 29 | const expectedSrc = '/assets/images/react-base-logo.png'; 30 | 31 | expect(expectedSrc).to.equal(src); 32 | 33 | }); 34 | 35 | it('should execute request fail', () => { 36 | var logg = console.log; 37 | console.log = function (m) { }; 38 | const action = { type: ActionTypes.LOGO_ERROR }; 39 | expect(mainReducer([], action)).to.deep.equal([]); 40 | console.log = logg; 41 | }); 42 | 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/app/containers/Main/spec/Main.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { spy } from 'sinon'; 3 | import { expect } from 'chai'; 4 | import { mount } from 'enzyme'; 5 | import { Provider } from 'react-redux'; 6 | 7 | import Main from '../'; 8 | import { MainModel, setInitialState } from '../models'; 9 | import configureStore from 'store/ConfigureStore'; 10 | 11 | describe('Container', () => { 12 | 13 | describe('', () => { 14 | 15 | it('Should init Correctly', () => { 16 | 17 | spy(Main.prototype, 'componentDidMount'); 18 | 19 | const store = configureStore([]); 20 | const component = mount( 21 | 22 | 23 | 24 | ); 25 | 26 | expect(Main.prototype.componentDidMount.calledOnce).to.equal(true); 27 | 28 | }); 29 | 30 | it('Should have Logo & LinkButton', () => { 31 | 32 | setInitialState({}); 33 | const fakeStore = {}; 34 | fakeStore.Main = new MainModel({ id: 2222, src:'fakePath', alt:'aaaalt!', width:500 }); 35 | const store = configureStore([], fakeStore); 36 | const component = mount( 37 | 38 | 39 | 40 | ); 41 | 42 | expect(component.find('LinkButton')).to.have.lengthOf(2); 43 | //TODO Improve ajax testing 44 | setTimeout(() => { 45 | expect(component.find('Logo')).to.have.lengthOf(1); 46 | done(); 47 | }, 1000) 48 | }); 49 | 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/app/containers/Main/styles.css: -------------------------------------------------------------------------------- 1 | @import '../../styles/base.css'; 2 | 3 | .Main { 4 | align-items: center; 5 | display: flex; 6 | height: 100%; 7 | justify-content: center; 8 | position: absolute; 9 | width: 100%; 10 | 11 | > div { 12 | height: 400px; 13 | width: var(--logoWidth); 14 | } 15 | } 16 | 17 | .txt { 18 | color: #666; 19 | font-weight: 600; 20 | margin: 30px 0; 21 | text-align: center; 22 | 23 | img { 24 | width: 40px; 25 | } 26 | 27 | a { 28 | display: block; 29 | margin-top: 90px; 30 | } 31 | } 32 | 33 | .buttons { 34 | align-items: center; 35 | display: flex; 36 | justify-content: center; 37 | 38 | a { 39 | width: 46%; 40 | } 41 | } -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/actionTypes/index.js: -------------------------------------------------------------------------------- 1 | import { createActionType } from 'base'; 2 | 3 | export default createActionType([ 4 | 'WEATHERSTATIONS_REQUEST', 5 | 'WEATHERSTATIONS_SUCCESS', 6 | 'WEATHERSTATIONS_ERROR', 7 | 'WEATHERSTATION_SELECTED', 8 | 'FORECAST_REQUEST', 9 | 'FORECAST_SUCCESS', 10 | 'FORECAST_ERROR' 11 | ]); 12 | -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/actions/index.js: -------------------------------------------------------------------------------- 1 | import api from '../api'; 2 | import ActionTypes from '../actionTypes'; 3 | 4 | export const getWeatherStations = ({ params }) => ({ 5 | type: ActionTypes.WEATHERSTATIONS_REQUEST, 6 | request: api.fetchWeatherStations(params) 7 | }); 8 | 9 | export const weatherStationSelected = id => ({ 10 | type: ActionTypes.WEATHERSTATION_SELECTED, 11 | id: id 12 | }); 13 | 14 | export const getWeather = (lat, lng) => ({ 15 | type: ActionTypes.FORECAST_REQUEST, 16 | request: api.fetchWeather(lat, lng) 17 | }); 18 | 19 | -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/actions/spec/WeatherStations.action.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import * as Actions from '../'; 3 | import ActionTypes from '../../actionTypes'; 4 | 5 | import api from '../../api'; 6 | 7 | describe('Actions', () => { 8 | describe('Actions WeatherStations', () => { 9 | it('Should create action getWeatherStations', () => { 10 | 11 | const params = {}; 12 | const expectedAction = { 13 | type: ActionTypes.WEATHERSTATIONS_REQUEST, 14 | request: api.fetchWeatherStations(params) 15 | }; 16 | 17 | expect(JSON.parse(JSON.stringify(Actions.getWeatherStations(params)))) 18 | .to.deep.equal(JSON.parse(JSON.stringify(expectedAction))); 19 | 20 | }); 21 | 22 | it('Should create action weatherStationSelected', () => { 23 | 24 | const id = 15; 25 | const expectedAction = { 26 | type: ActionTypes.WEATHERSTATION_SELECTED, 27 | id: id 28 | }; 29 | 30 | expect(Actions.weatherStationSelected(id)).to.deep.equal(expectedAction); 31 | 32 | }); 33 | 34 | it('Should create action getWeather', () => { 35 | 36 | const params= { 37 | lat: 0, 38 | lng: 0 39 | }; 40 | 41 | const expectedAction = { 42 | type: ActionTypes.FORECAST_REQUEST, 43 | request: api.fetchWeather(params.lat, params.lng) 44 | }; 45 | 46 | expect(JSON.parse(JSON.stringify(Actions.getWeather(params.lat, params.lng)))) 47 | .to.deep.equal(JSON.parse(JSON.stringify(expectedAction))); 48 | 49 | }); 50 | 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/api/index.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch'; 2 | import * as helpers from '../helpers'; 3 | import config from '../config'; 4 | 5 | export default { 6 | 7 | fetchWeatherStations() { 8 | const urlApi = config.WEATHER_API_URL + 9 | '/box/city?bbox=-9,36.6,2.22,43,10&appid=' + 10 | config.WEATHER_API_KEY; 11 | 12 | return fetch(urlApi) 13 | .then(req => req.json()) 14 | .then(data => helpers.parseWeatherStations(data.list)); 15 | }, 16 | 17 | fetchWeather(lat, lng) { 18 | 19 | let urlCall = config.WEATHER_API_URL + 20 | '/forecast/daily?lat=' + 21 | lat +'&lon='+ 22 | lng + '&cnt=8&lang=es&units=metric&appid=' + 23 | config.WEATHER_API_KEY; 24 | 25 | return fetch(urlCall) 26 | .then(req => req.json()) 27 | .then(data => helpers.parseWeather(data)); 28 | 29 | } 30 | 31 | }; 32 | 33 | -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/api/spec/weatherStations.api.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import * as helpers from '../../helpers'; 3 | import weatherStationMock from 'mocks/weatherStation.json'; 4 | 5 | 6 | describe('Actions', () => { 7 | describe('WeatherStations action', () => { 8 | 9 | it('Should return weatherStation ', () => { 10 | let obj = { 11 | "last": { 12 | "dt": 1467917065 13 | }, 14 | "params": [], 15 | "station": { 16 | "coord": { 17 | "lat": 41.761, 18 | "lon": -2.4588 19 | }, 20 | "id": 10912, 21 | "name": "ED1ZAI-1", 22 | "status": 30, 23 | "type": 2 24 | } 25 | }; 26 | 27 | const returnedData = helpers.getDataWeatherStation(obj); 28 | 29 | expect(returnedData[0].id).to.equal(obj.station.id ); 30 | 31 | }); 32 | 33 | }); 34 | }); -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/components/ForecastDetail/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Map } from 'immutable'; 4 | import { PropTypes } from 'prop-types'; 5 | 6 | import ForecastDetailItem from '../ForecastDetailItem'; 7 | 8 | import styles from './styles.css'; 9 | 10 | const propTypes = { 11 | forecast: PropTypes.instanceOf(Map).isRequired 12 | }; 13 | 14 | export const ForecastDetail = ({ forecast }) => { 15 | 16 | const forecastList = forecast.valueSeq().map((weather, i) => ( 17 | 18 | )); 19 | 20 | return ( 21 | 22 | FORECAST 23 | { forecastList } 24 | 25 | ); 26 | }; 27 | 28 | ForecastDetail.propTypes = propTypes; 29 | 30 | export default connect( 31 | state => ({ 32 | forecast: state.WeatherStations.get('forecast') 33 | }) 34 | )(ForecastDetail); 35 | -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/components/ForecastDetail/spec/ForecastDetail.component.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import React from 'react'; 3 | import ReactShallowRenderer from 'react-test-renderer/shallow'; 4 | 5 | import { ForecastDetail } from '../'; 6 | import ForecastMock from 'mocks/forecast.json'; 7 | import { generateMap } from 'base/shared/ModelHelper'; 8 | import { ForecastModel } from '../../../models'; 9 | 10 | function setup(props) { 11 | 12 | let renderer = new ReactShallowRenderer(); 13 | renderer.render(); 14 | let output = renderer.getRenderOutput(); 15 | 16 | return { 17 | props, 18 | output, 19 | renderer 20 | }; 21 | } 22 | 23 | describe('component ', () => { 24 | describe('ForecastDetail', () => { 25 | it('should render correctly', () => { 26 | 27 | let props = { 28 | forecast: generateMap(ForecastMock.list, ForecastModel) 29 | }; 30 | 31 | const { output } = setup(props); 32 | 33 | expect(output.type).to.equal('div'); 34 | expect(output.props.children[1].size).to.equal(8); 35 | }); 36 | 37 | it('should render correctly station not selected', () => { 38 | 39 | let props = { 40 | forecast: generateMap(ForecastMock.list, ForecastModel) 41 | }; 42 | 43 | const { output } = setup(props); 44 | expect(output.type).to.equal('div'); 45 | }); 46 | 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/components/ForecastDetail/styles.css: -------------------------------------------------------------------------------- 1 | .title{ 2 | font-size: 20px; 3 | margin-left: 50px; 4 | } -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/components/ForecastDetailItem/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PropTypes } from 'prop-types'; 3 | import dateformat from 'dateformat'; 4 | import styles from './styles.css'; 5 | 6 | const propTypes = { 7 | item: PropTypes.object.isRequired 8 | }; 9 | 10 | const WeatherDetailItem = ({ item }) => ( 11 | 12 | 13 | 14 | { dateformat(new Date(item.dt*1000), 'dd/mm/yyyy') } 15 | 16 | 17 | Day Temperature: { `${ item.temp.day } ºC` } 18 | 19 | 20 | Min Daily Temperature: { `${ item.temp.min } ºC` } 21 | 22 | 23 | Max Daily Temperature: { `${ item.temp.max } ºC` } 24 | 25 | 26 | Night Temperature: { `${ item.temp.night } ºC` } 27 | 28 | 29 | Evening Daily Temperature: { `${ item.temp.eve } ºC` } 30 | 31 | 32 | Morning Daily Temperature: { `${ item.temp.morn } ºC` } 33 | 34 | 35 | ); 36 | 37 | WeatherDetailItem.propTypes= propTypes; 38 | 39 | export default WeatherDetailItem; 40 | -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/components/ForecastDetailItem/spec/forecastDetailItem.component.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import React from 'react'; 3 | import ReactShallowRenderer from 'react-test-renderer/shallow'; 4 | 5 | import ForecastDetailItem from '../'; 6 | import ForecastMock from 'mocks/forecast.json'; 7 | 8 | function setup(props) { 9 | 10 | function dispatch() { } 11 | 12 | let renderer = new ReactShallowRenderer(); 13 | renderer.render(); 14 | let output = renderer.getRenderOutput(); 15 | 16 | return { 17 | props, 18 | output, 19 | renderer 20 | }; 21 | } 22 | 23 | describe('component ', () => { 24 | describe('ForecastDetailItem', () => { 25 | it('should render correctly', () => { 26 | 27 | let props = { 28 | item: ForecastMock.list[0] 29 | }; 30 | 31 | const { output } = setup(props); 32 | expect(output.type).to.equal('div'); 33 | }); 34 | 35 | 36 | }); 37 | }); -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/components/ForecastDetailItem/styles.css: -------------------------------------------------------------------------------- 1 | .weatherDetailRow{ 2 | border: 3px solid #FAB10A; 3 | border-radius: 15px; 4 | float: left; 5 | height: 270px; 6 | margin: 10px 10px 10px 10px; 7 | padding: 10px 10px 10px 10px; 8 | width: 265px; 9 | } 10 | 11 | .dayBox{ 12 | font-size: 20px; 13 | font-weight: bold; 14 | } 15 | 16 | .rowItem{ 17 | display: block; 18 | float: none; 19 | } 20 | -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/components/MapBox/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PropTypes } from 'prop-types'; 3 | import { pure } from 'recompose'; 4 | 5 | import GoogleMap from 'google-map-react'; 6 | 7 | import styles from './styles.css'; 8 | 9 | import MapMarker from '../MapMarker'; 10 | 11 | const propTypes = { 12 | key: PropTypes.string.isRequired, 13 | center: PropTypes.object, 14 | zoom: PropTypes.number, 15 | onClick: PropTypes.func.isRequired, 16 | stations: PropTypes.object.isRequired 17 | }; 18 | 19 | const defaultProps = { 20 | key: 'AIzaSyAUrK9ZaUL0Ga-RZYYFukBuTNm0qO3GbNI', 21 | center: { lat: 40.4047789, lng: -3.653974 }, 22 | zoom: 6 23 | }; 24 | 25 | export const MapBox = props => { 26 | const mapMarkerList = props.stations.valueSeq().map(item => ( 27 | 34 | )); 35 | 36 | return ( 37 | 38 | 45 | { mapMarkerList } 46 | 47 | 48 | ); 49 | }; 50 | 51 | MapBox.propTypes = propTypes; 52 | MapBox.defaultProps = defaultProps; 53 | 54 | export default pure(MapBox); 55 | -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/components/MapBox/spec/MapBox.component.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import React from 'react'; 3 | import ReactShallowRenderer from 'react-test-renderer/shallow'; 4 | 5 | import weatherStationsMock from 'mocks/weatherStations.json'; 6 | 7 | import { MapBox } from '../'; 8 | import * as helpers from '../../../helpers'; 9 | 10 | const setup = props => { 11 | 12 | let renderer = new ReactShallowRenderer(); 13 | renderer.render(); 14 | let output = renderer.getRenderOutput(); 15 | 16 | return { 17 | props, 18 | output, 19 | renderer 20 | }; 21 | }; 22 | 23 | describe('component ', () => { 24 | describe('MapBox', () => { 25 | it('should render correctly', () => { 26 | 27 | const parsedWeatherStation = helpers.parseWeatherStations(weatherStationsMock); 28 | 29 | let props = { 30 | stations: parsedWeatherStation, 31 | stationSelected: 14 32 | }; 33 | 34 | const { output } = setup(props); 35 | 36 | expect(output.type).to.equal('div'); 37 | }); 38 | 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/components/MapBox/styles.css: -------------------------------------------------------------------------------- 1 | .mapBox{ 2 | float: left; 3 | height: 650px; 4 | width: 800px; 5 | } -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/components/MapInfoWindow/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PropTypes } from 'prop-types'; 3 | import { pure } from 'recompose'; 4 | 5 | import styles from './styles.css'; 6 | 7 | const propTypes = { 8 | main: PropTypes.object.isRequired 9 | }; 10 | 11 | export const MapInfoWindow = props => { 12 | const { temp, humidity, pressure } = props.main; 13 | 14 | return ( 15 | 16 | 17 | Temp: { `${ temp } ºC` } 18 | 19 | 20 | Humidity: { `${ humidity } %` } 21 | 22 | 23 | Pressure: { `${ pressure } hpa` } 24 | 25 | 26 | ); 27 | }; 28 | 29 | MapInfoWindow.propTypes = propTypes; 30 | 31 | export default pure(MapInfoWindow); 32 | -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/components/MapInfoWindow/spec/MapInfoWindow.component.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import React from 'react'; 3 | import ShallowRenderer from 'react-test-renderer/shallow'; 4 | 5 | import weatherStationsMock from 'mocks/weatherStations.json'; 6 | 7 | import { MapInfoWindow } from '../'; 8 | 9 | const setup = data => { 10 | 11 | const props = { 12 | main: data 13 | }; 14 | 15 | const renderer = new ShallowRenderer(); 16 | renderer.render(); 17 | const output = renderer.getRenderOutput(); 18 | 19 | return { 20 | props, 21 | output, 22 | renderer 23 | }; 24 | }; 25 | 26 | describe('component ', () => { 27 | describe('MapInfoWindow', () => { 28 | it('should render correctly', () => { 29 | const { output } = setup(weatherStationsMock[0].main); 30 | expect(output.type).to.equal('div'); 31 | }); 32 | 33 | it('should render with data incompconste', () => { 34 | const { output } = setup(weatherStationsMock[1].main); 35 | expect(output.type).to.equal('div'); 36 | }); 37 | 38 | it('should render data not found', () => { 39 | const { output } = setup(weatherStationsMock[2].main); 40 | expect(output.type).to.equal('div'); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/components/MapInfoWindow/styles.css: -------------------------------------------------------------------------------- 1 | .infoWindowBox { 2 | background-color: white; 3 | width: 100px; 4 | padding-top: 10px; 5 | padding-bottom: 10px; 6 | } 7 | 8 | .infoWindowRow{ 9 | display: block; 10 | height: 20px; 11 | } -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/components/MapMarker/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PropTypes } from 'prop-types'; 3 | import { pure } from 'recompose'; 4 | 5 | import { stationStyle, 6 | stationCircleStyle, stationCircleStyleHover, 7 | stationInfoWindowStyleHover, stationInfoWindowStyle } from './stylesMarker'; 8 | 9 | import MapInfoWindow from '../MapInfoWindow'; 10 | 11 | const propTypes = { 12 | zIndex: PropTypes.number, 13 | main: PropTypes.object.isRequired, 14 | $hover: PropTypes.bool 15 | }; 16 | 17 | export const MapMarker = ({ zIndex, main, $hover }) => { 18 | 19 | const style = { 20 | ...stationStyle, 21 | zIndex: $hover ? 1000 : zIndex 22 | }; 23 | const circleStyle = $hover ? stationCircleStyleHover : stationCircleStyle; 24 | const InfoWindowStyle = $hover ? stationInfoWindowStyleHover : stationInfoWindowStyle; 25 | 26 | const info = ( 27 | 28 | 29 | 30 | ); 31 | return ( 32 | 33 | 34 | 35 | { $hover && info } 36 | 37 | ); 38 | 39 | }; 40 | 41 | MapMarker.propTypes = propTypes; 42 | 43 | export default pure(MapMarker); 44 | -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/components/MapMarker/spec/MapMarker.component.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import React from 'react'; 3 | import ReactShallowRenderer from 'react-test-renderer/shallow'; 4 | 5 | import weatherStationsMock from 'mocks/weatherStations.json'; 6 | import { MapMarker } from '../'; 7 | 8 | 9 | const setup = props => { 10 | 11 | const renderer = new ReactShallowRenderer(); 12 | renderer.render(); 13 | const output = renderer.getRenderOutput(); 14 | 15 | return { 16 | props, 17 | output, 18 | renderer 19 | }; 20 | }; 21 | 22 | describe('component ', () => { 23 | describe('MapMarker', () => { 24 | it('should render correctly', () => { 25 | const props = { 26 | main: weatherStationsMock[0].main, 27 | zIndex: 2, 28 | $hover: true 29 | }; 30 | 31 | const { output } = setup(props); 32 | expect(output.type).to.equal('div'); 33 | }); 34 | 35 | 36 | it('should render correctly hover false', () => { 37 | const props = { 38 | main: weatherStationsMock[0].main, 39 | zIndex: 2, 40 | $hover: false 41 | }; 42 | 43 | const { output } = setup(props); 44 | expect(output.type).to.equal('div'); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/components/MapMarker/styles.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/atSistemas/react-base/d594a3538695f0ac9c45179dbd5d69f728e40b52/src/app/containers/WeatherStations/components/MapMarker/styles.css -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/components/MapMarker/stylesMarker.js: -------------------------------------------------------------------------------- 1 | const CIRCLE_SIZE = 15; 2 | const STICK_SIZE = 6; 3 | const STICK_WIDTH = 3; 4 | 5 | const stationStyle = { 6 | position: 'absolute', 7 | width: CIRCLE_SIZE, 8 | height: CIRCLE_SIZE + STICK_SIZE, 9 | left: -CIRCLE_SIZE / 2, 10 | top: -(CIRCLE_SIZE + STICK_SIZE) 11 | }; 12 | 13 | const stationCircleStyle = { 14 | position: 'absolute', 15 | left: 0, 16 | top: 0, 17 | width: CIRCLE_SIZE, 18 | height: CIRCLE_SIZE, 19 | border: '10px solid #f44336', 20 | borderRadius: CIRCLE_SIZE, 21 | backgroundColor: 'white', 22 | textAlign: 'center', 23 | color: '#3f51b5', 24 | fontSize: 12, 25 | fontWeight: 'bold', 26 | padding: 0, 27 | cursor: 'pointer', 28 | boxShadow: '0 0 0 1px white' 29 | }; 30 | 31 | 32 | const stationCircleStyleHover = { 33 | ...stationCircleStyle, 34 | border: '10px solid #FAB10A', 35 | color: '#f44336' 36 | }; 37 | 38 | const stationStickStyleShadow = { 39 | position: 'absolute', 40 | left: CIRCLE_SIZE / 2 - STICK_WIDTH / 2, 41 | top: CIRCLE_SIZE, 42 | width: STICK_WIDTH, 43 | height: STICK_SIZE, 44 | backgroundColor: '#f44336', 45 | boxShadow: '0 0 0 1px white' 46 | }; 47 | 48 | 49 | const stationStickStyle = { 50 | position: 'absolute', 51 | left: CIRCLE_SIZE / 2 - STICK_WIDTH / 2, 52 | top: CIRCLE_SIZE, 53 | width: STICK_WIDTH, 54 | height: STICK_SIZE, 55 | backgroundColor: '#f44336' 56 | }; 57 | 58 | const stationStickStyleHover = { 59 | ...stationStickStyle, 60 | backgroundColor: '#3f51b5' 61 | }; 62 | 63 | const stationInfoWindowStyle = { 64 | position: 'absolute', 65 | left: CIRCLE_SIZE / 2 - STICK_WIDTH / 2, 66 | top: CIRCLE_SIZE, 67 | width:'100px', 68 | height: '80px', 69 | backgroundColor: '#f44336', 70 | display: 'none' 71 | }; 72 | 73 | const stationInfoWindowStyleHover = { 74 | ...stationStickStyle, 75 | backgroundColor: '#3f51b5', 76 | display:'block' 77 | }; 78 | 79 | export { 80 | stationStyle, 81 | stationCircleStyle, stationCircleStyleHover, 82 | stationStickStyle, stationStickStyleHover,stationStickStyleShadow, 83 | CIRCLE_SIZE, STICK_SIZE, 84 | stationInfoWindowStyleHover, stationInfoWindowStyle 85 | }; -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/components/WeatherStationDetails/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Record } from 'immutable'; 3 | import { PropTypes } from 'prop-types'; 4 | import dateformat from 'dateformat'; 5 | import styles from './styles.css'; 6 | 7 | const propTypes = { 8 | details: PropTypes.instanceOf(Record).isRequired 9 | }; 10 | 11 | const WeatherStationDetails = props => { 12 | const { details } = props; 13 | 14 | const windDirections = ["N","NNE","NE","ENE","E","ESE", "SE", "SSE","S","SSW","SW","WSW","W","WNW","NW","NNW"]; 15 | 16 | const wind = details.get('wind'); 17 | const windDirectionDegrees = parseInt((wind.deg/22.5) + 0.5); 18 | 19 | const windDirection = windDirections[windDirectionDegrees % 16]; 20 | 21 | const d = new Date(details.dt*1000); 22 | 23 | return ( 24 | 25 | Actual Data of Weather Station 26 | 27 | Temperature: { Math.round(details.get('temp')- 273.15) } ºC 28 | 29 | 30 | Humidity: { details.get('humidity') } % 31 | 32 | 33 | Temperature { details.get('pressure') } hpa 34 | 35 | 36 | Wind Speed { wind.speed } m/s 37 | 38 | 39 | Wind Direction { windDirection } 40 | 41 | 42 | Last Date Updated: 43 | { dateformat(d, 'HH:MM:ss dd/mm/yyyy') } 44 | 45 | 46 | ); 47 | }; 48 | 49 | WeatherStationDetails.propTypes = propTypes; 50 | 51 | export default WeatherStationDetails; 52 | -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/components/WeatherStationDetails/spec/WeatherStationDetails.component.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import React from 'react'; 3 | import ReactShallowRenderer from 'react-test-renderer/shallow'; 4 | 5 | import WeatherStationDetails from '../'; 6 | import WeatherStationMock from 'mocks/weatherStation.json'; 7 | 8 | import * as helpers from '../../../helpers'; 9 | import { WeatherStationDetailsModel } from '../../../models'; 10 | import { generateMap } from 'base/shared/ModelHelper'; 11 | 12 | function setup(props) { 13 | 14 | function dispatch() { } 15 | 16 | let renderer = new ReactShallowRenderer(); 17 | renderer.render(); 18 | let output = renderer.getRenderOutput(); 19 | 20 | return { 21 | props, 22 | output, 23 | renderer 24 | }; 25 | } 26 | 27 | describe('component ', () => { 28 | describe('WeatherStationDetails', () => { 29 | it('should render correctly', () => { 30 | 31 | let item = helpers.getDataWeatherStation(WeatherStationMock); 32 | 33 | let itemMap = generateMap(item, WeatherStationDetailsModel) 34 | 35 | let element = itemMap.keySeq().first(); 36 | 37 | let props = { 38 | details: itemMap.get(element) 39 | }; 40 | 41 | const { output } = setup(props); 42 | expect(output.type).to.equal('div'); 43 | }); 44 | 45 | 46 | }); 47 | }); -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/components/WeatherStationDetails/styles.css: -------------------------------------------------------------------------------- 1 | .weatherDetailBox{ 2 | border: 3px solid #FAB10A; 3 | border-radius: 15px; 4 | float: left; 5 | margin: 10px 10px 10px 10px; 6 | min-height: 200px; 7 | min-width: 200px; 8 | padding: 10px 10px 10px 10px; 9 | } 10 | 11 | .weatherDetailRow{ 12 | 13 | height: 40px; 14 | width: 200px; 15 | } 16 | 17 | .clear{ 18 | clear: both; 19 | } -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | GOOGLE_MAPS_API_KEY: 'AIzaSyAUrK9ZaUL0Ga-RZYYFukBuTNm0qO3GbNI', 3 | WEATHER_API_URL: 'http://api.openweathermap.org/data/2.5', 4 | WEATHER_API_KEY: '3cc68ed2bf53673eaf25aae12fb16469' 5 | }; 6 | 7 | export default config; -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/helpers/index.jsx: -------------------------------------------------------------------------------- 1 | import { generateMap, generateImmutable } from 'base/shared/ModelHelper'; 2 | import { WeatherStationsModel, ForecastModel, WeatherStationDetailsModel } from '../models'; 3 | 4 | export const parseWeatherStations = data => ( 5 | generateImmutable(data, WeatherStationsModel) 6 | ); 7 | 8 | export const parseWeatherStation = data => { 9 | const dataParsed = getDataWeatherStation(data); 10 | return generateMap(dataParsed, WeatherStationDetailsModel); 11 | }; 12 | 13 | export const parseWeather = data => { 14 | for (let i=0; i { 21 | let obj = {}; 22 | if (data.params.indexOf('temp') > -1) { 23 | obj.temp = data.last.main.temp; 24 | } 25 | 26 | if (data.params.indexOf('humidity') > -1) { 27 | obj.humidity = data.last.main.humidity; 28 | } 29 | 30 | if (data.params.indexOf('pressure') > -1) { 31 | obj.pressure = data.last.main.pressure; 32 | } 33 | 34 | if (data.params.indexOf('wind') > -1) { 35 | obj.wind = data.last.wind; 36 | } 37 | 38 | if (data.params.indexOf('visibility') > -1) { 39 | obj.visibility = data.last.visibility; 40 | } 41 | 42 | if (data.params.indexOf('rain') > -1) { 43 | obj.rain = data.last.rain; 44 | } 45 | 46 | obj.id = data.station.id; 47 | obj.dt = data.last.dt; 48 | 49 | let list = [obj]; 50 | 51 | return list; 52 | 53 | }; 54 | -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/helpers/spec/WeatherStations.helpers.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import Immutable from 'immutable'; 3 | import weatherStationsMock from 'mocks/weatherStations.json'; 4 | import weatherStationMock from 'mocks/weatherStation.json'; 5 | import ForecastMock from 'mocks/forecast.json'; 6 | 7 | import * as helpers from '..'; 8 | 9 | describe('Helpers', () => { 10 | describe('WeatherStations helpers', () => { 11 | 12 | it('parseWeatherStations return a Map of instances of WeatherStationsModel',() =>{ 13 | 14 | const returnedValue = helpers.parseWeatherStations(weatherStationsMock); 15 | 16 | expect(Immutable.Map.isMap(returnedValue)).to.equal(true); 17 | 18 | const item = returnedValue.first(); 19 | 20 | expect(item.get('id')).to.equal(weatherStationsMock[0].id); 21 | expect(item.get('stationId')).to.equal(weatherStationsMock[0].stationId); 22 | expect(item.get('distance')).to.equal(weatherStationsMock[0].distance); 23 | expect(typeof(item.get('last'))).to.equal('object'); 24 | expect(typeof(item.get('station'))).to.equal('object'); 25 | 26 | }); 27 | 28 | it('parseWeatherStation return a Map of instances of WeatherStationDetailsModel',() =>{ 29 | 30 | const returnedValue = helpers.parseWeatherStation(weatherStationMock); 31 | 32 | const item = returnedValue.first(); 33 | 34 | expect(item.get('temp')).to.equal(weatherStationMock.last.main.temp); 35 | expect(item.get('pressure')).to.equal(weatherStationMock.last.main.pressure); 36 | expect(item.get('humidity')).to.equal(weatherStationMock.last.main.humidity); 37 | expect(typeof(item.get('wind'))).to.equal('object'); 38 | expect(typeof(item.get('visibility'))).to.equal('object'); 39 | expect(typeof(item.get('rain'))).to.equal('object'); 40 | expect(item.get('dt')).to.equal(weatherStationMock.last.dt); 41 | 42 | }); 43 | 44 | it('parseWeather return a Map of instances of ForecastModel',() =>{ 45 | 46 | const returnedValue = helpers.parseWeather(ForecastMock); 47 | 48 | const item = returnedValue.first(); 49 | expect(item.get('clouds')).to.equal(ForecastMock.list[0].clouds); 50 | expect(item.get('deg')).to.equal(ForecastMock.list[0].deg); 51 | expect(item.get('dt')).to.equal(ForecastMock.list[0].dt); 52 | 53 | expect(item.get('humidity')).to.equal(ForecastMock.list[0].humidity); 54 | expect(item.get('pressure')).to.equal(ForecastMock.list[0].pressure); 55 | expect(item.get('speed')).to.equal(ForecastMock.list[0].speed); 56 | expect(typeof(item.get('temp'))).to.equal('object'); 57 | expect(typeof(item.get('weather'))).to.equal('object'); 58 | 59 | }); 60 | 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/index.jsx: -------------------------------------------------------------------------------- 1 | import { Map } from 'immutable'; 2 | import { connect } from 'react-redux'; 3 | import { PropTypes } from 'prop-types'; 4 | import React, { Component } from 'react'; 5 | import { bindActionCreators } from 'redux'; 6 | 7 | import { fetchRequiredActions } from 'base'; 8 | import * as Actions from './actions'; 9 | import MapBox from './components/MapBox'; 10 | import ForecastDetail from './components/ForecastDetail'; 11 | import styles from './styles.css'; 12 | 13 | export class WeatherStations extends Component { 14 | 15 | static propTypes = { 16 | dispatch: PropTypes.func.isRequired, 17 | stations: PropTypes.instanceOf(Map).isRequired, 18 | selected: PropTypes.number.isRequired 19 | }; 20 | 21 | static requiredActions = [Actions.getWeatherStations]; 22 | 23 | constructor (props) { 24 | super(props); 25 | this.actions = bindActionCreators(Actions, props.dispatch); 26 | this.onClick = this.onClick.bind(this); 27 | } 28 | 29 | componentDidMount() { 30 | fetchRequiredActions( 31 | WeatherStations.requiredActions, 32 | this.props, 33 | !this.props.stations.size 34 | ); 35 | } 36 | 37 | onClick(key, childProps) { 38 | const newSelected = parseInt(key); 39 | 40 | if (this.props.selected !== newSelected) { 41 | this.actions.getWeather(childProps.lat, childProps.lng); 42 | this.actions.weatherStationSelected(newSelected); 43 | } 44 | } 45 | 46 | render () { 47 | const { stations, selected } = this.props; 48 | return ( 49 | 50 | 54 | { !!~selected && } 55 | 56 | ); 57 | } 58 | } 59 | 60 | export default connect( 61 | state => ({ 62 | stations: state.WeatherStations.get('data'), 63 | selected: state.WeatherStations.get('stationSelected') 64 | }) 65 | )(WeatherStations); 66 | -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/models/ForecastModel.js: -------------------------------------------------------------------------------- 1 | import { Record } from 'immutable'; 2 | 3 | const ForecastModel = Record({ 4 | clouds: 0, 5 | deg: 0, 6 | dt: 0, 7 | humidity: 0, 8 | pressure:0, 9 | speed:0, 10 | temp:{}, 11 | weather: [] 12 | }); 13 | 14 | export default ForecastModel; -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/models/WeatherStationDetailsModel.js: -------------------------------------------------------------------------------- 1 | import { Record } from 'immutable'; 2 | 3 | const WeatherStationDetailsModel = Record({ 4 | id:-1, 5 | temp: 14, 6 | pressure: 0, 7 | humidity: 0, 8 | wind: null, 9 | visibility: null, 10 | rain:null, 11 | dt: 0 12 | }); 13 | 14 | export default WeatherStationDetailsModel; -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/models/WeatherStationsModel.js: -------------------------------------------------------------------------------- 1 | import { Record } from 'immutable'; 2 | 3 | const WeatherStationsModel = Record({ 4 | id: 0, 5 | stationId: 0, 6 | distance: 0, 7 | last: null, 8 | station: null, 9 | coord: null, 10 | main: null, 11 | }); 12 | 13 | export default WeatherStationsModel; -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/models/index.js: -------------------------------------------------------------------------------- 1 | import { Record, Map } from 'immutable'; 2 | import { generateImmutable } from 'base/shared/ModelHelper'; 3 | import WeatherStationsModel from './WeatherStationsModel'; 4 | import ForecastModel from './ForecastModel'; 5 | import WeatherStationDetailsModel from './WeatherStationDetailsModel'; 6 | 7 | const WeatherStationsCollection = Record({ 8 | data: Map(), 9 | stationSelected:-1, 10 | forecast: Map(), 11 | weatherStationDetails: Map() 12 | }); 13 | 14 | const setInitialState = initialState => initialState.WeatherStations = new WeatherStationsCollection({ 15 | data: generateImmutable(initialState.WeatherStations.data, WeatherStationsModel), 16 | forecast: generateImmutable(initialState.WeatherStations.forecast, ForecastModel), 17 | weatherStationDetails: generateImmutable(initialState.WeatherStations.weatherStationDetails, WeatherStationDetailsModel) 18 | }); 19 | 20 | export { WeatherStationsModel, WeatherStationsCollection, ForecastModel, WeatherStationDetailsModel, setInitialState }; 21 | -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { createReducer } from 'base'; 2 | import ActionTypes from '../actionTypes'; 3 | import { WeatherStationsCollection } from '../models'; 4 | 5 | const weatherStationsRequest = state => state; 6 | 7 | const weatherStationsError = state => state; 8 | 9 | const weatherStationsSuccess = (state, action) => ( 10 | state 11 | .set('data', action.payload) 12 | .set('stationSelected', -1) 13 | ); 14 | 15 | const weatherStationSelected = (state, action) => ( 16 | state.set('stationSelected', action.id) 17 | ); 18 | 19 | const forecastRequest = state => state; 20 | 21 | const forecastError = state => state; 22 | 23 | const forecastSuccess = (state, action) => ( 24 | state.update ('forecast', () => action.payload) 25 | ); 26 | 27 | const actionHandlers = { 28 | [ActionTypes.WEATHERSTATIONS_REQUEST]: weatherStationsRequest, 29 | [ActionTypes.WEATHERSTATIONS_SUCCESS]: weatherStationsSuccess, 30 | [ActionTypes.WEATHERSTATIONS_ERROR]: weatherStationsError, 31 | [ActionTypes.WEATHERSTATION_SELECTED]: weatherStationSelected, 32 | [ActionTypes.FORECAST_REQUEST]: forecastRequest, 33 | [ActionTypes.FORECAST_SUCCESS]: forecastSuccess, 34 | [ActionTypes.FORECAST_ERROR]: forecastError, 35 | }; 36 | 37 | export default createReducer(actionHandlers, new WeatherStationsCollection()); 38 | -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/reducers/spec/WeatherStations.reducer.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import reducer from '../'; 3 | import ActionTypes from '../../actionTypes'; 4 | import weatherStationsMock from 'mocks/weatherStations.json'; 5 | import weatherStationMock from 'mocks/weatherStation.json'; 6 | import ForecastMock from 'mocks/forecast.json'; 7 | 8 | import * as helpers from '../../helpers'; 9 | 10 | import { WeatherStationsCollection, WeatherStationsModel, ForecastModel } from '../../models'; 11 | import { generateMap } from 'base/shared/ModelHelper'; 12 | 13 | describe('Reducers', () => { 14 | describe('WeatherStations reducer', () => { 15 | 16 | it('Should return the initial state', () => { 17 | 18 | const action = { 19 | type: ActionTypes.WEATHERSTATIONS_REQUEST 20 | }; 21 | 22 | expect(reducer([], action)).to.deep.equal([]); 23 | }); 24 | 25 | it('Should return the state WeatherStations updated request successfull', () => { 26 | 27 | const data = [weatherStationsMock[0], weatherStationsMock[1]]; 28 | 29 | const action = { 30 | type: ActionTypes.WEATHERSTATIONS_SUCCESS, 31 | payload: generateMap(data, WeatherStationsModel) 32 | }; 33 | 34 | const model = new WeatherStationsCollection(); 35 | 36 | const returnedData = reducer(model, action).get('data'); 37 | 38 | let element = returnedData.keySeq().first(); 39 | 40 | expect(returnedData.get(element).id).to.deep.equal(data[0].id); 41 | }); 42 | 43 | 44 | it('should return the state of WeatherStations request fail', () => { 45 | 46 | const action = { 47 | type: ActionTypes.WEATHERSTATIONS_ERROR 48 | }; 49 | 50 | expect(reducer([], action)).to.deep.equal([]); 51 | }); 52 | 53 | it('Should return the initial state when request forecast ', () => { 54 | 55 | const action = { 56 | type: ActionTypes.FORECAST_REQUEST 57 | }; 58 | 59 | expect(reducer([], action)).to.deep.equal([]); 60 | }); 61 | 62 | it('Should return the state forecast updated request successfull', () => { 63 | 64 | const action = { 65 | type: ActionTypes.FORECAST_SUCCESS, 66 | payload: generateMap(ForecastMock.list, ForecastModel) 67 | }; 68 | const model = new WeatherStationsCollection(); 69 | 70 | const returnedData = reducer(model, action).get('forecast'); 71 | let element = returnedData.keySeq().first(); 72 | 73 | expect(returnedData.get(element).dt).to.deep.equal(ForecastMock.list[0].dt); 74 | }); 75 | 76 | 77 | it('should return the state of forecast request fail', () => { 78 | 79 | const action = { 80 | type: ActionTypes.FORECAST_ERROR 81 | }; 82 | 83 | expect(reducer([], action)).to.deep.equal([]); 84 | }); 85 | 86 | it('should return the state of station selected', () => { 87 | 88 | const action = { 89 | type: ActionTypes.WEATHERSTATION_SELECTED, 90 | id: 15 91 | }; 92 | 93 | const model = new WeatherStationsCollection(); 94 | 95 | const returnedData = reducer(model, action).get('stationSelected'); 96 | 97 | expect(returnedData).to.deep.equal(15); 98 | }); 99 | 100 | 101 | 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/spec/WeatherStations.container.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import React from 'react'; 3 | import ReactShallowRenderer from 'react-test-renderer/shallow'; 4 | 5 | import { WeatherStationsModel, setInitialState } from '../models'; 6 | import { WeatherStations } from '..'; 7 | import weatherStationsMock from 'mocks/weatherStations.json'; 8 | import weatherStationMock from 'mocks/weatherStation.json'; 9 | import forecastMock from 'mocks/forecast.json'; 10 | 11 | import * as helpers from '../helpers'; 12 | 13 | function setup() { 14 | 15 | function dispatch() { } 16 | let initialState = { 17 | WeatherStations: { 18 | data: weatherStationsMock, 19 | forecast: forecastMock.list, 20 | weatherStationDetails: helpers.getDataWeatherStation(weatherStationMock), 21 | StationSelected: 15 22 | } 23 | }; 24 | 25 | let initialStateProps = setInitialState(initialState); 26 | 27 | let props = { 28 | dispatch: dispatch, 29 | WeatherStationsModel: initialStateProps, 30 | WeatherStationDetailsState: initialStateProps.weatherStationDetails, 31 | StationSelected: initialStateProps.stationSelected 32 | }; 33 | 34 | let renderer = new ReactShallowRenderer(); 35 | renderer.render(); 36 | let output = renderer.getRenderOutput(); 37 | 38 | return { 39 | props, 40 | output, 41 | renderer 42 | }; 43 | } 44 | 45 | describe('containers', () => { 46 | describe('WeatherStations', () => { 47 | it('should render correctly', () => { 48 | const { output } = setup(); 49 | expect(output.type).to.equal('div'); 50 | }); 51 | }); 52 | }); -------------------------------------------------------------------------------- /src/app/containers/WeatherStations/styles.css: -------------------------------------------------------------------------------- 1 | .WeatherStations { 2 | margin: 30px 30px 30px 30px; 3 | } 4 | 5 | .clear{ 6 | clear: both; 7 | } -------------------------------------------------------------------------------- /src/app/styles/base.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --logoWidth: 500px; 3 | } 4 | 5 | body { 6 | background-color: #fff; 7 | color: #111; 8 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Helvetica, sans-serif; 9 | line-height: 1.5; 10 | margin: 0; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/styles/index.js: -------------------------------------------------------------------------------- 1 | require('normalize.css'); 2 | require('../containers/Main/styles'); 3 | require('../components/Logo/styles'); 4 | -------------------------------------------------------------------------------- /src/base/client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { hydrate } from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { Router, browserHistory } from 'react-router'; 5 | 6 | import InitialState from 'store/InitialState'; 7 | import ConfigureStore from 'store/ConfigureStore'; 8 | import routes from '../routes'; 9 | 10 | const store = ConfigureStore(browserHistory, InitialState); 11 | 12 | hydrate( 13 | 14 | 15 | , 16 | document.getElementById('root') 17 | ); 18 | -------------------------------------------------------------------------------- /src/base/conf/.react-base/logo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var dir = process.cwd()+'/src/base/conf/.react-base/logo.txt'; 5 | console.log(fs.readFileSync(dir, 'utf-8')); 6 | -------------------------------------------------------------------------------- /src/base/conf/.react-base/logo.txt: -------------------------------------------------------------------------------- 1 | 2 | ~~~~:???????= 3 | ::~~~~~===~~??+ 4 | :::::~~~~~~~~~~??= 5 | ~::::::~~~~~~~======~ 7II 6 | ,::~:::::~===========~: 7II 7 | :::~:=::~:::=====:~~~: 7II ,7$I ,7I7$I ?$7 II7$$7 8 | ,:=:~::=:::::::~~~~~~: 7I7??IIII I77?II7II I7$II7 I7??II77~ 9 | ,:::::::~?::::~~~:?~~ 7II II 777 7I7 III7 777 77 $$$ 10 | ::::::~=~~?:~?===~~ 7II $$ III 7I7 II $$$ II 7I, 11 | ::::~~~~~=====?= II III7~ II$7 77 + II7I ?7$$7 12 | ::::=~~====== 7 ?7 I$ I 7II? Z$77~ 13 | ::::~=~== 14 | ::::~ 15 | : Javascript has you! 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/base/conf/.react-base/templates/generator-react-base/generators/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var yeoman = require('yeoman-generator'); 4 | var optionOrPrompt = require('yeoman-option-or-prompt'); 5 | var fs = require("fs"); 6 | var listModules, listModulesArray; 7 | 8 | var typesDocument = { 9 | 10 | action:{ 11 | folder: '/actions/', 12 | template: '_action.js', 13 | nameFile: '/index.js', 14 | nameType: 'action' 15 | }, 16 | api:{ 17 | folder: '/api/', 18 | template: '_api.js', 19 | nameFile: '/index.js', 20 | nameType: 'api' 21 | }, 22 | component:{ 23 | folder: '/components/', 24 | template: '_component.js', 25 | nameFile: '/index.jsx', 26 | nameType: 'component' 27 | }, 28 | container:{ 29 | folder: '/', 30 | template: '_container.js', 31 | nameFile: '/index.jsx', 32 | nameType: 'container' 33 | }, 34 | models:{ 35 | folder: '/models/', 36 | template: '_models.js', 37 | nameFile: '/index.js', 38 | nameType: 'models' 39 | }, 40 | reducer:{ 41 | folder: '/reducers/', 42 | template: '_reducer.js', 43 | nameFile: '/index.js', 44 | nameType: 'reducer' 45 | }, 46 | styles:{ 47 | folder: '/', 48 | template: '_styles.css', 49 | nameFile: '/styles.css', 50 | nameType: 'styles' 51 | }, 52 | types:{ 53 | folder: '/actionTypes/', 54 | template: '_actionTypes.js', 55 | nameFile: '/index.js', 56 | nameType: 'actionTypes' 57 | } 58 | } 59 | 60 | var baseRoute = 'src/app'; 61 | var sectionCopy; 62 | 63 | function section (route, name, config) { 64 | this.fs.copyTpl( 65 | this.templatePath(config.template), 66 | this.destinationPath(route + name + config.folder + config.nameFile), { 67 | name: name, 68 | nameUpper: name.toUpperCase(), 69 | nameLower: name.toLowerCase(), 70 | namePascal: name.charAt(0).toUpperCase() + name.slice(1), 71 | nameGenerator: '_' + name.charAt(0).toUpperCase() + name.slice(1), 72 | nameState: '_' + name 73 | } 74 | ); 75 | } 76 | 77 | function sectionSpec(route, name, config){ 78 | this.fs.copyTpl( 79 | this.templatePath(config.template), 80 | this.destinationPath(route + name + config.folder + name + config.nameFile), { 81 | name: name, 82 | nameUpper: name.toUpperCase(), 83 | nameLower: name.toLowerCase(), 84 | namePascal: name.charAt(0).toUpperCase() + name.slice(1), 85 | nameGenerator: '_' + name.charAt(0).toUpperCase() + name.slice(1), 86 | nameState: '_' + name 87 | } 88 | ); 89 | } 90 | 91 | 92 | function createComponent(name){ 93 | let routeComponentName = baseRoute +'/components/' + name + '/' ; 94 | let namePascal = name.charAt(0).toUpperCase() + name.slice(1); 95 | 96 | /*COMPONENT*/ 97 | this.fs.copyTpl( 98 | this.templatePath(typesDocument.component.template), 99 | this.destinationPath(routeComponentName + typesDocument.component.nameFile , '/'), { 100 | name: namePascal, 101 | nameUpper: name.toUpperCase(), 102 | nameLower: name.toLowerCase() 103 | } 104 | ); 105 | 106 | /*STYLES*/ 107 | this.fs.copyTpl( 108 | this.templatePath(typesDocument.styles.template), 109 | this.destinationPath(routeComponentName + typesDocument.styles.nameFile, '/'), { 110 | name: namePascal, 111 | nameUpper: name.toUpperCase(), 112 | nameLower: name.toLowerCase() 113 | } 114 | ); 115 | /* 116 | this.fs.copyTpl( 117 | this.templatePath(typesDocument.component_spec.template), 118 | this.destinationPath(routeComponentName + '/spec/' + name + typesDocument.component_spec.nameFile), { 119 | name: namePascal, 120 | nameUpper: name.toUpperCase(), 121 | nameLower: name.toLowerCase() 122 | } 123 | );*/ 124 | 125 | } 126 | 127 | function createContainer(name){ 128 | let route = baseRoute + '/containers/' 129 | let namePascal = name.charAt(0).toUpperCase() + name.slice(1) 130 | sectionCopy(route, namePascal, typesDocument.action); 131 | sectionCopy(route, namePascal, typesDocument.api); 132 | sectionCopy(route, namePascal, typesDocument.component); 133 | sectionCopy(route, namePascal, typesDocument.container); 134 | sectionCopy(route, namePascal, typesDocument.models); 135 | sectionCopy(route, namePascal, typesDocument.reducer); 136 | sectionCopy(route, namePascal, typesDocument.styles); 137 | sectionCopy(route, namePascal, typesDocument.types); 138 | 139 | } 140 | 141 | module.exports = yeoman.Base.extend({ 142 | _optionOrPrompt: optionOrPrompt, 143 | 144 | prompting: function () { 145 | 146 | var done = this.async(); 147 | 148 | var promptsAll = [{ 149 | message: 'React-Base generator, choose your option: \n 1- New Container \n 2- New Component\n write you option: ', 150 | name: 'option', 151 | type: 'input' 152 | }, 153 | { 154 | message: 'Input Name', 155 | name: 'name', 156 | type: 'input' 157 | }]; 158 | 159 | this._optionOrPrompt(promptsAll, function (answers) { 160 | 161 | this.props = answers; 162 | this.props.option = parseInt(answers.option); 163 | this.props.name = answers.name; 164 | done(); 165 | }.bind(this)); 166 | }, 167 | 168 | writing: { 169 | config: function () { 170 | sectionCopy = section.bind(this); 171 | 172 | createComponent = createComponent.bind(this); 173 | switch(this.props.option){ 174 | case 1: 175 | createContainer(this.props.name); 176 | break; 177 | case 2: 178 | createComponent(this.props.name); 179 | break; 180 | default: 181 | return; 182 | } 183 | } 184 | }, 185 | end: function () { 186 | var done = this.async(); 187 | this.spawnCommand('npm', ['run', 'postYeomanGenerator']).on('close', done); 188 | } 189 | }); 190 | -------------------------------------------------------------------------------- /src/base/conf/.react-base/templates/generator-react-base/generators/templates/_action.js: -------------------------------------------------------------------------------- 1 | import api from '../api'; 2 | import ActionTypes from '../actionTypes'; 3 | 4 | export const get<%= namePascal %> = ({ params }) => ({ 5 | type: ActionTypes.<%= nameUpper %>_REQUEST, 6 | request: api.fetch<%= namePascal %>(params) 7 | }); -------------------------------------------------------------------------------- /src/base/conf/.react-base/templates/generator-react-base/generators/templates/_action.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import * as Actions from '../'; 3 | import ActionTypes from '../../actionTypes'; 4 | 5 | import api from '../../api'; 6 | 7 | describe('Actions', () => { 8 | describe('Actions <%= name %>', () => { 9 | it('Should create action get<%= namePascal %>', () => { 10 | 11 | /* const params = {}; 12 | const expectedAction = { 13 | type: ActionTypes.<%= nameUpper %>_REQUEST, 14 | request: api.fetch<%= namePascal %>(params) 15 | }; 16 | 17 | expect(Actions.get<%= namePascal %>(params)).toEqual(expectedAction); 18 | */ 19 | }); 20 | 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/base/conf/.react-base/templates/generator-react-base/generators/templates/_actionTypes.js: -------------------------------------------------------------------------------- 1 | import { createActionType } from 'base'; 2 | 3 | export default createActionType([ 4 | '<%= nameUpper %>_REQUEST','<%= nameUpper %>_SUCCESS','<%= nameUpper %>_ERROR', 5 | ]); 6 | -------------------------------------------------------------------------------- /src/base/conf/.react-base/templates/generator-react-base/generators/templates/_api.js: -------------------------------------------------------------------------------- 1 | import { generateMap } from 'base'; 2 | import fetch from 'isomorphic-fetch'; 3 | import { <%= namePascal %>Model } from '../models'; 4 | 5 | const url = 'http://localhost:8000/mocks/logo.json'; 6 | 7 | export default { 8 | 9 | fetch<%= namePascal %>() { 10 | return fetch(url) 11 | .then(req => req.json()) 12 | .then(data => generateMap(data, <%= namePascal %>Model)); 13 | } 14 | 15 | }; 16 | -------------------------------------------------------------------------------- /src/base/conf/.react-base/templates/generator-react-base/generators/templates/_component.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PropTypes } from 'prop-types'; 3 | 4 | const propTypes= { 5 | name: PropTypes.string.isRequired 6 | }; 7 | 8 | const <%= name %> = ({ name }) => { 9 | 10 | return ( 11 | 12 | Hello { name } Container ! 13 | 14 | ); 15 | }; 16 | 17 | <%= name %>.propTypes = propTypes; 18 | 19 | export default <%= name %>; -------------------------------------------------------------------------------- /src/base/conf/.react-base/templates/generator-react-base/generators/templates/_component.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { expect } from 'chai'; 3 | import { mount, shallow } from 'enzyme'; 4 | 5 | import <%= name %> from '../'; 6 | 7 | describe('Components', () => { 8 | describe('<%= name %> component', () => { 9 | 10 | it('Should has properties', () => { 11 | 12 | const component = mount(<<%= name %> name="<%= name %>"/>); 13 | expect(component.props().name).to.equal("<%= name %>"); 14 | 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/base/conf/.react-base/templates/generator-react-base/generators/templates/_container.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | import { connect } from 'react-redux'; 3 | import { bindActionCreators } from 'redux'; 4 | import React, { Component } from 'react'; 5 | import { PropTypes } from 'prop-types'; 6 | 7 | import * as Actions from './actions'; 8 | import <%= name %>Component from './components/'; 9 | import styles from './styles.css'; 10 | 11 | class <%= name %> extends Component { 12 | 13 | static propTypes = { 14 | dispatch: PropTypes.func.isRequired, 15 | <%= name %>Model: PropTypes.instanceOf(Immutable.Record) 16 | }; 17 | 18 | static requiredActions = []; 19 | 20 | constructor (props) { 21 | super(props); 22 | this.actions = bindActionCreators(Actions, props.dispatch); 23 | } 24 | 25 | render () { 26 | const { <%= name %>Model } = this.props; 27 | 28 | return ( 29 | }> 30 | <<%= name %>Component name={ <%= name %>Model.name } /> 31 | 32 | ); 33 | } 34 | } 35 | 36 | export default connect(state => ({ 37 | <%= name %>Model: state.<%= name %> 38 | }))(<%= name %>); -------------------------------------------------------------------------------- /src/base/conf/.react-base/templates/generator-react-base/generators/templates/_container.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import React from 'react'; 3 | import ReactShallowRenderer from 'react-test-renderer/shallow'; 4 | 5 | import { generateMap } from 'base/shared/ModelHelper'; 6 | 7 | import { <%= name %>Model, setInitialState } from '../models'; 8 | import { <%= name %> } from '..'; 9 | 10 | const mockData = [ 11 | { 12 | "id": 1, 13 | "alt": "React Base!", 14 | "name": "ReactBaseLogo", 15 | "width": 500, 16 | "url": "/assets/images/react-base-logo.png" 17 | } 18 | ]; 19 | 20 | const mockDataImmutable2 = generateMap(mockData,<%= name %>Model); 21 | 22 | function setup() { 23 | 24 | function dispatch() { } 25 | let initialState = { 26 | <%= name %>: { 27 | data: mockData 28 | } 29 | }; 30 | 31 | let props = { 32 | dispatch: dispatch, 33 | <%= name %>Model: setInitialState(initialState) 34 | }; 35 | 36 | let renderer = new ReactShallowRenderer(); 37 | renderer.render(<<%= name %> { ...props } />); 38 | let output = renderer.getRenderOutput(); 39 | 40 | return { 41 | props, 42 | output, 43 | renderer 44 | }; 45 | } 46 | 47 | describe('containers', () => { 48 | describe('<%= name %>', () => { 49 | /* it('should render correctly', () => { 50 | const { output } = setup(); 51 | expect(output.props.name).toBe('<%= name %>'); 52 | 53 | });*/ 54 | }); 55 | }); -------------------------------------------------------------------------------- /src/base/conf/.react-base/templates/generator-react-base/generators/templates/_models.js: -------------------------------------------------------------------------------- 1 | import { Record, Map } from 'immutable'; 2 | 3 | const <%= name %>Model = Record({ 4 | id: -1, 5 | name: '<%= name %>' 6 | }); 7 | 8 | const setInitialState = initialState => ( 9 | initialState.<%= name %> = new <%= name %>Model(initialState.<%= name %>) 10 | ); 11 | 12 | export { <%= name %>Model, setInitialState }; 13 | -------------------------------------------------------------------------------- /src/base/conf/.react-base/templates/generator-react-base/generators/templates/_reducer.js: -------------------------------------------------------------------------------- 1 | import { createReducer } from 'base'; 2 | import ActionTypes from '../actionTypes'; 3 | import { <%= name %>Model } from '../models'; 4 | 5 | const <%= nameLower %>Request = (state) => state; 6 | 7 | const <%= nameLower %>Error = (state) => state; 8 | 9 | const <%= nameLower %>Success = (state, action) => ( 10 | state.update ('data', () => action.payload) 11 | ); 12 | 13 | const actionHandlers = { 14 | [ActionTypes.<%= nameUpper %>_REQUEST]: <%= nameLower %>Request, 15 | [ActionTypes.<%= nameUpper %>_SUCCESS]: <%= nameLower %>Success, 16 | [ActionTypes.<%= nameUpper %>_ERROR]: <%= nameLower %>Error 17 | }; 18 | 19 | export default createReducer(actionHandlers, new <%= name %>Model()); 20 | -------------------------------------------------------------------------------- /src/base/conf/.react-base/templates/generator-react-base/generators/templates/_reducer.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import reducer from '..'; 3 | 4 | describe('Reducers', () => { 5 | describe('<%= name %> reducer', () => { 6 | 7 | /* it('Should return the initial state', () => { 8 | 9 | const action = { 10 | type: types.<%= nameUpper %>_REQUEST 11 | }; 12 | 13 | expect(reducer([], action)).toEqual([]); 14 | });*/ 15 | 16 | /* it('should return the state of <%= name %> request fail', () => { 17 | 18 | const action = { 19 | type: types.<%= nameUpper %>_ERROR 20 | }; 21 | 22 | expect(reducer([], action)).toEqual([]); 23 | });*/ 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/base/conf/.react-base/templates/generator-react-base/generators/templates/_styles.css: -------------------------------------------------------------------------------- 1 | .<%= name %> { 2 | width: 450px; 3 | height: 200px; 4 | position: absolute; 5 | left: 0; 6 | right: 0; 7 | top: 0; 8 | bottom: 0; 9 | margin: auto; 10 | max-width: 100%; 11 | font-size: 32px; 12 | font-family: Arial; 13 | color: #9E9E9E ; 14 | } 15 | -------------------------------------------------------------------------------- /src/base/conf/.react-base/templates/generator-react-base/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "", 3 | "main": "generators/index.js", 4 | "name": "generator-react-base", 5 | "version": "0.6.0", 6 | "keywords": [ 7 | "yeoman-generator" 8 | ], 9 | "author": { 10 | "name": "Luis Martinez Marina", 11 | "email": "lmartinez.marina@atsistemas.com", 12 | "url": "" 13 | }, 14 | "dependencies": { 15 | "yeoman-generator": "^0.20.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/base/index.js: -------------------------------------------------------------------------------- 1 | import createReducer from 'create-reduxreducer'; 2 | 3 | import * as console from './shared/console'; 4 | import env from './shared/Env'; 5 | 6 | export { env }; 7 | export default { console, env }; 8 | export { createReducer }; 9 | export { context } from './shared/Context'; 10 | export { generateMap } from './shared/ModelHelper'; 11 | export { generateList } from './shared/ModelHelper'; 12 | export { generateImmutable } from './shared/ModelHelper'; 13 | export { fetchRequiredActions } from './shared/FetchData'; 14 | export { createActionType } from './shared/CreateActionType'; 15 | -------------------------------------------------------------------------------- /src/base/models/index.js: -------------------------------------------------------------------------------- 1 | import * as CalculatorModel from 'containers/Calculator/models'; 2 | import * as MainModel from 'containers/Main/models'; 3 | import * as WeatherStationsModel from 'containers/WeatherStations/models'; 4 | 5 | const modelIndex = [CalculatorModel,MainModel,WeatherStationsModel]; 6 | 7 | export default { modelIndex }; -------------------------------------------------------------------------------- /src/base/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import Calculator from 'containers/Calculator/reducers'; 4 | import Main from 'containers/Main/reducers'; 5 | import WeatherStations from 'containers/WeatherStations/reducers'; 6 | 7 | export default combineReducers({ 8 | Calculator, 9 | Main, 10 | WeatherStations 11 | }); -------------------------------------------------------------------------------- /src/base/routes/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, IndexRoute } from 'react-router'; 3 | 4 | import App from 'containers/App/'; 5 | import Calculator from 'containers/Calculator/'; 6 | import Main from 'containers/Main/'; 7 | import WeatherStations from 'containers/WeatherStations/'; 8 | 9 | const routes = ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | 18 | export default routes; -------------------------------------------------------------------------------- /src/base/shared/Context.js: -------------------------------------------------------------------------------- 1 | const host = (typeof window !== 'undefined') ? 'client' : 'server'; 2 | 3 | export const context = { 4 | context :host, 5 | client : host === 'client' ? true : false, 6 | server : host === 'server' ? true : false 7 | }; -------------------------------------------------------------------------------- /src/base/shared/CreateActionType.js: -------------------------------------------------------------------------------- 1 | export const createActionType = types => ( 2 | types.reduce((acc, type) => { 3 | acc[type] = type; 4 | return acc; 5 | }, {}) 6 | ); 7 | -------------------------------------------------------------------------------- /src/base/shared/CreateReducer.js: -------------------------------------------------------------------------------- 1 | export function createReducer (actionHandler, initialState) { 2 | return (state = initialState, action) => { 3 | 4 | const handler = actionHandler[action.type]; 5 | 6 | if (!handler) return state; 7 | 8 | state = handler(state, action); 9 | 10 | return state; 11 | }; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/base/shared/Env.js: -------------------------------------------------------------------------------- 1 | const env = process.env.NODE_ENV || 'development'; 2 | export default env ; 3 | -------------------------------------------------------------------------------- /src/base/shared/Errors.js: -------------------------------------------------------------------------------- 1 | export default function ReactBaseError(msg) { 2 | this.msg = '[BASE ERROR] ' + msg; 3 | this.name = 'React Base Error'; 4 | this.stack = (new Error()).stack; 5 | } 6 | 7 | ReactBaseError.prototype = Object.create(Error.prototype); 8 | ReactBaseError.prototype.constructor = ReactBaseError; 9 | -------------------------------------------------------------------------------- /src/base/shared/FetchData.js: -------------------------------------------------------------------------------- 1 | export const fetchRequiredActions = (...args) => { 2 | const serverContext = ~args.indexOf('server'); 3 | return serverContext ? 4 | fetchServerData.apply(this, args) 5 | : 6 | fetchClientData.apply(this, args); 7 | }; 8 | 9 | const fetchServerData = (dispatch, components, params) => { 10 | const actions = components.reduce((prev, current) => ( 11 | Object.keys(current).reduce((acc, key) => { 12 | const hasRequiredActions = current[key].hasOwnProperty('requiredActions'); 13 | return hasRequiredActions ? current[key].requiredActions.concat(acc) : acc; 14 | }, prev) 15 | ), []); 16 | const requiredActions = actions.map(action => dispatch(action(params))); 17 | return Promise.all(requiredActions); 18 | }; 19 | 20 | const fetchClientData = (actions, props, force = false) => { 21 | const { params, dispatch } = props; 22 | force && actions.map(action => dispatch(action(params))); 23 | }; 24 | -------------------------------------------------------------------------------- /src/base/shared/FileSystem.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import ReactBaseError from './Errors'; 3 | 4 | function fileExists(filename) { 5 | try { 6 | fs.accessSync(filename); 7 | return true; 8 | } catch (e) { 9 | return false; 10 | } 11 | } 12 | 13 | function readDir(path) { 14 | return fs.readdirSync(path); 15 | } 16 | 17 | function readFile(path, options) { 18 | return fs.readFileSync(path, options); 19 | } 20 | 21 | function writeFile(file, content) { 22 | try { 23 | fs.writeFileSync(file, content, 'utf8'); 24 | return true; 25 | } catch (e) { 26 | throw new ReactBaseError(e); 27 | } 28 | } 29 | 30 | export { fileExists, readDir, readFile, writeFile }; 31 | -------------------------------------------------------------------------------- /src/base/shared/GetActionPrefix.js: -------------------------------------------------------------------------------- 1 | export default function getActionPrefix(action) { 2 | return action.substr(0, action.lastIndexOf('_')); 3 | } 4 | -------------------------------------------------------------------------------- /src/base/shared/ModelHelper.js: -------------------------------------------------------------------------------- 1 | import { Map, List } from 'immutable'; 2 | 3 | export const generateMap = (data, Model) => ( 4 | data.reduce((acc, item) => ( 5 | acc.set(item.id, new Model(item)) 6 | ), new Map()) 7 | ); 8 | 9 | export const generateList = (data, Model) => ( 10 | new List(data.map(item => new Model(item))) 11 | ); 12 | 13 | export const generateImmutable = (data, Model) => ( 14 | Object.keys(data).reduce((acc, key) => ( 15 | acc.set(data[key].id, new Model(data[key])) 16 | ), new Map()) 17 | ); 18 | -------------------------------------------------------------------------------- /src/base/shared/Regenerate.js: -------------------------------------------------------------------------------- 1 | require('babel-core/register'); 2 | const path = require('path'); 3 | const modelIndex = require('./regenerators/RegenerateModelIndex'); 4 | const reducerIndex = require('./regenerators/RegenerateReducerIndex'); 5 | const regenerateRoutes = require('./regenerators/RegenerateRoutes'); 6 | 7 | const modelFilePath = path.resolve(__dirname, '..', '..', 'base', 'models','index.js'); 8 | const reducerFilePath = path.resolve(__dirname, '..', '..', 'base','reducers','index.js'); 9 | 10 | const containersPath = path.resolve(__dirname, '..', '..', 'app', 'containers'); 11 | const args = process.argv.slice(2); 12 | if (~args.indexOf('--all') || ~args.indexOf('--routes')) { 13 | regenerateRoutes(); 14 | } 15 | 16 | if (~args.indexOf('--all') || ~args.indexOf('--model')) { 17 | modelIndex.RegenerateModelIndex(containersPath, modelFilePath); 18 | } 19 | 20 | if (~args.indexOf('--all') || ~args.indexOf('--reducer')) { 21 | reducerIndex.RegenerateReducerIndex(containersPath, reducerFilePath); 22 | } 23 | -------------------------------------------------------------------------------- /src/base/shared/ResolveRequestAction.js: -------------------------------------------------------------------------------- 1 | import getActionPrefix from './GetActionPrefix'; 2 | 3 | const resolveRequestAction = (action, response, result) => { 4 | return { 5 | type: `${getActionPrefix(action.type)}_${result}`, 6 | payload: response 7 | }; 8 | }; 9 | 10 | export default resolveRequestAction; 11 | -------------------------------------------------------------------------------- /src/base/shared/TestSetup.js: -------------------------------------------------------------------------------- 1 | /* Setting up Enzyme 3.x */ 2 | 3 | let enzyme = require('enzyme'); 4 | let Adapter = require('enzyme-adapter-react-16'); 5 | 6 | enzyme.configure({ adapter: new Adapter() }); 7 | 8 | 9 | /* Setting up JSDOM >v10 */ 10 | 11 | const { JSDOM } = require('jsdom'); 12 | 13 | const jsdom = new JSDOM(''); 14 | const { window } = jsdom; 15 | 16 | global.window = window; 17 | global.document = window.document; 18 | global.navigator = { 19 | userAgent: 'node.js' 20 | }; 21 | 22 | const props = Object.getOwnPropertyNames(window) 23 | .filter(prop => typeof global[prop] === 'undefined') 24 | .reduce((result, prop) => ({ 25 | ...result, 26 | [prop]: Object.getOwnPropertyDescriptor(window, prop), 27 | }), {}); 28 | 29 | Object.defineProperties(global, props); 30 | 31 | //export default global; 32 | -------------------------------------------------------------------------------- /src/base/shared/console.js: -------------------------------------------------------------------------------- 1 | exports.useColors = process.browser ? null : require('supports-color'); 2 | 3 | exports.colors = { 4 | success: 32, 5 | error: 31, 6 | default: 32, 7 | info: 34 8 | }; 9 | 10 | exports.symbols = { 11 | ok: '✓', 12 | err: '✖', 13 | info: 'i', 14 | dot: '․', 15 | CR: '\u000A' 16 | }; 17 | 18 | if (process.platform === 'win32') { 19 | exports.symbols.ok = '\u221A'; 20 | exports.symbols.err = '\u00D7'; 21 | exports.symbols.info = 'i'; 22 | exports.symbols.dot = '.'; 23 | exports.symbols.CR = '\u000D\u000A'; 24 | } 25 | 26 | exports.color = function (type, str) { 27 | if (!exports.useColors) { 28 | return String(str); 29 | } 30 | 31 | if (!exports.colors[type]) { 32 | type = 'default'; 33 | } 34 | 35 | return '\u001b[' + exports.colors[type] + 'm' + str + '\u001b[0m'; 36 | }; 37 | 38 | exports.line = function () { 39 | const args = Array.prototype.slice.call(arguments); 40 | return console.log.apply(console, [exports.symbols.CR].concat(args).concat(exports.symbols.CR)); 41 | }; 42 | 43 | exports.info = function () { 44 | const args = Array.prototype.slice.call(arguments); 45 | return console.log.apply(console, ['[BASE]', exports.color('info', exports.symbols.info)].concat(args)); 46 | }; 47 | 48 | exports.success = function () { 49 | const args = Array.prototype.slice.call(arguments); 50 | return console.log.apply(console, ['[BASE]', exports.color('success', exports.symbols.ok)].concat(args)); 51 | }; 52 | 53 | exports.error = function () { 54 | const args = Array.prototype.slice.call(arguments); 55 | return console.log.apply(console, ['[BASE]', exports.color('error', exports.symbols.error)].concat(args)); 56 | }; 57 | 58 | exports.clear = function () { 59 | process.stdout.write("\x1B[2J\x1B[0f"); 60 | }; 61 | -------------------------------------------------------------------------------- /src/base/shared/index.js: -------------------------------------------------------------------------------- 1 | import './console'; 2 | import './CreateReducer'; 3 | import './Errors'; 4 | import './FetchData'; 5 | import './ModelHelper'; 6 | import './FileSystem'; 7 | -------------------------------------------------------------------------------- /src/base/shared/regenerators/RegenerateModelIndex.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import base from '../../'; 4 | import { fileExists, readDir, writeFile } from '../FileSystem'; 5 | 6 | const exportTpl = '\n\nconst modelIndex = [@param];\n\nexport default { modelIndex };'; 7 | const importTpl = 'import * as @paramModel from \'containers/@param/models\';'; 8 | 9 | function RegenerateImportLine(container) { 10 | return importTpl.replace(/@param/g, container); 11 | } 12 | 13 | function RegenerateExportLine(modelExports) { 14 | return exportTpl.replace('@param', modelExports); 15 | } 16 | 17 | function RegenerateModelIndex(containersPath, modelFilePath) { 18 | let modelImports = ''; 19 | let modelExports = ''; 20 | const containerModels = getContainerModels(containersPath); 21 | 22 | containerModels.forEach(function(model, index) { 23 | if (model.import) { 24 | modelImports += (index === 1) ? model.import : '\n' + model.import; 25 | modelExports += model.name + 'Model'; 26 | modelExports += (index < containerModels.length-1) ? ',' : ''; 27 | } 28 | }); 29 | 30 | const content = modelImports + RegenerateExportLine(modelExports); 31 | 32 | try { 33 | writeFile(modelFilePath, content); 34 | base.console.success(`Model index regenerated correctly!`); 35 | return true; 36 | } catch (e) { 37 | base.console.error(`Model index error ${e.msg}`); 38 | return false; 39 | } 40 | } 41 | 42 | function getContainerModels(containersPath) { 43 | const containers = readDir(containersPath); 44 | return containers.map(function(container) { 45 | let modelPath = path.resolve(containersPath, container, 'models','index.js'); 46 | if (fileExists(modelPath)) { 47 | return { name:container, import: RegenerateImportLine(container)}; 48 | } else { 49 | return { name: container, import: null }; 50 | } 51 | }); 52 | } 53 | 54 | module.exports.RegenerateModelIndex = RegenerateModelIndex; 55 | module.exports.RegenerateImportLine = RegenerateImportLine; 56 | module.exports.RegenerateExportLine = RegenerateExportLine; 57 | module.exports.getContainerModels = getContainerModels; 58 | -------------------------------------------------------------------------------- /src/base/shared/regenerators/RegenerateReducerIndex.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import base from '../../'; 4 | import { fileExists, readDir, writeFile } from '../FileSystem'; 5 | 6 | const mainImportTpl = 'import { combineReducers } from \'redux\';\n'; 7 | const exportTpl = '\n\nexport default combineReducers({\n@param\n});'; 8 | const importTpl = 'import @param from \'containers/@param/reducers\';'; 9 | 10 | function RegenerateImportLine(container) { 11 | return importTpl.replace(/@param/g, container); 12 | } 13 | 14 | function RegenerateExportLine(reducerExports) { 15 | return exportTpl.replace('@param', reducerExports); 16 | } 17 | 18 | function RegenerateReducerIndex(containersPath, reducerFilePath) { 19 | let reducerImports = mainImportTpl; 20 | let reducerExports = ''; 21 | const containerReducers = getContainerReducers(containersPath); 22 | 23 | containerReducers.forEach(function(reducer, index) { 24 | if (reducer.import) { 25 | reducerImports += (index === 0) ? reducer.import : '\n' + reducer.import; 26 | reducerExports += ' ' + reducer.name; 27 | reducerExports += (index < containerReducers.length-1) ? ',\n' : ''; 28 | } 29 | }); 30 | 31 | const content = reducerImports + RegenerateExportLine(reducerExports); 32 | 33 | try { 34 | writeFile(reducerFilePath, content); 35 | base.console.success(`Reducer index regenerated correctly!`); 36 | return true; 37 | } catch (e) { 38 | base.console.error(`Reducer index error ${e.msg}`); 39 | return false; 40 | } 41 | } 42 | 43 | function getContainerReducers(containersPath) { 44 | const files = readDir(containersPath); 45 | return files.map(function(container) { 46 | let reducerPath = path.resolve(containersPath, container, 'reducers','index.js'); 47 | if (fileExists(reducerPath)) { 48 | return { name:container, import: RegenerateImportLine(container)}; 49 | } else { 50 | return { name: container, import: null }; 51 | } 52 | }); 53 | } 54 | 55 | module.exports.RegenerateReducerIndex = RegenerateReducerIndex; 56 | module.exports.RegenerateImportLine = RegenerateImportLine; 57 | module.exports.RegenerateExportLine = RegenerateExportLine; 58 | module.exports.getContainerReducers = getContainerReducers; 59 | -------------------------------------------------------------------------------- /src/base/shared/regenerators/RegenerateRoutes.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import base from '../../'; 4 | import { fileExists, readDir, writeFile } from '../FileSystem'; 5 | 6 | const containersPath = path.resolve(__dirname, '..', '..', '..', 'app', 'containers'); 7 | const routerPath = path.resolve(__dirname, '..', '..','..', 'base', 'routes','index.js'); 8 | 9 | let routesImports = 'import React from \'react\';\nimport { Route, IndexRoute } from \'react-router\';\n\nimport App from \'containers/App/\';'; 10 | 11 | function RegenerateRoutes() { 12 | const routes = getRoutes(); 13 | let routesExports = ''; 14 | let newRoutes = ''; 15 | 16 | routes.forEach(function(route, index) { 17 | if (route.import) { 18 | routesImports += (index === 0) ? route.import : '\n' + route.import; 19 | newRoutes += ' \n'; 20 | } 21 | }); 22 | 23 | newRoutes = generateRoutes(newRoutes); 24 | routesExports = generateRoutesExport(routesExports); 25 | let content = routesImports + newRoutes + routesExports; 26 | 27 | try { 28 | writeFile(routerPath, content); 29 | base.console.success(`Route index regenerated correctly!`); 30 | return true; 31 | } catch (e) { 32 | base.console.error(`Route index error ${e.msg}`); 33 | return false; 34 | } 35 | } 36 | 37 | function generateRoutes(newRoutes) { 38 | return '\n\nconst routes = (\n \n \n' + newRoutes + ' \n);\n'; 39 | } 40 | 41 | 42 | function generateRoutesExport() { 43 | return '\nexport default routes;'; 44 | } 45 | 46 | function getRoutes() { 47 | const files = readDir(containersPath); 48 | return files.map(function(container) { 49 | let containerPath = path.resolve(containersPath, container); 50 | if (fileExists(containerPath) && container !== 'App') { 51 | return { name:container, import:'import ' + container + ' from \'containers/'+ container +'/\';' }; 52 | } else { 53 | return { name: container, import: null }; 54 | } 55 | }); 56 | } 57 | 58 | module.exports = RegenerateRoutes; 59 | -------------------------------------------------------------------------------- /src/base/shared/regenerators/spec/RegenerateModelIndex.spec.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { expect } from 'chai'; 4 | import { fileExists, writeFile } from '../../FileSystem'; 5 | import { RegenerateImportLine, RegenerateExportLine, getContainerModels, RegenerateModelIndex } from '../RegenerateModelIndex'; 6 | 7 | describe('shared / Regenerators / RegenerateModelIndex', () => { 8 | 9 | describe('RegenerateImportLine', () => { 10 | 11 | it('Sould return the import line of a model', () => { 12 | 13 | const procesedImport = RegenerateImportLine('Main'); 14 | const expectedImport = 'import * as MainModel from \'containers/Main/models\';'; 15 | 16 | expect(procesedImport).to.equal(expectedImport); 17 | 18 | }); 19 | }); 20 | 21 | describe('RegenerateExportLine', () => { 22 | 23 | it('Sould return the export line for models', () => { 24 | 25 | const procesedExport = RegenerateExportLine('Main'); 26 | const expectedExport = '\n\nconst modelIndex = [Main];\n\nexport default { modelIndex };'; 27 | 28 | expect(procesedExport).to.equal(expectedExport); 29 | 30 | }); 31 | }); 32 | 33 | 34 | describe('getContainerModels', () => { 35 | 36 | it('Sould return an object with avaiable models', () => { 37 | 38 | const containersPath = path.resolve(__dirname, '..', '..', '..', '..', 'app', 'containers'); 39 | const result = getContainerModels(containersPath); 40 | 41 | const containers = fs.readdirSync(containersPath); 42 | const expectedResult = containers.map(function(container){ 43 | 44 | let modelPath = path.resolve(containersPath, container, 'models','index.js'); 45 | try{ 46 | fs.accessSync(modelPath); 47 | return { name:container, import: RegenerateImportLine(container)}; 48 | }catch(e){ 49 | return { name: container, import: null }; 50 | } 51 | }); 52 | 53 | expect(result).to.deep.equal(expectedResult); 54 | 55 | }); 56 | }); 57 | 58 | 59 | describe('RegenerateModelIndex', () => { 60 | 61 | it('Sould write the model index file', () => { 62 | const containersPath = path.resolve(__dirname, '..', '..', '..', '..', 'app', 'containers'); 63 | const fakeModelPath = path.resolve(__dirname, 'fake.js'); 64 | const result = RegenerateModelIndex(containersPath, fakeModelPath); 65 | 66 | expect(result).to.equal(true); 67 | expect(fileExists(fakeModelPath)).to.equal(true); 68 | fs.unlink(fakeModelPath, (err) => { 69 | if (err) { 70 | console.log("failed to delete local file:"+err); 71 | } else { 72 | console.log('successfully deleted local file'); 73 | } 74 | }); 75 | 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/base/shared/regenerators/spec/RegenerateReducerIndex.spec.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { expect } from 'chai'; 4 | import { fileExists, writeFile } from '../../FileSystem'; 5 | import { RegenerateImportLine, RegenerateExportLine, getContainerReducers, RegenerateReducerIndex } from '../RegenerateReducerIndex'; 6 | 7 | describe('shared / Generators / RegenerateReducerIndex', () => { 8 | 9 | describe('RegenerateImportLine', () => { 10 | 11 | it('Sould return the import line of a reducer', () => { 12 | 13 | const procesedImport = RegenerateImportLine('Main'); 14 | const expectedImport = 'import Main from \'containers/Main/reducers\';'; 15 | 16 | expect(procesedImport).to.equal(expectedImport); 17 | 18 | }); 19 | }); 20 | 21 | describe('RegenerateExportLine', () => { 22 | 23 | it('Sould return the export line for models', () => { 24 | 25 | const procesedExport = RegenerateExportLine('Main'); 26 | const expectedExport = '\n\nexport default combineReducers({\nMain\n});'; 27 | 28 | expect(procesedExport).to.equal(expectedExport); 29 | 30 | }); 31 | }); 32 | 33 | 34 | describe('getContainerModels', () => { 35 | 36 | it('Sould return an object with avaiable reducers for import', () => { 37 | 38 | const containersPath = path.resolve(__dirname, '..', '..', '..', '..', 'app', 'containers'); 39 | const result = getContainerReducers(containersPath); 40 | 41 | const containers = fs.readdirSync(containersPath); 42 | const expectedResult = containers.map(function(container){ 43 | 44 | let reducerPath = path.resolve(containersPath, container, 'reducers','index.js'); 45 | try{ 46 | fs.accessSync(reducerPath); 47 | return { name:container, import: RegenerateImportLine(container)}; 48 | }catch(e){ 49 | return { name: container, import: null }; 50 | } 51 | }); 52 | 53 | expect(result).to.deep.equal(expectedResult); 54 | 55 | }); 56 | }); 57 | 58 | describe('RegenerateReducerIndex', () => { 59 | 60 | it('Sould write the reducer index file', () => { 61 | const containersPath = path.resolve(__dirname, '..', '..', '..', '..', 'app', 'containers'); 62 | const fakeReducerPath = path.resolve(__dirname, 'fake.js'); 63 | const result = RegenerateReducerIndex(containersPath, fakeReducerPath); 64 | 65 | expect(result).to.equal(true); 66 | expect(fileExists(fakeReducerPath)).to.equal(true); 67 | fs.unlink(fakeReducerPath, (err) => { 68 | if (err) { 69 | console.log("failed to delete local file:"+err); 70 | } else { 71 | console.log('successfully deleted local file'); 72 | } 73 | }); 74 | 75 | }); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /src/base/shared/spec/Errors.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import ReactBaseError from '../Errors'; 3 | 4 | describe('shared / Errors', () => { 5 | 6 | describe('Error', () => { 7 | 8 | it('should be an Error prototype', () => { 9 | 10 | const err = new ReactBaseError('foo'); 11 | const isErr = err instanceof Error; 12 | expect(isErr).to.equal(true); 13 | 14 | }); 15 | 16 | it('should have a name and error msg ', () => { 17 | 18 | const msg = 'test'; 19 | const err = new ReactBaseError(msg); 20 | expect(err.name).to.equal('React Base Error'); 21 | expect(err.msg).to.equal('[BASE ERROR] test'); 22 | 23 | }); 24 | }); 25 | 26 | }); 27 | -------------------------------------------------------------------------------- /src/base/shared/spec/FetchData.shared.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { fetchRequiredActions } from '../FetchData' ; 3 | import action from 'containers/Main/actions'; 4 | import actionTypes from 'containers/Main/actionTypes'; 5 | import { MainModel } from 'containers/Main/models'; 6 | import { generateImmutable } from '../ModelHelper'; 7 | 8 | const mockData = [{"userId":1,"id":1,"title":"sunt aut facere repellat provident occaecati excepturi optio reprehenderit","body":"quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"},{"userId":1,"id":2,"title":"qui est esse","body":"est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"},{"userId":1,"id":3,"title":"ea molestias quasi exercitationem repellat qui ipsa sit aut","body":"et iusto sed quo iure\nvoluptatem occaecati omnis eligendi aut ad\nvoluptatem doloribus vel accusantium quis pariatur\nmolestiae porro eius odio et labore et velit aut"}]; 9 | 10 | describe('shared', () => { 11 | 12 | describe('fetch data', () => { 13 | 14 | it('fetch client action', (done) => { 15 | let requiredActions = [action.getLogo]; 16 | 17 | function dispatch({type}) { 18 | expect(type).to.equal(actionTypes.LOGO_REQUEST); 19 | done(); 20 | } 21 | 22 | const props= { 23 | params:{ 24 | category: 'news' 25 | }, 26 | logo:{ 27 | data:[] 28 | }, 29 | dispatch: dispatch 30 | }; 31 | 32 | let context ='client'; 33 | 34 | fetchRequiredActions(requiredActions, props, 'MainModel', context); 35 | }); 36 | 37 | it('fetch client action with data', () => { 38 | let requiredActions = [action.getLogo]; 39 | 40 | function dispatch({ type }) { 41 | expect(type).to.equal(actionTypes.LOGO_REQUEST); 42 | } 43 | const data = generateImmutable( mockData, MainModel ); 44 | 45 | const props= { 46 | params:{ 47 | category: 'news' 48 | }, 49 | logo:{ 50 | data: data 51 | }, 52 | dispatch: dispatch 53 | }; 54 | 55 | let context ='client'; 56 | 57 | fetchRequiredActions(requiredActions, props, 'MainModel', context); 58 | }); 59 | 60 | it('fetch server action', (done) => { 61 | function dispatch({ type }) { 62 | expect(type).to.equal(actionTypes.LOGO_REQUEST); 63 | done() ; 64 | } 65 | 66 | let requiredActions = [action.getLogo]; 67 | const components = [[ 68 | { displayName: 'Connect(Main)', 69 | WrappedComponent: { 70 | requiredActions: requiredActions, dispatch: dispatch, 71 | PropTypes: { dispatch: dispatch, logo: { } } 72 | }, 73 | dispatch: dispatch, 74 | requiredActions: requiredActions 75 | }, 76 | { 77 | displayName: 'Main' 78 | } 79 | ]]; 80 | 81 | let context ='server'; 82 | 83 | fetchRequiredActions(dispatch, components, [], context); 84 | }); 85 | 86 | }); 87 | 88 | }); 89 | -------------------------------------------------------------------------------- /src/base/shared/spec/FileSystem.shared.spec.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { expect } from 'chai'; 4 | import { fileExists, readDir, writeFile, readFile } from '../FileSystem'; 5 | 6 | describe('shared / FileSystem', () => { 7 | 8 | describe('fileExists', () => { 9 | 10 | it('Sould check if fileExist', () => { 11 | 12 | const realPath = path.resolve(__dirname, '..', 'FileSystem.js'); 13 | const fakePath = path.resolve(__dirname, '..', 'fake.js'); 14 | 15 | expect(fileExists(realPath)).to.equal(true); 16 | expect(fileExists(fakePath)).to.equal(false); 17 | 18 | }); 19 | }); 20 | 21 | describe('readDir', () => { 22 | 23 | it('Sould read a directory', () => { 24 | 25 | const dir = path.resolve(__dirname, '..', '..', 'client'); 26 | const files = readDir(dir); 27 | expect(files.length).to.equal(1); 28 | 29 | }); 30 | }); 31 | 32 | describe('readFile', () => { 33 | 34 | it('Sould write a file', () => { 35 | 36 | const file = path.resolve(__dirname, 'test.js'); 37 | const content = 'Hello!'; 38 | writeFile(file,content); 39 | 40 | const readed = readFile(file, 'utf8'); 41 | 42 | fs.unlink(file, (err) => { 43 | if (err) { 44 | console.log("failed to delete local file:"+err); 45 | } else { 46 | console.log('successfully deleted local file'); 47 | } 48 | }); 49 | expect(readed).to.equal(content); 50 | 51 | }); 52 | }); 53 | 54 | describe('writeFile', () => { 55 | 56 | it('Sould write a file', () => { 57 | 58 | const file = path.resolve(__dirname, 'test.js'); 59 | const content = 'Hello!'; 60 | writeFile(file,content); 61 | 62 | const wrote = fs.readFileSync(file, "utf8"); 63 | fs.unlink(file, (err) => { 64 | if (err) { 65 | console.log("failed to delete local file:"+err); 66 | } else { 67 | console.log('successfully deleted local file'); 68 | } 69 | }); 70 | 71 | expect(wrote).to.equal(content); 72 | 73 | }); 74 | }); 75 | 76 | }); 77 | -------------------------------------------------------------------------------- /src/base/shared/spec/ModelHelper.shared.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Map, List, Record } from 'immutable'; 3 | 4 | import { MainModel } from 'containers/Main/models'; 5 | import * as ModelHelper from '../ModelHelper'; 6 | 7 | const mockData = [ 8 | { 9 | "id": 1, 10 | "alt": "React Base!", 11 | "name": "ReactBaseLogo", 12 | "width": 500, 13 | "url": "/assets/images/react-base-logo.png" 14 | } 15 | ]; 16 | 17 | 18 | describe('shared / model-helper', () => { 19 | 20 | describe('generateImmutable', () => { 21 | 22 | it('should return empty immutable map', () => { 23 | 24 | const initialState = {}; 25 | const data = ModelHelper.generateImmutable(initialState, MainModel); 26 | 27 | const expectedData = Object.keys(initialState) 28 | .reduce((acc, key) => { 29 | let item = initialState[key]; 30 | return acc.set(item.id, new MainModel(item)); 31 | }, new Map() 32 | ); 33 | 34 | expect(data).to.equal(expectedData); 35 | 36 | }); 37 | 38 | it('should return immutable map', () => { 39 | 40 | const initialState = mockData; 41 | const data = ModelHelper.generateImmutable(initialState, MainModel); 42 | 43 | const expectedData = Object.keys(initialState) 44 | .reduce((acc, key) => { 45 | let item = initialState[key]; 46 | return acc.set(item.id, new MainModel(item)); 47 | }, new Map() 48 | ); 49 | 50 | 51 | expect(data).to.deep.equal(expectedData); 52 | 53 | }); 54 | 55 | }); 56 | 57 | describe('generateMap', () => { 58 | 59 | it('should return object', () => { 60 | 61 | const initialState = ModelHelper.generateImmutable(mockData, MainModel); 62 | const data = ModelHelper.generateMap(initialState, MainModel); 63 | 64 | const expectedData = initialState 65 | .reduce((acc, item) => { 66 | return acc.set(item.id, new MainModel(item)); 67 | }, new Map() 68 | ); 69 | 70 | expect(data).to.deep.equal(expectedData); 71 | 72 | }); 73 | 74 | it('should return empty object', () => { 75 | 76 | const initialState = ModelHelper.generateImmutable({}, MainModel); 77 | const data = ModelHelper.generateMap(initialState, MainModel); 78 | 79 | const expectedData = initialState 80 | .reduce((acc, item) => { 81 | return acc.set(item.id, new MainModel(item)); 82 | }, new Map() 83 | ); 84 | 85 | expect(data).to.deep.equal(expectedData); 86 | 87 | }); 88 | 89 | }); 90 | 91 | describe('generateList', () => { 92 | 93 | it('should return object', () => { 94 | 95 | const initialState = [mockData]; 96 | const data = ModelHelper.generateList(initialState, MainModel); 97 | 98 | const expectedData = new List( 99 | initialState.map(item => new MainModel(item)) 100 | ); 101 | expect(data).to.deep.equal(expectedData); 102 | 103 | }); 104 | 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /src/base/shared/spec/TypeHelper.shared.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { createActionType } from '../CreateActionType'; 3 | 4 | describe('shared / type-helper', () => { 5 | 6 | describe('createActionType', () => { 7 | 8 | it('should return type builder object', () => { 9 | 10 | let TypeExamples = ['typeExample', 'typeExample2']; 11 | let types = createActionType(TypeExamples); 12 | 13 | expect(types.typeExample).to.equal('typeExample'); 14 | expect(types.typeExample2).to.equal('typeExample2'); 15 | 16 | }); 17 | }); 18 | 19 | }); 20 | -------------------------------------------------------------------------------- /src/base/store/ConfigureStore.js: -------------------------------------------------------------------------------- 1 | import { createLogger } from 'redux-logger'; 2 | import reduxReqMiddleware from 'redux-req-middleware'; 3 | import { createStore, compose, applyMiddleware } from 'redux'; 4 | 5 | import base from 'base/'; 6 | import rootReducer from '../reducers'; 7 | 8 | function configureStore(history, initialState) { 9 | 10 | let composeEnhancer = compose; 11 | let middleware; 12 | 13 | if (base.env === 'development') { 14 | middleware = applyMiddleware( 15 | createLogger({ level: 'info', collapsed: true }), 16 | reduxReqMiddleware(), 17 | ); 18 | composeEnhancer = typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 19 | } else { 20 | middleware = applyMiddleware( 21 | reduxReqMiddleware() 22 | ); 23 | } 24 | 25 | const enhancer = composeEnhancer(middleware); 26 | const store = createStore(rootReducer, initialState, enhancer); 27 | 28 | if (module.hot) { 29 | module.hot.accept('../reducers', () => { 30 | const nextRootReducer = require('../reducers').default; 31 | 32 | store.replaceReducer(nextRootReducer); 33 | }); 34 | } 35 | 36 | return store; 37 | } 38 | 39 | export default configureStore; 40 | -------------------------------------------------------------------------------- /src/base/store/GlobalState.js: -------------------------------------------------------------------------------- 1 | const setGlobalState = (initialState, models) => ( 2 | models.default.modelIndex.map(model => ( 3 | model.setInitialState(initialState) 4 | )) 5 | ); 6 | 7 | export default setGlobalState; -------------------------------------------------------------------------------- /src/base/store/InitialState.js: -------------------------------------------------------------------------------- 1 | import * as models from '../models'; 2 | import setGlobalState from './GlobalState'; 3 | 4 | const initialState = window.$REACTBASE_STATE || {}; 5 | 6 | setGlobalState(initialState, models); 7 | 8 | export default initialState; 9 | -------------------------------------------------------------------------------- /src/base/store/spec/ConfigureStore.store.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import configureStore from '../ConfigureStore'; 3 | 4 | describe('store / configureStore', () => { 5 | 6 | describe('configureStore', () => { 7 | 8 | it('should return configureStore', () => { 9 | 10 | let output = configureStore(null,{}); 11 | 12 | let type = typeof(output.dispatch); 13 | 14 | expect(type).to.equal('function'); 15 | 16 | }); 17 | }); 18 | 19 | 20 | }); 21 | -------------------------------------------------------------------------------- /src/base/types/FetchTypes.js: -------------------------------------------------------------------------------- 1 | export default ['REQUEST','SUCCESS','ERROR']; 2 | -------------------------------------------------------------------------------- /src/base/wp-plugins/compileInfoPlugin.js: -------------------------------------------------------------------------------- 1 | import console from '../../../src/base/shared/console'; 2 | 3 | export default class compileInfoPlugin { 4 | 5 | apply(compiler) { 6 | compiler.plugin('done', function(stats) { 7 | if (stats.compilation.errors && stats.compilation.errors.length && process.argv.indexOf('--watch') == -1) { 8 | console.error(stats.compilation.errors); 9 | } else { 10 | console.success('Compilation completed without errors!'); 11 | } 12 | }); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/base/wp-plugins/fileHashPlugin.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import console from '../../../src/base/shared/console'; 4 | 5 | export default class fileHashPlugin { 6 | 7 | constructor(options) { 8 | this.options = options; 9 | } 10 | 11 | write(data) { 12 | try { 13 | fs.writeFileSync(path.resolve(this.options.path, this.options.fileName), data, 'utf8'); 14 | console.success(`${this.options.fileName} hash file generated correctly!`); 15 | } catch (e) { 16 | console.error(e); 17 | } 18 | } 19 | 20 | apply(compiler) { 21 | compiler.plugin('done', (stats) => { 22 | const assets = stats.toJson(); 23 | this.write(JSON.stringify({ 24 | hash : assets.hash, 25 | files : assets.assetsByChunkName, 26 | })); 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/base/wp-plugins/index.js: -------------------------------------------------------------------------------- 1 | import compileInfoPlugin from './compileInfoPlugin'; 2 | import fileHashPlugin from './fileHashPlugin'; 3 | 4 | export default { compileInfoPlugin, fileHashPlugin }; 5 | -------------------------------------------------------------------------------- /webpack/index.babel.js: -------------------------------------------------------------------------------- 1 | import env from '../src/base/shared/Env'; 2 | 3 | import * as development from './webpack.dev.config'; 4 | import * as production from './webpack.prod.config'; 5 | 6 | module.exports = ( env === 'development' ) ? development : production; 7 | -------------------------------------------------------------------------------- /webpack/webpack.common.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import chalk from 'chalk'; 3 | import webpack from 'webpack'; 4 | import AssetsPlugin from 'assets-webpack-plugin'; 5 | import ProgressBarPlugin from 'progress-bar-webpack-plugin'; 6 | import baseWpPlugins from '../src/base/wp-plugins'; 7 | 8 | export const mainPath = path.resolve(__dirname, '..'); 9 | export const context = path.resolve(__dirname, '../'); 10 | export const buildPath = path.resolve(__dirname, '../dist'); 11 | export const basePath = path.resolve(__dirname, '../src/base'); 12 | export const dllPath = path.resolve(__dirname, '../dist/dlls'); 13 | export const assetsPath = path.resolve(__dirname, '../dist/assets'); 14 | export const clientPath = path.resolve(__dirname, '../src/base/client/'); 15 | export const manifestPath = buildPath; 16 | 17 | export const entry = { 18 | vendor: [ 19 | 'react', 20 | 'redux', 21 | 'react-dom', 22 | 'immutable', 23 | 'prop-types', 24 | 'classnames', 25 | 'react-redux', 26 | 'react-router', 27 | 'isomorphic-fetch', 28 | 'google-map-react' 29 | ] 30 | }; 31 | 32 | export const module = { 33 | rules: [ 34 | { test: /\.json$/, loader: 'json-loader', include: [mainPath] }, 35 | { test: /\.html/, loader: 'raw-loader', include: [mainPath] }, 36 | { test: /\.(png|woff|woff2|eot|ttf|svg)$/, loader: 'url-loader?limit=100000&name=assets/images/[name].[ext]' }, 37 | ] 38 | }; 39 | 40 | export const plugins = [ 41 | new ProgressBarPlugin({ 42 | format: `[BASE] ${chalk.blue('i')} Bundling... [:bar] ${chalk.green(':percent')} (:elapsed seconds)`, 43 | clear: true, 44 | summary: false, 45 | }), 46 | new AssetsPlugin({ 47 | path: buildPath, 48 | filename: 'webpack-assets.json', 49 | prettyPrint: true 50 | }), 51 | new baseWpPlugins.compileInfoPlugin(), 52 | ]; 53 | 54 | export const postcss = [ 55 | require('postcss-import')(), 56 | require('postcss-url')(), 57 | require('postcss-modules-extract-imports'), 58 | require('postcss-nested')(), 59 | require('postcss-reporter')(), 60 | require('precss')(), 61 | require('postcss-mixins')(), 62 | ]; 63 | 64 | export const resolve = { 65 | extensions: ['.js', '.jsx', '.css'], 66 | modules: ['node_modules'], 67 | alias: { 68 | 'app': path.resolve(__dirname, '../src/app'), 69 | 'server': path.resolve(__dirname, '../server'), 70 | 'base': path.resolve(__dirname, '../src/base'), 71 | 'store': path.resolve(__dirname, '../src/base/store'), 72 | 'styles': path.resolve(__dirname, '../src/app/styles'), 73 | 'mocks': path.resolve(__dirname, '../server/api/mocks'), 74 | 'containers': path.resolve(__dirname, '../src/app/containers'), 75 | 'components': path.resolve(__dirname, '../src/app/components') 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /webpack/webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import * as common from './webpack.common.config'; 3 | 4 | export const cache = true; 5 | export const devtool = 'cheap-module-eval-source-map'; 6 | export const context = common.context; 7 | export const resolve = common.resolve; 8 | export const entry = { 9 | app: [ 10 | common.clientPath, 11 | 'webpack/hot/dev-server', 12 | 'webpack-hot-middleware/client' 13 | ], 14 | }; 15 | 16 | export const output = { 17 | path: common.buildPath, 18 | publicPath: '/', 19 | library: '[name]', 20 | filename: '[name].js', 21 | sourceMapFilename: '[name].map', 22 | chunkFilename: '[name].chunk.js', 23 | }; 24 | 25 | export const module = { 26 | rules: common.module.rules.concat([ 27 | { 28 | test: [/\.jsx?$/], 29 | include: [/src/], 30 | loader: 'babel-loader', 31 | exclude: [/node_modules/, /dist/, /server/], 32 | query: { 33 | cacheDirectory: true, 34 | presets: ['react-hmre', 'es2015', 'stage-0', 'react'] 35 | } 36 | }, 37 | { 38 | test: /\.css$/, 39 | exclude: /node_modules/, 40 | use: [ 41 | { 42 | loader: 'style-loader', 43 | }, 44 | { 45 | loader: 'css-loader', 46 | options: { 47 | modules: true, 48 | importLoaders: 1, 49 | localIdentName: '[local]-[hash:base64:4]' 50 | } 51 | }, 52 | { 53 | loader: 'postcss-loader', 54 | options: { 55 | plugins: () => common.postcss 56 | } 57 | } 58 | ] 59 | } 60 | ]) 61 | }; 62 | 63 | export const plugins = [ 64 | new webpack.DefinePlugin({'process.env': {'NODE_ENV': '"development"'}}), 65 | new webpack.HotModuleReplacementPlugin(), 66 | new webpack.DllReferencePlugin({ 67 | context: common.context, 68 | manifest: require(`${common.dllPath}/vendor-manifest.json`) 69 | }), 70 | ] 71 | .concat(common.plugins); 72 | -------------------------------------------------------------------------------- /webpack/webpack.dll.config.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | import AssetsPlugin from 'assets-webpack-plugin'; 4 | 5 | import * as common from './webpack.common.config'; 6 | 7 | export const cache = true; 8 | export const devtool = 'eval'; 9 | export const entry = common.entry; 10 | export const context = common.context; 11 | export const resolve = common.resolve; 12 | 13 | export const output = { 14 | path: common.buildPath, 15 | publicPath: '/', 16 | library: '[name]', 17 | filename: '[name].dll.js', 18 | }; 19 | 20 | export const module = { 21 | loaders: [ 22 | { 23 | test: [/\.jsx?$/], 24 | include: [/src/], 25 | loader: 'babel-loader', 26 | exclude: [/node_modules/, /dist/, /server/], 27 | query: { 28 | cacheDirectory: true, 29 | presets: ['es2015', 'stage-0', 'react'] 30 | } 31 | } 32 | ] 33 | }; 34 | 35 | export const plugins = [ 36 | new webpack.DllPlugin({ 37 | path: path.join(common.dllPath, "[name]-manifest.json"), 38 | name: "[name]", 39 | }), 40 | new AssetsPlugin({ 41 | path: common.buildPath, 42 | filename: 'webpack-assets.json', 43 | prettyPrint: true 44 | }), 45 | ].concat(common.plugins); 46 | -------------------------------------------------------------------------------- /webpack/webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | import copyWebpackPlugin from 'copy-webpack-plugin'; 4 | import ExtractTextPlugin from 'extract-text-webpack-plugin'; 5 | 6 | import * as common from './webpack.common.config'; 7 | 8 | export const cache = true; 9 | export const devtool = 'cheap-module-source-map'; 10 | export const context = common.context; 11 | export const resolve = common.resolve; 12 | export const entry = { 13 | app: common.clientPath, 14 | vendor: common.entry.vendor 15 | }; 16 | 17 | export const output = { 18 | path: common.assetsPath, 19 | publicPath: '/', 20 | library: '[name]', 21 | filename: '[name].[hash].js', 22 | sourceMapFilename: '[name].map', 23 | chunkFilename: '[name].[hash].chunk.js', 24 | }; 25 | 26 | 27 | export const module = { 28 | rules: common.module.rules.concat([ 29 | { 30 | test: [/\.jsx?$/], 31 | include: [/src/], 32 | loader: 'babel-loader', 33 | exclude: [/node_modules/, /dist/, /server/], 34 | query: { 35 | cacheDirectory: true, 36 | presets: ['es2015', 'stage-0', 'react'] 37 | } 38 | }, 39 | { 40 | test: /\.css/, 41 | exclude: /node_modules/, 42 | 43 | use: ExtractTextPlugin.extract({ 44 | fallback: 'style-loader', 45 | use: [ 46 | { 47 | loader: 'css-loader', 48 | options: { 49 | modules: true, 50 | importLoaders: 1, 51 | localIdentName: '[hash:base64:4]' 52 | } 53 | }, 54 | { 55 | loader: 'postcss-loader', 56 | options: { 57 | plugins: () => common.postcss.concat( 58 | require('postcss-clean')(), 59 | require('autoprefixer')() 60 | ) 61 | } 62 | } 63 | ] 64 | }) 65 | } 66 | ]) 67 | }; 68 | 69 | export const plugins = [ 70 | new webpack.DefinePlugin({'process.env': { NODE_ENV: JSON.stringify('production')}}), 71 | new webpack.DllReferencePlugin({ 72 | context: path.join(__dirname), 73 | manifest: require('../dist/dlls/vendor-manifest.json') 74 | }), 75 | new webpack.optimize.CommonsChunkPlugin({ 76 | name: 'vendor', 77 | chunks: ['app'], 78 | minChunks: module => /node_modules/.test(module.resource) 79 | }), 80 | new copyWebpackPlugin([{ from: 'src/app/assets', to: '../assets' }]), 81 | new webpack.NoEmitOnErrorsPlugin(), 82 | new webpack.optimize.UglifyJsPlugin({compressor: { warnings: false }, output: { comments: false }}), 83 | new ExtractTextPlugin({ filename: 'styles.[contenthash].css', allChunks: true }) 84 | ].concat(common.plugins); 85 | --------------------------------------------------------------------------------
Actual Data of Weather Station