From 68b62813b013dea67a01880377517d2bb15a8294 Mon Sep 17 00:00:00 2001 From: Roy Yu Date: Mon, 3 Dec 2018 16:55:46 -0800 Subject: [PATCH] Clean up, restructure folders, switch to css modulues and add comments --- README.md | 11 ++- bin/commands.js | 13 ++- package.json | 2 +- src/js/README.md | 15 +++ src/js/common/api/README.md | 10 ++ src/js/common/api/index.js | 7 ++ src/js/common/api/module/example.js | 9 ++ src/js/common/components/Example/Example.jsx | 4 +- .../components/Example/ExampleWithError.jsx | 4 +- src/js/common/components/Header/Header.jsx | 8 +- .../common/components/LazyLoading/Loading.jsx | 4 +- src/js/routes.jsx | 15 ++- src/js/utility/validation.js | 97 ------------------- src/js/views/README.md | 10 ++ src/js/views/example/{index.jsx => View.jsx} | 0 src/js/views/example/index.js | 1 + src/style/index.css | 13 +-- webpack.config.build.babel.js | 7 +- webpack.config.dev-server.babel.js | 7 +- 19 files changed, 107 insertions(+), 130 deletions(-) create mode 100644 src/js/README.md create mode 100644 src/js/common/api/README.md create mode 100644 src/js/common/api/index.js create mode 100644 src/js/common/api/module/example.js delete mode 100644 src/js/utility/validation.js create mode 100644 src/js/views/README.md rename src/js/views/example/{index.jsx => View.jsx} (100%) create mode 100644 src/js/views/example/index.js diff --git a/README.md b/README.md index 5b890af..74dcb2a 100644 --- a/README.md +++ b/README.md @@ -53,14 +53,14 @@ __Note:__ This boilerplate is not to show case how to code in React / Redux, it Features -* React 16 +* React 16.6 * Redux * Saga * ES6 / ES7 * ImmutableJS * PreCSS ( supports SASS-like markup in your CSS ) -* PostCSS ( it support CSS modules, and we recommended B.E.M style ) -* Webpack 3 +* PostCSS ( with CSS modules activated by default ) +* Webpack 4 * Reselect * Lazy Loading component supports * Type Checking with Babel Type Check ( Flow syntax ) @@ -198,8 +198,9 @@ All your javascript code lives in folder `src/js` -- src/ -- js/ -- common/ - -- components/ --> all share components here - -- types/ --> all flow types are here + -- api/ --> all api requests + -- components/ --> all share components + -- types/ --> all flow types -- redux/ -- modules/ --> all redux code -- saga/ --> all redux-saga code diff --git a/bin/commands.js b/bin/commands.js index b8497a0..7e7826b 100644 --- a/bin/commands.js +++ b/bin/commands.js @@ -2,17 +2,22 @@ const shell = require('shelljs'); const config = require('config'); const colors = require('colors'); -const host = config.get('host') || 'localhost'; -const port = config.get('port') || '8080'; - const option = process.argv[2]; +// The following will allow you to add your own +// - pre process +// - post process +// - parallel process +// - logging +// +// You can add your process here and have package.json to proxy your command +// Please look at package.json -> "scripts" section switch (option) { case 'lint': shell.exec('cross-env eslint --fix src/js/** --format node_modules/eslint-friendly-formatter . --ext .js --ext .jsx --cache; exit 0'); break; case 'dev': - shell.exec(`cross-env HOST=${host} PORT=${port} webpack-dev-server --config webpack.config.dev-server.babel.js --hot --progress --no-info --inline --colors --content-base ./docroot`); + shell.exec(`cross-env webpack-dev-server --config webpack.config.dev-server.babel.js --hot --progress --no-info --inline --colors`); break; case 'build': shell.exec(`cross-env rimraf docroot && webpack --config webpack.config.build.babel.js --progress --display-error-details`); diff --git a/package.json b/package.json index b6a5f1e..dc958ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-redux-boilerplate", - "version": "1.2.1", + "version": "1.2.2", "description": "React Redux Boilerplate is a workflow boilerplate that make life easier for developers by providing a virtual development environment and production ready build process framework out of the box.", "scripts": { "dev": "cross-env NODE_ENV=development DASHBOARD_PORT=9901 webpack-dashboard -p 9901 -c red -t dashboard -- node bin/commands.js dev", diff --git a/src/js/README.md b/src/js/README.md new file mode 100644 index 0000000..d43b6e4 --- /dev/null +++ b/src/js/README.md @@ -0,0 +1,15 @@ +## Before Your Start Coding + +This Boilerplate is providing a infrastructure to start your project as quickly as you can. + +__BUT__ before you start developing a real large scale project, please do ask yourself or your team a few questions :- + +* What is your domain layer looks like ? You can pretend redux as a data repository of everything, but it will provide no actual meaning for your application besides a data bag. For example, am I able to describe what a `User` is in the application. Having a domain layer will help yourself from having heart attack of arbitrary data structure that describe the same thing in different places. +* I know you are a smart engineer, but please remember it is better to write code everyone can understand. +* You only need to solve a problem when you have a real problem, don't prematurely making a decision based on preference. +* Strong convention is better for everyone, if you don't have convention today, you will have N+1 conventions the next month. + +If you read to this point, I assumed you are a very serious developer, please help me to read the following links if you haven't! + +[SOLID JavaScript](http://aspiringcraftsman.com/2011/12/08/solid-javascript-single-responsibility-principle/) +[Twelve Factor App](https://12factor.net/) diff --git a/src/js/common/api/README.md b/src/js/common/api/README.md new file mode 100644 index 0000000..4420087 --- /dev/null +++ b/src/js/common/api/README.md @@ -0,0 +1,10 @@ +## Convention + +The following will be convention for this api/ + +* You can only put your api request here, no business logic. +* You should separate your api files by "requested resources" +* Your api should return a "Promise" interface for consistency +* And you use `index.js` to expose your api + +This folder will contain an example using axios, it is not a working api, but show case how you should structure your apis. diff --git a/src/js/common/api/index.js b/src/js/common/api/index.js new file mode 100644 index 0000000..20bd673 --- /dev/null +++ b/src/js/common/api/index.js @@ -0,0 +1,7 @@ +/* +// This is an example will not work +// This is just showing case how I would orangize +export { + getTopTenAvengers, +} from './module/example' +*/ diff --git a/src/js/common/api/module/example.js b/src/js/common/api/module/example.js new file mode 100644 index 0000000..e2cb2ef --- /dev/null +++ b/src/js/common/api/module/example.js @@ -0,0 +1,9 @@ +/* +// This is an example will not work +// Just only show case how I would orangize +const API_ENDPOINT = __CONFIG__.apiUrl.avengers + +export const getTopTenAvengers = (query = {}) => { + return axios.get(`${API_ENDPOINT}?count=10`) +} +*/ diff --git a/src/js/common/components/Example/Example.jsx b/src/js/common/components/Example/Example.jsx index d548e71..d613b46 100644 --- a/src/js/common/components/Example/Example.jsx +++ b/src/js/common/components/Example/Example.jsx @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react'; -import './Example.css'; +import styles from './Example.css'; /** * Example @@ -45,7 +45,7 @@ class Example extends PureComponent { if (result && result.size && result.size > 0) { return ( -
+

