├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── config ├── webpack.base.config.js └── webpack.prod.config.js ├── enzyme.config.js ├── jest.config.js ├── package.json ├── prettier.config.js ├── server ├── index.js └── routes │ └── index.js └── src ├── App.js ├── Foo.js ├── Routes.js ├── __mocks__ ├── axios.js └── react-loadable.js ├── actions ├── actionTypes.js └── bookingActions.js ├── api ├── AuthenticationAPI.js ├── Config.js └── Constants.js ├── index.html ├── index.js ├── modules ├── auth │ ├── layout │ │ ├── MainLayout.js │ │ └── Routes.js │ └── user │ │ └── UserPage.js ├── common │ └── PageLoader.js ├── not-found │ ├── NoMatchPage.js │ └── NoMatchPage.test.js └── public │ └── login │ ├── LoginPage.js │ ├── LoginPage.test.js │ └── components │ ├── LoginForm.js │ ├── LoginForm.test.js │ ├── WelcomeMessage.js │ └── WelcomeMessage.test.js ├── myStyles.scss ├── reducers ├── index.js └── userReducer.js ├── static ├── images │ ├── header.jpg │ └── logo.png └── public │ └── style.css ├── store └── index.js ├── styles.scss └── styles ├── modules ├── _base.scss └── _reset.scss └── muiTheme.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": [ 7 | "@babel/plugin-syntax-dynamic-import", 8 | "@babel/plugin-proposal-class-properties", 9 | "@babel/plugin-proposal-export-namespace-from", 10 | "@babel/plugin-proposal-throw-expressions" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*.md] 7 | trim_trailing_whitespace = false 8 | 9 | [*.js] 10 | trim_trailing_whitespace = true 11 | 12 | # Unix-style newlines with a newline ending every file 13 | [*] 14 | indent_style = space 15 | indent_size = 2 16 | end_of_line = lf 17 | charset = utf-8 18 | insert_final_newline = true 19 | max_line_length = 100 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /.git 2 | /.vscode 3 | /dist 4 | node_modules -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | env: { 5 | es6: true, 6 | browser: true, 7 | node: true, 8 | }, 9 | extends: ['airbnb', 'plugin:jest/recommended', 'jest-enzyme'], 10 | plugins: [ 11 | 'babel', 12 | 'import', 13 | 'jsx-a11y', 14 | 'react', 15 | 'prettier', 16 | ], 17 | parser: 'babel-eslint', 18 | parserOptions: { 19 | ecmaVersion: 6, 20 | sourceType: 'module', 21 | ecmaFeatures: { 22 | jsx: true 23 | } 24 | }, 25 | // settings: { 26 | // 'import/resolver': { 27 | // webpack: { 28 | // config: path.join(__dirname, 'config', 'webpack.base.config.js'), 29 | // }, 30 | // }, 31 | // }, 32 | rules: { 33 | 'linebreak-style': 'off', // Don't play nicely with Windows. 34 | 35 | 'arrow-parens': 'off', // Incompatible with prettier 36 | 'object-curly-newline': 'off', // Incompatible with prettier 37 | 'no-mixed-operators': 'off', // Incompatible with prettier 38 | 'arrow-body-style': 'off', // Not our taste? 39 | 'function-paren-newline': 'off', // Incompatible with prettier 40 | 'no-plusplus': 'off', 41 | 'space-before-function-paren': 0, // Incompatible with prettier 42 | 43 | 'max-len': ['error', 100, 2, { ignoreUrls: true, }], // airbnb is allowing some edge cases 44 | 'no-console': 'error', // airbnb is using warn 45 | 'no-alert': 'error', // airbnb is using warn 46 | 47 | 'no-param-reassign': 'off', // Not our taste? 48 | "radix": "off", // parseInt, parseFloat radix turned off. Not my taste. 49 | 50 | 'react/require-default-props': 'off', // airbnb use error 51 | 'react/forbid-prop-types': 'off', // airbnb use error 52 | 'react/jsx-filename-extension': ['error', { extensions: ['.js'] }], // airbnb is using .jsx 53 | 54 | 'prefer-destructuring': 'off', 55 | 56 | 'react/no-find-dom-node': 'off', // I don't know 57 | 'react/no-did-mount-set-state': 'off', 58 | 'react/no-unused-prop-types': 'off', // Is still buggy 59 | 'react/jsx-one-expression-per-line': 'off', 60 | 61 | "jsx-a11y/anchor-is-valid": ["error", { "components": ["Link"], "specialLink": ["to"] }], 62 | "jsx-a11y/label-has-for": [2, { 63 | "required": { 64 | "every": ["id"] 65 | } 66 | }], // for nested label htmlFor error 67 | 68 | 'prettier/prettier': ['error'], 69 | }, 70 | }; -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # NPM packages folder. 2 | node_modules 3 | 4 | # Build files 5 | dist/ 6 | 7 | # lock files 8 | yarn.lock 9 | package-lock.json 10 | 11 | # Logs 12 | logs 13 | *.log 14 | npm-debug.log* 15 | 16 | # node-waf configuration 17 | .lock-wscript 18 | 19 | # Optional npm cache directory 20 | .npm 21 | 22 | # Optional REPL history 23 | .node_repl_history 24 | 25 | # Jest Coverage 26 | coverage -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.rulers": [100], 4 | "editor.formatOnSave": false, 5 | "eslint.autoFixOnSave": true, 6 | "editor.showFoldingControls": "always", 7 | "editor.folding": true, 8 | "editor.foldingStrategy": "indentation", 9 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at adeelimranr@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 withVoid 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | Webapp Was Built In: 4 | 5 | * ReactJS 6 | * Redux 7 | * Webpack 4 8 | * Babel 7 9 | * React Material UI 10 | * Bootstrap 4 11 | * SCSS, CSS Support 12 | * HMR 13 | * Code Splitting with React.lazy & React.Suspense 14 | * Code Formatter (Prettier) 15 | * Eslint configured extended with Airbnb style guide & support for prettier 16 | * Jest & Enzyme Configured 17 | * Automatically lint & format code, when committing it. [Husky/Lint-Staged] 18 | 19 | 20 | ### Tutorials 21 | 22 | Things I did while setting up the boiler plate for this code base, I wrote it all down in a series of articles 23 | 24 | * https://medium.freecodecamp.org/how-to-conquer-webpack-4-and-build-a-sweet-react-app-236d721e6745 25 | * https://medium.freecodecamp.org/how-to-combine-webpack-4-and-babel-7-to-create-a-fantastic-react-app-845797e036ff 26 | * https://medium.freecodecamp.org/these-tools-will-help-you-write-clean-code-da4b5401f68e 27 | * https://medium.freecodecamp.org/how-to-set-up-jest-enzyme-like-a-boss-8455a2bc6d56 28 | 29 | 30 | ### Deploying a Node Instance On Linux Server Using PM2 31 | 32 | How to start on PM2 [This is specific if you serve your files on a linux server where a NodeJS application is deployed as a server serving the .js files 33 | 34 | ``` 35 | npm i 36 | node_modules/.bin/webpack --config webpack.prod.config.js --colors --progress 37 | node server 38 | PORT=8082 pm2 start server --name "app-name-to-deploy" 39 | ``` -------------------------------------------------------------------------------- /config/webpack.base.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | const merge = require("webpack-merge"); 5 | 6 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 7 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 8 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 9 | 10 | const APP_DIR = path.resolve(__dirname, '../src'); 11 | 12 | module.exports = env => { 13 | const { PLATFORM, VERSION } = env; 14 | return merge([ 15 | { 16 | entry: ['@babel/polyfill', APP_DIR], 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.js$/, 21 | exclude: /node_modules/, 22 | use: { 23 | loader: 'babel-loader' 24 | } 25 | }, 26 | { 27 | test: /\.(scss|css)$/, 28 | use: [ 29 | PLATFORM === 'production' ? MiniCssExtractPlugin.loader : 'style-loader', 30 | 'css-loader', 31 | 'sass-loader' 32 | ] 33 | } 34 | ] 35 | }, 36 | plugins: [ 37 | new HtmlWebpackPlugin({ 38 | template: './src/index.html', 39 | filename: './index.html' 40 | }), 41 | new webpack.DefinePlugin({ 42 | 'process.env.VERSION': JSON.stringify(env.VERSION), 43 | 'process.env.PLATFORM': JSON.stringify(env.PLATFORM) 44 | }), 45 | new CopyWebpackPlugin([ { from: 'src/static' } ]), 46 | ], 47 | // output: { 48 | // filename: '[name].bundle.js', 49 | // chunkFilename: '[name].chunk.bundle.js', 50 | // path: path.resolve(__dirname, '..', 'dist'), 51 | // publicPath: '/', 52 | // }, 53 | } 54 | ]) 55 | }; 56 | -------------------------------------------------------------------------------- /config/webpack.prod.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const merge = require('webpack-merge'); 3 | // Plugins 4 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 5 | const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 6 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); 7 | const Visualizer = require('webpack-visualizer-plugin'); 8 | // Configs 9 | const baseConfig = require('./webpack.base.config'); 10 | 11 | const prodConfiguration = env => { 12 | return merge([ 13 | { 14 | optimization: { 15 | // runtimeChunk: 'single', 16 | // splitChunks: { 17 | // cacheGroups: { 18 | // vendor: { 19 | // test: /[\\/]node_modules[\\/]/, 20 | // name: 'vendors', 21 | // chunks: 'all' 22 | // } 23 | // } 24 | // }, 25 | minimizer: [new UglifyJsPlugin()], 26 | }, 27 | plugins: [ 28 | new MiniCssExtractPlugin(), 29 | new OptimizeCssAssetsPlugin(), 30 | new Visualizer({ filename: './statistics.html' }) 31 | ], 32 | }, 33 | ]); 34 | } 35 | 36 | module.exports = env => { 37 | return merge(baseConfig(env), prodConfiguration(env)); 38 | } -------------------------------------------------------------------------------- /enzyme.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | 3 | /** Used in jest.config.js */ 4 | 5 | import { configure } from 'enzyme'; 6 | import Adapter from 'enzyme-adapter-react-16'; 7 | 8 | configure({ adapter: new Adapter() }); 9 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | // For a detailed explanation regarding each configuration property, visit: 3 | // https://jestjs.io/docs/en/configuration.html 4 | 5 | module.exports = { 6 | // All imported modules in your tests should be mocked automatically 7 | // automock: false, 8 | 9 | // Stop running tests after the first failure 10 | // bail: false, 11 | 12 | // Respect "browser" field in package.json when resolving modules 13 | // browser: false, 14 | 15 | // The directory where Jest should store its cached dependency information 16 | // cacheDirectory: "C:\\Users\\VenD\\AppData\\Local\\Temp\\jest", 17 | 18 | // Automatically clear mock calls and instances between every test 19 | clearMocks: true, 20 | 21 | // Indicates whether the coverage information should be collected while executing the test 22 | // collectCoverage: false, 23 | 24 | // An array of glob patterns indicating a set of files for which coverage information should be collected 25 | collectCoverageFrom: ['src/modules/**/*.{js,jsx,mjs}'], 26 | 27 | // The directory where Jest should output its coverage files 28 | coverageDirectory: 'coverage', 29 | 30 | // An array of regexp pattern strings used to skip coverage collection 31 | // coveragePathIgnorePatterns: [ 32 | // "\\\\node_modules\\\\" 33 | // ], 34 | 35 | // A list of reporter names that Jest uses when writing coverage reports 36 | // coverageReporters: [ 37 | // "json", 38 | // "text", 39 | // "lcov", 40 | // "clover" 41 | // ], 42 | 43 | // An object that configures minimum threshold enforcement for coverage results 44 | // coverageThreshold: null, 45 | 46 | // Make calling deprecated APIs throw helpful error messages 47 | // errorOnDeprecated: false, 48 | 49 | // Force coverage collection from ignored files usin a array of glob patterns 50 | // forceCoverageMatch: [], 51 | 52 | // A path to a module which exports an async function that is triggered once before all test suites 53 | // globalSetup: null, 54 | 55 | // A path to a module which exports an async function that is triggered once after all test suites 56 | // globalTeardown: null, 57 | 58 | // A set of global variables that need to be available in all test environments 59 | // globals: {}, 60 | 61 | // An array of directory names to be searched recursively up from the requiring module's location 62 | // moduleDirectories: [ 63 | // "node_modules" 64 | // ], 65 | 66 | // An array of file extensions your modules use 67 | moduleFileExtensions: ['js', 'json', 'jsx'], 68 | 69 | // A map from regular expressions to module names that allow to stub out resources with a single module 70 | // moduleNameMapper: {}, 71 | 72 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 73 | // modulePathIgnorePatterns: [], 74 | 75 | // Activates notifications for test results 76 | // notify: false, 77 | 78 | // An enum that specifies notification mode. Requires { notify: true } 79 | // notifyMode: "always", 80 | 81 | // A preset that is used as a base for Jest's configuration 82 | // preset: null, 83 | 84 | // Run tests from one or more projects 85 | // projects: null, 86 | 87 | // Use this configuration option to add custom reporters to Jest 88 | // reporters: undefined, 89 | 90 | // Automatically reset mock state between every test 91 | // resetMocks: false, 92 | 93 | // Reset the module registry before running each individual test 94 | // resetModules: false, 95 | 96 | // A path to a custom resolver 97 | // resolver: null, 98 | 99 | // Automatically restore mock state between every test 100 | // restoreMocks: false, 101 | 102 | // The root directory that Jest should scan for tests and modules within 103 | // rootDir: null, 104 | 105 | // A list of paths to directories that Jest should use to search for files in 106 | // roots: [ 107 | // "" 108 | // ], 109 | 110 | // Allows you to use a custom runner instead of Jest's default test runner 111 | // runner: "jest-runner", 112 | 113 | // The paths to modules that run some code to configure or set up the testing environment before each test 114 | setupFiles: ['/enzyme.config.js'], 115 | 116 | // The path to a module that runs some code to configure or set up the testing framework before each test 117 | // setupTestFrameworkScriptFile: '', 118 | 119 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 120 | // snapshotSerializers: [], 121 | 122 | // The test environment that will be used for testing 123 | testEnvironment: 'jsdom', 124 | 125 | // Options that will be passed to the testEnvironment 126 | // testEnvironmentOptions: {}, 127 | 128 | // Adds a location field to test results 129 | // testLocationInResults: false, 130 | 131 | // The glob patterns Jest uses to detect test files 132 | testMatch: ['**/__tests__/**/*.js?(x)', '**/?(*.)+(spec|test).js?(x)'], 133 | 134 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 135 | testPathIgnorePatterns: ['\\\\node_modules\\\\'], 136 | 137 | // The regexp pattern Jest uses to detect test files 138 | // testRegex: "", 139 | 140 | // This option allows the use of a custom results processor 141 | // testResultsProcessor: null, 142 | 143 | // This option allows use of a custom test runner 144 | // testRunner: "jasmine2", 145 | 146 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 147 | testURL: 'http://localhost', 148 | 149 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 150 | // timers: "real", 151 | 152 | // A map from regular expressions to paths to transformers 153 | // transform: {}, 154 | 155 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 156 | transformIgnorePatterns: ['/node_modules/'], 157 | 158 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 159 | // unmockedModulePathPatterns: undefined, 160 | 161 | // Indicates whether each individual test should be reported during the run 162 | verbose: false, 163 | 164 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 165 | // watchPathIgnorePatterns: [], 166 | 167 | // Whether to use watchman for file crawling 168 | // watchman: true, 169 | }; 170 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-boiler-plate", 3 | "version": "1.0.0", 4 | "description": "A react boiler plate", 5 | "main": "src/index.js", 6 | "author": "Adeel Imran", 7 | "license": "MIT", 8 | "scripts": { 9 | "start": "webpack-dev-server --mode development --config config/webpack.base.config.js --open --hot --history-api-fallback --env.PLATFORM=local --env.VERSION=stag", 10 | "prebuild": "webpack --mode production --config config/webpack.prod.config.js --env.PLATFORM=production --env.VERSION=stag --progress", 11 | "build": "node server", 12 | "lint": "eslint --debug src/", 13 | "lint:write": "eslint --debug src/ --fix", 14 | "test": "jest", 15 | "test:watch": "jest --watch", 16 | "test:coverage": "jest --coverage --colors", 17 | "prettier": "prettier --write src/**/*.js" 18 | }, 19 | "husky": { 20 | "hooks": { 21 | "pre-commit": "lint-staged" 22 | } 23 | }, 24 | "lint-staged": { 25 | "*.(js|jsx)": [ 26 | "npm run lint:write", 27 | "git add" 28 | ] 29 | }, 30 | "dependencies": { 31 | "@material-ui/core": "^3.0.0", 32 | "@material-ui/icons": "^3.0.0", 33 | "axios": "^0.18.0", 34 | "bootstrap": "^4.1.1", 35 | "express": "^4.16.3", 36 | "prop-types": "^15.6.2", 37 | "react": "^16.6.0", 38 | "react-dom": "^16.6.0", 39 | "react-redux": "^5.0.7", 40 | "react-router-dom": "^4.3.1", 41 | "react-transition-group": "^2.4.0", 42 | "redux": "^4.0.0", 43 | "redux-thunk": "^2.3.0", 44 | "styled-components": "^3.3.3" 45 | }, 46 | "devDependencies": { 47 | "@babel/core": "^7.0.0", 48 | "@babel/plugin-proposal-class-properties": "^7.0.0", 49 | "@babel/plugin-proposal-export-namespace-from": "^7.0.0", 50 | "@babel/plugin-proposal-throw-expressions": "^7.0.0", 51 | "@babel/plugin-syntax-dynamic-import": "^7.0.0", 52 | "@babel/polyfill": "^7.0.0-beta.51", 53 | "@babel/preset-env": "^7.0.0-beta.51", 54 | "@babel/preset-react": "^7.0.0-beta.51", 55 | "babel-core": "^7.0.0-bridge.0", 56 | "babel-eslint": "^8.2.3", 57 | "babel-jest": "^23.4.2", 58 | "babel-loader": "^8.0.0-beta.0", 59 | "copy-webpack-plugin": "^4.5.1", 60 | "css-loader": "^0.28.11", 61 | "enzyme": "^3.3.0", 62 | "enzyme-adapter-react-16": "^1.1.1", 63 | "eslint": "^4.19.1", 64 | "eslint-config-airbnb": "^17.0.0", 65 | "eslint-config-jest-enzyme": "^6.0.2", 66 | "eslint-plugin-babel": "^5.1.0", 67 | "eslint-plugin-import": "^2.12.0", 68 | "eslint-plugin-jest": "^21.18.0", 69 | "eslint-plugin-jsx-a11y": "^6.0.3", 70 | "eslint-plugin-prettier": "^2.6.0", 71 | "eslint-plugin-react": "^7.9.1", 72 | "html-webpack-plugin": "^3.2.0", 73 | "husky": "^1.1.2", 74 | "jest": "^23.4.2", 75 | "lint-staged": "^7.3.0", 76 | "mini-css-extract-plugin": "^0.4.3", 77 | "node-sass": "^4.8.3", 78 | "optimize-css-assets-webpack-plugin": "^4.0.0", 79 | "prettier": "^1.14.3", 80 | "react-test-renderer": "^16.4.1", 81 | "sass-loader": "^7.0.3", 82 | "style-loader": "^0.21.0", 83 | "uglifyjs-webpack-plugin": "^1.2.5", 84 | "webpack": "4.28.4", 85 | "webpack-cli": "^3.2.3", 86 | "webpack-dev-server": "^3.1.14", 87 | "webpack-merge": "^4.1.3", 88 | "webpack-visualizer-plugin": "^0.1.11" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | singleQuote: true, 4 | trailingComma: 'all', 5 | bracketSpacing: true, 6 | jsxBracketSameLine: false, 7 | tabWidth: 2, 8 | semi: true, 9 | }; 10 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const express = require('express'); 3 | const path = require('path'); 4 | const http = require('http'); 5 | 6 | const app = express(); 7 | 8 | // Point static path to dist 9 | app.use('/', express.static(path.join(__dirname, '..', 'dist'))); 10 | app.use('/dist', express.static(path.join(__dirname, '..', 'dist'))); 11 | 12 | const routes = require('./routes'); 13 | 14 | app.use('/', routes); 15 | 16 | /** Get port from environment and store in Express. */ 17 | const port = process.env.PORT || '3000'; 18 | app.set('port', port); 19 | 20 | /** Create HTTP server. */ 21 | const server = http.createServer(app); 22 | /** Listen on provided port, on all network interfaces. */ 23 | server.listen(port, () => console.log(`Server Running on port ${port}`)); 24 | -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const router = require('express').Router(); 3 | 4 | router.get('*', (req, res) => { 5 | const route = path.join(__dirname, '..', '..', 'dist', 'index.html'); 6 | res.sendFile(route); 7 | }); 8 | 9 | module.exports = router; 10 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { BrowserRouter as Router } from 'react-router-dom'; 4 | import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles'; 5 | 6 | // Routes 7 | import Routes from './Routes'; 8 | 9 | // Material UI Theme Customization 10 | import Theme from './styles/muiTheme'; 11 | // Store Configuration 12 | import createStore from './store'; 13 | 14 | const THEME = createMuiTheme(Theme); 15 | const STORE = createStore(); 16 | 17 | const App = () => { 18 | return ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default App; 30 | -------------------------------------------------------------------------------- /src/Foo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => ( 4 |
5 |

