├── .gitignore
├── .prettierignore
├── .prettierrc.js
├── README.md
├── assets
└── screen-shot.png
├── dev-machine-setup.md
├── package.json
├── public
├── env.js
├── favicon.ico
├── index.html
└── manifest.json
├── src
├── App.tsx
├── components
│ ├── Header
│ │ ├── BackButton.tsx
│ │ ├── ExamplesButton.tsx
│ │ ├── Header.tsx
│ │ ├── HomeButton.tsx
│ │ ├── ToggleTheme.tsx
│ │ └── index.ts
│ └── index.ts
├── contexts
│ ├── RootStoreContext.ts
│ └── index.ts
├── features
│ ├── Examples
│ │ ├── ExamplesPage.tsx
│ │ ├── ProductEditor
│ │ │ ├── ProductContext.tsx
│ │ │ ├── ProductDetail.tsx
│ │ │ ├── ProductEditor.tsx
│ │ │ ├── ProductForm.tsx
│ │ │ ├── ProductMaster.tsx
│ │ │ └── index.ts
│ │ └── index.ts
│ ├── Home
│ │ ├── HomePage.tsx
│ │ ├── PersonList
│ │ │ ├── PersonList.tsx
│ │ │ ├── index.ts
│ │ │ └── usePeople.ts
│ │ └── index.ts
│ └── index.ts
├── index.tsx
├── init.ts
├── models
│ ├── Person.ts
│ └── index.ts
├── react-app-env.d.ts
├── stores
│ ├── LsKeys.ts
│ ├── PrefStore.ts
│ ├── RootStore.ts
│ ├── index.ts
│ └── routes.ts
├── utils
│ ├── constants.ts
│ ├── index.ts
│ └── math-utils.ts
└── viewMap.tsx
├── tsconfig.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # IDEs and editors
15 | /.idea
16 | /.vscode
17 |
18 | # misc
19 | .DS_Store
20 | .eslintcache
21 | .env.local
22 | .env.development.local
23 | .env.test.local
24 | .env.production.local
25 |
26 | npm-debug.log*
27 | yarn-debug.log*
28 | yarn-error.log*
29 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | package.json
2 | package-lock.json
3 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | tabWidth: 4,
3 | singleQuote: true,
4 | proseWrap: 'always'
5 | };
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Force Template
2 |
3 | This template is intended to be a starting point for serious React applications.
4 | It offers an opinionated directory structure and demonstrates best practices in
5 | layout, error handling, hooks and more.
6 |
7 | 
8 |
9 | Check out the following repos for more complex examples using this template:
10 |
11 | - [Stock Charts](https://github.com/nareshbhatia/stock-charts)
12 | - [Manage My Money](https://github.com/nareshbhatia/manage-my-money)
13 | - [GraphQL Bookstore](https://github.com/nareshbhatia/graphql-bookstore)
14 |
15 | ## Getting Started
16 |
17 | Make sure your development machine is set up for building React apps. See the
18 | recommended setup procedure [here](./dev-machine-setup.md).
19 |
20 | Now execute the following commands to build and run the template.
21 |
22 | ```bash
23 | $ yarn
24 | $ yarn start
25 | ```
26 |
27 | Now point your browser to http://localhost:3000/.
28 |
29 | ## Folder Structure
30 |
31 | This example follows best practices adopted by popular React projects including
32 | Material-UI.
33 |
34 | ```
35 | /src
36 | /components
37 | /contexts
38 | /features
39 | /models
40 | /services
41 | /utils
42 | ```
43 |
44 | - `components:` This is where we keep components that are reused across
45 | application features. Components are arranged in sub-folders - one or more
46 | tightly related components per sub-folder.
47 |
48 | - `contexts:` This folder contains the React contexts we need for our app.
49 |
50 | - `features:` Contains domain-specific features of the app - one folder per
51 | feature. Feature is an abstract concept which could map to a reasonable
52 | level of granularity for your application. For example, feature could map to
53 | a page, a group of pages or a tab on your app. The feature folder can hold
54 | multiple components that are tightly related to the feature and are not
55 | reusable across features. Keep the folder structure flat as long as
56 | possible. Consider creating sub-folders when the folder reaches seven or
57 | more files.
58 |
59 | - `models`: This is where we keep the application's domain entities, value
60 | objects, interfaces and types. Please refer to
61 | [Domain-Driven Design](https://archfirst.org/domain-driven-design/) to
62 | understand the meaning of these terms.
63 |
64 | - `services:` Contains functionality to access the outside world. This could
65 | be services for accessing REST or GraphQL APIs, listeners for WebSockets,
66 | access to data stores etc. This layer is also known as the `adapter` layer
67 | in the
68 | [Hexagonal Architecture](http://alistair.cockburn.us/Hexagonal+architecture)
69 | or the
70 | [Onion Architecture](http://jeffreypalermo.com/blog/the-onion-architecture-part-1/).
71 |
72 | - `utils:` - contains general purpose utilities such as date/time utilities
73 | and number parsing & formatting utilities.
74 |
75 | When reviewing the above folder structure, note how we control the items exposed
76 | by a folder. Each folder has an `index.ts` file which exports only what is
77 | needed by external consumers - nothing more, nothing less! If everything needs
78 | to be exported, we simply use `export * from './xyz';` - instead of repeating
79 | each item individually. This makes it easier to manage the index files.
80 |
--------------------------------------------------------------------------------
/assets/screen-shot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nareshbhatia/react-force-template/dba600d6ac444ef92ae17b7df556daa0083d3839/assets/screen-shot.png
--------------------------------------------------------------------------------
/dev-machine-setup.md:
--------------------------------------------------------------------------------
1 | # Development Machine Setup
2 |
3 | The instructions below are specific to MacOS. If you are on a Windows machine,
4 | some steps will be different.
5 |
6 | ### Install Homebrew & Required Packages
7 |
8 | ```bash
9 | /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
10 | brew install git wget
11 | ```
12 |
13 | ### Install Z Shell
14 |
15 | Starting MacOS 10.15 (Catalina), the default shell is zsh. If your default shell
16 | is not zsh, I highly recommend changing it to zsh.
17 |
18 | Execute the following command in your terminal window to find out your default
19 | shell:
20 |
21 | ```bash
22 | echo $SHELL
23 | ```
24 |
25 | If you see something other than `/bin/zsh`, then zsh is not your default shell.
26 | Change it using the following command:
27 |
28 | ```bash
29 | chsh -s /bin/zsh
30 | ```
31 |
32 | Now close the terminal and reopen it. Type `echo $SHELL` to make sure that zsh
33 | is the default.
34 |
35 | ### Install Oh My Zsh
36 |
37 | Oh My Zsh is a delightful framework for managing your Zsh configuration. It
38 | comes bundled with thousands of helpful functions, helpers, plugins and themes.
39 | Enter the following command in your shell to install Oh My Zsh:
40 |
41 | ```bash
42 | sh -c "$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
43 | ```
44 |
45 | The installation script creates a backup of your .zshrc file, and then replaces
46 | it with its own version. If you had any important configuration in your original
47 | .zshrc, copy it over to your new .zshrc.
48 |
49 | ### Install Node Version Manager & Node
50 |
51 | Node Version Manager (nvm) is a bash script to manage multiple node.js versions.
52 | It is better than using the Node.js installer or Homebrew (see
53 | [this article](https://pawelgrzybek.com/install-nodejs-installer-vs-homebrew-vs-nvm/)).
54 |
55 | ```bash
56 | curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.2/install.sh | bash
57 | ```
58 |
59 | Now install the latest LTS version of Node.js
60 |
61 | ```bash
62 | nvm install 12.18.2
63 | source "$HOME/.zshrc"
64 | node -v # should print v12.18.2
65 | ```
66 |
67 | If you get the error `nvm: command not found`, then follow the instructions
68 | under
69 | [Troubleshooting on macOS](https://github.com/nvm-sh/nvm#troubleshooting-on-macos).
70 |
71 | Now install Yarn.
72 |
73 | ```bash
74 | curl -o- -L https://yarnpkg.com/install.sh | bash
75 | source "$HOME/.zshrc"
76 | yarn -v # should print a version number like v1.22.4
77 | ```
78 |
79 | Note: Do note use Homebrew to install Yarn because we did not use it to install
80 | node. See [this issue](https://github.com/yarnpkg/website/issues/913).
81 |
82 | ### Verify ~/.zshrc
83 |
84 | At this point, the end of your .zshrc file should look something like this. Make
85 | sure that each section is in the correct order.
86 |
87 | ```bash
88 | # Node Version Manager (NVM)
89 | export NVM_DIR="$HOME/.nvm"
90 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
91 | [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion
92 |
93 | # Yarn
94 | export PATH="$HOME/.yarn/bin:$HOME/.config/yarn/global/node_modules/.bin:$PATH"
95 | ```
96 |
97 | ### Try building a React app
98 |
99 | Clone the following repo wherever you keep projects and verify that you can
100 | build and run a React app. I recommend keeping all your projects under
101 | ~/projects.
102 |
103 | ```bash
104 | cd ~/projects
105 | git clone https://github.com/nareshbhatia/react-force-template.git
106 | cd react-force-template
107 | yarn
108 | yarn start
109 | ```
110 |
111 | Congratulations! You machine is now certified to build React apps!
112 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-force-template",
3 | "description": "Template intended to be a starting point for serious React applications",
4 | "version": "0.1.0",
5 | "author": "Naresh Bhatia",
6 | "license": "MIT",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/nareshbhatia/react-force-template.git"
10 | },
11 | "scripts": {
12 | "analyze": "source-map-explorer 'build/static/js/*.js'",
13 | "build": "react-scripts build",
14 | "eject": "react-scripts eject",
15 | "format": "prettier --write README.md 'src/**/{*.md,*.json,*.ts*}'",
16 | "start": "react-scripts start",
17 | "test": "react-scripts test",
18 | "test:coverage": "react-scripts test --coverage --watchAll=false"
19 | },
20 | "dependencies": {
21 | "@material-ui/core": "^4.11.3",
22 | "@material-ui/icons": "^4.11.2",
23 | "@material-ui/lab": "4.0.0-alpha.57",
24 | "@react-force/core": "^3.3.1",
25 | "@react-force/date-utils": "^3.4.0",
26 | "@react-force/formik-mui": "^3.1.0",
27 | "@react-force/http-utils": "^1.2.0",
28 | "@react-force/mock-data": "^1.1.0",
29 | "@react-force/models": "^1.2.0",
30 | "@react-force/number-utils": "^2.1.0",
31 | "@react-force/utils": "^2.3.0",
32 | "@react-force/web-utils": "^2.1.0",
33 | "axios": "^0.21.1",
34 | "classnames": "^2.2.6",
35 | "mobx": "^5.15.7",
36 | "mobx-react": "^6.3.1",
37 | "mobx-state-router": "^5.2.0",
38 | "react": "^17.0.1",
39 | "react-dom": "^17.0.1",
40 | "react-query": "^2.23.1",
41 | "react-query-devtools": "^2.6.3",
42 | "uuid": "^8.3.2",
43 | "yup": "^0.32.8"
44 | },
45 | "devDependencies": {
46 | "@testing-library/jest-dom": "^5.11.9",
47 | "@testing-library/react": "^11.2.3",
48 | "@testing-library/user-event": "^12.6.2",
49 | "@types/classnames": "^2.2.11",
50 | "@types/jest": "26.0.20",
51 | "@types/node": "14.14.22",
52 | "@types/react": "17.0.0",
53 | "@types/react-dom": "17.0.0",
54 | "@types/yup": "^0.29.11",
55 | "husky": "^4.3.8",
56 | "prettier": "^2.2.1",
57 | "pretty-quick": "^3.1.0",
58 | "react-scripts": "4.0.1",
59 | "source-map-explorer": "^2.5.2",
60 | "typescript": "4.1.3"
61 | },
62 | "eslintConfig": {
63 | "extends": "react-app"
64 | },
65 | "husky": {
66 | "hooks": {
67 | "pre-commit": "pretty-quick --staged"
68 | }
69 | },
70 | "browserslist": {
71 | "production": [
72 | ">0.2%",
73 | "not dead",
74 | "not op_mini all"
75 | ],
76 | "development": [
77 | "last 1 chrome version",
78 | "last 1 firefox version",
79 | "last 1 safari version"
80 | ]
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/public/env.js:
--------------------------------------------------------------------------------
1 | window._env_ = {
2 | API_URL: 'https://jsonplaceholder.typicode.com',
3 | };
4 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nareshbhatia/react-force-template/dba600d6ac444ef92ae17b7df556daa0083d3839/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
13 |
14 |
23 |
24 | React Force Template
25 |
26 |
29 |
30 |
31 |
34 |
38 |
42 |
43 |
44 |
45 |
46 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React Force Template",
3 | "name": "React Force Template",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense } from 'react';
2 | import CssBaseline from '@material-ui/core/CssBaseline';
3 | import { ThemeProvider } from '@material-ui/core/styles';
4 | import {
5 | ErrorBoundary,
6 | Loading,
7 | MessageProvider,
8 | MessageRenderer,
9 | } from '@react-force/core';
10 | import { observer } from 'mobx-react';
11 | import { RouterContext, RouterView } from 'mobx-state-router';
12 | // import { ReactQueryDevtools } from 'react-query-devtools';
13 | import { RootStoreContext } from './contexts';
14 | import { initApp } from './init';
15 | import { viewMap } from './viewMap';
16 |
17 | // Initialize the app
18 | const rootStore = initApp();
19 | const { prefStore, routerStore } = rootStore;
20 |
21 | // Observer theme changes
22 | export const App = observer(() => {
23 | return (
24 |
25 | }>
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | {/* */}
34 |
35 |
36 |
37 |
38 |
39 |
40 | );
41 | });
42 |
--------------------------------------------------------------------------------
/src/components/Header/BackButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import IconButton from '@material-ui/core/IconButton';
3 | import ArrowBack from '@material-ui/icons/ArrowBack';
4 | import { browserHistory } from 'mobx-state-router';
5 |
6 | export const BackButton = () => {
7 | return (
8 |
14 |
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/Header/ExamplesButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Button from '@material-ui/core/Button';
3 | import { useRouterStore } from 'mobx-state-router';
4 |
5 | export const ExamplesButton = () => {
6 | const routerStore = useRouterStore();
7 |
8 | const handleClick = () => {
9 | routerStore.goTo('examples', {
10 | params: {
11 | navItemId: 'master-detail',
12 | },
13 | });
14 | };
15 |
16 | return (
17 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/src/components/Header/Header.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Header as BaseHeader, HeaderTitle } from '@react-force/core';
3 | import { BackButton } from './BackButton';
4 | import { ExamplesButton } from './ExamplesButton';
5 | import { ToggleTheme } from './ToggleTheme';
6 | import { HomeButton } from './HomeButton';
7 |
8 | export enum NavButtonEnum {
9 | None,
10 | Home,
11 | Back,
12 | }
13 |
14 | export interface HeaderProps {
15 | navButtonEnum?: NavButtonEnum;
16 | title?: string;
17 | }
18 |
19 | export const Header = ({
20 | navButtonEnum = NavButtonEnum.Home,
21 | title = 'React Force Template',
22 | }: HeaderProps) => {
23 | return (
24 |
25 | {navButtonEnum === NavButtonEnum.Home && }
26 | {navButtonEnum === NavButtonEnum.Back && }
27 | {title}
28 |
29 |
30 |
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/src/components/Header/HomeButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import IconButton from '@material-ui/core/IconButton';
3 | import Apps from '@material-ui/icons/Apps';
4 | import { useRouterStore } from 'mobx-state-router';
5 |
6 | export const HomeButton = () => {
7 | const routerStore = useRouterStore();
8 |
9 | const handleClick = () => {
10 | routerStore.goTo('home');
11 | };
12 |
13 | return (
14 |
20 |
21 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/src/components/Header/ToggleTheme.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import IconButton from '@material-ui/core/IconButton';
3 | import DarkIcon from '@material-ui/icons/Brightness2';
4 | import LightIcon from '@material-ui/icons/Brightness2Outlined';
5 | import { observer } from 'mobx-react';
6 | import { useRootStore } from '../../contexts';
7 |
8 | // Observe paletteType changes
9 | export const ToggleTheme = observer(() => {
10 | const rootStore = useRootStore();
11 | const { prefStore } = rootStore;
12 | const { paletteType } = prefStore;
13 |
14 | const handleToggleTheme = () => {
15 | prefStore.toggleTheme();
16 | };
17 |
18 | return (
19 |
24 | {paletteType === 'light' ? : }
25 |
26 | );
27 | });
28 |
--------------------------------------------------------------------------------
/src/components/Header/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Header';
2 |
--------------------------------------------------------------------------------
/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Header';
2 |
--------------------------------------------------------------------------------
/src/contexts/RootStoreContext.ts:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 | import { RootStore } from '../stores';
3 |
4 | // ---------- RootStoreContext ----------
5 | export const RootStoreContext = React.createContext(
6 | undefined
7 | );
8 |
9 | // ---------- useRootStore ----------
10 | export function useRootStore(): RootStore {
11 | const rootStore = useContext(RootStoreContext);
12 | if (rootStore === undefined) {
13 | /* istanbul ignore next */
14 | throw new Error('useRootStore must be used within a RootStoreProvider');
15 | }
16 | return rootStore;
17 | }
18 |
--------------------------------------------------------------------------------
/src/contexts/index.ts:
--------------------------------------------------------------------------------
1 | export * from './RootStoreContext';
2 |
--------------------------------------------------------------------------------
/src/features/Examples/ExamplesPage.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode } from 'react';
2 | import MasterDetailIcon from '@material-ui/icons/List';
3 | import { observer } from 'mobx-react';
4 | import { useRouterStore } from 'mobx-state-router';
5 | import {
6 | HorizontalContainer,
7 | NavComponent,
8 | SideBar,
9 | ViewVerticalContainer,
10 | } from '@react-force/core';
11 | import { Header } from '../../components';
12 | import { ProductEditor } from './ProductEditor';
13 |
14 | const navComponents: Array = [
15 | {
16 | type: 'group',
17 | items: [
18 | {
19 | id: 'master-detail',
20 | title: 'Master-Detail',
21 | icon: ,
22 | },
23 | ],
24 | },
25 | ];
26 |
27 | const navItemMap: { [navItemId: string]: ReactNode } = {
28 | 'master-detail': ,
29 | };
30 |
31 | // Observe routerState
32 | export const ExamplesPage = observer(() => {
33 | const routerStore = useRouterStore();
34 |
35 | const { navItemId } = routerStore.routerState.params;
36 |
37 | const handleNavItemSelected = (navItemId: string) => {
38 | routerStore.goTo('examples', { params: { navItemId } });
39 | };
40 |
41 | return (
42 |
43 |
44 |
45 |
50 | {navItemMap[navItemId]}
51 |
52 |
53 | );
54 | });
55 |
--------------------------------------------------------------------------------
/src/features/Examples/ProductEditor/ProductContext.tsx:
--------------------------------------------------------------------------------
1 | import React, { ReactNode, useContext, useState } from 'react';
2 | import { Product, productData } from '@react-force/mock-data';
3 |
4 | // ---------- ProductStore ----------
5 | interface ProductStore {
6 | products: Array;
7 | }
8 |
9 | const defaultProductStore: ProductStore = {
10 | products: productData,
11 | };
12 |
13 | // ---------- ProductStoreContext ----------
14 | type ProductStoreSetter = (store: ProductStore) => void;
15 |
16 | const ProductStoreContext = React.createContext(
17 | undefined
18 | );
19 | const ProductStoreSetterContext = React.createContext<
20 | ProductStoreSetter | undefined
21 | >(undefined);
22 |
23 | // ---------- Hooks ----------
24 | function useProductStore(): ProductStore {
25 | const productStore = useContext(ProductStoreContext);
26 | if (productStore === undefined) {
27 | /* istanbul ignore next */
28 | throw new Error(
29 | 'useProductStore must be used within a ProductStoreProvider'
30 | );
31 | }
32 | return productStore;
33 | }
34 |
35 | function useProductStoreSetter(): ProductStoreSetter {
36 | const setProductStore = useContext(ProductStoreSetterContext);
37 | if (setProductStore === undefined) {
38 | /* istanbul ignore next */
39 | throw new Error(
40 | 'useProductStoreSetter must be used within a ProductStoreProvider'
41 | );
42 | }
43 | return setProductStore;
44 | }
45 |
46 | // ---------- ProductStoreProvider ----------
47 | interface ProductStoreProviderProps {
48 | children: ReactNode;
49 | value?: ProductStore;
50 | }
51 |
52 | const ProductStoreProvider = ({
53 | children,
54 | value = defaultProductStore,
55 | }: ProductStoreProviderProps) => {
56 | const [productStore, setProductStore] = useState(value);
57 |
58 | return (
59 |
60 |
61 | {children}
62 |
63 |
64 | );
65 | };
66 |
67 | export { ProductStoreProvider, useProductStore, useProductStoreSetter };
68 |
--------------------------------------------------------------------------------
/src/features/Examples/ProductEditor/ProductDetail.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Box from '@material-ui/core/Box';
3 | import Typography from '@material-ui/core/Typography';
4 | import { MasterDetailChildProps, useMessageSetter } from '@react-force/core';
5 | import {
6 | addProduct,
7 | findProduct,
8 | newProduct,
9 | Product,
10 | updateProduct,
11 | } from '@react-force/mock-data';
12 | import { MessageFactory } from '@react-force/models';
13 | import { useProductStore, useProductStoreSetter } from './ProductContext';
14 | import { ProductForm } from './ProductForm';
15 |
16 | export const ProductDetail = ({
17 | selectionContext,
18 | onEntitySelected,
19 | onEntityUpdated,
20 | }: MasterDetailChildProps) => {
21 | const { isNew, entityId } = selectionContext;
22 | const setMessage = useMessageSetter();
23 | const productStore = useProductStore();
24 | const setProductStore = useProductStoreSetter();
25 |
26 | const { products, ...rest } = productStore;
27 | const product = isNew ? newProduct() : findProduct(products, entityId);
28 | if (!product) {
29 | return null;
30 | }
31 |
32 | const handleSave = async (product: Product) => {
33 | try {
34 | if (isNew) {
35 | setProductStore({
36 | products: addProduct(products, product),
37 | ...rest,
38 | });
39 | setMessage(MessageFactory.success('Product added'));
40 | onEntitySelected(product.id);
41 | } else {
42 | setProductStore({
43 | products: updateProduct(products, product),
44 | ...rest,
45 | });
46 | setMessage(MessageFactory.success('Product saved'));
47 | onEntityUpdated();
48 | }
49 | } catch (error) {
50 | setMessage(MessageFactory.error(error.message));
51 | }
52 | };
53 |
54 | return (
55 |
56 |
57 | {isNew ? 'Add Product' : 'Edit Product'}
58 |
59 |
60 |
61 | );
62 | };
63 |
--------------------------------------------------------------------------------
/src/features/Examples/ProductEditor/ProductEditor.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { MasterDetail } from '@react-force/core';
3 | import { ProductMaster } from './ProductMaster';
4 | import { ProductDetail } from './ProductDetail';
5 | import { ProductStoreProvider } from './ProductContext';
6 |
7 | export const ProductEditor = () => {
8 | return (
9 |
10 |
15 |
16 | );
17 | };
18 |
--------------------------------------------------------------------------------
/src/features/Examples/ProductEditor/ProductForm.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { makeStyles } from '@material-ui/core/styles';
3 | import { FormActions, TextField } from '@react-force/formik-mui';
4 | import { Product } from '@react-force/mock-data';
5 | import { Form, Formik } from 'formik';
6 | import * as yup from 'yup';
7 |
8 | const useStyles = makeStyles(() => ({
9 | form: {
10 | display: 'flex',
11 | flexDirection: 'column',
12 | },
13 | }));
14 |
15 | export interface ProductFormProps {
16 | product: Product;
17 | onSave: (product: Product) => void;
18 | }
19 |
20 | export const ProductForm = ({ product, onSave }: ProductFormProps) => {
21 | const classes = useStyles();
22 |
23 | const validationSchema = yup.object().shape({
24 | name: yup.string().required(),
25 | department: yup.string().required(),
26 | manufacturer: yup.string().required(),
27 | price: yup.number().required(),
28 | });
29 |
30 | return (
31 |
32 | initialValues={product}
33 | validationSchema={validationSchema}
34 | onSubmit={(values, actions) => {
35 | onSave(values);
36 | actions.setSubmitting(false);
37 | }}
38 | >
39 | {() => (
40 |
72 | )}
73 |
74 | );
75 | };
76 |
--------------------------------------------------------------------------------
/src/features/Examples/ProductEditor/ProductMaster.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Box from '@material-ui/core/Box';
3 | import { MasterDetailChildProps, MaterialTable } from '@react-force/core';
4 | import { findProduct, Product, sortProducts } from '@react-force/mock-data';
5 | import { ColumnDef } from '@react-force/models';
6 | import { useProductStore } from './ProductContext';
7 |
8 | export const ProductMaster = ({
9 | selectionContext,
10 | onEntitySelected,
11 | }: MasterDetailChildProps) => {
12 | const productStore = useProductStore();
13 |
14 | const columnDefs: Array> = [
15 | {
16 | field: 'name',
17 | headerName: 'Product',
18 | width: 250,
19 | },
20 | {
21 | field: 'manufacturer',
22 | headerName: 'Company',
23 | },
24 | ];
25 |
26 | const handleEntityClicked = (entity: Product) => {
27 | onEntitySelected(entity.id);
28 | };
29 |
30 | // Sort a copy of products by manufacturer
31 | const products = sortProducts(productStore.products);
32 |
33 | return (
34 |
35 |
36 | entityList={products}
37 | columnDefs={columnDefs}
38 | selectedEntity={findProduct(
39 | products,
40 | selectionContext.entityId
41 | )}
42 | onEntityClicked={handleEntityClicked}
43 | />
44 |
45 | );
46 | };
47 |
--------------------------------------------------------------------------------
/src/features/Examples/ProductEditor/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ProductEditor';
2 |
--------------------------------------------------------------------------------
/src/features/Examples/index.ts:
--------------------------------------------------------------------------------
1 | export * from './ExamplesPage';
2 |
--------------------------------------------------------------------------------
/src/features/Home/HomePage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Container from '@material-ui/core/Container';
3 | import { makeStyles, Theme } from '@material-ui/core/styles';
4 | import Typography from '@material-ui/core/Typography';
5 | import { ViewVerticalContainer } from '@react-force/core';
6 | import { Header } from '../../components';
7 | import { PersonList } from './PersonList';
8 |
9 | const useStyles = makeStyles((theme: Theme) => ({
10 | container: {
11 | paddingTop: theme.spacing(4),
12 | paddingBottom: theme.spacing(4),
13 | },
14 | section: {
15 | marginTop: theme.spacing(3),
16 | },
17 | }));
18 |
19 | export const HomePage = () => {
20 | const classes = useStyles();
21 |
22 | return (
23 |
24 |
25 |
26 |
27 | Hi people
28 |
29 |
30 | Welcome to React Force
31 |
32 |
33 |
34 | This template is intended to be a starting point for serious
35 | React applications. It offers an opinionated directory
36 | structure and demonstrates best practices in layout, error
37 | handling, hooks and more. To see a quick example, disconnect
38 | from Internet and try to reload this application.
39 |
40 |
41 |
42 |
43 | Sample Data (from JSONPlaceholder)
44 |
45 |
46 |
47 |
48 |
49 | );
50 | };
51 |
--------------------------------------------------------------------------------
/src/features/Home/PersonList/PersonList.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Table from '@material-ui/core/Table';
3 | import TableBody from '@material-ui/core/TableBody';
4 | import TableCell from '@material-ui/core/TableCell';
5 | import TableHead from '@material-ui/core/TableHead';
6 | import TableRow from '@material-ui/core/TableRow';
7 | import { usePeople } from './usePeople';
8 |
9 | export const PersonList = () => {
10 | const { isLoading, isError, data: people, error } = usePeople();
11 |
12 | // Allow ErrorBoundary to handle errors
13 | if (isError) {
14 | throw error;
15 | }
16 |
17 | if (isLoading || people === undefined) {
18 | return null;
19 | }
20 |
21 | return (
22 |
23 |
24 |
25 | Name
26 | Company
27 | Email
28 | City
29 |
30 |
31 |
32 | {people.map((person) => (
33 |
34 | {person.name}
35 | {person.company}
36 | {person.email}
37 | {person.city}
38 |
39 | ))}
40 |
41 |
42 | );
43 | };
44 |
--------------------------------------------------------------------------------
/src/features/Home/PersonList/index.ts:
--------------------------------------------------------------------------------
1 | export * from './PersonList';
2 |
--------------------------------------------------------------------------------
/src/features/Home/PersonList/usePeople.ts:
--------------------------------------------------------------------------------
1 | import { useEnv } from '@react-force/core';
2 | import axios from 'axios';
3 | import { useQuery } from 'react-query';
4 | import { Person } from '../../../models';
5 | import { EnvVar } from '../../../utils';
6 |
7 | /**
8 | * Maps person received from server to domain
9 | * @param person
10 | */
11 | const mapPersonToDomain = (person: any): Person => {
12 | const { id, name, company, email, address } = person;
13 |
14 | return {
15 | id,
16 | name,
17 | company: company.name,
18 | email,
19 | city: address.city,
20 | };
21 | };
22 |
23 | /**
24 | * Fetches people from server
25 | * @param url
26 | */
27 | async function fetchPeople(url: string): Promise> {
28 | const resp = await axios.get(url);
29 | const data = resp.data;
30 |
31 | // Convert to application domain and return
32 | const people: Array = data.map((person: any) =>
33 | mapPersonToDomain(person)
34 | );
35 |
36 | // Sort by name
37 | people.sort((a, b) => {
38 | if (a.name < b.name) return -1;
39 | if (a.name > b.name) return 1;
40 | return 0;
41 | });
42 |
43 | return people;
44 | }
45 |
46 | /**
47 | * Hook to fetch people from server
48 | */
49 | export const usePeople = () => {
50 | const env = useEnv();
51 | const apiUrl = env.get(EnvVar.API_URL);
52 |
53 | return useQuery, 'people'>(
54 | 'people',
55 | async () => {
56 | return fetchPeople(`${apiUrl}/users`);
57 | },
58 | {
59 | refetchOnWindowFocus: false,
60 | staleTime: Infinity,
61 | }
62 | );
63 | };
64 |
--------------------------------------------------------------------------------
/src/features/Home/index.ts:
--------------------------------------------------------------------------------
1 | export * from './HomePage';
2 |
--------------------------------------------------------------------------------
/src/features/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Examples';
2 | export * from './Home';
3 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { EnvProvider } from '@react-force/core';
4 | import { App } from './App';
5 |
6 | ReactDOM.render(
7 |
8 |
9 |
10 |
11 | ,
12 | document.getElementById('root')
13 | );
14 |
--------------------------------------------------------------------------------
/src/init.ts:
--------------------------------------------------------------------------------
1 | import { configure } from 'mobx';
2 | import { browserHistory, HistoryAdapter } from 'mobx-state-router';
3 | import { RootStore } from './stores';
4 |
5 | function initMobX() {
6 | // Enable strict mode for MobX.
7 | // This disallows state changes outside of an action.
8 | configure({ enforceActions: 'observed' });
9 | }
10 |
11 | function initStores() {
12 | // Create the rootStore
13 | const rootStore = new RootStore();
14 | const { prefStore, routerStore } = rootStore;
15 |
16 | // Load preferences
17 | prefStore.loadFromStorage();
18 |
19 | // Observe history changes
20 | const historyAdapter = new HistoryAdapter(routerStore, browserHistory);
21 | historyAdapter.observeRouterStateChanges();
22 |
23 | return rootStore;
24 | }
25 |
26 | export function initApp() {
27 | initMobX();
28 | return initStores();
29 | }
30 |
--------------------------------------------------------------------------------
/src/models/Person.ts:
--------------------------------------------------------------------------------
1 | export interface Person {
2 | id: number;
3 | name: string;
4 | company: string;
5 | email: string;
6 | city: string;
7 | }
8 |
--------------------------------------------------------------------------------
/src/models/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Person';
2 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/stores/LsKeys.ts:
--------------------------------------------------------------------------------
1 | // Local storage keys
2 | export const LsKeys = {
3 | paletteType: 'paletteType',
4 | };
5 |
--------------------------------------------------------------------------------
/src/stores/PrefStore.ts:
--------------------------------------------------------------------------------
1 | import { createMuiTheme, PaletteType } from '@material-ui/core';
2 | import red from '@material-ui/core/colors/red';
3 |
4 | import { Storage } from '@react-force/web-utils';
5 | import { action, computed, decorate, observable } from 'mobx';
6 | import { LsKeys } from './LsKeys';
7 | import { RootStore } from './RootStore';
8 |
9 | export class PrefStore {
10 | rootStore: RootStore;
11 | paletteType: PaletteType = 'light';
12 |
13 | constructor(rootStore: RootStore) {
14 | this.rootStore = rootStore;
15 | }
16 |
17 | // ----- Load from storage -----
18 | loadFromStorage() {
19 | this.paletteType = Storage.get(LsKeys.paletteType, 'light');
20 | }
21 |
22 | // ----- Actions -----
23 | toggleTheme = () => {
24 | this.paletteType = this.paletteType === 'light' ? 'dark' : 'light';
25 | Storage.set(LsKeys.paletteType, this.paletteType);
26 | };
27 |
28 | // ----- Computed -----
29 | get theme() {
30 | const palette = {
31 | primary: {
32 | main: '#556CD6',
33 | },
34 | secondary: {
35 | main: '#FBC02D',
36 | },
37 | error: {
38 | main: red.A400,
39 | },
40 | type: this.paletteType,
41 | // Initialize background to white (default is #fafafa)
42 | // This allows pictures with white background to blend in.
43 | background: {
44 | default: this.paletteType === 'light' ? '#ffffff' : '#303030',
45 | },
46 | };
47 |
48 | return createMuiTheme({ palette });
49 | }
50 | }
51 |
52 | decorate(PrefStore, {
53 | paletteType: observable,
54 | theme: computed,
55 | loadFromStorage: action,
56 | toggleTheme: action,
57 | });
58 |
--------------------------------------------------------------------------------
/src/stores/RootStore.ts:
--------------------------------------------------------------------------------
1 | import { createRouterState, RouterStore } from 'mobx-state-router';
2 | import { PrefStore } from './PrefStore';
3 | import { routes } from './routes';
4 |
5 | const notFound = createRouterState('notFound');
6 |
7 | export class RootStore {
8 | prefStore = new PrefStore(this);
9 |
10 | // Pass rootStore as an option to RouterStore
11 | routerStore = new RouterStore(routes, notFound, {
12 | rootStore: this,
13 | });
14 | }
15 |
--------------------------------------------------------------------------------
/src/stores/index.ts:
--------------------------------------------------------------------------------
1 | export { RootStore } from './RootStore';
2 |
--------------------------------------------------------------------------------
/src/stores/routes.ts:
--------------------------------------------------------------------------------
1 | // Routes are matched from top to bottom. Make sure they are sequenced
2 | // in the order of priority. It is generally best to sort them by pattern,
3 | // prioritizing specific patterns over generic patterns (patterns with
4 | // one or more parameters). For example:
5 | // /items
6 | // /items/:id
7 | export const routes = [
8 | { name: 'home', pattern: '/' },
9 | {
10 | name: 'examples',
11 | pattern: '/examples/:navItemId',
12 | },
13 | { name: 'notFound', pattern: '/not-found' },
14 | ];
15 |
--------------------------------------------------------------------------------
/src/utils/constants.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Environment variables
3 | */
4 |
5 | export const EnvVar = {
6 | API_URL: 'API_URL',
7 | };
8 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './constants';
2 | export * from './math-utils';
3 |
--------------------------------------------------------------------------------
/src/utils/math-utils.ts:
--------------------------------------------------------------------------------
1 | export const add = (a: number, b: number) => a + b;
2 |
--------------------------------------------------------------------------------
/src/viewMap.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { NotFound } from '@react-force/core';
3 | import { ExamplesPage, HomePage } from './features';
4 |
5 | export const viewMap = {
6 | examples: ,
7 | home: ,
8 | notFound: ,
9 | };
10 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "module": "esnext",
16 | "moduleResolution": "node",
17 | "resolveJsonModule": true,
18 | "isolatedModules": true,
19 | "noEmit": true,
20 | "jsx": "react-jsx",
21 | "noFallthroughCasesInSwitch": true
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------