Let's Get Started diff --git a/src/js/common/components/Example/ExampleWithError.jsx b/src/js/common/components/Example/ExampleWithError.jsx index b60f88c..6eda4fe 100644 --- a/src/js/common/components/Example/ExampleWithError.jsx +++ b/src/js/common/components/Example/ExampleWithError.jsx @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react'; -import './Example.css'; +import styles from './Example.css'; class ExampleWithError extends PureComponent { render() { @@ -9,7 +9,7 @@ class ExampleWithError extends PureComponent { if (result && result.size && result.size > 0) { return ( -
+

This should catch by ErrorBoundary

{result.something_not_existed.get('something_not_existed')}
diff --git a/src/js/common/components/Header/Header.jsx b/src/js/common/components/Header/Header.jsx index be7327d..bb3fb86 100644 --- a/src/js/common/components/Header/Header.jsx +++ b/src/js/common/components/Header/Header.jsx @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; import { Link } from 'react-router-dom'; -import './Header.css'; +import styles from './Header.css'; class Header extends PureComponent { render() { @@ -12,16 +12,16 @@ class Header extends PureComponent { const isJustAnotherPage = pathname === '/page'; return ( -
+
    -
  • +
  • { isHome ? 'Home' : Home }
  • -
  • +
  • { isJustAnotherPage ? 'Just Another Page' : Just Another Page diff --git a/src/js/common/components/LazyLoading/Loading.jsx b/src/js/common/components/LazyLoading/Loading.jsx index ad23566..e68ecff 100644 --- a/src/js/common/components/LazyLoading/Loading.jsx +++ b/src/js/common/components/LazyLoading/Loading.jsx @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react'; -import './Loading.css' +import styles from './Loading.css' class Loading extends PureComponent { getMessage() { @@ -17,7 +17,7 @@ class Loading extends PureComponent { if (timedOut) { return
    {errorMessage}
    ; } if (pastDelay) { - return
    Loading...
    ; + return
    Loading...
    ; } return null; } if (error) { diff --git a/src/js/routes.jsx b/src/js/routes.jsx index 881eb6d..ecfa211 100644 --- a/src/js/routes.jsx +++ b/src/js/routes.jsx @@ -5,11 +5,15 @@ import { withRouter, } from 'react-router-dom' -import LazyLoading from './common/components/LazyLoading' +import LazyLoading from 'common/components/LazyLoading' -const ExampleRouteHandler = LazyLoading(() => import('./views/example')) -const Header = LazyLoading(() => import('./common/components/Header/Header')) +import styles from '../style/index.css' +// This is show case how you can lazy loading component +const ExampleRouteHandler = LazyLoading(() => import('views/example')) +const Header = LazyLoading(() => import('common/components/Header/Header')) + +// Please remove that, it is an example const JustAnotherPage = () => (

    This is Just Another Page

    @@ -17,13 +21,14 @@ const JustAnotherPage = () => (
    ) +// This show case how you can access routing info in your component const HeaderWithRouter = withRouter((props) =>
    ) module.exports = ( -
    +

    -
    +
    diff --git a/src/js/utility/validation.js b/src/js/utility/validation.js deleted file mode 100644 index 04a03d1..0000000 --- a/src/js/utility/validation.js +++ /dev/null @@ -1,97 +0,0 @@ -const isEmpty = (value) => value === undefined || value === null || value === '' -function join(rules) { - return (value, data) => rules.map((rule) => rule(value, data)).filter((error) => !!error)[0] -} - -export function email(value: string) { - // Let's not start a debate on email regex! This one is quite standard - if (!isEmpty(value) && !/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(value)) { - return 'Invalid email address' - } - - return null -} - -export function phone(value: string) { - // Let's not start a debate on phone regex! This one is the best I can find, the best way to - // do it correctly is utilizing a third party verification, but for our use case, it is - // just overkill - if (!isEmpty(value) && !/^[+]*[(]{0,1}[0-9]{1,4}[)]{0,1}[-\s/0-9]*$/g.test(value)) { - return 'Invalid phone number' - } - - return null -} - -export function required(value: string) { - if (isEmpty(value)) { - return 'Required' - } - - return null -} - -export function minLength(min: number) { - return (value) => { - if (!isEmpty(value) && value.length < min) { - return `Must be at least ${min} characters` - } - - return null - }; -} - -export function maxLength(max: number) { - return (value) => { - if (!isEmpty(value) && value.length > max) { - return `Must be no more than ${max} characters` - } - - return null - }; -} - -export function integer(value: string|number) { - if (!Number.isInteger(Number(value))) { - return 'Must be an integer' - } - - return null -} - -export function oneOf(enumeration) { - return (value) => { - if (!enumeration.indexOf(value)) { - return `Must be one of: ${enumeration.join(', ')}` - } - - return null - }; -} - -export function match(field) { - return (value, data) => { - if (data) { - if (value !== data[field]) { - return 'Do not match' - } - } - - return null - }; -} - -export function createValidator(rules) { - return (data = {}) => { - const errors = {} - Object.keys(rules).forEach((key) => { - // concat enables both functions and arrays of functions - const rule = join([].concat(rules[key])) - const error = rule(data[key], data) - if (error) { - errors[key] = error - } - }) - return errors - } -} diff --git a/src/js/views/README.md b/src/js/views/README.md new file mode 100644 index 0000000..02e250c --- /dev/null +++ b/src/js/views/README.md @@ -0,0 +1,10 @@ +## Convention + +The organization of `views` is suggested as follows: + +* Folders correlate to the most significant portion of the URL to map to in case and spelling within the application, and not including the application mount point if there is one. + * Example: if a URL named `/avengers-search/` maps to a page, then we should have a `views/avengers-search/` folder that exports a component that manages the `avengers-search` page. + * NOTE: Due to React component naming requirements, the variable that references the "page" should always be PascalCased, e.g. avengers-search -> AvengersSearch. +* Each folder should make use of the `index.js` trick to allow importing of the folder. +* Major sub views that correlate to URLs should, if appropriate, also be organized within the parent view folder. +* Besides `index.js`, the main coordinating file should be named `View.jsx`, and this is the file that should be exported as default from `index.js`. diff --git a/src/js/views/example/index.jsx b/src/js/views/example/View.jsx similarity index 100% rename from src/js/views/example/index.jsx rename to src/js/views/example/View.jsx diff --git a/src/js/views/example/index.js b/src/js/views/example/index.js new file mode 100644 index 0000000..9a224ed --- /dev/null +++ b/src/js/views/example/index.js @@ -0,0 +1 @@ +export { default } from './View' diff --git a/src/style/index.css b/src/style/index.css index a168063..5d87a74 100644 --- a/src/style/index.css +++ b/src/style/index.css @@ -6,12 +6,13 @@ font-size: 16px; margin-left: 60px; margin-right: 60px; -} -.container__content { -} + .content { + + } -h1, h2 { - /* This is how you use custom fonts */ - font-family: YourFavoriteFont; + h1, h2 { + /* This is how you use custom fonts */ + font-family: YourFavoriteFont; + } } diff --git a/webpack.config.build.babel.js b/webpack.config.build.babel.js index e4287df..f979d42 100644 --- a/webpack.config.build.babel.js +++ b/webpack.config.build.babel.js @@ -62,7 +62,12 @@ webpackConfig.module.rules = webpackConfig.module.rules.concat({ }, { loader: 'css-loader', - options: { sourceMap: true, importLoaders: 1 }, + options: { + sourceMap: true, + importLoaders: 1, + modules: true, + localIdentName: '[name]__[local]_[hash:base64]', + }, }, { loader: 'postcss-loader', diff --git a/webpack.config.dev-server.babel.js b/webpack.config.dev-server.babel.js index 09773e8..85e45e4 100644 --- a/webpack.config.dev-server.babel.js +++ b/webpack.config.dev-server.babel.js @@ -97,7 +97,12 @@ webpackConfig.module.rules = webpackConfig.module.rules.concat({ }, { loader: 'css-loader', - options: { sourceMap: true, importLoaders: 1 }, + options: { + sourceMap: true, + importLoaders: 1, + modules: true, + localIdentName: '[name]__[local]_[hash:base64]', + }, }, { loader: 'postcss-loader',