I am Foo! Pleasure to meet you.

6 |
7 | ); 8 | -------------------------------------------------------------------------------- /src/Routes.js: -------------------------------------------------------------------------------- 1 | import React, { lazy, Suspense } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Switch, Route, Redirect } from 'react-router-dom'; 4 | 5 | // Helpers 6 | import { APP_TOKEN } from './api/Constants'; 7 | // Utils 8 | import PageLoader from './modules/common/PageLoader'; 9 | 10 | // Routes 11 | const AuthLayout = lazy(() => import('./modules/auth/layout/MainLayout')); 12 | const LoginPage = lazy(() => import('./modules/public/login/LoginPage')); 13 | const NoMatchPage = lazy(() => import('./modules/not-found/NoMatchPage')); 14 | 15 | const Routes = () => { 16 | return ( 17 | }> 18 | 19 | } /> 20 | { 24 | return APP_TOKEN.notEmpty ? : ; 25 | }} 26 | /> 27 | { 30 | // return APP_TOKEN.notEmpty ? : ; 31 | return ; 32 | }} 33 | /> 34 | 35 | 36 | 37 | ); 38 | }; 39 | 40 | Routes.propTypes = { 41 | location: PropTypes.object, // React Router Passed Props 42 | }; 43 | 44 | export default Routes; 45 | -------------------------------------------------------------------------------- /src/__mocks__/axios.js: -------------------------------------------------------------------------------- 1 | export default { 2 | isCancel: jest.fn(), 3 | CancelToken: { source: jest.fn() }, 4 | }; 5 | -------------------------------------------------------------------------------- /src/__mocks__/react-loadable.js: -------------------------------------------------------------------------------- 1 | export default ({ loading = jest.fn(), loader = jest.fn() }) => { 2 | const isOkay = true; 3 | if (isOkay) { 4 | return loading; 5 | } 6 | return loader; 7 | }; 8 | -------------------------------------------------------------------------------- /src/actions/actionTypes.js: -------------------------------------------------------------------------------- 1 | // Booking actions 2 | export const GET_USER = 'GET_USER'; 3 | export const DUMMY_1 = 'TEST_DELETE_THIS_LATER_1'; 4 | export const DUMMY_2 = 'TEST_DELETE_THIS_LATER_2'; 5 | -------------------------------------------------------------------------------- /src/actions/bookingActions.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from './actionTypes'; 2 | import AuthenticationAPI from '../api/AuthenticationAPI'; 3 | 4 | const onLoadSuccess = list => ({ 5 | type: actionTypes.DUMMY_1, 6 | list, 7 | }); 8 | 9 | export function onLoadBookingList() { 10 | return async dispatch => { 11 | try { 12 | const list = await AuthenticationAPI.onGetBookingList(); 13 | dispatch(onLoadSuccess(list)); 14 | return list; 15 | } catch (error) { 16 | return error; 17 | } 18 | }; 19 | } 20 | 21 | export function deleteThisLaterDummyProcedure() { 22 | return async dispatch => { 23 | try { 24 | const list = await AuthenticationAPI.onGetBookingList(); 25 | dispatch(onLoadSuccess(list)); 26 | return list; 27 | } catch (error) { 28 | return error; 29 | } 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /src/api/AuthenticationAPI.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import * as c from './Constants'; 3 | 4 | const PARAMS = ({ methodType = 'GET' }) => ({ 5 | method: methodType, 6 | headers: { 7 | 'Content-Type': 'application/json', 8 | }, 9 | }); 10 | 11 | export default { 12 | onLogin: async ({ cancelToken, ...payload }) => { 13 | const URL = `${c.API_CONSUMER}/colleague/auth/login`; 14 | try { 15 | const { data } = await axios( 16 | URL, 17 | Object.assign({}, PARAMS({ methodType: 'POST' }), { 18 | cancelToken, 19 | data: payload, 20 | }), 21 | ); 22 | return data; 23 | } catch (error) { 24 | throw error; 25 | } 26 | }, 27 | onValidate: async ({ cancelToken, accessToken }) => { 28 | const URL = `${c.API_CONSUMER}/something`; 29 | try { 30 | const { data } = await axios(URL, { 31 | method: 'GET', 32 | headers: { 33 | access_token: accessToken, 34 | }, 35 | cancelToken, 36 | }); 37 | return data; 38 | } catch (error) { 39 | throw error; 40 | } 41 | }, 42 | onRefresh: async ({ cancelToken, ...payload }) => { 43 | const URL = `${c.API_CONSUMER}/something`; 44 | try { 45 | const { data } = await axios( 46 | URL, 47 | Object.assign({}, PARAMS({ methodType: 'POST' }), { 48 | cancelToken, 49 | data: payload, 50 | }), 51 | ); 52 | return data; 53 | } catch (error) { 54 | throw error; 55 | } 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /src/api/Config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'development-local-stag': { 3 | API_URL: '', 4 | AUTH_URL: '', 5 | }, 6 | get 'production-local-stag'() { 7 | return this['development-local-stag']; 8 | }, 9 | get 'test-local-stag'() { 10 | return this['development-local-stag']; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/api/Constants.js: -------------------------------------------------------------------------------- 1 | import Config from './Config'; 2 | 3 | const ENV = process.env.NODE_ENV ? process.env.NODE_ENV : 'development'; 4 | const PLATFORM = process.env.PLATFORM ? process.env.PLATFORM : 'local'; 5 | const VERSION = process.env.VERSION ? process.env.VERSION : 'stag'; 6 | 7 | const KEY = `${ENV}-${PLATFORM}-${VERSION}`; 8 | // console.log('>>>>', KEY); 9 | export const API_URL = Config[KEY].API_URL; 10 | export const AUTH_URL = Config[KEY].AUTH_URL; 11 | 12 | // Helpers 13 | export const APP_TOKEN = { 14 | set: ({ token, refreshToken }) => { 15 | localStorage.setItem('token', token); 16 | localStorage.setItem('refresh_token', refreshToken); 17 | }, 18 | remove: () => { 19 | localStorage.removeItem('token'); 20 | localStorage.removeItem('refresh_token'); 21 | }, 22 | get: () => ({ 23 | token: localStorage.getItem('token'), 24 | refreshToken: localStorage.getItem('refresh_token'), 25 | }), 26 | get notEmpty() { 27 | const cond1 = this.get().token !== null; 28 | const cond2 = this.get().token !== ''; 29 | return cond1 && cond2; 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Sample App 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 |
20 |

