Skip to content

Commit

Permalink
React Redux app structure
Browse files Browse the repository at this point in the history
  • Loading branch information
Vasyl Stokolosa committed Nov 16, 2017
1 parent e9e9924 commit 278784d
Show file tree
Hide file tree
Showing 72 changed files with 12,904 additions and 2 deletions.
6 changes: 6 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"presets": ["react", "es2015", "stage-0"],
"plugins": [
"transform-async-to-generator"
]
}
17 changes: 17 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
yarn-error.log

node_modules
npm-debug*

*.js.map

.idea

app/tests/__tests__/coverage/*

.DS_Store

app/config.json

build/*
index.html
10 changes: 10 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
language: node_js

node_js:
- stable

install:
- npm install

script:
- npm run test
104 changes: 104 additions & 0 deletions Gruntfile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
'use strict';

const config = require('./app/config.json');
const TARGET = process.env.npm_lifecycle_event;

module.exports = function(grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),

'cache-busting': {
libsJs: {
replace: ['./index.html'],
replacement: 'libs.js',
file: './build/libs.js',
cleanup: true
},
indexJs: {
replace: ['./index.html'],
replacement: 'index.js',
file: './build/index.js',
cleanup: true
},
css: {
replace: ['./index.html'],
replacement: 'styles.min.css',
file: './build/styles.min.css',
cleanup: true
}
},

strip_code: {
options: {
blocks: [
{
start_block: '/* staging-code */',
end_block: '/* end-staging-code */'
}
]
},
target: {
src: `${config.PATHS.app}/app.jsx`,
dest: `${config.PATHS.app}/app.jsx`
}
},

replace: {
js_images: {
options: {
patterns: [
{
match: new RegExp('src:"./images/', 'g'),
replacement: () => `src:"${config[TARGET].assetHost}/images/`
}
]
},
files: [
{
src: ['./build/index.js'],
dest: './'
}
]
},
fonts: {
options: {
patterns: [
{
match: new RegExp('../fonts/', 'g'),
replacement: () => `${config[TARGET].assetHost}/fonts/`
}
]
},
files: [
{
src: ['./build/styles.min.css'],
dest: './'
}
]
},
css_images: {
options: {
patterns: [
{
match: new RegExp('../images/', 'g'),
replacement: () => `${config[TARGET].assetHost}/images/`
}
]
},
files: [
{
src: ['./build/styles.min.css'],
dest: './'
}
]
}
}
});

grunt.loadNpmTasks('grunt-cache-busting');
grunt.loadNpmTasks('grunt-strip-code');
grunt.loadNpmTasks('grunt-replace');

