├── .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 |
react-template
, 49 | 50 |
51 | 52 | 53 | 54 |
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 [
react-template
, 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 --------------------------------------------------------------------------------