INITIALIZING

21 |

bear with us ...

22 |
23 | 24 | 25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import App from './App'; 5 | import './styles.scss'; 6 | 7 | ReactDOM.render(, document.getElementById('app')); 8 | 9 | // import './myStyles.scss'; 10 | 11 | // class App extends React.Component { 12 | // state = { 13 | // CaptainKirkBio: {}, 14 | // Foo: null, // Foo is out component 15 | // }; 16 | 17 | // componentDidMount() { 18 | // this.onGetKirkBio(); 19 | // import(/* webpackChunkName: 'Foo' */ './Foo').then(Foo => { 20 | // this.setState({ Foo: Foo.default }); 21 | // }); 22 | // } 23 | 24 | // onGetKirkBio = async () => { 25 | // try { 26 | // const result = await fetch('http://stapi.co/api/v1/rest/character/search', { 27 | // method: 'POST', 28 | // headers: { 29 | // 'Content-Type': 'application/x-www-form-urlencoded', 30 | // }, 31 | // body: { 32 | // title: 'James T. Kirk', 33 | // name: 'James T. Kirk', 34 | // }, 35 | // }); 36 | // const resultJSON = await result.json(); 37 | // const character = resultJSON.characters[0]; 38 | // this.setState({ CaptainKirkBio: character }); 39 | // } catch (error) { 40 | // // console.log('error', error); 41 | // } 42 | // }; 43 | 44 | // render() { 45 | // const { CaptainKirkBio, Foo } = this.state; 46 | // return ( 47 | //
48 | // header 49 | //