grunt.registerTask('remove-code', ['strip_code']);
grunt.registerTask('default', ['replace', 'cache-busting']);
};
50 changes: 48 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,48 @@
# create-react-redux-app-structure
Soon, you will find here react redux app with build configurations
# Create React Redux App Structure #
[![MIT Licence](https://badges.frapsoft.com/os/mit/mit.svg?v=103)](https://opensource.org/licenses/mit-license.php) [![Build Status](https://travis-ci.org/shystruk/create-react-redux-app-structure.svg?branch=master)](https://travis-ci.org/shystruk/create-react-redux-app-structure)
Create React + Redux app structure with build configurations

## Prepare config.json for build configurations ##
For running builds you need to have **config.json** in app/ folder.
So you can create new one or rename **app/config.json.example**.
Inside that file:
- **PATHS** is used in Grunt and Gulp tasks
- **assetHost** CDN path for each build
- **serverHost** is used for running e2e Cypress tests

## Getting Started ##
### Installation ###
**`npm install`** or **`yarn install`**

#### Run server ####
`node index.js`


## Build scripts ##
Development - **npm run dev** or **yarn run dev**

Production - **npm run prod** or **npm run prod**

Staging - **npm run staging** or **npm run staging**


## Tests ##
Unit - **npm run test**

Unit with watch - **npm run test:watch**

E2E - **npm run e2e**

Coverage is here - *app/tests/__tests__/coverage/Icon-report/index.html*


## Automation tests ##
Let's images that for automation tests we need to get access to the Redux store.
We can do that by adding to the `window` object property with reference to the store. For e.g. in `app.jsx` file.
Automation tests run only in **staging**, so for production build we remove them out by Grunt task `strip_code`

```javascript
/* staging-code */
window.store = store;
/* end-staging-code */
```
24 changes: 24 additions & 0 deletions app/actions/alert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use strict';

export const SHOW_ALERT = 'SHOW_ALERT';
export const HIDE_ALERT = 'HIDE_ALERT';

/**
* @param {String} message
* @return {Object}
*/
export function showAlert(message) {
return {
type: SHOW_ALERT,
message
}
}

/**
* @return {Object}
*/
export function hideAlert() {
return {
type: HIDE_ALERT
}
}
16 changes: 16 additions & 0 deletions app/actions/weather.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use strict';

export const PUSH_WEATHER = 'PUSH_WEATHER';
export const PUSH_WEATHER_LIST = 'PUSH_WEATHER_LIST';
export const REMOVE_WEATHER_FROM_LIST = 'REMOVE_WEATHER_FROM_LIST';

/**
* @param {Object} weather
* @return {Object}
*/
export function pushWeather(weather) {
return {
type: PUSH_WEATHER,
weather
}
}
28 changes: 28 additions & 0 deletions app/actions/weather_list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict';

export const PUSH_WEATHER_LIST = 'PUSH_WEATHER_LIST';
export const REMOVE_WEATHER_FROM_LIST = 'REMOVE_WEATHER_FROM_LIST';

/**
* @param {Object} weather
* @return {Object}
*/
export function pushWeatherList(weather) {
return {
type: PUSH_WEATHER_LIST,
weather
}
}

/**
* @param {Number} index
* @return {Object}
*/
export function removeWeatherFromList(index) {
return {
type: REMOVE_WEATHER_FROM_LIST,
index
}
}


23 changes: 23 additions & 0 deletions app/app.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use strict';

import React from 'react';
import ReactDom from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom'
import { Provider } from 'react-redux';
import store from './store';

import App from './pages/App';

/* staging-code */
window.store = store;
/* end-staging-code */

ReactDom.render(
<Provider store={store}>
<Router>
<App/>
</Router>
</Provider>,

document.getElementById('app')
);
95 changes: 95 additions & 0 deletions app/components/Open_Weather_Search/Open_Weather_Search.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
'use strict';

import React, { Component } from 'react';
import store from './../../store';
import { pushWeather } from './../../actions/weather';
import { pushWeatherList } from './../../actions/weather_list';
import { showAlert } from './../../actions/alert';
import { getWeatherData } from './../../resources/Open_Weather.resource';
import { Open_Weather_Interface } from './../../helpers/interfaces';
import { parseWeatherResponseForUI } from './Open_Weather_Search.utils';
import { KEY_CODES } from './../../constants/keyCodes.constant';

export default class Open_Weather extends Component {
constructor() {
super();

this.state = Open_Weather_Interface;

this.handleChange = this.handleChange.bind(this);
this.handleKeyUp = this.handleKeyUp.bind(this);
this.handleSearch = this.handleSearch.bind(this);
}

/**
* @param {Object} event
*/
handleKeyUp(event) {
if (event.keyCode === KEY_CODES.ENTER) {
this.handleSearch();
}
}

/**
* @param {Object} event
*/
handleChange(event) {
const target = event.target;
const value = target.type === 'checkbox' ? target.checked : target.value;

this.setState(() => ({[target.name]: value}));
}

async handleSearch() {
let weather = {};

this.setState(() => ({preload: true}));

try {
weather = await getWeatherData(this.state.cityName, this.state.isCityList);

Open_Weather.addWeatherToStore(weather);

} catch(error) {
// do not handle Weather API 'find' endpoint error :)
if (!this.state.isCityList) {
store.dispatch(showAlert(error.message));
}
} finally {
this.setState(() => ({preload: false}));
}
}

/**
* @param {Object} weather
*/
static addWeatherToStore(weather) {
if (weather.list) {
for (let item of weather.list) {
store.dispatch(pushWeatherList(parseWeatherResponseForUI(item)));
}
} else {
store.dispatch(pushWeather(parseWeatherResponseForUI(weather)));
}
}

render() {
return <div className="app-open-weather-search">

<h3>Current weather in your city</h3>

<input type="text" name="cityName" placeholder="Your city name" value={this.state.cityName}
onChange={this.handleChange} onKeyUp={this.handleKeyUp}/>
<label>
<input type="checkbox" name="isCityList" value={this.state.isCityList} onChange={this.handleChange}/>
<span>List of cities</span>
</label>

<button onClick={this.handleSearch} type="button">Search</button>

<img className={"app-preload " + (this.state.preload ? 'displayBlock' : 'displayNone')}
src="./images/loader.gif"/>

</div>
}
}
Loading

0 comments on commit 278784d

Please sign in to comment.