├── .babelrc
├── .browserslistrc
├── .dockerignore
├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .istanbul.yml
├── .nycrc
├── .travis.yml
├── Dockerfile
├── Dockerfile.test
├── Jenkinsfile
├── LICENSE
├── README.md
├── jenkins
├── Dockerfile
└── docker-compose.yml
├── package-lock.json
├── package.json
├── postcss.config.js
├── server
├── index.js
├── schema
│ ├── index.js
│ ├── mutation.js
│ ├── query.js
│ └── user.js
└── storage.js
├── src
├── favicon.ico
├── index.html
├── index.jsx
├── modules
│ ├── actions
│ │ ├── actions
│ │ │ └── index.js
│ │ ├── components
│ │ │ ├── index.jsx
│ │ │ └── style.scss
│ │ ├── constants
│ │ │ └── index.js
│ │ ├── containers
│ │ │ └── index.js
│ │ ├── epics
│ │ │ ├── index.js
│ │ │ └── init.js
│ │ ├── index.jsx
│ │ └── reducers
│ │ │ └── index.js
│ ├── home
│ │ ├── index.jsx
│ │ └── logo.png
│ └── layout
│ │ ├── actions
│ │ └── index.js
│ │ ├── components
│ │ ├── bar
│ │ │ └── index.jsx
│ │ ├── drawer
│ │ │ ├── index.jsx
│ │ │ └── style.scss
│ │ ├── index.jsx
│ │ ├── link
│ │ │ ├── index.jsx
│ │ │ └── style.scss
│ │ ├── menu
│ │ │ ├── index.jsx
│ │ │ └── style.scss
│ │ ├── placeholder
│ │ │ └── index.jsx
│ │ └── style.scss
│ │ ├── constants
│ │ └── index.js
│ │ ├── containers
│ │ └── index.js
│ │ ├── epics
│ │ ├── hideDrawer.js
│ │ └── index.js
│ │ ├── index.jsx
│ │ └── reducers
│ │ └── index.js
├── routes.js
└── service-worker.js
├── tests
├── server
│ └── index.js
└── src
│ ├── helper.js
│ └── modules
│ └── actions
│ ├── components
│ └── index.jsx
│ ├── epics
│ ├── index.js
│ └── init.js
│ └── redux
│ ├── actions.js
│ ├── constants.js
│ └── index.js
├── webpack.config.js
└── webpack
├── dev_server.js
├── devtool.js
├── index.js
├── plugins.js
├── rules.js
└── vendor.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "env", {
5 | "useBuiltIns": true
6 | }
7 | ],
8 | "react"
9 | ],
10 | "plugins": [
11 | "transform-class-properties",
12 | "transform-object-rest-spread",
13 | "react-hot-loader/babel",
14 | "syntax-dynamic-import",
15 | "lodash"
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/.browserslistrc:
--------------------------------------------------------------------------------
1 | # Browsers that we support
2 |
3 | > 1%
4 | Last 2 versions
5 | IE 10 # sorry
6 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 | coverage
4 | .nyc_output
5 | npm-debug.log
6 | jenkins
7 |
8 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig is awesome: http://EditorConfig.org
2 |
3 | # top-most EditorConfig file
4 | root = true
5 |
6 | # Unix-style newlines with a newline ending every file
7 | [*]
8 | end_of_line = lf
9 | insert_final_newline = true
10 |
11 | # Set default charse
12 | charset = utf-8
13 |
14 | # 2 space indentation
15 | indent_style = space
16 | indent_size = 2
17 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | webpack/*.js
2 | dist/*.js
3 | src/service-worker.js
4 | coverage
5 | tests/src/helper.js
6 | jenkins
7 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "extends": "airbnb",
4 | "env": {
5 | "browser": true,
6 | "node": true,
7 | "mocha": true
8 | }
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | node_modules
3 | dist
4 | !dist/.gitkeep
5 | coverage
6 | npm-debug.log
7 | .nyc_output
8 | jenkins/jenkins_home
9 |
--------------------------------------------------------------------------------
/.istanbul.yml:
--------------------------------------------------------------------------------
1 | instrumentation:
2 | default-excludes: true
3 | excludes: ['**/dist/**', '**/coverage/**']
4 | include-all-sources: true
5 | root: .
6 | extensions: ['.js', '.jsx']
7 | check:
8 | global:
9 | statements: 80
10 | lines: 80
11 | branches: 80
12 | functions: 80
13 |
--------------------------------------------------------------------------------
/.nycrc:
--------------------------------------------------------------------------------
1 | {
2 | "extension": [".js",".jsx"],
3 | "require": ["./tests/src/helper.js"],
4 | "exclude": [
5 | "node_modules",
6 | "dist",
7 | "webpack",
8 | "coverage",
9 | "tests",
10 | "src/service-worker.js",
11 | "postcss.config.js",
12 | "webpack.config.js",
13 | "jenkins"
14 | ],
15 | "check-coverage": true,
16 | "per-file": true,
17 | "lines": 0,
18 | "statements": 0,
19 | "functions": 0,
20 | "branches": 0,
21 | "reporter": [
22 | "lcov",
23 | "text",
24 | "text-summary",
25 | "html"
26 | ],
27 | "all": true
28 | }
29 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "8"
4 | - "9"
5 | script:
6 | - npm test
7 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Extending image
2 | FROM node:carbon
3 |
4 | RUN apt-get update
5 | RUN apt-get upgrade -y
6 | RUN apt-get -y install autoconf automake libtool nasm make pkg-config git apt-utils
7 |
8 | # Create app directory
9 | RUN mkdir -p /usr/src/app
10 | WORKDIR /usr/src/app
11 |
12 | # Versions
13 | RUN npm -v
14 | RUN node -v
15 |
16 | # Install app dependencies
17 | COPY package.json /usr/src/app/
18 | COPY package-lock.json /usr/src/app/
19 |
20 | RUN npm install
21 |
22 | # Bundle app source
23 | COPY . /usr/src/app
24 |
25 | # Port to listener
26 | EXPOSE 3000
27 |
28 | # Environment variables
29 | ENV NODE_ENV production
30 | ENV PORT 3000
31 | ENV PUBLIC_PATH "/"
32 |
33 | RUN npm run start:build
34 |
35 | # Main command
36 | CMD [ "npm", "run", "start:server" ]
37 |
--------------------------------------------------------------------------------
/Dockerfile.test:
--------------------------------------------------------------------------------
1 | # Extending image
2 | FROM node:carbon
3 |
4 | RUN apt-get update
5 | RUN apt-get upgrade -y
6 | RUN apt-get -y install autoconf automake libtool nasm make pkg-config git apt-utils
7 |
8 | # Create app directory
9 | RUN mkdir -p /usr/src/app
10 | WORKDIR /usr/src/app
11 |
12 | # Versions
13 | RUN npm -v
14 | RUN node -v
15 |
16 | # Install app dependencies
17 | COPY package.json /usr/src/app/
18 | COPY package-lock.json /usr/src/app/
19 |
20 | RUN npm install
21 |
22 | # Bundle app source
23 | COPY . /usr/src/app
24 |
25 | # Environment variables
26 | ENV NODE_ENV test
27 |
28 | # Main command
29 | CMD [ "npm", "test" ]
30 |
--------------------------------------------------------------------------------
/Jenkinsfile:
--------------------------------------------------------------------------------
1 | node {
2 | try {
3 | stage('Checkout') {
4 | checkout scm
5 | }
6 | stage('Environment') {
7 | sh 'git --version'
8 | echo "Branch: ${env.BRANCH_NAME}"
9 | sh 'docker -v'
10 | sh 'printenv'
11 | }
12 | stage('Build Docker test'){
13 | sh 'docker build -t react-test -f Dockerfile.test --no-cache . '
14 | }
15 | stage('Docker test'){
16 | sh 'docker run --rm react-test'
17 | }
18 | stage('Clean Docker test'){
19 | sh 'docker rmi react-test'
20 | }
21 | stage('Deploy'){
22 | if(env.BRANCH_NAME == 'master'){
23 | sh 'docker build -t react-app --no-cache .'
24 | sh 'docker tag react-app localhost:5000/react-app'
25 | sh 'docker push localhost:5000/react-app'
26 | sh 'docker rmi -f react-app localhost:5000/react-app'
27 | }
28 | }
29 | }
30 | catch (err) {
31 | throw err
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 Evheniy Bystrov
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
13 | all 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
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React app from scratch
2 |
3 | It's repository for code from article
4 | [React app from scratch](https://medium.com/@evheniybystrov/react-app-from-scratch-d694300d1631)
5 |
6 | ## Docker
7 |
8 | To build
9 |
10 | npm run docker:build
11 |
12 | To run
13 |
14 | npm run docker:run
15 |
16 | To stop
17 |
18 | docker:stop
19 |
20 | ## Run
21 |
22 | npm start
23 |
24 | ## Development
25 |
26 | npm run dev
27 |
28 | ## Testing
29 |
30 | npm test
31 |
32 |
--------------------------------------------------------------------------------
/jenkins/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM jenkins/jenkins:lts
2 |
3 | USER root
4 |
--------------------------------------------------------------------------------
/jenkins/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 |
5 | jenkins:
6 | build: .
7 | container_name: jenkins
8 | privileged: true
9 | restart: always
10 | ports:
11 | - 8080:8080
12 | volumes:
13 | - ./jenkins_home:/var/jenkins_home
14 | - /var/run/docker.sock:/var/run/docker.sock
15 | - /usr/local/bin/docker:/usr/bin/docker
16 |
17 | registry:
18 | image: registry
19 | container_name: registry
20 | restart: always
21 | ports:
22 | - 5000:5000
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "app",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "dev": "webpack-dev-server",
8 | "test": "NODE_ENV=test PORT=4000 npm-run-all test:*",
9 | "start": "NODE_ENV=production npm-run-all start:*",
10 | "start:build": "webpack -p",
11 | "start:server": "nodemon server/index.js",
12 | "test:security": "nsp check",
13 | "test:lint": "eslint .",
14 | "test:server": "NODE_ENV=test mocha tests/server/**/*.js",
15 | "test:src": "mocha --require tests/src/helper.js tests/src/**/*.{js,jsx}",
16 | "test:coverage": "NODE_ENV=test nyc mocha tests/**/**/*.{js,jsx}",
17 | "test:build": "NODE_ENV=production npm run start:build",
18 | "precommit": "npm t",
19 | "prepush": "npm t",
20 | "docker:build": "docker build -t react .",
21 | "docker:run": "docker run -d -p 3000:3000 --name react react",
22 | "docker:stop": "docker rm -f react"
23 | },
24 | "keywords": [],
25 | "author": "",
26 | "license": "MIT",
27 | "devDependencies": {
28 | "babel-core": "^6.26.0",
29 | "babel-eslint": "^8.2.1",
30 | "babel-loader": "^7.1.2",
31 | "babel-plugin-lodash": "^3.3.2",
32 | "babel-plugin-react-hot-loader": "^3.0.0-beta.6",
33 | "babel-plugin-syntax-dynamic-import": "^6.18.0",
34 | "babel-plugin-transform-class-properties": "^6.24.1",
35 | "babel-plugin-transform-object-rest-spread": "^6.26.0",
36 | "babel-preset-env": "^1.6.1",
37 | "babel-preset-react": "^6.24.1",
38 | "chai": "^4.1.2",
39 | "chai-http": "^3.0.0",
40 | "clean-webpack-plugin": "^0.1.18",
41 | "copy-webpack-plugin": "^4.4.1",
42 | "css-loader": "^0.28.9",
43 | "cssnano": "^3.10.0",
44 | "dirty-chai": "^2.0.1",
45 | "enzyme": "^3.3.0",
46 | "enzyme-adapter-react-16": "^1.1.1",
47 | "eslint": "^4.18.0",
48 | "eslint-config-airbnb": "^16.1.0",
49 | "eslint-plugin-import": "^2.8.0",
50 | "eslint-plugin-jsx-a11y": "^6.0.3",
51 | "eslint-plugin-react": "^7.6.1",
52 | "extract-text-webpack-plugin": "^3.0.2",
53 | "favicons-webpack-plugin": "0.0.7",
54 | "file-loader": "^1.1.6",
55 | "html-webpack-plugin": "^2.30.1",
56 | "husky": "^0.14.3",
57 | "ignore-styles": "^5.0.1",
58 | "image-webpack-loader": "^4.1.0",
59 | "lodash-webpack-plugin": "^0.11.4",
60 | "mocha": "^5.0.1",
61 | "node-sass": "^4.7.2",
62 | "nodemon": "^1.15.0",
63 | "npm-install-webpack-plugin": "^4.0.5",
64 | "npm-run-all": "^4.1.2",
65 | "nsp": "^3.2.1",
66 | "nyc": "^11.4.1",
67 | "postcss": "^6.0.19",
68 | "postcss-cssnext": "^3.1.0",
69 | "postcss-loader": "^2.1.0",
70 | "preload-webpack-plugin": "^2.2.0",
71 | "react-test-renderer": "^16.2.0",
72 | "sass-loader": "^6.0.6",
73 | "sinon": "^4.3.0",
74 | "style-loader": "^0.20.2",
75 | "webpack": "^3.11.0",
76 | "webpack-bundle-analyzer": "^2.10.0",
77 | "webpack-dev-server": "^2.11.1",
78 | "webpack-pwa-manifest": "^3.5.0",
79 | "workbox-sw": "^2.1.2",
80 | "workbox-webpack-plugin": "^2.1.2"
81 | },
82 | "dependencies": {
83 | "@redux-offline/redux-offline": "2.2.1",
84 | "babel-polyfill": "^6.26.0",
85 | "debug": "^3.1.0",
86 | "graphql": "^0.13.1",
87 | "history": "^4.7.2",
88 | "localforage": "^1.5.6",
89 | "lodash": "^4.17.5",
90 | "mime-types": "^2.1.18",
91 | "moment": "^2.20.1",
92 | "mz": "^2.7.0",
93 | "prop-types": "^15.6.0",
94 | "react": "^16.2.0",
95 | "react-dom": "^16.2.0",
96 | "react-hot-loader": "^4.0.0-beta.14",
97 | "react-loadable": "^5.3.1",
98 | "react-loader": "^2.4.2",
99 | "react-media": "^1.8.0",
100 | "react-perf-devtool": "^3.0.2",
101 | "react-proxy": "^1.1.8",
102 | "react-redux": "^5.0.7",
103 | "react-router": "^4.2.0",
104 | "react-router-config": "^1.0.0-beta.4",
105 | "react-router-dom": "^4.2.2",
106 | "react-router-redux": "^5.0.0-alpha.9",
107 | "react-toolbox": "^2.0.0-beta.12",
108 | "react-transition-group": "^2.2.1",
109 | "react-virtualized": "^9.18.5",
110 | "redux": "^3.7.2",
111 | "redux-logger": "^3.0.6",
112 | "redux-observable": "^0.18.0",
113 | "redux-thunk": "^2.2.0",
114 | "resolve-path": "^1.4.0",
115 | "rxjs": "^5.5.6",
116 | "wpb": "0.0.11",
117 | "yeps": "^1.1.1",
118 | "yeps-error": "^1.3.1",
119 | "yeps-graphql": "^0.1.1",
120 | "yeps-index": "0.0.3",
121 | "yeps-router": "^1.2.0",
122 | "yeps-server": "^1.1.2",
123 | "yeps-static": "^1.4.3"
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | 'postcss-cssnext': {
4 | warnForDuplicates: false,
5 | },
6 | cssnano: {},
7 | },
8 | };
9 |
10 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | const App = require('yeps');
2 |
3 | const error = require('yeps-error');
4 | const serve = require('yeps-static');
5 | const index = require('yeps-index');
6 | const graphql = require('yeps-graphql');
7 | const Router = require('yeps-router');
8 | const server = require('yeps-server');
9 |
10 | const { resolve } = require('path');
11 | const { createReadStream } = require('fs');
12 |
13 | const schema = require('./schema');
14 |
15 | const root = resolve(__dirname, '..', 'dist');
16 | const graphiql = true;
17 |
18 | const app = new App();
19 |
20 | app.all([
21 | error(),
22 | serve({ root, index: false }),
23 | index({ root }),
24 | ]);
25 |
26 | const router = new Router();
27 |
28 | router.all('/graphql').then(graphql({ schema, graphiql }));
29 |
30 | app.then(router.resolve());
31 |
32 | app.then(ctx => new Promise((res, rej) => {
33 | createReadStream(resolve(__dirname, '..', 'dist', 'index.html'))
34 | .pipe(ctx.res)
35 | .on('error', rej)
36 | .on('close', rej)
37 | .on('finish', res);
38 | }));
39 |
40 | module.exports = server.createHttpServer(app);
41 |
--------------------------------------------------------------------------------
/server/schema/index.js:
--------------------------------------------------------------------------------
1 | const { GraphQLSchema } = require('yeps-graphql/graphql');
2 |
3 | const QueryType = require('./query');
4 | const MutationType = require('./mutation');
5 |
6 | const schema = new GraphQLSchema({
7 | query: QueryType,
8 | mutation: MutationType,
9 | });
10 |
11 | module.exports = schema;
12 |
--------------------------------------------------------------------------------
/server/schema/mutation.js:
--------------------------------------------------------------------------------
1 | const {
2 | GraphQLObjectType,
3 | GraphQLInt,
4 | GraphQLString,
5 | GraphQLNonNull,
6 | } = require('yeps-graphql/graphql');
7 |
8 | const UserType = require('./user');
9 | const storage = require('../storage');
10 |
11 |
12 | const MutationType = new GraphQLObjectType({
13 | name: 'MutationType',
14 | fields: () => ({
15 | createUser: {
16 | type: UserType,
17 | description: 'User creating',
18 | args: {
19 | id: {
20 | type: new GraphQLNonNull(GraphQLInt),
21 | },
22 | name: {
23 | type: new GraphQLNonNull(GraphQLString),
24 | },
25 | },
26 | resolve: (value, { id, name }) => {
27 | const index = storage.findIndex(user => user.id === parseInt(id, 10));
28 |
29 | if (index === -1) {
30 | storage.push({ id, name });
31 | return { id, name };
32 | }
33 |
34 | return Promise.reject(new Error('User exists!'));
35 | },
36 | },
37 | updateUser: {
38 | type: UserType,
39 | description: 'User updating',
40 | args: {
41 | id: {
42 | type: new GraphQLNonNull(GraphQLInt),
43 | },
44 | name: {
45 | type: new GraphQLNonNull(GraphQLString),
46 | },
47 | },
48 | resolve: (value, { id, name }) => {
49 | const index = storage.findIndex(user => user.id === parseInt(id, 10));
50 |
51 | if (index !== -1) {
52 | Object.assign(storage[index], { name });
53 | return storage[index];
54 | }
55 |
56 | return Promise.reject(new Error('User not found!'));
57 | },
58 | },
59 | deleteUser: {
60 | type: UserType,
61 | description: 'User deleting',
62 | args: {
63 | id: {
64 | type: new GraphQLNonNull(GraphQLInt),
65 | },
66 | },
67 | resolve: (value, { id }) => {
68 | const index = storage.findIndex(user => user.id === parseInt(id, 10));
69 |
70 | if (index !== -1) {
71 | storage.splice(index, 1);
72 | return { id };
73 | }
74 |
75 | return Promise.reject(new Error('User not found!'));
76 | },
77 | },
78 | }),
79 | });
80 |
81 | module.exports = MutationType;
82 |
--------------------------------------------------------------------------------
/server/schema/query.js:
--------------------------------------------------------------------------------
1 | const {
2 | GraphQLObjectType,
3 | GraphQLList,
4 | GraphQLNonNull,
5 | GraphQLID,
6 | } = require('yeps-graphql/graphql');
7 |
8 | const UserType = require('./user');
9 | const storage = require('../storage');
10 |
11 | const QueryType = new GraphQLObjectType({
12 | name: 'QueryType',
13 | description: 'User information',
14 | fields: {
15 | users: {
16 | type: new GraphQLList(UserType),
17 | resolve() {
18 | return storage;
19 | },
20 | },
21 | user: {
22 | type: UserType,
23 | args: {
24 | id: {
25 | type: new GraphQLNonNull(GraphQLID),
26 | },
27 | },
28 | resolve(parent, { id }) {
29 | const index = storage.findIndex(user => user.id === parseInt(id, 10));
30 |
31 | if (index !== -1) {
32 | return storage[index];
33 | }
34 |
35 | return Promise.reject(new Error('User not found!'));
36 | },
37 | },
38 | },
39 | });
40 |
41 | module.exports = QueryType;
42 |
--------------------------------------------------------------------------------
/server/schema/user.js:
--------------------------------------------------------------------------------
1 | const {
2 | GraphQLObjectType,
3 | GraphQLInt,
4 | GraphQLString,
5 | } = require('yeps-graphql/graphql');
6 |
7 | const UserType = new GraphQLObjectType({
8 | name: 'UserType',
9 | description: 'User type',
10 | fields: {
11 | id: {
12 | type: GraphQLInt,
13 | },
14 | name: {
15 | type: GraphQLString,
16 | },
17 | },
18 | });
19 |
20 | module.exports = UserType;
21 |
--------------------------------------------------------------------------------
/server/storage.js:
--------------------------------------------------------------------------------
1 | const storage = [];
2 |
3 | module.exports = storage;
4 |
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evheniy/react-app/420570cd2050672e7b14d4894f7b17e7ed454d57/src/favicon.ico
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | PWA APP
8 |
9 |
10 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
--------------------------------------------------------------------------------
/src/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import app from 'wpb/lib/app';
3 | import { renderRoutes } from 'react-router-config';
4 | import Layout from './modules/layout';
5 | import routes from './routes';
6 |
7 | app(() => (
8 |
9 | {renderRoutes(routes)}
10 |
11 | ));
12 |
13 | if (module.hot) {
14 | module.hot.accept();
15 | }
16 |
17 | if (process.env.NODE_ENV !== 'production') {
18 | require('react-perf-devtool')();
19 | }
20 |
21 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
22 | window.addEventListener('load', () => {
23 | navigator.serviceWorker.register('service-worker.js').catch(() => {});
24 | });
25 | }
26 |
--------------------------------------------------------------------------------
/src/modules/actions/actions/index.js:
--------------------------------------------------------------------------------
1 | import * as constants from '../constants';
2 |
3 | export const initActions = () => ({
4 | type: constants.ACTIONS_INIT,
5 | });
6 |
7 | export const idleActions = () => ({
8 | type: constants.ACTIONS_IDLE,
9 | });
10 |
11 | export const clearActions = () => ({
12 | type: constants.ACTIONS_CLEAR,
13 | });
14 |
--------------------------------------------------------------------------------
/src/modules/actions/components/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { CSSTransition, TransitionGroup } from 'react-transition-group';
4 | import style from './style.scss';
5 |
6 | const classNames = {
7 | appear: style.appear,
8 | appearActive: style['appear-active'],
9 | enter: style.enter,
10 | enterActive: style['enter-active'],
11 | exit: style.exit,
12 | exitActive: style['exit-active'],
13 | };
14 |
15 | const transitionProps = {
16 | timeout: 1000,
17 | classNames,
18 | };
19 |
20 | const Component = ({ status, initActions, clearActions }) => (
21 |
22 | Status: {status}
23 |
24 | {status !== 'idle' && (
25 |
26 |
27 |
28 | )}
29 | {status === 'idle' && (
30 |
31 |
32 |
33 | )}
34 |
35 |
36 | );
37 |
38 | Component.propTypes = {
39 | status: PropTypes.string.isRequired,
40 | initActions: PropTypes.func.isRequired,
41 | clearActions: PropTypes.func.isRequired,
42 | };
43 |
44 | export default Component;
45 |
--------------------------------------------------------------------------------
/src/modules/actions/components/style.scss:
--------------------------------------------------------------------------------
1 | .enter {
2 | opacity: 0;
3 | }
4 |
5 | .enter.enter-active {
6 | opacity: 0;
7 | transition: opacity 300ms ease-in;
8 | }
9 |
10 | .exit {
11 | opacity: 0;
12 | }
13 |
14 | .exit.exit-active {
15 | opacity: 0.01;
16 | transition: opacity 200ms ease-in;
17 | }
18 |
19 | .appear {
20 | opacity: 0;
21 | }
22 |
23 | .appear.appear-active {
24 | opacity: 0.01;
25 | transition: opacity 100ms ease-in;
26 | }
27 |
--------------------------------------------------------------------------------
/src/modules/actions/constants/index.js:
--------------------------------------------------------------------------------
1 | export const ACTIONS = '@@actions/ACTIONS';
2 | export const ACTIONS_INIT = '@@actions/INIT';
3 | export const ACTIONS_IDLE = '@@actions/IDLE';
4 | export const ACTIONS_CLEAR = '@@actions/CLEAR';
5 |
--------------------------------------------------------------------------------
/src/modules/actions/containers/index.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import Component from '../components';
3 | import { initActions, clearActions } from '../actions';
4 | import { ACTIONS } from '../constants';
5 |
6 | const mapStateToProps = state => ({ status: state[ACTIONS].status });
7 | const mapDispatchToProps = { initActions, clearActions };
8 |
9 | export default connect(mapStateToProps, mapDispatchToProps)(Component);
10 |
--------------------------------------------------------------------------------
/src/modules/actions/epics/index.js:
--------------------------------------------------------------------------------
1 | import { combineEpics } from 'redux-observable';
2 |
3 | import initEpic from './init';
4 |
5 | export default combineEpics(initEpic);
6 |
--------------------------------------------------------------------------------
/src/modules/actions/epics/init.js:
--------------------------------------------------------------------------------
1 | import { ACTIONS_INIT } from '../constants';
2 | import { idleActions } from '../actions';
3 |
4 | export default action$ => action$.ofType(ACTIONS_INIT).mapTo(idleActions());
5 |
--------------------------------------------------------------------------------
/src/modules/actions/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { injectReducer, injectEpic } from 'wpb/lib/store';
3 | import Container from './containers';
4 | import redux from './reducers';
5 | import epics from './epics';
6 | import { ACTIONS } from './constants';
7 |
8 | injectEpic(ACTIONS, epics);
9 | injectReducer(ACTIONS, redux);
10 |
11 | export default props => (
12 |
13 | );
14 |
--------------------------------------------------------------------------------
/src/modules/actions/reducers/index.js:
--------------------------------------------------------------------------------
1 | import * as constants from '../constants';
2 |
3 | const status = 'null';
4 |
5 | const defaultState = {
6 | status,
7 | };
8 |
9 | export default (state = defaultState, action) => {
10 | switch (action.type) {
11 | case constants.ACTIONS_INIT:
12 | return { ...state, status: 'init' };
13 | case constants.ACTIONS_IDLE:
14 | return { ...state, status: 'idle' };
15 | case constants.ACTIONS_CLEAR:
16 | return { ...state, status };
17 | default:
18 | return state;
19 | }
20 | };
21 |
--------------------------------------------------------------------------------
/src/modules/home/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react';
2 | import logo from './logo.png';
3 |
4 | export default () => (
5 |
6 |
7 |
12 |
13 | );
14 |
--------------------------------------------------------------------------------
/src/modules/home/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evheniy/react-app/420570cd2050672e7b14d4894f7b17e7ed454d57/src/modules/home/logo.png
--------------------------------------------------------------------------------
/src/modules/layout/actions/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | LAYOUT_SHOW_DRAWER,
3 | LAYOUT_HIDE_DRAWER,
4 | } from '../constants';
5 |
6 | export const showDrawerAction = () => ({
7 | type: LAYOUT_SHOW_DRAWER,
8 | });
9 |
10 | export const hideDrawerAction = () => ({
11 | type: LAYOUT_HIDE_DRAWER,
12 | });
13 |
--------------------------------------------------------------------------------
/src/modules/layout/components/bar/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Media from 'react-media';
4 | import { AppBar } from 'react-toolbox';
5 |
6 | const Bar = ({ children, showDrawerAction, title }) => (
7 |
8 | {
9 | matches => matches ? (
10 |
15 | ) : (
16 |
17 | {children}
18 |
19 | )
20 | }
21 |
22 | );
23 |
24 | Bar.propTypes = {
25 | children: PropTypes.node,
26 | title: PropTypes.string.isRequired,
27 | showDrawerAction: PropTypes.func.isRequired,
28 | };
29 |
30 | Bar.defaultProps = {
31 | children: null,
32 | };
33 |
34 | export default Bar;
35 |
--------------------------------------------------------------------------------
/src/modules/layout/components/drawer/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { NavDrawer } from 'react-toolbox';
4 | import style from './style.scss';
5 |
6 | const Drawer = ({ children, isDrawerActive, hideDrawerAction }) => (
7 |
12 | {children}
13 |
14 | );
15 |
16 | Drawer.propTypes = {
17 | children: PropTypes.node,
18 | isDrawerActive: PropTypes.bool.isRequired,
19 | hideDrawerAction: PropTypes.func.isRequired,
20 | };
21 |
22 | Drawer.defaultProps = {
23 | children: null,
24 | };
25 |
26 | export default Drawer;
27 |
--------------------------------------------------------------------------------
/src/modules/layout/components/drawer/style.scss:
--------------------------------------------------------------------------------
1 | .drawer {
2 | background-color: grey;
3 | }
4 |
--------------------------------------------------------------------------------
/src/modules/layout/components/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Layout, Panel } from 'react-toolbox';
4 | import Drawer from './drawer';
5 | import Menu from './menu';
6 | import Bar from './bar';
7 | import style from './style.scss';
8 |
9 | const Component = ({ children, isDrawerActive, showDrawerAction, hideDrawerAction, routes, title }) => (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | {children}
20 |
21 |
22 |
23 | );
24 |
25 | Component.propTypes = {
26 | children: PropTypes.node,
27 | isDrawerActive: PropTypes.bool.isRequired,
28 | showDrawerAction: PropTypes.func.isRequired,
29 | hideDrawerAction: PropTypes.func.isRequired,
30 | routes: PropTypes.arrayOf(PropTypes.object),
31 | title: PropTypes.string.isRequired,
32 | };
33 |
34 | Component.defaultProps = {
35 | children: null,
36 | routes: [],
37 | };
38 |
39 | export default Component;
40 |
--------------------------------------------------------------------------------
/src/modules/layout/components/link/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Button } from 'react-toolbox';
4 | import { NavLink } from 'react-router-dom';
5 | import style from './style.scss';
6 |
7 | const Link = ({ url, label, icon }) => (
8 |
14 |
19 |
20 | );
21 |
22 | Link.propTypes = {
23 | url: PropTypes.string,
24 | label: PropTypes.string.isRequired,
25 | icon: PropTypes.string,
26 | };
27 |
28 | Link.defaultProps = {
29 | url: '/',
30 | icon: 'inbox',
31 | };
32 |
33 | export default Link;
34 |
--------------------------------------------------------------------------------
/src/modules/layout/components/link/style.scss:
--------------------------------------------------------------------------------
1 | .link > button {
2 | color: white !important;
3 | font-weight: normal;
4 | }
5 |
6 | .selected > button {
7 | font-weight: 800;
8 | }
9 |
--------------------------------------------------------------------------------
/src/modules/layout/components/menu/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Navigation } from 'react-toolbox';
4 | import style from './style.scss';
5 |
6 | import Link from '../link';
7 |
8 | const getLink = (url, type) => {
9 | const key = url + type;
10 | const props = { key, url };
11 |
12 | switch (url) {
13 | case '/actions':
14 | props.label = 'Actions';
15 | props.icon = 'person';
16 | break;
17 | default:
18 | props.label = 'Home';
19 | props.icon = 'inbox';
20 | }
21 |
22 | return ;
23 | };
24 |
25 | const Menu = ({ type, routes }) => (
26 |
27 | {routes.map(({ path }) => getLink(path, type))}
28 |
29 | );
30 |
31 | Menu.propTypes = {
32 | type: PropTypes.string,
33 | routes: PropTypes.arrayOf(PropTypes.object),
34 | };
35 |
36 | Menu.defaultProps = {
37 | type: 'vertical',
38 | routes: [],
39 | };
40 |
41 | export default Menu;
42 |
--------------------------------------------------------------------------------
/src/modules/layout/components/menu/style.scss:
--------------------------------------------------------------------------------
1 | .vertical {
2 | > a {
3 | display: block;
4 | }
5 | }
6 |
7 | .horizontal {
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/src/modules/layout/components/placeholder/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Placeholder = () => (
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | );
20 |
21 | export default Placeholder;
22 |
--------------------------------------------------------------------------------
/src/modules/layout/components/style.scss:
--------------------------------------------------------------------------------
1 | .content {
2 | margin-top: 10px;
3 | width: 100%;
4 | text-align: center;
5 | > img {
6 | width: 100%;
7 | max-width: 1213px;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/modules/layout/constants/index.js:
--------------------------------------------------------------------------------
1 | export const LAYOUT = '@@layout/LAYOUT';
2 | export const LAYOUT_SHOW_DRAWER = '@@layout/SHOW_DRAWER';
3 | export const LAYOUT_HIDE_DRAWER = '@@layout/HIDE_DRAWER';
4 |
--------------------------------------------------------------------------------
/src/modules/layout/containers/index.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import Component from '../components';
3 | import { showDrawerAction, hideDrawerAction } from '../actions';
4 | import { LAYOUT } from '../constants';
5 |
6 | const mapStateToProps = state => ({
7 | isDrawerActive: state[LAYOUT].isDrawerActive,
8 | });
9 |
10 | const mapDispatchToProps = { showDrawerAction, hideDrawerAction };
11 |
12 | export default connect(mapStateToProps, mapDispatchToProps)(Component);
13 |
--------------------------------------------------------------------------------
/src/modules/layout/epics/hideDrawer.js:
--------------------------------------------------------------------------------
1 | import { LOCATION_CHANGE } from 'react-router-redux';
2 | import { hideDrawerAction } from '../actions';
3 | import { LAYOUT } from '../constants';
4 |
5 | export default (action$, store) => action$.ofType(LOCATION_CHANGE)
6 | .filter(() => store.getState()[LAYOUT].isDrawerActive)
7 | .map(hideDrawerAction);
8 |
--------------------------------------------------------------------------------
/src/modules/layout/epics/index.js:
--------------------------------------------------------------------------------
1 | import { combineEpics } from 'redux-observable';
2 |
3 | import hideDrawerEpic from './hideDrawer';
4 |
5 | export default combineEpics(hideDrawerEpic);
6 |
--------------------------------------------------------------------------------
/src/modules/layout/index.jsx:
--------------------------------------------------------------------------------
1 | import { injectReducer, injectEpic } from 'wpb/lib/store';
2 | import Container from './containers';
3 | import reducers from './reducers';
4 | import epics from './epics';
5 | import { LAYOUT } from './constants';
6 |
7 | injectReducer(LAYOUT, reducers);
8 | injectEpic(LAYOUT, epics);
9 |
10 | export default Container;
11 |
--------------------------------------------------------------------------------
/src/modules/layout/reducers/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | LAYOUT_SHOW_DRAWER,
3 | LAYOUT_HIDE_DRAWER,
4 | } from '../constants';
5 |
6 | const defaultState = {
7 | isDrawerActive: false,
8 | };
9 |
10 | export default (state = defaultState, action) => {
11 | switch (action.type) {
12 | case LAYOUT_SHOW_DRAWER:
13 | return { ...state, isDrawerActive: true };
14 | case LAYOUT_HIDE_DRAWER:
15 | return { ...state, isDrawerActive: false };
16 | default:
17 | return state;
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/src/routes.js:
--------------------------------------------------------------------------------
1 | import DynamicComponent from 'wpb/lib/dynamic';
2 | import Placeholder from './modules/layout/components/placeholder';
3 |
4 | const routes = [{
5 | path: '/',
6 | exact: true,
7 | component: DynamicComponent(import('./modules/home'), Placeholder),
8 | }, {
9 | path: '/actions',
10 | exact: true,
11 | component: DynamicComponent(import('./modules/actions'), Placeholder),
12 | }];
13 |
14 | export default routes;
15 |
--------------------------------------------------------------------------------
/src/service-worker.js:
--------------------------------------------------------------------------------
1 | importScripts('workbox-sw.prod.js');
2 |
3 | const workboxSW = new self.WorkboxSW({
4 | "skipWaiting": true,
5 | "clientsClaim": true,
6 | });
7 |
8 | workboxSW.precache([]);
9 |
10 | workboxSW.router.registerRoute('https://fonts.googleapis.com/(.*)',
11 | workboxSW.strategies.cacheFirst({
12 | cacheName: 'googleapis',
13 | cacheExpiration: {
14 | maxEntries: 20,
15 | },
16 | cacheableResponse: { statuses: [0, 200] },
17 | })
18 | );
19 |
20 | workboxSW.router.registerRoute('https://fonts.gstatic.com/(.*)',
21 | workboxSW.strategies.cacheFirst({
22 | cacheName: 'gstatic',
23 | cacheExpiration: {
24 | maxEntries: 20,
25 | },
26 | cacheableResponse: { statuses: [0, 200] },
27 | })
28 | );
29 |
30 | // We want no more than 50 images in the cache. We check using a cache first strategy
31 | workboxSW.router.registerRoute(/\.(?:png|gif|jpg)$/,
32 | workboxSW.strategies.cacheFirst({
33 | cacheName: 'images-cache',
34 | cacheExpiration: {
35 | maxEntries: 50,
36 | },
37 | })
38 | );
39 |
40 | workboxSW.router.registerRoute(/index.html/, workboxSW.strategies.staleWhileRevalidate());
41 | workboxSW.router.registerRoute('/actions', workboxSW.strategies.staleWhileRevalidate());
42 |
43 |
--------------------------------------------------------------------------------
/tests/server/index.js:
--------------------------------------------------------------------------------
1 | const chai = require('chai');
2 | const chaiHttp = require('chai-http');
3 | const dirtyChai = require('dirty-chai');
4 |
5 | const server = require('../../server');
6 |
7 | chai.use(dirtyChai);
8 | chai.use(chaiHttp);
9 |
10 | const { expect } = chai;
11 |
12 | describe('GraphQL test', () => {
13 | after((done) => {
14 | server.close(done);
15 | });
16 |
17 | it('should test empty storage', async () => {
18 | let isTestFinished = false;
19 |
20 | const data = {
21 | query: `
22 | {
23 | users {
24 | id,
25 | name,
26 | }
27 | }
28 | `,
29 | };
30 |
31 | await chai.request(server)
32 | .get('/graphql')
33 | .query(data)
34 | .send()
35 | .then((res) => {
36 | expect(res).to.have.status(200);
37 | expect(res.body.data.users).is.a('array');
38 | isTestFinished = true;
39 | });
40 |
41 | expect(isTestFinished).is.true();
42 | });
43 |
44 | it('should test create a new user', async () => {
45 | let isTestFinished1 = false;
46 | let isTestFinished2 = false;
47 |
48 | const id = 1;
49 | const name = 'User 1';
50 |
51 | const data = {
52 | query: `
53 | mutation {
54 | createUser(id: ${id}, name: "${name}") {
55 | id,
56 | name,
57 | }
58 | }
59 | `,
60 | };
61 |
62 | await chai.request(server)
63 | .post('/graphql')
64 | .send(data)
65 | .then((res) => {
66 | expect(res).to.have.status(200);
67 | expect(res.body.data.createUser.id).to.be.equal(id);
68 | expect(res.body.data.createUser.name).to.be.equal(name);
69 | isTestFinished1 = true;
70 | });
71 |
72 | const data2 = {
73 | query: `
74 | {
75 | users {
76 | id,
77 | name,
78 | }
79 | }
80 | `,
81 | };
82 |
83 | await chai.request(server)
84 | .get('/graphql')
85 | .query(data2)
86 | .send()
87 | .then((res) => {
88 | expect(res).to.have.status(200);
89 | expect(res.body.data.users).is.a('array');
90 | expect(res.body.data.users[0].id).to.be.equal(id);
91 | expect(res.body.data.users[0].name).to.be.equal(name);
92 | isTestFinished2 = true;
93 | });
94 |
95 | expect(isTestFinished1).is.true();
96 | expect(isTestFinished2).is.true();
97 | });
98 |
99 | it('should test user', async () => {
100 | let isTestFinished = false;
101 |
102 | const id = 1;
103 |
104 | const data = {
105 | query: `
106 | {
107 | user(id: ${id}) {
108 | id,
109 | name,
110 | }
111 | }
112 | `,
113 | };
114 |
115 | await chai.request(server)
116 | .get('/graphql')
117 | .query(data)
118 | .send()
119 | .then((res) => {
120 | expect(res).to.have.status(200);
121 | expect(res.body.data.user.id).to.be.equal(id);
122 | isTestFinished = true;
123 | });
124 |
125 | expect(isTestFinished).is.true();
126 | });
127 |
128 | it('should test user with wrong id', async () => {
129 | let isTestFinished = false;
130 |
131 | const id = 100;
132 |
133 | const data = {
134 | query: `
135 | {
136 | user(id: ${id}) {
137 | id,
138 | name,
139 | }
140 | }
141 | `,
142 | };
143 |
144 | await chai.request(server)
145 | .get('/graphql')
146 | .query(data)
147 | .send()
148 | .then((res) => {
149 | expect(res).to.have.status(200);
150 | expect(res.body.data.user).is.null();
151 | expect(res.body.errors).is.a('array');
152 | expect(res.body.errors[0].message).to.be.equal('User not found!');
153 | isTestFinished = true;
154 | });
155 |
156 | expect(isTestFinished).is.true();
157 | });
158 |
159 | it('should test updating user', async () => {
160 | let isTestFinished1 = false;
161 | let isTestFinished2 = false;
162 |
163 | const id = 1;
164 | const name = 'New User name';
165 |
166 | const data = {
167 | query: `
168 | mutation {
169 | updateUser(id: ${id}, name: "${name}") {
170 | id,
171 | name,
172 | }
173 | }
174 | `,
175 | };
176 |
177 | await chai.request(server)
178 | .post('/graphql')
179 | .send(data)
180 | .then((res) => {
181 | expect(res).to.have.status(200);
182 | expect(res.body.data.updateUser.id).to.be.equal(id);
183 | expect(res.body.data.updateUser.name).to.be.equal(name);
184 | isTestFinished1 = true;
185 | });
186 |
187 | const data2 = {
188 | query: `
189 | {
190 | users {
191 | id,
192 | name,
193 | }
194 | }
195 | `,
196 | };
197 |
198 | await chai.request(server)
199 | .get('/graphql')
200 | .query(data2)
201 | .send()
202 | .then((res) => {
203 | expect(res).to.have.status(200);
204 | expect(res.body.data.users).is.a('array');
205 | expect(res.body.data.users[0].id).to.be.equal(id);
206 | expect(res.body.data.users[0].name).to.be.equal(name);
207 | isTestFinished2 = true;
208 | });
209 |
210 | expect(isTestFinished1).is.true();
211 | expect(isTestFinished2).is.true();
212 | });
213 |
214 | it('should test deleting user', async () => {
215 | let isTestFinished1 = false;
216 | let isTestFinished2 = false;
217 |
218 | const id = 1;
219 |
220 | const data = {
221 | query: `
222 | mutation {
223 | deleteUser(id: ${id}) {
224 | id,
225 | name,
226 | }
227 | }
228 | `,
229 | };
230 |
231 | await chai.request(server)
232 | .post('/graphql')
233 | .send(data)
234 | .then((res) => {
235 | expect(res).to.have.status(200);
236 | expect(res.body.data.deleteUser.id).to.be.equal(id);
237 | expect(res.body.data.deleteUser.name).is.null();
238 | isTestFinished1 = true;
239 | });
240 |
241 | const data2 = {
242 | query: `
243 | {
244 | users {
245 | id,
246 | name,
247 | }
248 | }
249 | `,
250 | };
251 |
252 | await chai.request(server)
253 | .get('/graphql')
254 | .query(data2)
255 | .send()
256 | .then((res) => {
257 | expect(res).to.have.status(200);
258 | expect(res.body.data.users).is.a('array');
259 | expect(res.body.data.users.length).to.be.equal(0);
260 | isTestFinished2 = true;
261 | });
262 |
263 | expect(isTestFinished1).is.true();
264 | expect(isTestFinished2).is.true();
265 | });
266 | });
267 |
--------------------------------------------------------------------------------
/tests/src/helper.js:
--------------------------------------------------------------------------------
1 | require('babel-polyfill');
2 | require('babel-register');
3 | require('ignore-styles');
4 | const { configure } = require('enzyme');
5 | const Adapter = require('enzyme-adapter-react-16');
6 |
7 | configure({ adapter: new Adapter() });
8 |
--------------------------------------------------------------------------------
/tests/src/modules/actions/components/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from 'enzyme';
3 | import { expect } from 'chai';
4 |
5 | import Component from '../../../../../src/modules/actions/components';
6 |
7 | describe('Testing actions component', () => {
8 | const { error } = console;
9 |
10 | beforeEach(() => {
11 | console.error = () => '';
12 | });
13 |
14 | afterEach(() => {
15 | console.error = error;
16 | });
17 |
18 | it('should test required property: status', () => {
19 | let tested = false;
20 |
21 | console.error = (text) => {
22 | expect(text).to.include(
23 | 'Warning: Failed prop type: The prop `status` is marked as required in `Component`, but its value is `undefined`.'
24 | );
25 | tested = true;
26 | };
27 |
28 | shallow( 1}
30 | clearActions={() => 1}
31 | />);
32 |
33 | expect(tested).to.be.true();
34 | });
35 |
36 | it('should test required property: initActions', () => {
37 | let tested = false;
38 |
39 | console.error = (text) => {
40 | expect(text).to.include(
41 | 'Warning: Failed prop type: The prop `initActions` is marked as required in `Component`, but its value is `undefined`.'
42 | );
43 | tested = true;
44 | };
45 |
46 | shallow( 1}
49 | />);
50 |
51 | expect(tested).to.be.true();
52 | });
53 |
54 | it('should test required property: clearActions', () => {
55 | let tested = false;
56 |
57 | console.error = (text) => {
58 | expect(text).to.include(
59 | 'Warning: Failed prop type: The prop `clearActions` is marked as required in `Component`, but its value is `undefined`.'
60 | );
61 | tested = true;
62 | };
63 |
64 | shallow( 1}
67 | />);
68 |
69 | expect(tested).to.be.true();
70 | });
71 |
72 | it('should test required property: status', () => {
73 | const wrapper = shallow( 1}
76 | clearActions={() => 1}
77 | />);
78 |
79 | expect(wrapper.find('h1').text()).to.be.equal('Status: test');
80 | });
81 |
82 | it('should test required property: status', () => {
83 | const wrapper = shallow( 1}
86 | clearActions={() => 1}
87 | />);
88 |
89 | expect(wrapper.find('h1').text()).to.be.equal('Status: test');
90 | });
91 | });
92 |
--------------------------------------------------------------------------------
/tests/src/modules/actions/epics/index.js:
--------------------------------------------------------------------------------
1 | import 'rxjs';
2 | import chai, { expect } from 'chai';
3 | import dirtyChai from 'dirty-chai';
4 | import { ActionsObservable } from 'redux-observable';
5 | import * as actions from '../../../../../src/modules/actions/actions';
6 | import initEpic from '../../../../../src/modules/actions/epics';
7 |
8 | chai.use(dirtyChai);
9 |
10 | describe('Testing actions epics', () => {
11 | it('should test initEpic', (done) => {
12 | const action$ = ActionsObservable.of(actions.initActions());
13 |
14 | const expectedOutputActions = [
15 | actions.idleActions(),
16 | ];
17 |
18 | initEpic(action$)
19 | .toArray()
20 | .subscribe((actualOutputActions) => {
21 | expect(actualOutputActions).to.be.eql(expectedOutputActions);
22 | done();
23 | });
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/tests/src/modules/actions/epics/init.js:
--------------------------------------------------------------------------------
1 | import 'rxjs';
2 | import chai, { expect } from 'chai';
3 | import dirtyChai from 'dirty-chai';
4 | import { ActionsObservable } from 'redux-observable';
5 | import * as actions from '../../../../../src/modules/actions/actions';
6 | import initEpic from '../../../../../src/modules/actions/epics/init';
7 |
8 | chai.use(dirtyChai);
9 |
10 | describe('Testing actions init epics', () => {
11 | it('should test initEpic', (done) => {
12 | const action$ = ActionsObservable.of(actions.initActions());
13 |
14 | const expectedOutputActions = [
15 | actions.idleActions(),
16 | ];
17 |
18 | initEpic(action$)
19 | .toArray()
20 | .subscribe((actualOutputActions) => {
21 | expect(actualOutputActions).to.be.eql(expectedOutputActions);
22 | done();
23 | });
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/tests/src/modules/actions/redux/actions.js:
--------------------------------------------------------------------------------
1 | import chai, { expect } from 'chai';
2 | import dirtyChai from 'dirty-chai';
3 | import * as constants from '../../../../../src/modules/actions/constants';
4 | import * as actions from '../../../../../src/modules/actions/actions';
5 |
6 | chai.use(dirtyChai);
7 |
8 | describe('Testing actions actions', () => {
9 | it('should test initActions', () => {
10 | const result = actions.initActions();
11 | expect(result.type).to.exist();
12 | expect(result.type).to.be.equal(constants.ACTIONS_INIT);
13 | });
14 |
15 | it('should test idleActions', () => {
16 | const result = actions.idleActions();
17 | expect(result.type).to.exist();
18 | expect(result.type).to.be.equal(constants.ACTIONS_IDLE);
19 | });
20 |
21 | it('should test clearActions', () => {
22 | const result = actions.clearActions();
23 | expect(result.type).to.exist();
24 | expect(result.type).to.be.equal(constants.ACTIONS_CLEAR);
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/tests/src/modules/actions/redux/constants.js:
--------------------------------------------------------------------------------
1 | import chai, { expect } from 'chai';
2 | import dirtyChai from 'dirty-chai';
3 | import * as constants from '../../../../../src/modules/actions/constants';
4 |
5 | chai.use(dirtyChai);
6 |
7 | describe('Testing actions constants', () => {
8 | it('should have ACTIONS_INIT', () => {
9 | expect(constants.ACTIONS_INIT).to.exist();
10 | expect(constants.ACTIONS_INIT).to.be.equal('@@actions/INIT');
11 | });
12 |
13 | it('should have ACTIONS_IDLE', () => {
14 | expect(constants.ACTIONS_IDLE).to.exist();
15 | expect(constants.ACTIONS_IDLE).to.be.equal('@@actions/IDLE');
16 | });
17 |
18 | it('should have ACTIONS_CLEAR', () => {
19 | expect(constants.ACTIONS_CLEAR).to.exist();
20 | expect(constants.ACTIONS_CLEAR).to.be.equal('@@actions/CLEAR');
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/tests/src/modules/actions/redux/index.js:
--------------------------------------------------------------------------------
1 | import chai, { expect } from 'chai';
2 | import dirtyChai from 'dirty-chai';
3 | import * as constants from '../../../../../src/modules/actions/constants';
4 | import reducer from '../../../../../src/modules/actions/reducers';
5 |
6 | chai.use(dirtyChai);
7 |
8 | describe('Testing actions reducer', () => {
9 | it('should test default state', () => {
10 | const state = reducer(undefined, { type: 'test' });
11 |
12 | expect(state.status).to.exist();
13 | expect(state.status).to.be.equal('null');
14 | });
15 |
16 | it('should test ACTIONS_INIT action', () => {
17 | const state = reducer(undefined, { type: constants.ACTIONS_INIT });
18 |
19 | expect(state.status).to.exist();
20 | expect(state.status).to.be.equal('init');
21 | });
22 |
23 | it('should test ACTIONS_IDLE action', () => {
24 | const state = reducer(undefined, { type: constants.ACTIONS_IDLE });
25 |
26 | expect(state.status).to.exist();
27 | expect(state.status).to.be.equal('idle');
28 | });
29 |
30 | it('should test ACTIONS_CLEAR action', () => {
31 | const state = reducer(undefined, { type: constants.ACTIONS_CLEAR });
32 |
33 | expect(state.status).to.exist();
34 | expect(state.status).to.be.equal('null');
35 | });
36 |
37 | it('should test default state with value', () => {
38 | const state = reducer({ test: true }, { type: 'test' });
39 |
40 | expect(state.test).to.exist();
41 | expect(state.test).to.be.true();
42 |
43 | expect(state.status).to.not.exist();
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | require('webpack');
2 |
3 | const settings = require('./webpack');
4 |
5 | module.exports = settings;
6 |
--------------------------------------------------------------------------------
/webpack/dev_server.js:
--------------------------------------------------------------------------------
1 | const { join } = require('path');
2 |
3 | const devServer = {
4 | quiet: false,
5 | port: 8000,
6 | contentBase: join(__dirname, '..', 'dist'),
7 | hot: true,
8 | historyApiFallback: true,
9 | inline: true,
10 | noInfo: false,
11 | headers: { 'Access-Control-Allow-Origin': '*' },
12 | stats: {
13 | assets: false,
14 | colors: true,
15 | version: false,
16 | hash: false,
17 | timings: false,
18 | chunks: false,
19 | chunkModules: false,
20 | },
21 | };
22 | module.exports = devServer;
23 |
--------------------------------------------------------------------------------
/webpack/devtool.js:
--------------------------------------------------------------------------------
1 | const isProduction = process.env.NODE_ENV === 'production';
2 |
3 | const devtool = isProduction ? 'source-map' : 'inline-cheap-module-source-map';
4 |
5 | module.exports = devtool;
6 |
--------------------------------------------------------------------------------
/webpack/index.js:
--------------------------------------------------------------------------------
1 | const { resolve } = require('path');
2 | const vendor = require('./vendor');
3 | const rules = require('./rules');
4 | const plugins = require('./plugins');
5 | const devServer = require('./dev_server');
6 | const devtool = require('./devtool');
7 |
8 | const settings = {
9 | resolve: {
10 | extensions: ['*', '.js', '.jsx', '.css', '.scss', '.png'],
11 | },
12 | context: resolve(__dirname, '..'),
13 | entry: {
14 | app: [
15 | 'react-hot-loader/patch',
16 | 'babel-polyfill',
17 | './src/index'
18 | ],
19 | vendor,
20 | },
21 | output: {
22 | filename: '[name].[hash].js',
23 | path: resolve(__dirname, '..', 'dist'),
24 | },
25 | module: {
26 | rules,
27 | },
28 | plugins,
29 | devServer,
30 | devtool,
31 | };
32 | module.exports = settings;
33 |
--------------------------------------------------------------------------------
/webpack/plugins.js:
--------------------------------------------------------------------------------
1 | const { resolve, join } = require('path');
2 | const webpack = require('webpack');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
5 | const CleanWebpackPlugin = require('clean-webpack-plugin');
6 | const WorkboxPlugin = require('workbox-webpack-plugin');
7 | const CopyWebpackPlugin = require('copy-webpack-plugin');
8 | const LodashModuleReplacementPlugin = require('lodash-webpack-plugin');
9 | const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
10 | const FaviconsWebpackPlugin = require('favicons-webpack-plugin');
11 | const PreloadWebpackPlugin = require('preload-webpack-plugin');
12 | const WebpackPwaManifest = require('webpack-pwa-manifest');
13 | const NpmInstallPlugin = require('npm-install-webpack-plugin');
14 |
15 | const isProduction = process.env.NODE_ENV === 'production';
16 | process.env.PUBLIC_PATH = process.env.PUBLIC_PATH || '/';
17 |
18 | const dist = 'dist';
19 |
20 | // the path(s) that should be cleaned
21 | const pathsToClean = [
22 | `${dist}/*.*`,
23 | ];
24 |
25 | // the clean options to use
26 | const cleanOptions = {
27 | root: resolve(__dirname, '..'),
28 | exclude: [`${dist}/.gitignore`],
29 | verbose: true,
30 | dry: false,
31 | };
32 |
33 | const plugins = [
34 | new NpmInstallPlugin(),
35 | new webpack.NoEmitOnErrorsPlugin(),
36 | new webpack.optimize.OccurrenceOrderPlugin(),
37 | new webpack.EnvironmentPlugin({
38 | NODE_ENV: 'development',
39 | PUBLIC_PATH: JSON.stringify(process.env.PUBLIC_PATH),
40 | }),
41 | new LodashModuleReplacementPlugin(),
42 | new HtmlWebpackPlugin({ template: join('src', 'index.html') }),
43 | new ExtractTextPlugin('[name]_[contenthash].css', { allChunks: true }),
44 | new webpack.optimize.CommonsChunkPlugin({
45 | name: 'vendor',
46 | minChunks: (m) => /node_modules/.test(m.context)
47 | }),
48 | new webpack.optimize.CommonsChunkPlugin({
49 | name: 'react',
50 | minChunks: (m) => /node_modules\/(?:react)/.test(m.context)
51 | }),
52 | new webpack.optimize.CommonsChunkPlugin({
53 | name: 'lodash',
54 | minChunks: (m) => /node_modules\/(?:lodash)/.test(m.context)
55 | }),
56 | new webpack.optimize.CommonsChunkPlugin({
57 | name: 'moment',
58 | minChunks: (m) => /node_modules\/(?:moment)/.test(m.context)
59 | }),
60 | new webpack.optimize.CommonsChunkPlugin({
61 | name: "manifest",
62 | minChunks: Infinity
63 | }),
64 | new webpack.NamedModulesPlugin(),
65 | new FaviconsWebpackPlugin(join(__dirname, '..', 'src', 'modules', 'home', 'logo.png')),
66 | ];
67 |
68 | if (isProduction) {
69 | plugins.push(
70 | new CleanWebpackPlugin(pathsToClean, cleanOptions),
71 | new webpack.LoaderOptionsPlugin({
72 | minimize: true,
73 | debug: false,
74 | }),
75 | new webpack.optimize.UglifyJsPlugin({
76 | sourceMap: true,
77 | compress: {
78 | warnings: false,
79 | screw_ie8: true,
80 | conditionals: true,
81 | unused: true,
82 | comparisons: true,
83 | sequences: true,
84 | dead_code: true,
85 | evaluate: true,
86 | if_return: true,
87 | join_vars: true,
88 | },
89 | output: {
90 | comments: false,
91 | },
92 | }),
93 | new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
94 | new CopyWebpackPlugin([{
95 | from: require.resolve('workbox-sw'),
96 | to: 'workbox-sw.prod.js',
97 | }]),
98 | new CopyWebpackPlugin([{ from: join('src', 'favicon.ico') }]),
99 | new WorkboxPlugin({
100 | globDirectory: dist,
101 | globPatterns: ['**/*.{html,js,css,png,json}'],
102 | swSrc: join('src', 'service-worker.js'),
103 | swDest: join(dist, 'service-worker.js'),
104 | clientsClaim: true,
105 | skipWaiting: true,
106 | navigateFallback: '/index.html',
107 | }),
108 | new PreloadWebpackPlugin({
109 | rel: 'preload',
110 | include: 'all',
111 | }),
112 | new WebpackPwaManifest({
113 | name: 'My Progressive Web App',
114 | short_name: 'MyPWA',
115 | description: 'My awesome Progressive Web App!',
116 | background_color: '#ffffff',
117 | inject: true,
118 | theme_color: '#ffffff',
119 | icons: [
120 | {
121 | src: join('src', 'modules', 'home', 'logo.png'),
122 | sizes: [96, 128, 192, 256, 384, 512]
123 | },
124 | ]
125 | })
126 | );
127 | } else {
128 | plugins.push(
129 | new webpack.LoaderOptionsPlugin({
130 | debug: true,
131 | }),
132 | new webpack.HotModuleReplacementPlugin(),
133 | new BundleAnalyzerPlugin({
134 | openAnalyzer: false,
135 | })
136 | );
137 | }
138 | module.exports = plugins;
139 |
--------------------------------------------------------------------------------
/webpack/rules.js:
--------------------------------------------------------------------------------
1 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
2 |
3 | const postcss = {
4 | loader: 'postcss-loader',
5 | options: {
6 | sourceMap: true,
7 | },
8 | };
9 |
10 | const css = {
11 | loader: 'css-loader',
12 | options: {
13 | sourceMap: true,
14 | importLoaders: 2,
15 | modules: true,
16 | localIdentName: '[name]__[local]___[hash:base64:5]'
17 | },
18 | };
19 |
20 | const sass = {
21 | loader: 'sass-loader',
22 | options: {
23 | sourceMap: true,
24 | },
25 | };
26 |
27 | const style = {
28 | loader: 'style-loader',
29 | };
30 |
31 | const rules = [{
32 | test: /.jsx?$/,
33 | loader: 'babel-loader',
34 | exclude: /node_modules/,
35 | }, {
36 | test: /\.scss$/,
37 | exclude: /node_modules/,
38 | use: ExtractTextPlugin.extract({
39 | fallback: style,
40 | use: [css, postcss, sass],
41 | }),
42 | }, {
43 | test: /\.css$/,
44 | exclude: /node_modules/,
45 | use: ExtractTextPlugin.extract({
46 | fallback: style,
47 | use: [css, postcss],
48 | }),
49 | }, {
50 | // for node_modules
51 | test: /\.css$/,
52 | use: ExtractTextPlugin.extract({
53 | fallback: style,
54 | use: [css, postcss],
55 | }),
56 | }, {
57 | test: /\.(gif|png|jpe?g|svg)$/i,
58 | use: [{
59 | loader: 'file-loader',
60 | options: {
61 | name: '[name]_[hash].[ext]',
62 | },
63 | }, {
64 | loader: 'image-webpack-loader',
65 | options: {
66 | mozjpeg: {
67 | progressive: true,
68 | quality: 65
69 | },
70 | optipng: {
71 | enabled: true,
72 | },
73 | pngquant: {
74 | quality: '65-90',
75 | speed: 4
76 | },
77 | gifsicle: {
78 | interlaced: false,
79 | },
80 | webp: {
81 | quality: 75
82 | }
83 | }
84 | },
85 | ],
86 | }];
87 |
88 | module.exports = rules;
89 |
90 |
--------------------------------------------------------------------------------
/webpack/vendor.js:
--------------------------------------------------------------------------------
1 | const vendor = [
2 | 'babel-polyfill',
3 | 'react',
4 | 'react-dom',
5 | 'react-redux',
6 | 'react-proxy',
7 | 'react-router',
8 | 'react-router-redux',
9 | 'react-virtualized',
10 | 'redux',
11 | 'redux-observable',
12 | 'react-toolbox',
13 | 'react-hot-loader',
14 | 'rxjs',
15 | 'lodash',
16 | 'moment',
17 | 'localforage',
18 | 'react-loadable',
19 | 'global',
20 | 'react-hot-loader/patch',
21 | 'create-react-class',
22 | 'react-loader',
23 | ];
24 | module.exports = vendor;
25 |
--------------------------------------------------------------------------------