├── .babelrc ├── .eslintrc.json ├── .gitignore ├── README.md ├── _assets ├── banner.png ├── repo.png └── sample-app.png ├── config ├── custom_scripts │ ├── Test │ │ ├── jest-script-preprocessor.js │ │ └── jestSetup.js │ ├── copy_assets.js │ ├── create_component.js │ └── create_container.js ├── webpack.dev.js └── webpack.prod.js ├── package-lock.json ├── package.json ├── public └── index.html └── src ├── HandleAPICalls ├── actions.js ├── constants.js └── saga.js ├── RandomQuote ├── actions.js ├── component.jsx ├── constants.js ├── container.jsx ├── reducer.js └── style.css ├── app ├── App.jsx ├── App.test.jsx ├── TodoApp │ ├── component.jsx │ ├── container.jsx │ └── style.css └── __snapshots__ │ └── App.test.jsx.snap ├── index.css ├── index.js ├── reducers └── index.js └── sagas └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "presets": [ 5 | "@babel/preset-env", 6 | "@babel/preset-react" 7 | ], 8 | "plugins": [ 9 | "@babel/plugin-transform-modules-commonjs", 10 | "dynamic-import-node", 11 | "@babel/plugin-proposal-class-properties", 12 | "@babel/plugin-proposal-object-rest-spread" 13 | ] 14 | } 15 | }, 16 | "plugins": [ 17 | "@babel/plugin-transform-modules-commonjs", 18 | "dynamic-import-node", 19 | "@babel/plugin-proposal-class-properties", 20 | "@babel/plugin-proposal-object-rest-spread" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "browser": true, 5 | "es6": true, 6 | "jest": true, 7 | "node": true 8 | } 9 | "parser": "babel-eslint" 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | yarn-error.log 4 | yarn.lock -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Open Source Love](https://badges.frapsoft.com/os/v2/open-source.svg?v=103)](https://github.com/vinitshahdeo/HacktoberFest) 2 | 3 | --- 4 | 5 | # React Simple Starter 6 | 7 | :school_satchel: Get up and running with :fire: [hot reload] and optimized production build :gem: 8 | 9 | Result of revisiting to webpack basics :blush: 10 | 11 | ![banner](_assets/banner.png) 12 | 13 | ### Quick start 14 | 15 | * **Clone this repo** 16 | * Run **npm run quick/yarn quick** 17 | * Go to `http://localhost:5050` 18 | 19 | ### Features 20 | 21 | ![tech](_assets/repo.png) 22 | 23 | * :fire: **Hot Reload** 24 | * Creates files using commands 25 | 26 | * Containers with test files : `yarn cont MyContainer1 MyContainer2` 27 | * Component with test file: `yarn comp MyComponent1 MyConponent2` 28 | 29 | * Linting using `ESLint` following `Airbnb` style guide 30 | * Testing using **Jest** 31 | 32 | ### Commands 33 | 34 | * **clean-init**: To start with a fresh repo 35 | * **comp**: Create component folder and files (including test file) 36 | * **cont**: Create container folder and files (including test file) 37 | * **start**: Start the `dev` server running at `http://localhost:5050` 38 | * **build**: Build for `production`, ready to host 39 | * **test**: Run tests in watch mode 40 | * **quick**: Clean and install dependencies, start the server 41 | 42 | ### Example app 43 | 44 | ![sample app](_assets/sample-app.png) 45 | 46 | More docs will be updated soon 47 | 48 | - If you use it somewhere, you have to link back to Knaxus(https://ashokdey.in/opensource) 49 | 50 | Creative Commons License
This work is licensed under a Creative Commons Attribution 3.0 Unported License. 51 | -------------------------------------------------------------------------------- /_assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/knaxus/react-simple-starter/30d44e1f40d2aaf5f696b9e9a30123e7f977d57e/_assets/banner.png -------------------------------------------------------------------------------- /_assets/repo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/knaxus/react-simple-starter/30d44e1f40d2aaf5f696b9e9a30123e7f977d57e/_assets/repo.png -------------------------------------------------------------------------------- /_assets/sample-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/knaxus/react-simple-starter/30d44e1f40d2aaf5f696b9e9a30123e7f977d57e/_assets/sample-app.png -------------------------------------------------------------------------------- /config/custom_scripts/Test/jest-script-preprocessor.js: -------------------------------------------------------------------------------- 1 | const babelJest = require('babel-jest'); 2 | 3 | module.exports = { 4 | process(src, filename) { 5 | return babelJest.process(src, filename) 6 | .replace(/^require.*\.less.*;$/gm, ''); 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /config/custom_scripts/Test/jestSetup.js: -------------------------------------------------------------------------------- 1 | import Enzyme, { shallow, render, mount } from 'enzyme'; 2 | import Adapter from 'enzyme-adapter-react-16'; 3 | 4 | // React 16 Enzyme adapter 5 | Enzyme.configure({ adapter: new Adapter() }); 6 | 7 | // Make Enzyme functions available in all test files without importing 8 | global.shallow = shallow; 9 | global.render = render; 10 | global.mount = mount; 11 | -------------------------------------------------------------------------------- /config/custom_scripts/copy_assets.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contain the script to copy all the assets that 3 | * are kept in /public directory to the /build directory. 4 | * What happens is if assets that are not includes in js/jsx 5 | * are not automatically moved to the build directory. 6 | */ 7 | 8 | const fs = require('fs-extra'); 9 | const path = require('path'); 10 | 11 | const sourcePath = path.resolve(__dirname, '../../public'); 12 | const desctinationPath = path.resolve(__dirname, '../../build'); 13 | 14 | fs.copySync(sourcePath, desctinationPath, { 15 | dereference: true, 16 | filter: file => file !== 'public/index.html' 17 | }); 18 | -------------------------------------------------------------------------------- /config/custom_scripts/create_component.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | 4 | const dirBaseAddress = '../../src'; 5 | const argumentsLength = process.argv.length; 6 | 7 | // create directory and files using the fileName 8 | try { 9 | for (let i = 2; i < argumentsLength; i += 1) { 10 | fs.ensureDirSync(path.resolve(__dirname, `${dirBaseAddress}/${process.argv[i]}`)); 11 | fs.ensureFileSync(path.resolve(__dirname, `${dirBaseAddress}/${process.argv[i]}/component.jsx`)); 12 | 13 | // test dir 14 | fs.ensureDirSync(path.resolve(__dirname, `${dirBaseAddress}/${process.argv[i]}/Test`)); 15 | fs.ensureFileSync(path.resolve( 16 | __dirname, 17 | `${dirBaseAddress}/${process.argv[i]}/Test/${process.argv[i]}.component.test.jsx`, 18 | )); 19 | } 20 | } catch (err) { 21 | throw new Error(err); 22 | } 23 | -------------------------------------------------------------------------------- /config/custom_scripts/create_container.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | 4 | const dirBaseAddress = '../../src'; 5 | const argumentsLength = process.argv.length; 6 | 7 | // create directory and files using the fileName 8 | try { 9 | for (let i = 2; i < argumentsLength; i += 1) { 10 | fs.ensureDirSync(path.resolve(__dirname, `${dirBaseAddress}/${process.argv[i]}`)); 11 | fs.ensureFileSync(path.resolve(__dirname, `${dirBaseAddress}/${process.argv[i]}/component.jsx`)); 12 | fs.ensureFileSync(path.resolve(__dirname, `${dirBaseAddress}/${process.argv[i]}/container.jsx`)); 13 | fs.ensureFileSync(path.resolve(__dirname, `${dirBaseAddress}/${process.argv[i]}/actions.js`)); 14 | fs.ensureFileSync(path.resolve(__dirname, `${dirBaseAddress}/${process.argv[i]}/constants.js`)); 15 | fs.ensureFileSync(path.resolve(__dirname, `${dirBaseAddress}/${process.argv[i]}/reducer.js`)); 16 | 17 | // test dir 18 | fs.ensureDirSync(path.resolve(__dirname, `${dirBaseAddress}/${process.argv[i]}/Test`)); 19 | fs.ensureFileSync(path.resolve( 20 | __dirname, 21 | `${dirBaseAddress}/${process.argv[i]}/Test/${process.argv[i]}.component.test.jsx`, 22 | )); 23 | fs.ensureFileSync(path.resolve( 24 | __dirname, 25 | `${dirBaseAddress}/${process.argv[i]}/Test/${process.argv[i]}.container.test.jsx`, 26 | )); 27 | fs.ensureFileSync(path.resolve( 28 | __dirname, 29 | `${dirBaseAddress}/${process.argv[i]}/Test/${process.argv[i]}.actions.test.js`, 30 | )); 31 | fs.ensureFileSync(path.resolve( 32 | __dirname, 33 | `${dirBaseAddress}/${process.argv[i]}/Test/${process.argv[i]}.constants.test.js`, 34 | )); 35 | fs.ensureFileSync(path.resolve( 36 | __dirname, 37 | `${dirBaseAddress}/${process.argv[i]}/Test/${process.argv[i]}.reducer.test.js`, 38 | )); 39 | } 40 | } catch (err) { 41 | throw new Error(err); 42 | } 43 | -------------------------------------------------------------------------------- /config/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 5 | 6 | module.exports = { 7 | entry: { 8 | main: [ 9 | '@babel/polyfill', 10 | 'react-hot-loader/patch', 11 | 'webpack-dev-server/client?http://localhost:5050', 12 | 'webpack/hot/only-dev-server', 13 | path.resolve(__dirname, '../src/index.js'), 14 | ], 15 | }, 16 | output: { 17 | path: path.resolve(__dirname, '../public'), 18 | filename: '[name]-bundle.js', 19 | publicPath: '/', 20 | }, 21 | devServer: { 22 | contentBase: path.resolve(__dirname, '../public'), 23 | historyApiFallback: true, // redirect 404s to index page 24 | inline: true, 25 | hot: true, // enable hot reload 26 | overlay: true, // display errors in browaer window 27 | port: 5050, 28 | }, 29 | module: { 30 | rules: [ 31 | { 32 | enforce: 'pre', 33 | test: /\.js$/, 34 | exclude: /node_modules/, 35 | use: ['eslint-loader'], // can also be like loader: 'eslint-loader' 36 | }, 37 | { 38 | test: /\.(js|jsx)$/, 39 | exclude: /node_modules/, 40 | use: { 41 | loader: 'babel-loader', 42 | options: { 43 | presets: [['@babel/preset-env', { modules: false }], '@babel/preset-react'], 44 | plugins: [ 45 | 'react-hot-loader/babel', 46 | '@babel/plugin-proposal-object-rest-spread', 47 | '@babel/plugin-proposal-class-properties', 48 | ], 49 | }, 50 | }, 51 | }, 52 | { 53 | test: /\.css$/, 54 | use: ['style-loader', 'css-loader'], 55 | }, 56 | { 57 | test: /\.scss$/, 58 | use: [{ 59 | loader: 'style-loader', 60 | }, { 61 | loader: 'css-loader', 62 | options: { 63 | sourceMap: true, 64 | }, 65 | }, { 66 | loader: 'sass-loader', 67 | options: { 68 | sourceMap: true, 69 | }, 70 | }], 71 | }, 72 | { 73 | test: /\.(ttf|eot|woff|woff2)$/, 74 | use: { 75 | loader: 'file-loader', // user: ['file-loader'] 76 | options: { 77 | name: 'fonts/[name].[ext]', 78 | }, 79 | }, 80 | }, 81 | { 82 | test: /\.(png|svg|jpg|jpeg|gif)$/, 83 | use: { 84 | loader: 'file-loader', // user: ['file-loader'] 85 | options: { 86 | name: 'images/[name].[ext]', 87 | }, 88 | }, 89 | }, 90 | ], 91 | }, 92 | resolve: { 93 | // allow to import both js and jsx 94 | extensions: ['.js', '.jsx'], 95 | }, 96 | plugins: [ 97 | new webpack.HotModuleReplacementPlugin(), 98 | new HtmlWebpackPlugin({ 99 | inject: true, 100 | template: path.resolve(__dirname, '../public/index.html'), 101 | }), 102 | new webpack.NamedModulesPlugin(), 103 | // prints more readable module names in the browser console on HMR updates 104 | new ExtractTextPlugin({ filename: 'styles.css', allChunks: true }), 105 | ], 106 | devtool: 'source-map', 107 | }; 108 | -------------------------------------------------------------------------------- /config/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const ManifestPlugin = require('webpack-manifest-plugin'); 5 | 6 | module.exports = { 7 | entry: { 8 | main: ['@babel/polyfill', path.resolve(__dirname, '../src/index.js')], 9 | }, 10 | output: { 11 | path: path.resolve(__dirname, '../build'), 12 | filename: '[name]-[hash:8].js', // generate hashed version for cache-bursting 13 | publicPath: './', 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | enforce: 'pre', 19 | test: /\.js$/, 20 | exclude: /node_modules/, 21 | use: ['eslint-loader'], // can also be like loader: 'eslint-loader' 22 | }, 23 | { 24 | test: /\.(js|jsx)$/, 25 | exclude: /node_modules/, 26 | use: { 27 | loader: 'babel-loader', 28 | options: { 29 | presets: [['@babel/preset-env', { modules: false }], '@babel/preset-react'], 30 | plugins: ['@babel/plugin-proposal-object-rest-spread', '@babel/plugin-proposal-class-properties'], 31 | }, 32 | }, 33 | }, 34 | { 35 | test: /\.css$/, 36 | use: ['style-loader', 'css-loader'], 37 | }, 38 | { 39 | test: /\.scss$/, 40 | exclude: /node_modules/, 41 | use: ['style-loader', 'css-loader', 'sass-loader'], 42 | }, 43 | { 44 | test: /\.(png|svg|jpg|jpeg|gif)$/, 45 | use: { 46 | loader: 'file-loader', 47 | options: { 48 | name: 'images/[name]-[hash:8].[ext]', 49 | }, 50 | }, 51 | }, 52 | 53 | { 54 | test: /.*\.(gif|png|jpe?g)$/i, 55 | use: [ 56 | { 57 | loader: 'url-loader', 58 | options: { 59 | name: 'images/[name]-[hash:8].[ext]', 60 | limit: 80000, 61 | }, 62 | }, 63 | ], 64 | }, 65 | { 66 | exclude: [/\.html$/, /\.(js|jsx)$/, /\.css$/, /\.scss$/, /\.json$/], 67 | use: { 68 | loader: 'file-loader', 69 | options: { 70 | name: 'static/media/[name]-[hash:8].[ext]', 71 | }, 72 | }, 73 | }, 74 | ], 75 | }, 76 | resolve: { 77 | // allow to import both js and jsx 78 | extensions: ['.js', '.jsx'], 79 | }, 80 | plugins: [ 81 | new webpack.DefinePlugin({ 82 | 'process.env': { 83 | NODE_ENV: JSON.stringify('production'), 84 | }, 85 | }), 86 | new HtmlWebpackPlugin({ 87 | inject: true, 88 | template: path.resolve(__dirname, '../public/index.html'), 89 | minify: { 90 | removeComments: true, 91 | collapseWhitespace: true, 92 | removeRedundantAttributes: true, 93 | useShortDoctype: true, 94 | removeEmptyAttributes: true, 95 | removeStyleLinkTypeAttributes: true, 96 | keepClosingSlash: true, 97 | minifyJS: true, 98 | minifyCSS: true, 99 | minifyURLs: true, 100 | }, 101 | }), 102 | new webpack.optimize.UglifyJsPlugin({ 103 | compress: { 104 | warnings: false, 105 | reduce_vars: false, 106 | }, 107 | output: { 108 | comments: false, 109 | }, 110 | sourceMap: true, 111 | }), 112 | new ManifestPlugin({ 113 | fileName: 'asset-manifest.json', 114 | }), 115 | ], 116 | devtool: 'source-map', 117 | }; 118 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-starter-v2", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "author": "Ashok Dey ", 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "webpack-dev-server --config=config/webpack.dev.js", 9 | "build": "npm run clear && node ./config/custom_scripts/copy_assets && webpack --config=config/webpack.prod.js", 10 | "quick": "npm run clean-init && npm install && npm start", 11 | "clear": "rm -rf build", 12 | "clean-init": "rm -rf .git _assets README.md && git init && git add . && git commit -m \"initial commit\"", 13 | "cont": "node ./config/custom_scripts/create_container", 14 | "comp": "node ./config/custom_scripts/create_component", 15 | "test": "jest --watch", 16 | "test:cover": "jest --coverage" 17 | }, 18 | "dependencies": { 19 | "axios": "^0.19.0", 20 | "extract-text-webpack-plugin": "^3.0.2", 21 | "fs-extra": "^8.1.0", 22 | "html-webpack-plugin": "^3.2.0", 23 | "identity-obj-proxy": "^3.0.0", 24 | "prop-types": "^15.7.2", 25 | "react": "^16.10.1", 26 | "react-dom": "^16.10.1", 27 | "react-redux": "^7.1.1", 28 | "react-redux-toastr": "^7.5.2", 29 | "react-router-dom": "^5.1.2", 30 | "redux": "^4.0.4", 31 | "redux-saga": "^1.1.1", 32 | "validator": "^11.1.0", 33 | "webpack": "4.41.0", 34 | "webpack-manifest-plugin": "^2.2.0" 35 | }, 36 | "devDependencies": { 37 | "@babel/core": "^7.6.2", 38 | "@babel/plugin-proposal-class-properties": "^7.0.0", 39 | "@babel/plugin-proposal-object-rest-spread": "^7.6.2", 40 | "@babel/plugin-transform-modules-commonjs": "^7.0.0", 41 | "@babel/polyfill": "^7.6.0", 42 | "@babel/preset-env": "^7.6.2", 43 | "@babel/preset-react": "^7.0.0", 44 | "@babel/register": "^7.6.2", 45 | "babel-eslint": "^10.0.3", 46 | "babel-jest": "^24.9.0", 47 | "babel-loader": "^8.0.6", 48 | "babel-plugin-dynamic-import-node": "^2.3.0", 49 | "css-loader": "^3.2.0", 50 | "enzyme": "^3.10.0", 51 | "enzyme-adapter-react-16": "^1.14.0", 52 | "enzyme-to-json": "^3.4.2", 53 | "eslint": "^6.5.1", 54 | "eslint-config-airbnb": "^18.0.1", 55 | "eslint-loader": "^3.0.2", 56 | "eslint-plugin-import": "^2.18.2", 57 | "eslint-plugin-jsx-a11y": "^6.2.3", 58 | "eslint-plugin-react": "^7.15.1", 59 | "file-loader": "^4.2.0", 60 | "jest-cli": "^24.9.0", 61 | "node-sass": "^4.12.0", 62 | "prettier-eslint": "^9.0.0", 63 | "react-addons-test-utils": "^15.6.2", 64 | "react-hot-loader": "4.12.14", 65 | "react-test-renderer": "^16.10.1", 66 | "sass-loader": "^8.0.0", 67 | "style-loader": "^1.0.0", 68 | "webpack": "^4.41.0", 69 | "webpack-cli": "^3.3.9", 70 | "webpack-dev-server": "^3.8.2" 71 | }, 72 | "jest": { 73 | "transform": { 74 | "^.+\\.jsx?$": "babel-jest" 75 | }, 76 | "setupFiles": [ 77 | "./config/custom_scripts/Test/jestSetup.js" 78 | ], 79 | "snapshotSerializers": [ 80 | "enzyme-to-json/serializer" 81 | ], 82 | "moduleNameMapper": { 83 | "^.+\\.(css|scss)$": "identity-obj-proxy" 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | React Simple Starter 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /src/HandleAPICalls/actions.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { HANDLE_API_CALLS } from './constants'; 3 | 4 | /* eslint-disable import/prefer-default-export */ 5 | export function getDataFromAPI( 6 | url, 7 | method, 8 | body, 9 | handleSuccess, 10 | handleError, 11 | showToast = false, 12 | ) { 13 | return { 14 | type: HANDLE_API_CALLS, 15 | url, 16 | method, 17 | body, 18 | handleSuccess, 19 | handleError, 20 | showToast, 21 | }; 22 | } 23 | 24 | /* eslint-anble */ -------------------------------------------------------------------------------- /src/HandleAPICalls/constants.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export const HANDLE_API_CALLS = '@handleAPICalls/HANDLE_API_CALLS'; 3 | /* eslint-enable */ 4 | -------------------------------------------------------------------------------- /src/HandleAPICalls/saga.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { takeEvery, call } from 'redux-saga/effects'; 3 | import { toastr } from 'react-redux-toastr'; 4 | import { HANDLE_API_CALLS } from './constants'; 5 | 6 | // function that makes the api request and returns a Promise for response 7 | function callToAPI(method, url, data) { 8 | return axios({ 9 | method, 10 | url, 11 | data, 12 | }); 13 | } 14 | 15 | // worker saga: makes the api call when watcher saga sees the action 16 | function* handleAPICalls(action) { 17 | try { 18 | const response = yield call( 19 | callToAPI, 20 | action.method, 21 | action.url, 22 | action.body 23 | ); 24 | if (action.handleSuccess) { 25 | yield call(action.handleSuccess, response.data); 26 | } 27 | } catch (err) { 28 | // eslint-disable-next-line no-console 29 | console.log(err); 30 | if (!action.showToast) { 31 | toastr.error('ERROR', 'Failed to request'); 32 | } 33 | if (action.handleError) { 34 | yield call(action.handleError, err.response); 35 | } 36 | } 37 | } 38 | 39 | // watcher saga: watches for actions dispatched to the store, starts worker saga 40 | function* callToAPIWatcher() { 41 | yield takeEvery(HANDLE_API_CALLS, handleAPICalls); 42 | } 43 | 44 | /* eslint-disable */ 45 | export { callToAPIWatcher }; 46 | 47 | /* eslint-enable */ 48 | -------------------------------------------------------------------------------- /src/RandomQuote/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_QUOTE, 3 | CLEAR_QUOTE, 4 | TOGGLE_BTN_DISABLE, 5 | } from './constants'; 6 | 7 | export function setQuote(quote, author) { 8 | return { 9 | type: SET_QUOTE, 10 | quote, 11 | author, 12 | }; 13 | } 14 | 15 | export function clearQuote() { 16 | return { 17 | type: CLEAR_QUOTE, 18 | }; 19 | } 20 | 21 | export function disableButton(disabled) { 22 | return { 23 | type: TOGGLE_BTN_DISABLE, 24 | disabled, 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/RandomQuote/component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './style.css'; 3 | 4 | export default function RandomQuote({ getQuote, quote }) { 5 | return ( 6 |
7 | 8 | { 9 | quote && quote.quote && 10 |
11 |
12 |

13 | {quote && quote.quote} 14 |

15 |
— {quote && quote.author}
16 |
17 |
18 | } 19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/RandomQuote/constants.js: -------------------------------------------------------------------------------- 1 | export const SET_QUOTE = '@randomQuote/SET_QUOTE'; 2 | export const CLEAR_QUOTE = '@randomQuote/CLEAR_QUOTE'; 3 | export const TOGGLE_BTN_DISABLE = '@randomQuote/TOGGLE_BTN_DISABLE'; 4 | -------------------------------------------------------------------------------- /src/RandomQuote/container.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import RamdomQuote from './component'; 4 | import { setQuote, clearQuote, disableButton } from './actions'; 5 | import { getDataFromAPI } from '../HandleAPICalls/actions'; 6 | 7 | class RamdomQuoteContainer extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.getQuote = this.getQuote.bind(this); 11 | } 12 | 13 | getQuote() { 14 | this.props.disableButton(true); 15 | this.props.clearQuote(); 16 | 17 | this.props.getDataFromAPI( 18 | 'https://api.quotable.io/random', 19 | 'GET', 20 | { per_page: 1, orderby: 'rand' }, 21 | ({ author, content }) => { 22 | this.props.setQuote(content, author); 23 | this.props.disableButton(false); 24 | } 25 | // err => console.log(err), 26 | ); 27 | } 28 | 29 | render() { 30 | const quote = this.props.quote; 31 | return ; 32 | } 33 | } 34 | 35 | function mapSateToprops(state) { 36 | return { 37 | quote: state.quote, 38 | }; 39 | } 40 | 41 | export default connect(mapSateToprops, { 42 | setQuote, 43 | clearQuote, 44 | disableButton, 45 | getDataFromAPI, 46 | })(RamdomQuoteContainer); 47 | -------------------------------------------------------------------------------- /src/RandomQuote/reducer.js: -------------------------------------------------------------------------------- 1 | import { 2 | SET_QUOTE, 3 | CLEAR_QUOTE, 4 | TOGGLE_BTN_DISABLE, 5 | } from './constants'; 6 | 7 | const initialState = { 8 | quote: '', 9 | author: '', 10 | disabled: false, 11 | }; 12 | 13 | export default (state = initialState, action) => { 14 | switch (action.type) { 15 | case SET_QUOTE: 16 | return { 17 | ...state, 18 | quote: action.quote, 19 | author: action.author, 20 | }; 21 | 22 | case CLEAR_QUOTE: 23 | return { 24 | ...state, 25 | quote: '', 26 | author: '', 27 | }; 28 | 29 | case TOGGLE_BTN_DISABLE: 30 | return { 31 | ...state, 32 | disabled: action.disabled, 33 | }; 34 | 35 | default: return state; 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /src/RandomQuote/style.css: -------------------------------------------------------------------------------- 1 | .myButton { 2 | -moz-box-shadow: 0px 0px 0px 0px #3e7327; 3 | -webkit-box-shadow: 0px 0px 0px 0px #3e7327; 4 | box-shadow: 0px 0px 0px 0px #3e7327; 5 | background-color:#77b55a; 6 | -moz-border-radius:28px; 7 | -webkit-border-radius:28px; 8 | border-radius:28px; 9 | border:1px solid #4b8f29; 10 | display:inline-block; 11 | cursor:pointer; 12 | color:#ffffff; 13 | font-family:Impact; 14 | font-size:25px; 15 | font-weight:bold; 16 | padding:16px 31px; 17 | text-decoration:none; 18 | text-shadow:0px 1px 0px #5b8a3c; 19 | margin: 5% 35%; 20 | width: 30vw; 21 | } 22 | .myButton:hover { 23 | background-color:#72b352; 24 | } 25 | .myButton:active { 26 | position:relative; 27 | top:1px; 28 | } 29 | 30 | blockquote{ 31 | font-size: 30px; 32 | background: #f9f9f9; 33 | border-left: 10px solid #ccc; 34 | margin: .5em 10px; 35 | padding: 0 10px; 36 | quotes: "\201C""\201D""\2018""\2019"; 37 | padding: 10px 20px; 38 | line-height: 1.4; 39 | } 40 | 41 | blockquote:before { 42 | content: open-quote; 43 | display: inline; 44 | height: 0; 45 | line-height: 0; 46 | left: -10px; 47 | position: relative; 48 | top: 30px; 49 | color: #ccc; 50 | font-size: 3em; 51 | } 52 | 53 | p{ 54 | margin: 0; 55 | } 56 | 57 | footer{ 58 | margin:0; 59 | text-align: right; 60 | font-size: 1em; 61 | font-style: italic; 62 | } 63 | 64 | .container { 65 | width: 70%; 66 | margin: 0 auto; 67 | } -------------------------------------------------------------------------------- /src/app/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import RandomQuote from '../RandomQuote/container'; 3 | import TodoApp from './TodoApp/container'; 4 | 5 | export default function App() { 6 | return ( 7 |
8 |

Hello React with Hot Reload !

9 |
10 | 11 |
12 |

Todos App - React hooks🔥

13 |
14 | 15 |
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/app/App.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import App from './App'; 4 | 5 | describe('App', () => { 6 | const app = shallow(); 7 | it('should render App', () => { 8 | expect(app).toMatchSnapshot(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/app/TodoApp/component.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './style.css'; 3 | 4 | function TodoView({todos}){ 5 | return ( 6 |
7 | 8 | 9 | { 10 | todos.map((todo, index)=>) 11 | } 12 |
{todo}
13 |
14 | ) 15 | } 16 | 17 | export default TodoView; -------------------------------------------------------------------------------- /src/app/TodoApp/container.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import TodoView from './component'; 3 | import './style.css'; 4 | function TodoApp() { 5 | // Declare a new state variable, which we'll call "count" 6 | const [todos, setTodo] = useState(['your sample todo']); 7 | const [todoInput, setTodoInput] = useState(""); 8 | return ( 9 |
10 | setTodoInput(event.target.value)} value={todoInput}/> 11 | 16 | 17 |
18 | ); 19 | } 20 | 21 | export default TodoApp; -------------------------------------------------------------------------------- /src/app/TodoApp/style.css: -------------------------------------------------------------------------------- 1 | input { 2 | padding-left: 5%; 3 | padding-right: 5%; 4 | padding-top: 15px; 5 | padding-bottom: 15px; 6 | width: 50%; 7 | background: linear-gradient(top, #f9f9f9, #e3e3e3); 8 | border: 1px solid #999; 9 | border-radius: 3px; 10 | -webkit-box-shadow: 6px 0px 33px -3px rgba(0,0,0,0.75); 11 | -moz-box-shadow: 6px 0px 33px -3px rgba(0,0,0,0.75); 12 | box-shadow: 6px 0px 33px -3px rgba(0,0,0,0.75); 13 | margin-top: 2%; 14 | } 15 | 16 | .add-button { 17 | background-color: #4CAF50; /* Green */ 18 | border: none; 19 | color: white; 20 | padding: 15px 32px; 21 | text-align: center; 22 | text-decoration: none; 23 | display: inline-block; 24 | font-size: 16px; 25 | } 26 | 27 | #todo-table { 28 | font-family: "Trebuchet MS", Arial, Helvetica, sans-serif; 29 | border-collapse: collapse; 30 | width: 50%; 31 | margin-top: 3%; 32 | -webkit-box-shadow: 6px 0px 33px -3px rgba(0,0,0,0.75); 33 | -moz-box-shadow: 6px 0px 33px -3px rgba(0,0,0,0.75); 34 | box-shadow: 6px 0px 33px -3px rgba(0,0,0,0.75); 35 | } 36 | 37 | #todo-table td, #todo-table th { 38 | border: 1px solid #ddd; 39 | padding: 8px; 40 | } 41 | 42 | #todo-table tr:nth-child(even){background-color: #f2f2f2;} 43 | 44 | #todo-table tr:hover {background-color: #ddd;} 45 | 46 | #todo-table tr { 47 | margin-top :3%; 48 | } 49 | 50 | #todo-table th { 51 | padding-top: 12px; 52 | padding-bottom: 12px; 53 | text-align: left; 54 | background-color: lightsalmon; 55 | color: black; 56 | } 57 | 58 | .todoView-container { 59 | align-items: center; 60 | } -------------------------------------------------------------------------------- /src/app/__snapshots__/App.test.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`App should render App 1`] = ` 4 |

5 | Hello React with hot reload ! 6 |

7 | `; 8 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Dancing+Script'); 2 | body { 3 | height: 100%; 4 | } 5 | 6 | h1 { 7 | color: blueviolet; 8 | margin: 0 auto; 9 | font-size: 5em; 10 | text-align: center; 11 | padding-top: 15vh; 12 | font-family: 'Dancing Script', cursive; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { createStore, applyMiddleware, compose } from 'redux'; 4 | import { Provider } from 'react-redux'; 5 | import createSagaMiddleware from 'redux-saga'; 6 | 7 | import App from './app/App'; 8 | import rootReducer from './reducers'; 9 | import rootSaga from './sagas'; 10 | 11 | // import css here 12 | import './index.css'; 13 | 14 | // initial state of the app as empty object 15 | const initialState = {}; 16 | 17 | // create the saga middleware 18 | const sagaMiddleware = createSagaMiddleware(); 19 | /* eslint-disable */ 20 | 21 | // dev tools middleware 22 | const reduxDevTools = 23 | window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(); 24 | 25 | const store = createStore( 26 | rootReducer, 27 | initialState, 28 | compose(applyMiddleware(sagaMiddleware), reduxDevTools) 29 | ); 30 | 31 | if (module.hot) { 32 | // Enable Webpack hot module replacement for reducers 33 | module.hot.accept('./reducers', () => { 34 | const nextRootReducer = require('./reducers'); 35 | store.replaceReducer(nextRootReducer); 36 | }); 37 | } 38 | 39 | /** 40 | * Refer to this link for more about hot reloading of sagas 41 | * https://stackoverflow.com/questions/37148592/redux-saga-hot-reloading 42 | */ 43 | 44 | // let sagaTask = sagaMiddleware.run(function* () { 45 | // yield rootSaga(); 46 | // }); 47 | 48 | // if (module.hot) { 49 | // // Enable Webpack hot module replacement for reducers 50 | // module.hot.accept('./reducers', () => { 51 | // const nextRootReducer = require('./reducers'); 52 | // store.replaceReducer(nextRootReducer); 53 | // }); 54 | 55 | // module.hot.accept('./sagas', () => { 56 | // const getNewSagas = require('./sagas'); 57 | // sagaTask.cancel() 58 | // sagaTask.done.then(() => { 59 | // sagaTask = sagaMiddleware.run(function* replacedSaga(action) { 60 | // yield getNewSagas() 61 | // }) 62 | // }) 63 | // }) 64 | // } 65 | 66 | // run the saga 67 | store.runSaga = sagaMiddleware.run(rootSaga, store.dispatch); 68 | 69 | ReactDOM.render( 70 | 71 | 72 | , 73 | document.getElementById('root'), 74 | ); 75 | 76 | /* eslint-enable react/jsx-filename-extension */ 77 | 78 | if (module.hot) { 79 | module.hot.accept('./app/App', () => { 80 | const NextApp = require('./app/App').default; 81 | ReactDOM.render( 82 | 83 | 84 | , 85 | document.getElementById('root'), 86 | ); 87 | }); 88 | } 89 | /* eslint-enable */ 90 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import randomQuoteReducer from '../RandomQuote/reducer'; 3 | 4 | export default combineReducers({ 5 | quote: randomQuoteReducer, 6 | }); 7 | -------------------------------------------------------------------------------- /src/sagas/index.js: -------------------------------------------------------------------------------- 1 | import { all } from 'redux-saga/effects'; 2 | import { callToAPIWatcher } from '../HandleAPICalls/saga'; 3 | 4 | // single entry point to start all Sagas at once 5 | export default function* rootSaga() { 6 | yield all([ 7 | callToAPIWatcher(), 8 | ]); 9 | } 10 | --------------------------------------------------------------------------------