├── .eslintrc
├── .gitignore
├── .prettierrc
├── CODE_OF_CONDUCT.md
├── README.md
├── config-overrides.js
├── jsconfig.json
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
├── server
├── .gitignore
├── README.md
├── package-lock.json
├── package.json
└── src
│ ├── index.js
│ └── logger.js
└── src
├── assets
└── img
│ └── fun_image.png
├── index.js
├── module
├── auth-form
│ ├── api.js
│ ├── auth-form.jsx
│ ├── index.js
│ └── workflow.js
├── main
│ ├── index.js
│ └── main.jsx
└── private
│ ├── index.js
│ ├── initialize.jsx
│ ├── private.jsx
│ ├── workflow.js
│ └── workflow.test.js
├── shared
├── api.js
├── browserHistory.js
├── context-master.js
├── context-master
│ └── context-factory.js
├── mock.js
├── network.js
├── reference.js
├── store.js
├── themeSC.js
├── updaters.js
└── validator.js
└── ui
└── index.js
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "react-app"
3 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # production
7 | /build
8 |
9 | # misc
10 | .DS_Store
11 | .env.local
12 | .env.development.local
13 | .env.test.local
14 | .env.production.local
15 | npm-debug.log*
16 | yarn-debug.log*
17 | yarn-error.log*
18 |
19 | # IDE
20 | .vscode
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 100,
3 | "singleQuote": true,
4 | "trailingComma": "es5"
5 | }
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 |
11 |
12 | ### Структура кода компонента в файле
13 | Последовательно:
14 | 1. Импорты из сторонних библиотек
15 | 2. `\n` Локальные импорты по абсолютным путям
16 | 3. Локальные импорты по относительным путям путям
17 | > При этом каждая группа импортов должна соблюдать внутреннюю последовательность:
18 | > 1. Типы
19 | > 2. Константы
20 | > 3. Функции
21 | > 4. Компоненты
22 | > 5. Стили
23 | 4. Объявление типов
24 | 5. Объявление констант
25 | 6. Объявление функций
26 | 7. Объявление стилизованных компонентов (`styled-components`)
27 | > Следует объявлять от вложенных к верхнеуровневым компонентам, т.к. в последних вложенные компоненты могуть использоваться как селекторы
28 | 8. Объявление и экспорт основного (и единственного) класса компонента с приставкой `Raw`
29 | > `Raw` - сырой компонент необходим для модульного тестирования
30 |
31 | Наполнение компонента:
32 | 1. Объявление типов `static propTypes = {}`
33 | 2. Объявление свойств `state = {}`
34 | 3. Объявление методов `getState() { return this.state }`
35 | 4. Объявление методов-обработчиков `handleClick = event => {}`
36 | 5. `render() {}`
37 | 9. Объявление и экспорт основного класса компонента без приставки `Raw`, с присвоением `Raw` класса, при необходимости, обернутого в HOC.
38 | > `export const App = connect(mstp)(AppRaw)`
39 |
40 | При этом файл не должен быть более **200** строк. Когда количество кода в файле переваливает за 100 - 150 строк - стоит задуматься о разбиение его на модули.
41 |
45 |
46 | ## IDE
47 |
48 | ### Снипеты
49 |
50 | #### Создание компонента
51 |
52 | ##### vscode
53 |
54 | ```json
55 | {
56 | "Create React Component": {
57 | "prefix": "crc",
58 | "body": [
59 | "import * as React from 'react';",
60 | "import styled from 'styled-components';",
61 | "",
62 | "const Container = styled.div`",
63 | "\tdisplay: flex;",
64 | "`;",
65 | "",
66 | "export class ${1:App}Raw extends React.Component {",
67 | "\trender() {",
68 | "\t\tconst { children } = this.props;",
69 | "",
70 | "\t\treturn {children};",
71 | "\t}",
72 | "}",
73 | "",
74 | "export const ${1:App} = ${1:App}Raw;",
75 | "",
76 | ]
77 | }
78 | }
79 | ```
80 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Это новая ветка `v1`, в ней все по другому, но я еще не успел подготовить описание
2 | # В ветке `master` есть полноценный проверенный шаблон с кучей плюшек
3 |
4 | # TODO:
5 |
6 | * [ ] componentDidCatch
7 | * [ ] exports (no default)
8 | * [ ] types
9 | * [ ] SSR
10 | * [ ] Код шаблона для ui-kit
11 | * [ ] примеры со своими компонентами
12 | * [ ] документация с примерами модификации других ui-китов
13 | * [ ] расширяемая тема
14 | * [ ] селекторы для темы
15 | * [ ] color palette
16 | * [ ] storybook
17 | * [x] структура компонента
18 | * [ ] структура проекта
19 |
20 | ### Структура кода компонента
21 |
22 | https://github.com/artalar/blog/blob/master/src/pages/react-component-structure.md
23 |
24 | ___
25 |
26 | ### неструктурированное
27 |
28 | > в `shared` не должно быть объединяющего `index.js` что бы исключить циклические импорты
29 |
30 | > внутри `shared` импорты должны быть абсолютными, внутри модулей нет.
31 |
--------------------------------------------------------------------------------
/config-overrides.js:
--------------------------------------------------------------------------------
1 | const { injectBabelPlugin } = require('react-app-rewired');
2 |
3 | module.exports = (config, env) => {
4 | const path = require('path');
5 |
6 | /* FIXME: not worked
7 | if (env === 'development') {
8 | const BitBarWebpackProgressPlugin = require('bitbar-webpack-progress-plugin');
9 | config.plugins.push(new BitBarWebpackProgressPlugin());
10 | } */
11 |
12 | config = injectBabelPlugin(['babel-plugin-styled-components', { displayName: true }], config);
13 |
14 | config.resolve.modules = [
15 | ...(config.resolve.modules || []),
16 | path.resolve(__dirname, 'src'),
17 | ];
18 |
19 | return config;
20 | };
21 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "experimentalDecorators": true,
4 | "baseUrl": "./src",
5 | "jsx": "react",
6 | "paths": {
7 | "assets": ["./assets"],
8 | "components": ["./components"],
9 | "reference": ["./reference"],
10 | "service": ["./service"],
11 | "ui": ["./ui"],
12 | "workflow": ["./workflow"]
13 | }
14 | },
15 | "exclude": ["node_modules", "build"]
16 | }
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cra-boilerplate",
3 | "version": "1.0.0",
4 | "private": false,
5 | "dependencies": {
6 | "axios": "^0.18.0",
7 | "coach-stm": "^2.0.0-beta8",
8 | "create-subscription": "^16.3.1",
9 | "normalize.css": "^8.0.0",
10 | "prop-types": "^15.6.1",
11 | "react": "^16.3.1",
12 | "react-dom": "^16.3.1",
13 | "react-router-dom": "^4.2.2",
14 | "react-scripts": "^1.1.4",
15 | "styled-components": "^3.2.5"
16 | },
17 | "scripts": {
18 | "start": "react-app-rewired start",
19 | "start_server": "node server/src/index.js",
20 | "build": "react-app-rewired build",
21 | "test": "react-app-rewired test --env=jsdom"
22 | },
23 | "devDependencies": {
24 | "babel-eslint": "^8.2.2",
25 | "babel-plugin-styled-components": "^1.5.1",
26 | "bitbar-webpack-progress-plugin": "^1.0.0",
27 | "eslint": "^4.19.0",
28 | "eslint-config-react-app": "^2.1.0",
29 | "eslint-plugin-flowtype": "^2.46.1",
30 | "eslint-plugin-import": "^2.9.0",
31 | "eslint-plugin-jsx-a11y": "^6.0.3",
32 | "eslint-plugin-react": "^7.7.0",
33 | "prettier": "^1.11.1",
34 | "react-app-rewired": "^1.5.0"
35 | },
36 | "jest": {
37 | "moduleDirectories": [
38 | "node_modules",
39 | "src"
40 | ]
41 | },
42 | "proxy": {
43 | "/api": {
44 | "target": "http://localhost:4000",
45 | "changeOrigin": true
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artalar/react-template/01daae40379059089da50f27243d03d82c3f72c8/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | React App
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # production
7 | /build
8 |
9 | # misc
10 | .DS_Store
11 | .env.local
12 | .env.development.local
13 | .env.test.local
14 | .env.production.local
15 | npm-debug.log*
16 | yarn-debug.log*
17 | yarn-error.log*
18 |
19 | # IDE
20 | .vscode
--------------------------------------------------------------------------------
/server/README.md:
--------------------------------------------------------------------------------
1 | # Сервер для заглушек данных
--------------------------------------------------------------------------------
/server/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-template-server",
3 | "version": "1.0.0",
4 | "lockfileVersion": 1,
5 | "requires": true,
6 | "dependencies": {
7 | "accepts": {
8 | "version": "1.3.5",
9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
10 | "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=",
11 | "requires": {
12 | "mime-types": "2.1.18",
13 | "negotiator": "0.6.1"
14 | }
15 | },
16 | "array-flatten": {
17 | "version": "1.1.1",
18 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
19 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
20 | },
21 | "body-parser": {
22 | "version": "1.18.2",
23 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz",
24 | "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=",
25 | "requires": {
26 | "bytes": "3.0.0",
27 | "content-type": "1.0.4",
28 | "debug": "2.6.9",
29 | "depd": "1.1.2",
30 | "http-errors": "1.6.3",
31 | "iconv-lite": "0.4.19",
32 | "on-finished": "2.3.0",
33 | "qs": "6.5.1",
34 | "raw-body": "2.3.2",
35 | "type-is": "1.6.16"
36 | }
37 | },
38 | "bytes": {
39 | "version": "3.0.0",
40 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
41 | "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
42 | },
43 | "content-disposition": {
44 | "version": "0.5.2",
45 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
46 | "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ="
47 | },
48 | "content-type": {
49 | "version": "1.0.4",
50 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
51 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
52 | },
53 | "cookie": {
54 | "version": "0.3.1",
55 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
56 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
57 | },
58 | "cookie-signature": {
59 | "version": "1.0.6",
60 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
61 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
62 | },
63 | "debug": {
64 | "version": "2.6.9",
65 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
66 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
67 | "requires": {
68 | "ms": "2.0.0"
69 | }
70 | },
71 | "depd": {
72 | "version": "1.1.2",
73 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
74 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
75 | },
76 | "destroy": {
77 | "version": "1.0.4",
78 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
79 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
80 | },
81 | "ee-first": {
82 | "version": "1.1.1",
83 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
84 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
85 | },
86 | "encodeurl": {
87 | "version": "1.0.2",
88 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
89 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
90 | },
91 | "escape-html": {
92 | "version": "1.0.3",
93 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
94 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
95 | },
96 | "etag": {
97 | "version": "1.8.1",
98 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
99 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
100 | },
101 | "express": {
102 | "version": "4.16.3",
103 | "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz",
104 | "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=",
105 | "requires": {
106 | "accepts": "1.3.5",
107 | "array-flatten": "1.1.1",
108 | "body-parser": "1.18.2",
109 | "content-disposition": "0.5.2",
110 | "content-type": "1.0.4",
111 | "cookie": "0.3.1",
112 | "cookie-signature": "1.0.6",
113 | "debug": "2.6.9",
114 | "depd": "1.1.2",
115 | "encodeurl": "1.0.2",
116 | "escape-html": "1.0.3",
117 | "etag": "1.8.1",
118 | "finalhandler": "1.1.1",
119 | "fresh": "0.5.2",
120 | "merge-descriptors": "1.0.1",
121 | "methods": "1.1.2",
122 | "on-finished": "2.3.0",
123 | "parseurl": "1.3.2",
124 | "path-to-regexp": "0.1.7",
125 | "proxy-addr": "2.0.3",
126 | "qs": "6.5.1",
127 | "range-parser": "1.2.0",
128 | "safe-buffer": "5.1.1",
129 | "send": "0.16.2",
130 | "serve-static": "1.13.2",
131 | "setprototypeof": "1.1.0",
132 | "statuses": "1.4.0",
133 | "type-is": "1.6.16",
134 | "utils-merge": "1.0.1",
135 | "vary": "1.1.2"
136 | }
137 | },
138 | "finalhandler": {
139 | "version": "1.1.1",
140 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
141 | "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
142 | "requires": {
143 | "debug": "2.6.9",
144 | "encodeurl": "1.0.2",
145 | "escape-html": "1.0.3",
146 | "on-finished": "2.3.0",
147 | "parseurl": "1.3.2",
148 | "statuses": "1.4.0",
149 | "unpipe": "1.0.0"
150 | }
151 | },
152 | "forwarded": {
153 | "version": "0.1.2",
154 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
155 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
156 | },
157 | "fresh": {
158 | "version": "0.5.2",
159 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
160 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
161 | },
162 | "http-errors": {
163 | "version": "1.6.3",
164 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
165 | "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
166 | "requires": {
167 | "depd": "1.1.2",
168 | "inherits": "2.0.3",
169 | "setprototypeof": "1.1.0",
170 | "statuses": "1.4.0"
171 | }
172 | },
173 | "iconv-lite": {
174 | "version": "0.4.19",
175 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz",
176 | "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ=="
177 | },
178 | "inherits": {
179 | "version": "2.0.3",
180 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
181 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
182 | },
183 | "ipaddr.js": {
184 | "version": "1.6.0",
185 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz",
186 | "integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs="
187 | },
188 | "media-typer": {
189 | "version": "0.3.0",
190 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
191 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
192 | },
193 | "merge-descriptors": {
194 | "version": "1.0.1",
195 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
196 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
197 | },
198 | "methods": {
199 | "version": "1.1.2",
200 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
201 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
202 | },
203 | "mime": {
204 | "version": "1.4.1",
205 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
206 | "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ=="
207 | },
208 | "mime-db": {
209 | "version": "1.33.0",
210 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
211 | "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ=="
212 | },
213 | "mime-types": {
214 | "version": "2.1.18",
215 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz",
216 | "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==",
217 | "requires": {
218 | "mime-db": "1.33.0"
219 | }
220 | },
221 | "ms": {
222 | "version": "2.0.0",
223 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
224 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
225 | },
226 | "negotiator": {
227 | "version": "0.6.1",
228 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
229 | "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
230 | },
231 | "on-finished": {
232 | "version": "2.3.0",
233 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
234 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
235 | "requires": {
236 | "ee-first": "1.1.1"
237 | }
238 | },
239 | "parseurl": {
240 | "version": "1.3.2",
241 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
242 | "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M="
243 | },
244 | "path-to-regexp": {
245 | "version": "0.1.7",
246 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
247 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
248 | },
249 | "proxy-addr": {
250 | "version": "2.0.3",
251 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz",
252 | "integrity": "sha512-jQTChiCJteusULxjBp8+jftSQE5Obdl3k4cnmLA6WXtK6XFuWRnvVL7aCiBqaLPM8c4ph0S4tKna8XvmIwEnXQ==",
253 | "requires": {
254 | "forwarded": "0.1.2",
255 | "ipaddr.js": "1.6.0"
256 | }
257 | },
258 | "qs": {
259 | "version": "6.5.1",
260 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz",
261 | "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A=="
262 | },
263 | "range-parser": {
264 | "version": "1.2.0",
265 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
266 | "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4="
267 | },
268 | "raw-body": {
269 | "version": "2.3.2",
270 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz",
271 | "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=",
272 | "requires": {
273 | "bytes": "3.0.0",
274 | "http-errors": "1.6.2",
275 | "iconv-lite": "0.4.19",
276 | "unpipe": "1.0.0"
277 | },
278 | "dependencies": {
279 | "depd": {
280 | "version": "1.1.1",
281 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz",
282 | "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k="
283 | },
284 | "http-errors": {
285 | "version": "1.6.2",
286 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz",
287 | "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=",
288 | "requires": {
289 | "depd": "1.1.1",
290 | "inherits": "2.0.3",
291 | "setprototypeof": "1.0.3",
292 | "statuses": "1.4.0"
293 | }
294 | },
295 | "setprototypeof": {
296 | "version": "1.0.3",
297 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz",
298 | "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ="
299 | }
300 | }
301 | },
302 | "safe-buffer": {
303 | "version": "5.1.1",
304 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
305 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
306 | },
307 | "send": {
308 | "version": "0.16.2",
309 | "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
310 | "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==",
311 | "requires": {
312 | "debug": "2.6.9",
313 | "depd": "1.1.2",
314 | "destroy": "1.0.4",
315 | "encodeurl": "1.0.2",
316 | "escape-html": "1.0.3",
317 | "etag": "1.8.1",
318 | "fresh": "0.5.2",
319 | "http-errors": "1.6.3",
320 | "mime": "1.4.1",
321 | "ms": "2.0.0",
322 | "on-finished": "2.3.0",
323 | "range-parser": "1.2.0",
324 | "statuses": "1.4.0"
325 | }
326 | },
327 | "serve-static": {
328 | "version": "1.13.2",
329 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
330 | "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==",
331 | "requires": {
332 | "encodeurl": "1.0.2",
333 | "escape-html": "1.0.3",
334 | "parseurl": "1.3.2",
335 | "send": "0.16.2"
336 | }
337 | },
338 | "setprototypeof": {
339 | "version": "1.1.0",
340 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
341 | "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
342 | },
343 | "statuses": {
344 | "version": "1.4.0",
345 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
346 | "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
347 | },
348 | "type-is": {
349 | "version": "1.6.16",
350 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
351 | "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==",
352 | "requires": {
353 | "media-typer": "0.3.0",
354 | "mime-types": "2.1.18"
355 | }
356 | },
357 | "unpipe": {
358 | "version": "1.0.0",
359 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
360 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
361 | },
362 | "utils-merge": {
363 | "version": "1.0.1",
364 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
365 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
366 | },
367 | "vary": {
368 | "version": "1.1.2",
369 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
370 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
371 | }
372 | }
373 | }
374 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-template-server",
3 | "version": "1.0.0",
4 | "description": "server for mocks",
5 | "main": "src/index.js",
6 | "scripts": {
7 | "start": "node src/index.js",
8 | "test": "echo \"Error: no test specified\" && exit 1"
9 | },
10 | "author": "artalar",
11 | "license": "GPL-3.0",
12 | "dependencies": {
13 | "express": "^4.16.3"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/server/src/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const bodyParser = require('body-parser');
3 |
4 | const logger = require('./logger');
5 |
6 | const app = express();
7 | const port = process.env.PORT || 4000;
8 |
9 | const delay = (ms = 5) => new Promise(r => setTimeout(r, ms));
10 |
11 | app.use(bodyParser.json());
12 |
13 | app.get('/api/me', async (req, res) => {
14 | logger('/api/me', '\n', req.body);
15 | await delay(500);
16 | res.send({ permissions: ['admin'] });
17 | });
18 |
19 | app.listen(port, () => logger(`Start at ${port} port`));
20 |
--------------------------------------------------------------------------------
/server/src/logger.js:
--------------------------------------------------------------------------------
1 | module.exports = (...args) => console.log(...args);
2 |
--------------------------------------------------------------------------------
/src/assets/img/fun_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artalar/react-template/01daae40379059089da50f27243d03d82c3f72c8/src/assets/img/fun_image.png
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { ThemeProvider } from 'styled-components';
4 | import { Router } from 'react-router-dom';
5 | import { Switch, Route, Redirect } from 'react-router';
6 | import 'normalize.css';
7 |
8 | import { PATH, PERMISSIONS } from 'shared/reference';
9 | import { history } from 'shared/browserHistory';
10 | import { themeSC } from 'shared/themeSC';
11 | import { ContextMaster } from 'shared/context-master';
12 | import { Initialize, Private } from 'module/private';
13 | import { AuthForm } from 'module/auth-form';
14 | import { Main } from 'module/main';
15 |
16 | const Providers = ({ children }) => (
17 |
18 |
19 | {children}
20 |
21 |
22 | );
23 |
24 | const Root = () => (
25 |
26 |
27 |
28 | {/* FIXME: Switch not update context without it =\ */}
29 |
30 |
31 |
32 | }>
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | );
41 |
42 | ReactDOM.render(, document.getElementById('root'));
43 |
--------------------------------------------------------------------------------
/src/module/auth-form/api.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | export const authUser = ({ email, password }) => axios.post('/api/auth', { email, password });
4 |
--------------------------------------------------------------------------------
/src/module/auth-form/auth-form.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import styled from 'styled-components';
3 |
4 | import { CONTEXT } from 'shared/reference';
5 | import { contextConnectors } from 'shared/context-master';
6 |
7 | const Header = styled.header`
8 | display: flex;
9 | `;
10 |
11 | const ErrorMessage = styled.span`
12 | color: red;
13 | `;
14 |
15 | const Form = styled.form`
16 | display: flex;
17 | flex-direction: column;
18 | width: 20rem;
19 | `;
20 |
21 | const Container = styled.main`
22 | display: flex;
23 | flex-direction: column;
24 | justify-content: center;
25 | align-items: center;
26 | width: 100%;
27 | height: 100%;
28 | `;
29 |
30 | export class AuthFormRaw extends React.Component {
31 | values = {};
32 |
33 | handleChange = e => {
34 | this.values[e.target.name] = e.target.value;
35 | this.props.clearError();
36 | };
37 |
38 | handleSubmit = e => {
39 | e.preventDefault();
40 | try {
41 | this.props.authUser(this.values);
42 | } catch (e) {}
43 | };
44 |
45 | render() {
46 | const { error } = this.props;
47 | return [
48 | ,
49 |
50 |
55 | {error !== null && {error}}
56 | ,
57 | ];
58 | }
59 | }
60 |
61 | export const AuthForm = contextConnectors[CONTEXT.AUTH](
62 | ({ workflow: { authUser, clearError }, state: { error } }) => ({
63 | authUser,
64 | clearError,
65 | error,
66 | })
67 | )(AuthFormRaw);
68 |
--------------------------------------------------------------------------------
/src/module/auth-form/index.js:
--------------------------------------------------------------------------------
1 | // trigger adding provider
2 | import './workflow';
3 |
4 | export { AuthForm } from './auth-form';
5 |
--------------------------------------------------------------------------------
/src/module/auth-form/workflow.js:
--------------------------------------------------------------------------------
1 | import { Coach } from 'coach-stm';
2 | import middleware, { withMeta } from 'coach-stm/es/middleware';
3 |
4 | import { STATUS, CONTEXT, PATH } from 'shared/reference';
5 | import { Store } from 'shared/store';
6 | import { setStatusLoading, setStatusLoaded, onError } from 'shared/updaters';
7 | import { history } from 'shared/browserHistory';
8 | import { addContext } from 'shared/context-master';
9 | import { isEmail, isPassword } from 'shared/validator';
10 | import * as api from './api';
11 | import { updatePermissions } from 'module/private/workflow';
12 |
13 | const initialState = {
14 | status: STATUS.INITIAL,
15 | error: null,
16 | };
17 |
18 | const store = new Store(initialState);
19 |
20 | const coach = new Coach({
21 | middleware: {
22 | store: withMeta({ store }),
23 | api: withMeta({ api }),
24 | ...middleware,
25 | },
26 | });
27 |
28 | const successRedirect = () => history.push(PATH.HOME);
29 |
30 | const fetchAuthUser = async (p, { api }) => await api.authUser(p);
31 |
32 | const setClearError = (p, { store }) => void store.merge({ error: null });
33 |
34 | // Goals
35 |
36 | export const clearError = coach.goal({ setClearError });
37 |
38 | export const formValid = coach.goal({
39 | isEmail: ({ email }) => void isEmail(email),
40 | isPassword: ({ password }) => void isPassword(password),
41 | });
42 |
43 | export const authUser = coach.goal(
44 | 'authenticate user',
45 | {
46 | formValid,
47 | setStatusLoading,
48 | fetchAuthUser,
49 | updatePermissions,
50 | setStatusLoaded,
51 | successRedirect,
52 | },
53 | onError
54 | );
55 |
56 | addContext({ name: CONTEXT.AUTH, store, workflow: { authUser, clearError } });
57 |
--------------------------------------------------------------------------------
/src/module/main/index.js:
--------------------------------------------------------------------------------
1 | export { Main } from './main.jsx';
2 |
--------------------------------------------------------------------------------
/src/module/main/main.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import styled from 'styled-components';
3 |
4 | const Header = styled.header`
5 | display: flex;
6 | `;
7 |
8 | const Container = styled.main`
9 | display: flex;
10 | `;
11 |
12 | export class MainRaw extends React.Component {
13 | render() {
14 | return [, Main];
15 | }
16 | }
17 |
18 | export const Main = MainRaw;
19 |
--------------------------------------------------------------------------------
/src/module/private/index.js:
--------------------------------------------------------------------------------
1 | // trigger adding provider
2 | import './workflow';
3 |
4 | export { Initialize } from './initialize';
5 | export { Private } from './private';
6 |
--------------------------------------------------------------------------------
/src/module/private/initialize.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { STATUS, CONTEXT } from 'shared/reference';
5 | import { contextConnectors } from 'shared/context-master';
6 |
7 | export class InitializeRaw extends React.PureComponent {
8 | static = {
9 | getMe: PropTypes.func.isRequired,
10 | status: PropTypes.string.isRequired,
11 | };
12 |
13 | componentDidMount() {
14 | this.props.getMe();
15 | }
16 | render() {
17 | const { status } = this.props;
18 | // TODO: add loading animation
19 | if (status === STATUS.INITIAL || status === STATUS.LOADING) return Loading...;
20 | return this.props.children;
21 | }
22 | }
23 | export const Initialize = contextConnectors[CONTEXT.PRIVATE](
24 | ({ workflow: { getMe }, state: { status } }) => ({ getMe, status })
25 | )(InitializeRaw);
26 |
--------------------------------------------------------------------------------
/src/module/private/private.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | import { CONTEXT } from 'shared/reference';
5 | import { contextConnectors } from 'shared/context-master';
6 |
7 | export class PrivateRaw extends React.Component {
8 | static propTypes = {
9 | to: PropTypes.arrayOf(PropTypes.string).isRequired,
10 | any: PropTypes.bool,
11 | permissions: PropTypes.arrayOf(PropTypes.string).isRequired,
12 | children: PropTypes.node.isRequired,
13 | fallback: PropTypes.func,
14 | };
15 |
16 | static defaultProps = {
17 | any: true,
18 | };
19 |
20 | render() {
21 | const { to, any, permissions, children, fallback } = this.props;
22 |
23 | if (to[any ? 'some' : 'every'](permission => permissions.includes(permission))) {
24 | return children;
25 | } else if (fallback) {
26 | return React.createElement(fallback);
27 | } else {
28 | return null;
29 | }
30 | }
31 | }
32 |
33 | export const Private = contextConnectors[CONTEXT.PRIVATE](
34 | ({ state: { permissions = [] } = {} }) => ({ permissions })
35 | )(PrivateRaw);
36 |
--------------------------------------------------------------------------------
/src/module/private/workflow.js:
--------------------------------------------------------------------------------
1 | import { Coach } from 'coach-stm';
2 | import middleware, { withMeta } from 'coach-stm/es/middleware';
3 |
4 | import { STATUS, CONTEXT } from 'shared/reference';
5 | import { Store } from 'shared/store';
6 | import { setStatusLoading, setStatusLoaded, onError } from 'shared/updaters';
7 | import { addContext } from 'shared/context-master';
8 | import * as api from 'shared/api';
9 |
10 | const initialState = {
11 | permissions: [],
12 | status: STATUS.INITIAL,
13 | error: null,
14 | };
15 |
16 | const store = new Store(initialState);
17 |
18 | const coach = new Coach({
19 | middleware: {
20 | store: withMeta({ store }),
21 | api: withMeta({ api }),
22 | ...middleware,
23 | },
24 | });
25 |
26 | const setPermissions = (p, { store }) => void store.merge(p);
27 |
28 | const setLogOut = (p, { store }) => void store.merge({ permissions: [] }); // FIXME:
29 |
30 | const selectPermissions = ({ data: { permissions } }) => ({ permissions });
31 |
32 | const fetchGetMe = async (p, { api }) => await api.getMe(p);
33 |
34 | // Goals
35 |
36 | export const updatePermissions = coach.goal({ selectPermissions, setPermissions });
37 |
38 | export const getMe = coach.goal(
39 | 'fetch user info',
40 | {
41 | setStatusLoading,
42 | fetchGetMe,
43 | updatePermissions,
44 | setStatusLoaded,
45 | },
46 | onError
47 | );
48 |
49 | export const logOut = coach.goal('log out', {
50 | setLogOut,
51 | });
52 |
53 | addContext({ name: CONTEXT.PRIVATE, store, workflow: { getMe } });
54 |
--------------------------------------------------------------------------------
/src/module/private/workflow.test.js:
--------------------------------------------------------------------------------
1 | import { withMeta } from 'coach-stm/es/middleware';
2 | import { getMe } from './workflow';
3 |
4 | describe('тестирование авторизации', () => {
5 | const testData = { data: { permissions: ['test'] } };
6 |
7 | const fetchGetMeMocked = () => new Promise(r => setTimeout(r, 5, testData));
8 |
9 | const getMeMocked = getMe.replaceMiddleware({
10 | ...getMe.middleware,
11 | api: withMeta({ api: { getMe: fetchGetMeMocked } }),
12 | });
13 |
14 | it('получение данных текущего пользователя', async () =>
15 | expect(await getMeMocked()).toEqual(testData.data));
16 | });
17 |
--------------------------------------------------------------------------------
/src/shared/api.js:
--------------------------------------------------------------------------------
1 | import { network } from 'shared/network';
2 |
3 | export const getMe = () => network('/admin/me');
4 |
--------------------------------------------------------------------------------
/src/shared/browserHistory.js:
--------------------------------------------------------------------------------
1 | import createBrowserHistory from 'history/createBrowserHistory';
2 |
3 | export const history = createBrowserHistory();
4 |
--------------------------------------------------------------------------------
/src/shared/context-master.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Coach } from 'coach-stm/lib';
3 | import middleware, { withMeta } from 'coach-stm/es/middleware';
4 |
5 | import { CONTEXT } from 'shared/reference';
6 | import { Store } from 'shared/store';
7 | import { contextFactory } from './context-master/context-factory';
8 |
9 | const initialState = {
10 | providers: {},
11 | };
12 |
13 | const allContextStore = new Store(initialState);
14 |
15 | const coach = new Coach({
16 | middleware: {
17 | store: withMeta({ store: allContextStore }),
18 | ...middleware,
19 | },
20 | });
21 |
22 | const contextConnectors = {};
23 | contextConnectors.prototype = Object.keys(CONTEXT).reduce((acc, contextName) => {
24 | Object.defineProperty(acc, contextName, {
25 | get() {
26 | console.error(new Error(`Context "${contextName}" is not set yet`));
27 | return (/* selector */) => (/* Component */) => null;
28 | },
29 | });
30 | return acc;
31 | }, {});
32 |
33 | const createContext = ({ name, store, workflow }) => ({ name, ...contextFactory(store, workflow) });
34 |
35 | const setNewContextProvider = ({ name, connect, Provider }, { store }) =>
36 | void store.mergeForce({ providers: { ...store.state.providers, [name]: { connect, Provider } } });
37 |
38 | const setNewContextConnect = ({ name }) =>
39 | Object.assign(contextConnectors, {
40 | get [name]() {
41 | return allContextStore.state.providers[name].connect;
42 | },
43 | });
44 |
45 | const addContext = coach.goal('add provider', {
46 | createContext,
47 | setNewContextProvider,
48 | setNewContextConnect,
49 | });
50 |
51 | const ContextMaster = ({ children }) => {
52 | const { providers } = allContextStore.state;
53 | return Object.keys(providers).reduce(
54 | (master, providerName) =>
55 | React.createElement(providers[providerName].Provider, { name: providerName }, master),
56 | children
57 | );
58 | };
59 |
60 | export { ContextMaster, addContext, contextConnectors };
--------------------------------------------------------------------------------
/src/shared/context-master/context-factory.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { createSubscription } from 'create-subscription';
3 |
4 | const shallowCompare = (newObj, oldObj) => {
5 | const newObjKeys = Object.keys(newObj);
6 | const oldObjKeys = Object.keys(oldObj);
7 | return (
8 | newObjKeys.length === oldObjKeys.length && newObjKeys.every(key => newObj[key] === oldObj[key])
9 | );
10 | };
11 |
12 | const contextFactory = (store, workflow) => {
13 | const { Provider: ProviderBase, Consumer } = React.createContext(store.state);
14 |
15 | let cachedProviderValue = { workflow, state: store.state };
16 |
17 | const Subscription = createSubscription({
18 | getCurrentValue: ({ state }) =>
19 | cachedProviderValue.state === state ? cachedProviderValue : { workflow, state },
20 | subscribe: (store, callback) => {
21 | store.subscribe(state =>
22 | callback(cachedProviderValue.state === state ? cachedProviderValue : { workflow, state })
23 | );
24 | return () => store.unsubscribe(callback);
25 | },
26 | });
27 |
28 | const Provider = ({ children }) => (
29 |
30 | {value => {children}}
31 |
32 | );
33 |
34 | const connect = selector => target => ({ children, ...props }) => {
35 | let updateFromParent = true;
36 | let cachedState = null;
37 | let cacheComponent = null;
38 | return (
39 |
40 | {value => {
41 | const state = selector(value, props);
42 | if (!updateFromParent && (state === cachedState || shallowCompare(state, cachedState))) {
43 | updateFromParent = false;
44 | return cacheComponent;
45 | } else {
46 | updateFromParent = false;
47 | cachedState = state;
48 | return (cacheComponent = React.createElement(target, { ...props, ...state }, children));
49 | }
50 | }}
51 |
52 | );
53 | };
54 |
55 | return {
56 | connect,
57 | Provider,
58 | };
59 | };
60 |
61 | export { contextFactory };
62 |
--------------------------------------------------------------------------------
/src/shared/mock.js:
--------------------------------------------------------------------------------
1 | export const getMe = () =>
2 | new Promise(r => setTimeout(r, 500, { data: { permissions: ['admin'] } }));
3 |
--------------------------------------------------------------------------------
/src/shared/network.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | import { PATH } from 'shared/reference';
4 | import { logOut } from 'module/private/workflow';
5 |
6 | const network = axios.create({
7 | baseURL: PATH.API,
8 | });
9 |
10 | network.interceptors.request.use(config => {
11 | const auth = sessionStorage.getItem('auth');
12 | config.headers.Authorization = `Token ${auth}`;
13 | return config;
14 | });
15 |
16 | network.interceptors.response.use(undefined, error => {
17 | const { response: { status } = {} } = error || {};
18 | if (status === 401) setTimeout(logOut);
19 | return Promise.reject(error);
20 | });
21 |
22 | export { network };
23 |
--------------------------------------------------------------------------------
/src/shared/reference.js:
--------------------------------------------------------------------------------
1 | // for hosting at the path
2 | // like: `domain.com/some_path`
3 | const URL_PREFIX = process.env.URL_PREFIX || '';
4 |
5 | export const PATH = {
6 | API: `${URL_PREFIX}/api`,
7 | AUTH: `${URL_PREFIX}/auth`,
8 | HOME: `${URL_PREFIX}/`,
9 | };
10 |
11 | export const STATUS = {
12 | INITIAL: 'initial',
13 | LOADING: 'loading',
14 | LOADED: 'loaded',
15 | ERROR: 'error',
16 | };
17 |
18 | let ALL_PERMISSION;
19 | export const PERMISSIONS = {
20 | ADMIN: 'admin',
21 | USER: 'user',
22 | getAll() {
23 | // exclude method
24 | return ALL_PERMISSION || (ALL_PERMISSION = Object.values(this).filter(value => typeof value === 'string'));
25 | },
26 | };
27 |
28 | export const CONTEXT = {
29 | PRIVATE: 'private',
30 | AUTH: 'auth',
31 | };
32 |
--------------------------------------------------------------------------------
/src/shared/store.js:
--------------------------------------------------------------------------------
1 | export class Store {
2 | constructor(initialState = {}) {
3 | this.state = initialState;
4 | let subscriptions = [];
5 | let updatesQueue = [];
6 | let lastUpdate;
7 |
8 | const updateState = update => {
9 | if (update !== undefined) updatesQueue.push(update);
10 | this.state = updatesQueue.reduce((acc, update) => ({ ...acc, ...update }), this.state);
11 | updatesQueue = [];
12 | lastUpdate = Math.random();
13 | subscriptions.forEach(callback => callback(this.state));
14 | };
15 |
16 | this.merge = update => {
17 | updatesQueue.push(update);
18 | const updateStamp = (lastUpdate = Math.random());
19 | Promise.resolve().then(() => {
20 | if (updateStamp === lastUpdate) updateState();
21 | });
22 | };
23 |
24 | this.mergeForce = update => {
25 | updateState({ ...this.state, ...update });
26 | return this.state;
27 | };
28 |
29 | this.subscribe = callback => {
30 | subscriptions.push(callback);
31 | return () => this.unsubscribe(callback);
32 | };
33 |
34 | this.unsubscribe = deleteCallback => {
35 | const oldLength = subscriptions.length;
36 | subscriptions = subscriptions.filter(callback => callback !== deleteCallback);
37 | return oldLength !== subscriptions.length;
38 | };
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/shared/themeSC.js:
--------------------------------------------------------------------------------
1 | export const themeSC = { // FIXME: rewrite me
2 | colors: {
3 | primary: 'rgba(33, 150, 243, 1)',
4 | secondary: 'rgba(0, 188, 212, 1)'
5 | },
6 | transitionFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
7 | transition: 'transition all 300ms cubic-bezier(0.4, 0, 0.2, 1)',
8 | getTransition: (target = 'all', duration = '.3s') =>
9 | `transition ${target} ${duration} cubic-bezier(0.4, 0, 0.2, 1)`,
10 | };
11 |
--------------------------------------------------------------------------------
/src/shared/updaters.js:
--------------------------------------------------------------------------------
1 | import { STATUS } from 'shared/reference';
2 |
3 | export const setStatusLoading = (p, { store }) =>
4 | void store.merge({ status: STATUS.LOADING, error: null });
5 |
6 | export const setStatusLoaded = (p, { store }) =>
7 | void store.merge({ status: STATUS.LOADED, error: null });
8 |
9 | export const onError = (error, { store }) => {
10 | store.merge({ status: STATUS.ERROR, error: error.message });
11 | throw error;
12 | };
13 |
--------------------------------------------------------------------------------
/src/shared/validator.js:
--------------------------------------------------------------------------------
1 | export const isEmail = email => {
2 | if (typeof email === 'string' && email.includes('@')) {
3 | return;
4 | } else {
5 | throw new Error ('Wrong email')
6 | }
7 | }
8 |
9 | export const isPassword = password => {
10 | if (typeof password === 'string' && password.length > 6) {
11 | return;
12 | } else {
13 | throw new Error ('Password to short')
14 | }
15 | }
--------------------------------------------------------------------------------
/src/ui/index.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/artalar/react-template/01daae40379059089da50f27243d03d82c3f72c8/src/ui/index.js
--------------------------------------------------------------------------------