50 | // We are a most promising species, Mr. Spock, as predators go. Did you know that? I I 51 | // frequently have my doubts. I dont. Not any more. And maybe in a thousand 52 | // years or so will 53 | // be able to prove it. 54 | //

55 | //

- Captain Kirk

56 | //
57 | // {Object.values(CaptainKirkBio).length === 0 ? ( 58 | //

Loading User Information

59 | // ) : ( 60 | //

{JSON.stringify(CaptainKirkBio)}

61 | // )} 62 | //
63 | // {Foo ? :

Foo is loading

} 64 | //
65 | // ); 66 | // } 67 | // } 68 | 69 | // ReactDOM.render(, document.getElementById('app')); 70 | -------------------------------------------------------------------------------- /src/modules/auth/layout/MainLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Route } from 'react-router-dom'; 4 | 5 | // Common Components 6 | import { withStyles } from '@material-ui/core/styles'; 7 | // Routes 8 | import Routes from './Routes'; 9 | 10 | const drawerWidth = 295; 11 | 12 | const styles = theme => ({ 13 | root: { 14 | flexGrow: 1, 15 | height: '100vh', 16 | zIndex: 1, 17 | overflow: 'hidden', 18 | position: 'relative', 19 | display: 'flex', 20 | }, 21 | appBar: { 22 | zIndex: theme.zIndex.drawer + 1, 23 | }, 24 | toolbar: theme.mixins.toolbar, 25 | drawerPaper: { 26 | width: drawerWidth, 27 | height: '100vh', 28 | [theme.breakpoints.up('sm')]: { 29 | paddingTop: 65, 30 | }, 31 | [theme.breakpoints.up('md')]: { 32 | position: 'relative', 33 | }, 34 | }, 35 | content: { 36 | flexGrow: 1, 37 | padding: theme.spacing.unit * 3, 38 | overflowY: 'auto', 39 | }, 40 | }); 41 | 42 | const MainLayout = ({ classes }) => { 43 | return ( 44 |
45 |
46 |
47 | } /> 48 |
49 |
50 | ); 51 | }; 52 | 53 | MainLayout.displayName = 'AuthLayout'; 54 | 55 | MainLayout.propTypes = { 56 | classes: PropTypes.object, // Material UI Injected; 57 | history: PropTypes.object, // React Router Injected; 58 | }; 59 | 60 | export default withStyles(styles)(MainLayout); 61 | -------------------------------------------------------------------------------- /src/modules/auth/layout/Routes.js: -------------------------------------------------------------------------------- 1 | import React, { lazy } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Switch, Route } from 'react-router-dom'; 4 | import { TransitionGroup, CSSTransition } from 'react-transition-group'; 5 | import styled from 'styled-components'; 6 | 7 | /* Auth Pages Starts Here */ 8 | const UserPage = lazy(() => import('../user/UserPage')); 9 | 10 | /* Auth Pages Ends Here */ 11 | 12 | const Transition = styled(TransitionGroup)` 13 | & .fade-enter { 14 | opacity: 0; 15 | } 16 | 17 | & .fade-enter.fade-enter-active { 18 | opacity: 1; 19 | transition: opacity 500ms ease-in; 20 | } 21 | 22 | & .fade-exit { 23 | opacity: 0; 24 | } 25 | `; 26 | 27 | const Routes = ({ match, location }) => ( 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | ); 37 | 38 | Routes.propTypes = { 39 | match: PropTypes.object, // React Router Injected; 40 | location: PropTypes.object, // React Router Injected; 41 | }; 42 | 43 | export default Routes; 44 | -------------------------------------------------------------------------------- /src/modules/auth/user/UserPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () =>

user page

; 4 | -------------------------------------------------------------------------------- /src/modules/common/PageLoader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | // Material UI 4 | import { withStyles } from '@material-ui/core/styles'; 5 | import CircularProgress from '@material-ui/core/CircularProgress'; 6 | 7 | const styles = theme => ({ 8 | progress: { 9 | display: 'flex', 10 | flexDirection: 'column', 11 | alignItems: 'center', 12 | justifyContent: 'center', 13 | height: '100vh', 14 | margin: theme.spacing.unit * 5, 15 | }, 16 | }); 17 | 18 | const PageLoader = ({ classes }) => { 19 | return ( 20 |
21 | 22 |
23 | ); 24 | }; 25 | 26 | PageLoader.propTypes = { 27 | classes: PropTypes.object.isRequired, // Material UI Injected 28 | }; 29 | 30 | export default withStyles(styles)(PageLoader); 31 | -------------------------------------------------------------------------------- /src/modules/not-found/NoMatchPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | // Material UI 6 | import { withStyles } from '@material-ui/core/styles'; 7 | import Button from '@material-ui/core/Button'; 8 | import Typography from '@material-ui/core/Typography'; 9 | 10 | const styles = theme => ({ 11 | wrapper: { 12 | marginTop: 85, 13 | }, 14 | logo: { 15 | paddingBottom: 20, 16 | height: 100, 17 | }, 18 | sadFaceIcon: { 19 | maxHeight: 400, 20 | marginTop: theme.margin, 21 | }, 22 | button: { 23 | color: '#fff', 24 | marginTop: theme.margin * 2, 25 | marginBottom: theme.margin, 26 | }, 27 | }); 28 | 29 | const NoMatchPage = ({ classes }) => ( 30 |
31 |
32 |
33 | careem-logo 34 | 35 | 404 36 | 37 | 38 | This is an error. 39 | 40 | 41 | The requested URL /hosting was not found on this server. 42 | 43 | 44 | This is all we know. 45 | 46 | 47 | 56 |
57 |
58 | sad-face 59 |
60 |
61 |
62 | ); 63 | 64 | NoMatchPage.propTypes = { 65 | location: PropTypes.object, // react router 66 | classes: PropTypes.object, // Material UI Injecte 67 | }; 68 | 69 | export default withStyles(styles)(NoMatchPage); 70 | -------------------------------------------------------------------------------- /src/modules/not-found/NoMatchPage.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | // Components 5 | import NoMatchPage from './NoMatchPage'; 6 | 7 | function setup() { 8 | const wrapper = shallow().dive(); 9 | return { wrapper }; 10 | } 11 | 12 | describe('NoMatchPage', () => { 13 | it('Should have Typography', () => { 14 | const { wrapper } = setup(); 15 | const el = wrapper.find('WithStyles(Typography)'); 16 | expect(el).toHaveLength(4); 17 | }); 18 | it('Should have a careem logo image', () => { 19 | const { wrapper } = setup(); 20 | const el = wrapper.find('[alt="careem-logo"]'); 21 | expect(el.exists()).toBe(true); 22 | }); 23 | it('Should have a sad face image', () => { 24 | const { wrapper } = setup(); 25 | const el = wrapper.find('[alt="sad-face"]'); 26 | expect(el.exists()).toBe(true); 27 | }); 28 | it('Should have a button', () => { 29 | const { wrapper } = setup(); 30 | const el = wrapper.find('WithStyles(Button)'); 31 | expect(el).toHaveLength(1); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/modules/public/login/LoginPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import axios from 'axios'; 4 | import styled from 'styled-components'; 5 | 6 | // Material UI 7 | import Snackbar from '@material-ui/core/Snackbar'; 8 | 9 | // API 10 | import { APP_TOKEN } from '../../../api/Constants'; 11 | // Components 12 | import LoginForm from './components/LoginForm'; 13 | import WelcomeMessage from './components/WelcomeMessage'; 14 | 15 | const Container = styled.section` 16 | display: flex; 17 | flex-direction: column; 18 | justify-content: center; 19 | align-items: center; 20 | height: 100vh; 21 | background-color: #607d8b; 22 | `; 23 | 24 | class LoginPage extends Component { 25 | isTokenSource = axios.CancelToken.source(); 26 | 27 | state = { 28 | form: { 29 | username: '', 30 | password: '', 31 | }, 32 | isLoading: false, 33 | isSnackbarOpen: false, 34 | snackbarMessage: '', 35 | }; 36 | 37 | componentWillUnmount() { 38 | this.isTokenSource.cancel('API Cancel'); 39 | } 40 | 41 | onHandleChangeForm = event => { 42 | const { form } = this.state; 43 | form[event.target.name] = event.target.value; 44 | this.setState({ form }); 45 | }; 46 | 47 | onHandleSubmitForm = async event => { 48 | event.preventDefault(); 49 | 50 | const { history } = this.props; 51 | const { form } = this.state; 52 | 53 | const isFormEmpty = Object.values(form).every(item => item === ''); 54 | if (isFormEmpty) { 55 | return; 56 | } 57 | try { 58 | this.setState({ isLoading: true }); 59 | // const result = await AuthenticationAPI.onLogin({ 60 | // cancelToken: this.isTokenSource.token, 61 | // username: form.username, 62 | // password: form.password, 63 | // }); 64 | this.setState({ isLoading: false }); 65 | APP_TOKEN.set({ 66 | token: '', 67 | refreshToken: '', 68 | }); 69 | history.push('/auth'); 70 | } catch (error) { 71 | if (axios.isCancel(error)) { 72 | // console.log('Request canceled', error.message); 73 | } else { 74 | const { message, errorCode } = error.response.data; 75 | if (errorCode === '401') { 76 | this.onToggleSnackbar({ message }); 77 | } 78 | this.setState({ isLoading: false }); 79 | } 80 | } 81 | }; 82 | 83 | onToggleSnackbar = ({ message = 'Error' }) => { 84 | this.setState(state => ({ 85 | isSnackbarOpen: !state.isSnackbarOpen, 86 | snackbarMessage: message, 87 | })); 88 | }; 89 | 90 | render() { 91 | const { form, isLoading, isSnackbarOpen, snackbarMessage } = this.state; 92 | return ( 93 | 94 | 95 | 101 | 102 | {snackbarMessage}} 111 | /> 112 | 113 | ); 114 | } 115 | } 116 | 117 | LoginPage.propTypes = { 118 | history: PropTypes.object, // React Router Injected 119 | }; 120 | 121 | export default LoginPage; 122 | -------------------------------------------------------------------------------- /src/modules/public/login/LoginPage.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | // Component 5 | import LoginPage from './LoginPage'; 6 | 7 | function setup() { 8 | const wrapper = shallow(); 9 | return { wrapper }; 10 | } 11 | 12 | describe('LoginPage', () => { 13 | it('Should render', () => { 14 | const { wrapper } = setup(); 15 | expect(wrapper.children()).toHaveLength(3); 16 | }); 17 | it('Should have WelcomeMessage', () => { 18 | const { wrapper } = setup(); 19 | expect(wrapper.find('WithStyles(WelcomeMessage)')).toHaveLength(1); 20 | }); 21 | it('Should have LoginForm', () => { 22 | const { wrapper } = setup(); 23 | expect(wrapper.find('WithStyles(LoginForm)')).toHaveLength(1); 24 | }); 25 | it('Should have Snackbar', () => { 26 | const { wrapper } = setup(); 27 | expect(wrapper.find('WithStyles(Snackbar)')).toHaveLength(1); 28 | }); 29 | it('Should call onHandleChangeForm & update form state', () => { 30 | const { wrapper } = setup(); 31 | const event = { 32 | target: { name: 'username', value: 'testvalue' }, 33 | }; 34 | wrapper.instance().onHandleChangeForm(event); 35 | wrapper.update(); 36 | expect(wrapper.state().form.username).toBe('testvalue'); 37 | }); 38 | it('Should call onToggleSnackbar & set snackbar to true', () => { 39 | const { wrapper } = setup(); 40 | wrapper.instance().onToggleSnackbar({ message: 'test' }); 41 | wrapper.update(); 42 | const el = wrapper.find('WithStyles(Snackbar)'); 43 | expect(el.prop('open')).toBe(true); 44 | }); 45 | it('Should do nothing when empty form submit', () => { 46 | const { wrapper } = setup(); 47 | const event = { preventDefault: jest.fn() }; 48 | wrapper.instance().onHandleSubmitForm(event); 49 | wrapper.update(); 50 | expect(wrapper.state().isLoading).toBe(false); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/modules/public/login/components/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | // import { Link } from 'react-router-dom'; 4 | 5 | // Material UI 6 | import { withStyles } from '@material-ui/core/styles'; 7 | import TextField from '@material-ui/core/TextField'; 8 | import Button from '@material-ui/core/Button'; 9 | import CircularProgress from '@material-ui/core/CircularProgress'; 10 | 11 | const styles = theme => ({ 12 | container: { 13 | backgroundColor: '#fff', 14 | padding: `${theme.margin * 1.5}px ${theme.margin}px`, 15 | width: 450, 16 | borderRadius: 6, 17 | margin: '0 auto', 18 | }, 19 | button: { 20 | borderColor: theme.palette.primary.main, 21 | marginTop: theme.margin, 22 | }, 23 | forgotContainer: { 24 | textAlign: 'center', 25 | marginTop: theme.margin * 2, 26 | }, 27 | }); 28 | 29 | const LoginForm = ({ value, isLoading, onChange, onSubmit, classes }) => { 30 | const isFormEnabled = Object.values(value).every(item => item !== ''); 31 | return ( 32 |
33 | 43 | 52 | 63 | {/*

64 | Forgot Password? 65 |

*/} 66 | 67 | ); 68 | }; 69 | 70 | LoginForm.propTypes = { 71 | value: PropTypes.object, 72 | isLoading: PropTypes.bool, 73 | onChange: PropTypes.func, 74 | onSubmit: PropTypes.func, 75 | classes: PropTypes.object, // Material UI Injected 76 | }; 77 | 78 | export default withStyles(styles)(LoginForm); 79 | -------------------------------------------------------------------------------- /src/modules/public/login/components/LoginForm.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | // Components 5 | import LoginForm from './LoginForm'; 6 | 7 | function setup() { 8 | const props = { 9 | value: { 10 | username: '', 11 | password: '', 12 | }, 13 | isLoading: false, 14 | onChange: jest.fn(), 15 | onSubmit: jest.fn(), 16 | }; 17 | const wrapper = shallow().dive(); 18 | return { props, wrapper }; 19 | } 20 | 21 | describe('LoginPage LoginForm', () => { 22 | it('Should render', () => { 23 | const { wrapper } = setup(); 24 | expect(wrapper.exists()).toBe(true); 25 | }); 26 | it('Should have username field as user', () => { 27 | const { props, wrapper } = setup(); 28 | wrapper.setProps({ 29 | ...props, 30 | value: { 31 | username: 'test', 32 | password: 'test', 33 | }, 34 | }); 35 | const elUser = wrapper.find('[name="username"]'); 36 | const elPass = wrapper.find('[name="password"]'); 37 | expect(elUser.prop('value')).toBe('test'); 38 | expect(elPass.prop('value')).toBe('test'); 39 | }); 40 | it('Should have the button say Login', () => { 41 | const { props, wrapper } = setup(); 42 | wrapper.setProps({ 43 | ...props, 44 | isLoading: false, 45 | }); 46 | const el = wrapper.find('[type="submit"]'); 47 | expect(el.html()).toMatch('Login'); 48 | }); 49 | it('Should have the button be in loading state', () => { 50 | const { props, wrapper } = setup(); 51 | wrapper.setProps({ 52 | ...props, 53 | isLoading: true, 54 | }); 55 | const el = wrapper.find('[type="submit"]'); 56 | expect(el.find('WithStyles(CircularProgress)').exists()).toBe(true); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/modules/public/login/components/WelcomeMessage.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | // Material UI 5 | import { withStyles } from '@material-ui/core/styles'; 6 | import Typography from '@material-ui/core/Typography'; 7 | 8 | const styles = theme => ({ 9 | heading: { 10 | color: '#fff', 11 | textAlign: 'center', 12 | marginBottom: theme.margin * 2, 13 | }, 14 | logo: { 15 | width: 250, 16 | heading: 250, 17 | objectFit: 'cover', 18 | }, 19 | }); 20 | 21 | const WelcomeMessage = ({ classes }) => { 22 | return ( 23 | 24 | 25 | Welcome To 26 | 27 | app logo 28 | 29 | ); 30 | }; 31 | 32 | WelcomeMessage.propTypes = { 33 | classes: PropTypes.object, // Material UI Injected 34 | }; 35 | 36 | export default withStyles(styles)(WelcomeMessage); 37 | -------------------------------------------------------------------------------- /src/modules/public/login/components/WelcomeMessage.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | 4 | // Components 5 | import WelcomeMessage from './WelcomeMessage'; 6 | 7 | function setup() { 8 | const wrapper = shallow().dive(); 9 | return { wrapper }; 10 | } 11 | 12 | describe('LoginPage WelcomeMessage', () => { 13 | it('Should have an image', () => { 14 | const { wrapper } = setup(); 15 | expect(wrapper.find('img').exists()).toBe(true); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/myStyles.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: skyblue; 3 | color: black; 4 | font-size: 20px; 5 | } 6 | 7 | .app { 8 | width: 450px; 9 | margin: 0 auto; 10 | padding-top: 50px; 11 | 12 | & .app-header { 13 | height: 250px; 14 | width: inherit; 15 | object-fit: cover; 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import user from './userReducer'; 4 | 5 | const rootReducer = combineReducers({ 6 | user, 7 | }); 8 | 9 | export default rootReducer; 10 | -------------------------------------------------------------------------------- /src/reducers/userReducer.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from '../actions/actionTypes'; 2 | 3 | const initialState = { 4 | user: [], 5 | dummy: '', 6 | }; 7 | 8 | export default function(state = initialState, action = { type: '' }) { 9 | switch (action.type) { 10 | case actionTypes.GET_USER: { 11 | return { ...state }; 12 | } 13 | case actionTypes.DUMMY_1: { 14 | return { ...state, dummy: 'dummy1' }; 15 | } 16 | case actionTypes.DUMMY_2: { 17 | return { ...state, dummy: 'dummy2' }; 18 | } 19 | default: { 20 | return state; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/static/images/header.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adeelibr/react-starter-kit/a260260e81f14d0ddbac53cfb1d5eccc4249c61d/src/static/images/header.jpg -------------------------------------------------------------------------------- /src/static/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adeelibr/react-starter-kit/a260260e81f14d0ddbac53cfb1d5eccc4249c61d/src/static/images/logo.png -------------------------------------------------------------------------------- /src/static/public/style.css: -------------------------------------------------------------------------------- 1 | .app-intro { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: center; 6 | height: 100vh; 7 | background-color: #fff; 8 | color: #000; 9 | overflow-y: hidden; 10 | } 11 | 12 | .app-intro .heading { 13 | font-size: 36px; 14 | text-align: center; 15 | animation: heart-beat 4s infinite ease-in; 16 | z-index: 1; 17 | } 18 | 19 | .app-intro .sub-heading { 20 | font-size: 18px; 21 | text-align: center; 22 | color: #4b4b4b; 23 | z-index: 1; 24 | } 25 | 26 | @keyframes heart-beat { 27 | 0% { 28 | opacity: 0.1; 29 | } 30 | 50% { 31 | opacity: 1; 32 | } 33 | 100% { 34 | opacity: 0.1; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | import { createStore, compose, applyMiddleware } from 'redux'; 3 | import thunk from 'redux-thunk'; 4 | 5 | import rootReducer from '../reducers'; 6 | 7 | const middlewares = [ 8 | applyMiddleware(thunk), 9 | ...(process.env.NODE_ENV !== 'production' && window.__REDUX_DEVTOOLS_EXTENSION__ 10 | ? [window.__REDUX_DEVTOOLS_EXTENSION__()] 11 | : []), 12 | ]; 13 | 14 | const enhancer = compose(...middlewares); 15 | 16 | const store = initialState => { 17 | return createStore(rootReducer, initialState, enhancer); 18 | }; 19 | 20 | export default store; 21 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Roboto'); 2 | 3 | // include bootstrap 4 | @import "~bootstrap/scss/bootstrap"; 5 | 6 | // my custom stylesheets 7 | @import "styles/modules/reset.scss"; 8 | @import "styles/modules/base.scss"; 9 | -------------------------------------------------------------------------------- /src/styles/modules/_base.scss: -------------------------------------------------------------------------------- 1 | * { 2 | outline: none !important; 3 | } 4 | 5 | $primary: #37b44e; 6 | 7 | body { 8 | background-color: #f8f9fa; 9 | font-family: 'Roboto', sans-serif; 10 | font-size: 16px; 11 | color: #000; 12 | } -------------------------------------------------------------------------------- /src/styles/modules/_reset.scss: -------------------------------------------------------------------------------- 1 | html, body, div, span, applet, object, iframe, 2 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 3 | a, abbr, acronym, address, big, cite, code, 4 | del, dfn, em, img, ins, kbd, q, s, samp, 5 | small, strike, strong, sub, sup, tt, var, 6 | b, u, i, center, 7 | dl, dt, dd, ol, ul, li, 8 | fieldset, form, label, legend, 9 | table, caption, tbody, tfoot, thead, tr, th, td, 10 | article, aside, canvas, details, figcaption, figure, 11 | footer, header, hgroup, menu, nav, section, summary, 12 | time, mark, audio, video { 13 | margin: 0; 14 | padding: 0; 15 | border: 0; 16 | outline: 0; 17 | font-size: 100%; 18 | font: inherit; 19 | vertical-align: baseline; 20 | } 21 | /* HTML5 display-role reset for older browsers */ 22 | article, aside, details, figcaption, figure, 23 | footer, header, hgroup, menu, nav, section { 24 | display: block; 25 | } 26 | body { 27 | line-height: 1; 28 | } 29 | ol, ul { 30 | list-style: none; 31 | } 32 | blockquote, q { 33 | quotes: none; 34 | } 35 | blockquote:before, blockquote:after, 36 | q:before, q:after { 37 | content: ''; 38 | content: none; 39 | } 40 | 41 | /* remember to highlight inserts somehow! */ 42 | ins { 43 | text-decoration: none; 44 | } 45 | del { 46 | text-decoration: line-through; 47 | } 48 | 49 | table { 50 | border-collapse: collapse; 51 | border-spacing: 0; 52 | } -------------------------------------------------------------------------------- /src/styles/muiTheme.js: -------------------------------------------------------------------------------- 1 | export default { 2 | direction: 'ltr', 3 | palette: { 4 | type: 'light', 5 | primary: { 6 | main: '#607d8b', 7 | }, 8 | secondary: { 9 | main: '#000', 10 | }, 11 | }, 12 | margin: 15, 13 | typography: { 14 | fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', 15 | fontSize: 14, 16 | fontWeightLight: 300, 17 | fontWeightRegular: 400, 18 | fontWeightMedium: 500, 19 | display4: { 20 | fontSize: '7rem', 21 | fontWeight: 300, 22 | fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', 23 | letterSpacing: '-.04em', 24 | lineHeight: '1.14286em', 25 | marginLeft: '-.06em', 26 | color: 'rgba(0, 0, 0, 0.54)', 27 | }, 28 | display3: { 29 | fontSize: '3.5rem', 30 | fontWeight: 400, 31 | fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', 32 | letterSpacing: '-.02em', 33 | lineHeight: '1.30357em', 34 | marginLeft: '-.04em', 35 | color: 'rgba(0, 0, 0, 0.54)', 36 | }, 37 | display2: { 38 | fontSize: '2.8125rem', 39 | fontWeight: 400, 40 | fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', 41 | lineHeight: '1.06667em', 42 | marginLeft: '-.04em', 43 | color: 'rgba(0, 0, 0, 0.54)', 44 | }, 45 | display1: { 46 | fontSize: '2.125rem', 47 | fontWeight: 400, 48 | fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', 49 | lineHeight: '1.20588em', 50 | marginLeft: '-.04em', 51 | color: 'rgba(0, 0, 0, 0.54)', 52 | }, 53 | headline: { 54 | fontSize: '1.5rem', 55 | fontWeight: 400, 56 | fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', 57 | lineHeight: '1.35417em', 58 | color: 'rgba(0, 0, 0, 0.87)', 59 | }, 60 | title: { 61 | fontSize: '1.3125rem', 62 | fontWeight: 500, 63 | fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', 64 | lineHeight: '1.16667em', 65 | color: 'rgba(0, 0, 0, 0.87)', 66 | }, 67 | subheading: { 68 | fontSize: '1rem', 69 | fontWeight: 400, 70 | fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', 71 | lineHeight: '1.5em', 72 | color: 'rgba(0, 0, 0, 0.87)', 73 | }, 74 | body2: { 75 | fontSize: '0.875rem', 76 | fontWeight: 500, 77 | fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', 78 | lineHeight: '1.71429em', 79 | color: 'rgba(0, 0, 0, 0.87)', 80 | }, 81 | body1: { 82 | fontSize: '0.875rem', 83 | fontWeight: 400, 84 | fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', 85 | lineHeight: '1.46429em', 86 | color: 'rgba(0, 0, 0, 0.87)', 87 | }, 88 | caption: { 89 | fontSize: '0.75rem', 90 | fontWeight: 400, 91 | fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', 92 | lineHeight: '1.375em', 93 | color: 'rgba(0, 0, 0, 0.54)', 94 | }, 95 | button: { 96 | fontSize: 14, 97 | textTransform: 'uppercase', 98 | fontWeight: 500, 99 | fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', 100 | }, 101 | }, 102 | }; 103 | --------------------------------------------------------------------------------