├── .gitignore ├── README.md ├── package.json ├── packages ├── backend-example │ ├── .babelrc │ ├── .gitignore │ ├── countries.js │ ├── index.js │ ├── package.json │ ├── static │ │ └── locales │ │ │ ├── en │ │ │ └── translation.json │ │ │ └── es │ │ │ └── translation.json │ └── users.js └── web │ ├── .babelrc │ ├── .env │ ├── .env.example │ ├── .eslintignore │ ├── .eslintrc │ ├── .gitignore │ ├── README.md │ ├── _config.yml │ ├── config-overrides.js │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ ├── manifest.json │ └── polyfill.min.js │ ├── screenshot.png │ └── src │ ├── components │ ├── Auth │ │ ├── Login.js │ │ ├── Register.js │ │ ├── hooks.js │ │ └── index.js │ ├── Layout │ │ ├── Header.js │ │ ├── HeaderUser.js │ │ ├── Logo.js │ │ ├── MenuHeader.js │ │ ├── MenuPrimary.js │ │ ├── hooks.js │ │ └── index.js │ └── Shared │ │ ├── ChangeLanguage.js │ │ ├── Title.js │ │ └── index.js │ ├── config │ ├── constants.js │ ├── cruds │ │ └── user.js │ ├── localization │ │ ├── antdLocale.js │ │ └── i18n.js │ ├── menus.js │ ├── routes.js │ ├── services.js │ └── store.js │ ├── img │ └── logo.png │ ├── index.js │ ├── models │ ├── auth.js │ ├── home.js │ └── index.js │ ├── pages │ ├── 404.js │ ├── About.js │ ├── ForgotPassword.js │ ├── Form.js │ ├── Home.js │ ├── Layout.js │ ├── List.js │ ├── Login.js │ └── Register.js │ ├── registerServiceWorker.js │ ├── services │ ├── auth.js │ └── instance.js │ ├── styles │ ├── auth.less │ ├── index.css │ ├── index.css.map │ ├── index.less │ ├── layout.less │ ├── table.less │ └── title.less │ └── utils │ └── general.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | # Editors 24 | .idea 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

⚛ React Starter Kit Dashboard ⚛

2 | 3 | ![ScreenShot](packages/web/screenshot.png) 4 | 5 | #### Features: 6 | * Internationalization loading files from backend with i18next 7 | * Easy to add new routes 8 | * Easy to add new menus 9 | * Create a CRUD very easy 10 | * Forms such as Sign in, Sign up and Recovery password, connected with the store 11 | 12 | #### It uses the following modules: 13 | * [React](https://reactjs.org) ([Create React App](https://github.com/facebook/create-react-app)) 14 | * [Rematch](https://rematch.gitbooks.io/rematch/content/#getting-started) 15 | * [React Router v5](https://reacttraining.com/react-router/) 16 | * [Ant Design](https://ant.design) 17 | * [Less](http://lesscss.org) for Ant Design customization 18 | * [Axios](https://github.com/axios/axios) 19 | * [React easy CRUD](https://github.com/cognox-sas/react-easy-crud.git) 20 | 21 | #### Clone project 22 | 23 | ``` 24 | git clone https://github.com/miguelcast/cra-init-dashboard.git 25 | ``` 26 | 27 | #### Install dependencies: 28 | 29 | ``` 30 | yarn 31 | ``` 32 | or 33 | ``` 34 | npm install 35 | ``` 36 | 37 | #### Start project: 38 | 39 | ``` 40 | yarn start 41 | ``` 42 | or 43 | ``` 44 | npm start 45 | ``` 46 | 47 | #### Structure folders 48 | ``` 49 | src 50 | |-- components 51 | | |-- Auth 52 | | | |-- index.js 53 | | | |-- hooks.js 54 | | | |-- Register.js 55 | | | `-- Login.js 56 | | |-- Shared 57 | | | |-- Title.js 58 | | | |-- index.js 59 | | | `-- ChangeLanguage.js 60 | | `-- Layout 61 | | |-- Logo.js 62 | | |-- index.js 63 | | |-- hooks.js 64 | | |-- MenuHeader.js 65 | | |-- MenuPrimary.js 66 | | |-- Header.js 67 | | `-- HeaderUser.js 68 | |-- img 69 | | `-- logo.png 70 | |-- registerServiceWorker.js 71 | |-- utils 72 | | `-- general.js 73 | |-- models 74 | | |-- home.js 75 | | |-- index.js 76 | | `-- auth.js 77 | |-- config 78 | | |-- services.js 79 | | |-- routes.js 80 | | |-- constants.js 81 | | |-- menus.js 82 | | |-- store.js 83 | | |-- cruds 84 | | | `-- user.js 85 | | `-- localization 86 | | |-- antdLocale.js 87 | | `-- i18n.js 88 | |-- index.js 89 | |-- services 90 | | |-- auth.js 91 | | `-- instance.js 92 | |-- styles 93 | | |-- auth.less 94 | | |-- title.less 95 | | |-- index.less 96 | | |-- table.less 97 | | `-- layout.less 98 | `-- pages 99 | |-- Form.js 100 | |-- Register.js 101 | |-- Login.js 102 | |-- List.js 103 | |-- 404.js 104 | |-- Layout.js 105 | |-- About.js 106 | |-- Home.js 107 | `-- ForgotPassword.js 108 | ``` 109 | 110 | #### Add news routes 111 | 112 | Adding routes, modify src/config/routes.js file: 113 | 114 | ```javascript 115 | import { LOGGED, GUEST } from './constants'; 116 | import Home from './pages/Home'; 117 | import Login from './pages/login'; 118 | import List from './pages/List'; 119 | 120 | export default [ 121 | createRoute('/', Home, null, true), 122 | createRoute('/login', Login, GUEST), 123 | createRoute('/list', List, LOGGED), 124 | ]; 125 | ``` 126 | This example shows you how config you routes using the function createRoute, this function receives a string as the first 127 | parameter with the URL route, the second parameter is a React Component, the third parameter must be a string with a value 128 | of "logged" to show that component when the user is logged in or "guest" when the user is not logged in or null when 129 | both apply, the fourth parameter is a boolean type, which is the same as 130 | "[exact](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/Route.md)" in React Router. 131 | 132 | Code Splitting: 133 | 134 | ```javascript 135 | const AsyncAbout = lazy(() => import('../pages/About.js')); 136 | 137 | export default [ 138 | createRoute('/about', AsyncAbout), 139 | ]; 140 | ``` 141 | 142 | #### Menu configuration 143 | 144 | Adding menus, modify src/config/menus.js file: 145 | 146 | ```javascript 147 | const menus = { 148 | primary: [ 149 | createMenu( 150 | '/', // (string) URL to navigate (previously configured in the routes ) 151 | 'Home', // (string) Title 152 | 'home', // (string) Icon name from https://feathericons.com/ 153 | GUEST // (string "logged" or "guest") The same as explained in the paragraph above 154 | ), 155 | ], 156 | header: [ 157 | createMenu('/register', 'Register', 'user', GUEST), 158 | createComponent( 159 | () => HeaderUser, // (function () return ReactElement) Render this component in the menu 160 | LOGGED // The same as createMenu 161 | ), 162 | ], 163 | }; 164 | ``` 165 | 166 | #### Customization Ant Design 167 | 168 | For custom Ant Design styles, modify src/styles/index.less, the Less variables that you can modify [here.](https://github.com/ant-design/ant-design/blob/master/components/style/themes/default.less) 169 | 170 | #### API Configuration 171 | 172 | Do not import **axios** directly but import the instance of axios from **api/instance.js**. 173 | 174 | For Url API config add Key **REACT_APP_API** to .env files. 175 | 176 | ``` 177 | REACT_APP_API=http://localhost/my-api 178 | ``` 179 | 180 | ## License 181 | 182 | MIT 183 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cra-rr4-redux-antd", 3 | "title": "cra rr4 redux antd", 4 | "description": "Create React App, React Router v4, Rematch and Ant Design", 5 | "version": "0.3.0", 6 | "private": true, 7 | "license": "MIT", 8 | "author": "Miguel Cast", 9 | "workspaces": [ 10 | "packages/*" 11 | ], 12 | "scripts": { 13 | "start": "yarn wsrun start" 14 | }, 15 | "devDependencies": { 16 | "wsrun": "^5.0.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/backend-example/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ] 5 | } -------------------------------------------------------------------------------- /packages/backend-example/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # misc 10 | .DS_Store 11 | .env.local 12 | .env.development.local 13 | .env.test.local 14 | .env.production.local 15 | 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | 20 | # Editors 21 | .idea 22 | -------------------------------------------------------------------------------- /packages/backend-example/countries.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | key: 'arg', 4 | name: 'Argentina', 5 | }, 6 | { 7 | key: 'bra', 8 | name: 'Brazil', 9 | }, 10 | { 11 | key: 'col', 12 | name: 'Colombia', 13 | }, 14 | { 15 | key: 'per', 16 | name: 'Peru', 17 | }, 18 | { 19 | key: 'ury', 20 | name: 'Uruguay', 21 | }, 22 | ]; 23 | -------------------------------------------------------------------------------- /packages/backend-example/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import cors from 'cors'; 3 | import bodyParser from 'body-parser'; 4 | import users from './users'; 5 | import countries from './countries'; 6 | 7 | const app = express(); 8 | 9 | app.use(cors()); 10 | app.use(express.static('static')); 11 | app.use(bodyParser.json()); 12 | app.use(express.static('../build')); 13 | 14 | app.get('/', function(req, res) { 15 | res.send('Hello world my friend'); 16 | }); 17 | 18 | app.post('/auth/login', function(req, res) { 19 | res.json({ 20 | token: 'token-here', 21 | user: { 22 | key: 1, 23 | username: 'miguelcast', 24 | role: 'admin', 25 | }, 26 | }); 27 | }); 28 | 29 | app.post('/auth/logout', function(req, res) { 30 | res.json({ result: 'ok' }); 31 | }); 32 | 33 | app.get('/users', function(req, res) { 34 | res.json(users); 35 | }); 36 | 37 | app.get('/user/:key', function(req, res) { 38 | res.json(users.find(item => item.key === req.params.key)); 39 | }); 40 | 41 | app.get('/countries', function(req, res) { 42 | res.json(countries); 43 | }); 44 | 45 | const server = app.listen(4000, function() { 46 | console.log('Example app listening on port ' + server.address().port); 47 | }); 48 | -------------------------------------------------------------------------------- /packages/backend-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend-example", 3 | "private": true, 4 | "version": "1.0.0", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "nodemon --exec babel-node index.js" 9 | }, 10 | "dependencies": { 11 | "body-parser": "^1.18.3", 12 | "cors": "^2.8.5", 13 | "express": "^4.16.4" 14 | }, 15 | "devDependencies": { 16 | "@babel/core": "^7.3.3", 17 | "@babel/node": "^7.2.2", 18 | "@babel/preset-env": "^7.3.1", 19 | "nodemon": "^1.18.10" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/backend-example/static/locales/en/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "menu": { 3 | "home": "Home", 4 | "about": "About", 5 | "register": "Register", 6 | "login": "Login", 7 | "users": "Users" 8 | }, 9 | "common": { 10 | "spanish": "Spanish", 11 | "english": "English", 12 | "pageNotFound": "Page not found", 13 | "usernameRequired": "Please input your username!", 14 | "emailRequired": "Please input your email address!", 15 | "invalidEmail": "Invalid email address", 16 | "email": "Email", 17 | "password": "Password", 18 | "passwordRequired": "Please input your Password!", 19 | "or": "Or", 20 | "logout": "Logout", 21 | "return": "Return" 22 | }, 23 | "home": { 24 | "title": "Home", 25 | "goToAbout": "Go to About" 26 | }, 27 | "about": { 28 | "title": "About", 29 | "goToHome": "Go to home" 30 | }, 31 | "register": { 32 | "title": "Register", 33 | "name": "Name", 34 | "nameRequired": "Please input your name!", 35 | "signIn": "Sign in" 36 | }, 37 | "login": { 38 | "title": "Login", 39 | "forgotPassword": "Forgot password", 40 | "login": "Login", 41 | "registerNow": "register now!" 42 | }, 43 | "forgotPassword": { 44 | "title": "Forgot password", 45 | "description": "Please, input your email address associated with the application.", 46 | "send": "Send" 47 | } 48 | } -------------------------------------------------------------------------------- /packages/backend-example/static/locales/es/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "menu": { 3 | "home": "Inicio", 4 | "about": "Sobre nosotros", 5 | "register": "Registro", 6 | "login": "Iniciar sesión", 7 | "users": "Usuarios" 8 | }, 9 | "common": { 10 | "spanish": "Español", 11 | "english": "Ingles", 12 | "pageNotFound": "Página no encontrada", 13 | "usernameRequired": "Debe ingresar el nombre de usuario!", 14 | "emailRequired": "Debe ingresar el email!", 15 | "invalidEmail": "Email incorrecto!", 16 | "email": "Email", 17 | "password": "Contraseña", 18 | "passwordRequired": "Debe ingresar la contraseña!", 19 | "or": "O", 20 | "logout": "Cerrar sesión", 21 | "return": "Regresar" 22 | }, 23 | "home": { 24 | "title": "Inicio", 25 | "goToAbout": "Ir a Sobre nosotros" 26 | }, 27 | "about": { 28 | "title": "Sobre nosotros", 29 | "goToHome": "Ir a inicio" 30 | }, 31 | "register": { 32 | "title": "Registro", 33 | "name": "Nombre", 34 | "nameRequired": "Debe ingresar el nombre!", 35 | "signIn": "Registrarse" 36 | }, 37 | "login": { 38 | "title": "Iniciar sesión", 39 | "forgotPassword": "Olvido su contraseña", 40 | "login": "Iniciar sesión", 41 | "registerNow": "registrate ahora!" 42 | }, 43 | "forgotPassword": { 44 | "title": "Olvido la contraseña", 45 | "description": "Por favor ingresa un email asociado con la aplicación.", 46 | "send": "Enviar" 47 | } 48 | } -------------------------------------------------------------------------------- /packages/backend-example/users.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | key: '1', 4 | name: 'Mike', 5 | age: 32, 6 | address: '10 Downing Street', 7 | status: true, 8 | gender: 'male', 9 | birthday: '1990-04-13', 10 | color: 'red', 11 | country: 'col', 12 | countryName: 'Colombia', 13 | }, 14 | { 15 | key: '2', 16 | name: 'Daniel', 17 | age: 12, 18 | address: '14 Downing Street', 19 | status: false, 20 | gender: 'female', 21 | birthday: '1990-04-14', 22 | color: 'green', 23 | country: 'col', 24 | countryName: 'Colombia', 25 | }, 26 | { 27 | key: '3', 28 | name: 'John', 29 | age: 42, 30 | address: '10 Downing Street', 31 | status: true, 32 | gender: 'male', 33 | birthday: '1990-04-15', 34 | color: 'black', 35 | country: 'arg', 36 | countryName: 'Argentina', 37 | }, 38 | ]; 39 | -------------------------------------------------------------------------------- /packages/web/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "react-app" 4 | ], 5 | "plugins": [ 6 | [ 7 | "import", 8 | { 9 | "libraryName": "antd", 10 | "libraryDirectory": "es", 11 | "style": true 12 | } 13 | ] 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /packages/web/.env: -------------------------------------------------------------------------------- 1 | REACT_APP_API=http://localhost:4000 2 | REACT_APP_API_LOCALIZATION=http://localhost:4000 3 | -------------------------------------------------------------------------------- /packages/web/.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_API=http://example.api:4000 2 | REACT_APP_API_LOCALIZATION=http://localization.api:4000 3 | -------------------------------------------------------------------------------- /packages/web/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | public -------------------------------------------------------------------------------- /packages/web/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "es6": true, 5 | "browser": true, 6 | "node": true 7 | }, 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | } 12 | }, 13 | "plugins": [ 14 | "react-hooks", 15 | "prettier", 16 | "react", 17 | "jsx-a11y", 18 | "import", 19 | "standard" 20 | ], 21 | "rules": { 22 | "prettier/prettier": ["error", { 23 | "singleQuote": true, 24 | "trailingComma": "all", 25 | "bracketSpacing": true, 26 | "jsxBracketSameLine": true 27 | }], 28 | "react-hooks/rules-of-hooks": "error", 29 | "react-hooks/exhaustive-deps": "warn" 30 | }, 31 | "extends": [ 32 | "plugin:react/recommended", 33 | "prettier", 34 | "prettier/react", 35 | "prettier/standard" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /packages/web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | # Editors 24 | ../../.idea 25 | -------------------------------------------------------------------------------- /packages/web/README.md: -------------------------------------------------------------------------------- 1 |

⚛ React Starter Kit Dashboard ⚛

2 | 3 | ![ScreenShot](screenshot.png) 4 | 5 | #### Features: 6 | * Internationalization loading files from backend with i18next 7 | * Easy to add new routes 8 | * Easy to add new menus 9 | * Create a CRUD very easy 10 | * Forms such as Sign in, Sign up and Recovery password, connected with the store 11 | 12 | #### It uses the following modules: 13 | * [React](https://reactjs.org) ([Create React App](https://github.com/facebook/create-react-app)) 14 | * [Rematch](https://rematch.gitbooks.io/rematch/content/#getting-started) 15 | * [React Router v5](https://reacttraining.com/react-router/) 16 | * [Ant Design](https://ant.design) 17 | * [Less](http://lesscss.org) for Ant Design customization 18 | * [Axios](https://github.com/axios/axios) 19 | * [React easy CRUD](https://github.com/cognox-sas/react-easy-crud.git) 20 | 21 | #### Clone project 22 | 23 | ``` 24 | git clone https://github.com/miguelcast/cra-init-dashboard.git 25 | ``` 26 | 27 | #### Install dependencies: 28 | 29 | ``` 30 | yarn 31 | ``` 32 | or 33 | ``` 34 | npm install 35 | ``` 36 | 37 | #### Start project: 38 | 39 | ``` 40 | yarn start 41 | ``` 42 | or 43 | ``` 44 | npm start 45 | ``` 46 | 47 | #### Structure folders 48 | ``` 49 | src 50 | |-- components 51 | | |-- Crud 52 | | | |-- Form.js 53 | | | |-- index.js 54 | | | |-- List.js 55 | | | |-- typeForms.js 56 | | | |-- DateTableFilter.js 57 | | | |-- SearchTableFilter.js 58 | | | `-- hooks.js 59 | | |-- Auth 60 | | | |-- index.js 61 | | | |-- hooks.js 62 | | | |-- Register.js 63 | | | `-- Login.js 64 | | |-- Shared 65 | | | |-- Title.js 66 | | | |-- index.js 67 | | | `-- ChangeLanguage.js 68 | | `-- Layout 69 | | |-- Logo.js 70 | | |-- index.js 71 | | |-- hooks.js 72 | | |-- MenuHeader.js 73 | | |-- MenuPrimary.js 74 | | |-- Header.js 75 | | `-- HeaderUser.js 76 | |-- img 77 | | `-- logo.png 78 | |-- registerServiceWorker.js 79 | |-- utils 80 | | `-- general.js 81 | |-- models 82 | | |-- home.js 83 | | |-- index.js 84 | | `-- auth.js 85 | |-- config 86 | | |-- services.js 87 | | |-- routes.js 88 | | |-- constants.js 89 | | |-- menus.js 90 | | |-- store.js 91 | | |-- cruds 92 | | | `-- user.js 93 | | `-- localization 94 | | |-- antdLocale.js 95 | | `-- i18n.js 96 | |-- index.js 97 | |-- services 98 | | |-- auth.js 99 | | `-- instance.js 100 | |-- styles 101 | | |-- auth.less 102 | | |-- title.less 103 | | |-- index.less 104 | | |-- table.less 105 | | `-- layout.less 106 | `-- pages 107 | |-- Form.js 108 | |-- Register.js 109 | |-- Login.js 110 | |-- List.js 111 | |-- 404.js 112 | |-- Layout.js 113 | |-- About.js 114 | |-- Home.js 115 | `-- ForgotPassword.js 116 | ``` 117 | 118 | #### Add news routes 119 | 120 | Adding routes, modify src/config/routes.js file: 121 | 122 | ```javascript 123 | import { LOGGED, GUEST } from './constants'; 124 | import Home from './pages/Home'; 125 | import Login from './pages/login'; 126 | import List from './pages/List'; 127 | 128 | export default [ 129 | createRoute('/', Home, null, true), 130 | createRoute('/login', Login, GUEST), 131 | createRoute('/list', List, LOGGED), 132 | ]; 133 | ``` 134 | This example shows you how config you routes using the function createRoute, this function receives a string as the first 135 | parameter with the URL route, the second parameter is a React Component, the third parameter must be a string with a value 136 | of "logged" to show that component when the user is logged in or "guest" when the user is not logged in or null when 137 | both apply, the fourth parameter is a boolean type, which is the same as 138 | "[exact](https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/api/Route.md)" in React Router. 139 | 140 | Code Splitting: 141 | 142 | ```javascript 143 | const AsyncAbout = lazy(() => import('../pages/About.js')); 144 | 145 | export default [ 146 | createRoute('/about', AsyncAbout), 147 | ]; 148 | ``` 149 | 150 | #### Menu configuration 151 | 152 | Adding menus, modify src/config/menus.js file: 153 | 154 | ```javascript 155 | const menus = { 156 | primary: [ 157 | createMenu( 158 | '/', // (string) URL to navigate (previously configured in the routes ) 159 | 'Home', // (string) Title 160 | 'home', // (string) Icon name from https://feathericons.com/ 161 | GUEST // (string "logged" or "guest") The same as explained in the paragraph above 162 | ), 163 | ], 164 | header: [ 165 | createMenu('/register', 'Register', 'user', GUEST), 166 | createComponent( 167 | () => HeaderUser, // (function () return ReactElement) Render this component in the menu 168 | LOGGED // The same as createMenu 169 | ), 170 | ], 171 | }; 172 | ``` 173 | 174 | #### Customization Ant Design 175 | 176 | For custom Ant Design styles, modify src/styles/index.less, the Less variables that you can modify [here.](https://github.com/ant-design/ant-design/blob/master/components/style/themes/default.less) 177 | 178 | #### API Configuration 179 | 180 | Do not import **axios** directly but import the instance of axios from **api/instance.js**. 181 | 182 | For Url API config add Key **REACT_APP_API** to .env files. 183 | 184 | ``` 185 | REACT_APP_API=http://localhost/my-api 186 | ``` 187 | 188 | ## License 189 | 190 | MIT 191 | -------------------------------------------------------------------------------- /packages/web/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /packages/web/config-overrides.js: -------------------------------------------------------------------------------- 1 | const { override, addLessLoader, addPostcssPlugins } = require('customize-cra'); 2 | 3 | module.exports = override( 4 | addLessLoader({ 5 | strictMath: false, 6 | noIeCompat: true, 7 | localIdentName: '[local]--[hash:base64:5]', // if you use CSS Modules, and custom `localIdentName`, default is '[local]--[hash:base64:5]'. 8 | javascriptEnabled: true, 9 | }), 10 | addPostcssPlugins([]), 11 | ); 12 | -------------------------------------------------------------------------------- /packages/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "title": "Web dashboard", 4 | "description": "Create React App, React Router v4, Rematch and Ant Design", 5 | "keywords": [ 6 | "template", 7 | "react", 8 | "rematch", 9 | "router", 10 | "antd", 11 | "app", 12 | "boilerplate" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/miguelcast/cra-rr4-redux-antd.git" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/miguelcast/cra-rr4-redux-antd/issues" 20 | }, 21 | "version": "0.3.0", 22 | "private": true, 23 | "license": "MIT", 24 | "author": "Miguel Cast", 25 | "resolutions": { 26 | "**/event-stream": "^4.0.1" 27 | }, 28 | "dependencies": { 29 | "@rematch/core": "^1.0.6", 30 | "@rematch/loading": "^1.1.2", 31 | "@rematch/persist": "^1.1.5", 32 | "antd": "^3.15.0", 33 | "axios": "^0.19.0", 34 | "i18next": "^15.0.7", 35 | "i18next-browser-languagedetector": "^3.0.1", 36 | "i18next-chained-backend": "^1.0.1", 37 | "i18next-localstorage-backend": "^2.1.2", 38 | "i18next-xhr-backend": "^2.0.1", 39 | "moment": "^2.24.0", 40 | "prop-types": "^15.6.2", 41 | "react": "^16.6.0", 42 | "react-dom": "^16.6.0", 43 | "react-easy-crud": "^0.2.13", 44 | "react-i18next": "^10.5.1", 45 | "react-redux": "^7.1.1", 46 | "react-router-dom": "^5.0.0", 47 | "react-scripts": "^3.2.0", 48 | "redux-persist": "^6.0.0" 49 | }, 50 | "css": { 51 | "preprocess": "less" 52 | }, 53 | "scripts": { 54 | "analyze": "source-map-explorer build/static/js/main.*", 55 | "lint": "eslint .", 56 | "lint:fix": "eslint . --fix", 57 | "precommit": "lint-staged", 58 | "start": "react-app-rewired start", 59 | "build": "react-app-rewired build", 60 | "test": "react-app-rewired test --env=jsdom" 61 | }, 62 | "husky": { 63 | "hooks": { 64 | "pre-commit": "yarn lint:fix" 65 | } 66 | }, 67 | "devDependencies": { 68 | "@ljharb/eslint-config": "^14.1.0", 69 | "babel-plugin-import": "^1.7.0", 70 | "customize-cra": "^0.8.0", 71 | "eslint-config-prettier": "^6.4.0", 72 | "eslint-config-standard": "14.1.0", 73 | "eslint-plugin-node": "^10.0.0", 74 | "eslint-plugin-prettier": "^3.0.1", 75 | "eslint-plugin-promise": "^4.0.1", 76 | "eslint-plugin-react": "^7.12.4", 77 | "eslint-plugin-react-hooks": "^2.1.2", 78 | "eslint-plugin-standard": "^4.0.0", 79 | "husky": "^3.0.8", 80 | "less": "^3.9.0", 81 | "less-loader": "^5.0.0", 82 | "prettier": "^1.12.1", 83 | "react-app-rewired": "^2.1.1", 84 | "source-map-explorer": "^2.1.0" 85 | }, 86 | "browserslist": [ 87 | ">0.2%", 88 | "not dead", 89 | "not ie <= 11", 90 | "not op_mini all" 91 | ] 92 | } 93 | -------------------------------------------------------------------------------- /packages/web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miguelcast/cra-init-dashboard/52ef1e8832cfe0da798cec78e50406234f55eeac/packages/web/public/favicon.ico -------------------------------------------------------------------------------- /packages/web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React Client-Side boilerplate 23 | 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /packages/web/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } -------------------------------------------------------------------------------- /packages/web/public/polyfill.min.js: -------------------------------------------------------------------------------- 1 | function __cons(t,a){return eval("new t("+a.map(function(t,e){return"a["+e+"]"}).join(",")+")")}Object.getPrototypeOf||(Object.getPrototypeOf=function(t){if(t!==Object(t))throw TypeError("Object.getPrototypeOf called on non-object");return t.__proto__||t.constructor.prototype||Object.prototype}),"function"!=typeof Object.getOwnPropertyNames&&(Object.getOwnPropertyNames=function(t){if(t!==Object(t))throw TypeError("Object.getOwnPropertyNames called on non-object");var e,r=[];for(e in t)Object.prototype.hasOwnProperty.call(t,e)&&r.push(e);return r}),"function"!=typeof Object.create&&(Object.create=function(t,e){function r(){}if("object"!=typeof t)throw TypeError();r.prototype=t;var n=new r;if(t&&(n.constructor=r),void 0!==e){if(e!==Object(e))throw TypeError();Object.defineProperties(n,e)}return n}),function(){if(!Object.defineProperty||!function(){try{return Object.defineProperty({},"x",{}),!0}catch(t){return!1}}()){var t=Object.defineProperty;Object.defineProperty=function(e,r,n){if(t)try{return t(e,r,n)}catch(o){}if(e!==Object(e))throw TypeError("Object.defineProperty called on non-object");return Object.prototype.__defineGetter__&&"get"in n&&Object.prototype.__defineGetter__.call(e,r,n.get),Object.prototype.__defineSetter__&&"set"in n&&Object.prototype.__defineSetter__.call(e,r,n.set),"value"in n&&(e[r]=n.value),e}}}(),"function"!=typeof Object.defineProperties&&(Object.defineProperties=function(t,e){if(t!==Object(t))throw TypeError("Object.defineProperties called on non-object");var r;for(r in e)Object.prototype.hasOwnProperty.call(e,r)&&Object.defineProperty(t,r,e[r]);return t}),Object.keys||(Object.keys=function(t){if(t!==Object(t))throw TypeError("Object.keys called on non-object");var e,r=[];for(e in t)Object.prototype.hasOwnProperty.call(t,e)&&r.push(e);return r}),Function.prototype.bind||(Function.prototype.bind=function(t){if("function"!=typeof this)throw TypeError("Bind must be called on a function");var e=Array.prototype.slice.call(arguments,1),r=this,n=function(){},o=function(){return r.apply(this instanceof n?this:t,e.concat(Array.prototype.slice.call(arguments)))};return this.prototype&&(n.prototype=this.prototype),o.prototype=new n,o}),Array.isArray=Array.isArray||function(t){return Boolean(t&&"[object Array]"===Object.prototype.toString.call(Object(t)))},Array.prototype.indexOf||(Array.prototype.indexOf=function(t){if(void 0===this||null===this)throw TypeError();var e=Object(this),r=e.length>>>0;if(0===r)return-1;var n=0;if(arguments.length>0&&(n=Number(arguments[1]),isNaN(n)?n=0:0!==n&&n!==1/0&&n!==-(1/0)&&(n=(n>0||-1)*Math.floor(Math.abs(n)))),n>=r)return-1;for(var o=n>=0?n:Math.max(r-Math.abs(n),0);o>>0;if(0===r)return-1;var n=r;arguments.length>1&&(n=Number(arguments[1]),n!==n?n=0:0!==n&&n!==1/0&&n!==-(1/0)&&(n=(n>0||-1)*Math.floor(Math.abs(n))));for(var o=n>=0?Math.min(n,r-1):r-Math.abs(n);o>=0;o--)if(o in e&&e[o]===t)return o;return-1}),Array.prototype.every||(Array.prototype.every=function(t){if(void 0===this||null===this)throw TypeError();var e=Object(this),r=e.length>>>0;if("function"!=typeof t)throw TypeError();var n,o=arguments[1];for(n=0;n>>0;if("function"!=typeof t)throw TypeError();var n,o=arguments[1];for(n=0;n>>0;if("function"!=typeof t)throw TypeError();var n,o=arguments[1];for(n=0;n>>0;if("function"!=typeof t)throw TypeError();var n=[];n.length=r;var o,i=arguments[1];for(o=0;o>>0;if("function"!=typeof t)throw TypeError();var n,o=[],i=arguments[1];for(n=0;n>>0;if("function"!=typeof t)throw TypeError();if(0===r&&1===arguments.length)throw TypeError();var n,o=0;if(arguments.length>=2)n=arguments[1];else for(;;){if(o in e){n=e[o++];break}if(++o>=r)throw TypeError()}for(;o>>0;if("function"!=typeof t)throw TypeError();if(0===r&&1===arguments.length)throw TypeError();var n,o=r-1;if(arguments.length>=2)n=arguments[1];else for(;;){if(o in this){n=this[o--];break}if(--o<0)throw TypeError()}for(;o>=0;)o in e&&(n=t.call(void 0,n,e[o],o,e)),o--;return n}),String.prototype.trim||(String.prototype.trim=function(){return String(this).replace(/^\s+/,"").replace(/\s+$/,"")}),Date.now||(Date.now=function(){return Number(new Date)}),Date.prototype.toISOString||(Date.prototype.toISOString=function(){function t(t){return("00"+t).slice(-2)}function e(t){return("000"+t).slice(-3)}return this.getUTCFullYear()+"-"+t(this.getUTCMonth()+1)+"-"+t(this.getUTCDate())+"T"+t(this.getUTCHours())+":"+t(this.getUTCMinutes())+":"+t(this.getUTCSeconds())+"."+e(this.getUTCMilliseconds())+"Z"}),function(t){"use strict";function e(e){return e===t?k:e}function r(t,r,n){var o=t[r];t[r]=function(){var t=e(this),r=n.apply(t,arguments);return r!==k?r:o.apply(t,arguments)}}function n(t,e){for(var r=Object.getOwnPropertyDescriptor(t,e),n=Object.getPrototypeOf(t);!r&&n;)r=Object.getOwnPropertyDescriptor(n,e),n=Object.getPrototypeOf(n);return r}function o(t,e,r,n){e in t&&!n&&!I||("function"==typeof r?Object.defineProperty(t,e,{value:r,configurable:!0,enumerable:!1,writable:!0}):Object.defineProperty(t,e,{value:r,configurable:!1,enumerable:!1,writable:!1}))}function i(t,e,r){Object.defineProperty(t,e,{value:r,configurable:!1,enumerable:!1,writable:!0})}function a(){function t(t){var e=t.valueOf,n=A(null);return Object.defineProperty(t,"valueOf",{value:function(r){return function(o){return o===r?n:e.apply(t,arguments)}}(r),configurable:!0,writeable:!0,enumerable:!1}),n}function e(t){var e="function"==typeof t.valueOf&&t.valueOf(r);return e===t?null:e}var r=A(null);return{clear:function(){r=A(null)},remove:function(t){var r=e(t);return!(!r||!g(r,"value"))&&(delete r.value,!0)},get:function(t,r){var n=e(t);return n&&g(n,"value")?n.value:r},has:function(t){var r=e(t);return Boolean(r&&g(r,"value"))},set:function(r,n){var o=e(r)||t(r);o.value=n}}}function u(e){switch(typeof e){case"undefined":return"undefined";case"boolean":return"boolean";case"number":return"number";case"string":return"string";case"symbol":return"symbol";default:return null===e?"null":e instanceof t.Symbol?"symbol":"object"}}function c(t){return t=Number(t),U(t)?0:0===t||t===1/0||t===-(1/0)?t:(t<0?-1:1)*z(X(t))}function s(t){return t>>>0}function f(t){if(null===t||t===k)throw TypeError();return Object(t)}function l(t){var e=c(t);return e<=0?0:e===1/0?9007199254740991:J(e,9007199254740991)}function p(t){return"function"==typeof t}function h(t){return!!/Constructor/.test(Object.prototype.toString.call(t))||(!!/Function/.test(Object.prototype.toString.call(t))||"function"==typeof t)}function y(t,e){if(typeof t!=typeof e)return!1;switch(typeof t){case"undefined":return!0;case"number":return t!==t&&e!==e||(0===t&&0===e?1/t===1/e:t===e);case"boolean":case"string":case"object":default:return t===e}}function v(t,e){if(typeof t!=typeof e)return!1;switch(typeof t){case"undefined":return!0;case"number":return t!==t&&e!==e||t===e;case"boolean":case"string":case"object":default:return t===e}}function d(t,e){var r=f(t);return r[e]}function m(t,e){var r=d(t,e);if(r===k||null===r)return k;if(!p(r))throw TypeError();return r}function b(t,e){for(;t;){if(Object.prototype.hasOwnProperty.call(t,e))return!0;if("object"!==u(t))return!1;t=Object.getPrototypeOf(t)}return!1}function g(t,e){return Object.prototype.hasOwnProperty.call(t,e)}function E(t,e){arguments.length<2&&(e=m(t,at));var r=e.call(t);if("object"!==u(r))throw TypeError();return r}function w(t,e){if(arguments.length<2)var r=t.next();else r=t.next(e);if("object"!==u(r))throw TypeError();return r}function S(t){return t.value}function T(t,e){var r=w(t,e),n=r.done;return Boolean(n)!==!0&&r}function j(t,e){var r={};return r.value=t,r.done=e,r}function O(t,e,r){var n=function(){e.apply(k,r)};L(n)}function _(t){}function P(t){var e=[];if(Object(t)!==t)return e;for(var r=new Set;null!==t;)Object.getOwnPropertyNames(t).forEach(function(n){if(!r.has(n)){var o=Object.getOwnPropertyDescriptor(t,n);o&&(r.add(n),o.enumerable&&e.push(n))}}),t=Object.getPrototypeOf(t);return e[at]()}function R(t){return Object.getOwnPropertyNames(t)}function A(t,e){return Object.create(t,e)}function D(){}function N(t,e){var r=String(t),n=new D;return i(n,"[[IteratedString]]",r),i(n,"[[StringIteratorNextIndex]]",0),i(n,"[[StringIterationKind]]",e),n}function M(){}function x(t,e){var r=f(t),n=new M;return i(n,"[[IteratedObject]]",r),i(n,"[[ArrayIteratorNextIndex]]",0),i(n,"[[ArrayIterationKind]]",e),n}var C,I=!1,k=void 0,L=function(t,e){return t?function(e){t.resolve().then(function(){e()})}:e?function(t){e(t)}:function(t){setTimeout(t,0)}}(t.Promise,t.setImmediate),U=t.isNaN,F=t.parseInt,W=t.parseFloat,q=Math.E,B=Math.LOG10E,H=Math.LOG2E,X=Math.abs,G=Math.ceil,V=Math.exp,z=Math.floor,$=Math.log,K=Math.max,J=Math.min,Y=Math.pow,Z=Math.random,Q=Math.sqrt,tt=String.prototype.match,et=String.prototype.replace,rt=String.prototype.search,nt=String.prototype.split,ot=Object.create(null);!function(){function r(t){return Array(t+1).join("x").replace(/x/g,function(){return Z()<.5?"‌":"‍"})}function n(t){if(!(this instanceof n))return new n(t,a);if(this instanceof n&&arguments[1]!==a)throw TypeError();var e=t===k?k:String(t);return i(this,"[[SymbolData]]",r(128)),i(this,"[[Description]]",e),u[this]=this,this}var a=Object.create(null),u={};C=function(t){return u[t]};var c=[];"Symbol"in t&&!I||(t.Symbol=n),o(n,"for",function(t){for(var e=String(t),r=0;r>24):16711680&t?e(t>>16)+8:65280&t?e(t>>8)+16:e(t)+24}),o(Math,"cosh",function(t){return t=Number(t),(Y(q,t)+Y(q,-t))/2}),o(Math,"expm1",function(t){return t=Number(t),y(t,-0)?-0:X(t)<1e-5?t+.5*t*t:V(t)-1}),o(Math,"fround",function(t){return U(t)?NaN:1/t===+(1/0)||1/t===-(1/0)||t===+(1/0)||t===-(1/0)?t:new Float32Array([t])[0]}),o(Math,"hypot",function(){for(var t=[],e=0,r=!1,n=0;ne&&(e=o),t[n]=o}if(r)return NaN;if(0===e)return 0;var i=0;for(n=0;n>>16&65535,i=65535&r,a=n>>>16&65535,u=65535&n;return i*u+(o*u+i*a<<16>>>0)|0},"imul"in Math&&0===Math.imul(1,2147483648)),o(Math,"log1p",function(t){return t=Number(t),t<-1?NaN:y(t,-0)?-0:X(t)>1e-4?$(1+t):(-.5*t+1)*t}),o(Math,"log10",function(t){return t=Number(t),$(t)*B}),o(Math,"log2",function(t){return t=Number(t),$(t)*H}),o(Math,"sign",function(t){return t=Number(t),t<0?-1:t>0?1:t}),o(Math,"sinh",function(t){return t=Number(t),y(t,-0)?t:(Y(q,t)-Y(q,-t))/2}),o(Math,"tanh",function(t){t=Number(t);var e=Y(q,2*t)-1,r=Y(q,2*t)+1;return y(t,-0)?t:e===r?1:e/r}),o(Math,"trunc",function(t){return t=Number(t),U(t)?NaN:t<0?G(t):z(t)});var pt=function(){var t={},e=Symbol();return t[Symbol.match]=function(){return e},"".match(t)===e}();o(String,"fromCodePoint",function(){for(var t=arguments,e=t.length,r=[],n=0;n1114111)throw RangeError("Invalid code point "+i);i<65536?r.push(String.fromCharCode(i)):(i-=65536,r.push(String.fromCharCode((i>>10)+55296)),r.push(String.fromCharCode(i%1024+56320))),n+=1}return r.join("")}),o(String,"raw",function dt(t){var e=[].slice.call(arguments,1),r=Object(t),n=r.raw,dt=Object(n),o=dt.length,i=l(o);if(i<=0)return"";for(var a=[],u=0;;){var c=dt[u],s=String(c);if(a.push(s),u+1===i)return a.join("");c=e[u];var f=String(c);a.push(f),u+=1}}),o(String.prototype,"codePointAt",function(t){var r=e(this),n=String(r),o=c(t),i=n.length;if(o<0||o>=i)return k;var a=n.charCodeAt(o);if(a<55296||a>56319||o+1===i)return a;var u=n.charCodeAt(o+1);return u<56320||u>57343?a:1024*(a-55296)+(u-56320)+65536}),o(String.prototype,"endsWith",function(t){var r=arguments[1],n=e(this),o=String(n),i=String(t),a=o.length,u=r===k?a:c(r),s=J(K(u,0),a),f=i.length,l=s-f;return!(l<0)&&o.substring(l,l+f)===i}),o(String.prototype,"includes",function(t){var r=arguments[1],n=e(this),o=String(n),i=String(t),a=c(r),u=o.length,s=J(K(a,0),u);return o.indexOf(i,s)!==-1}),o(String.prototype,"match",function(t){var r=e(this),n=String(r);if(b(t,ut))var o=t;else o=new RegExp(t);return o[ut](n)},!pt),o(String.prototype,"repeat",function(t){var r=e(this),n=String(r),o=c(t);if(o<0)throw RangeError();if(o===1/0)throw RangeError();var i=new Array(o+1).join(n);return i}),o(String.prototype,"replace",function(t,r){var n=e(this);return b(t,ct)?t[ct](n,r):et.call(n,t,r)},!pt),o(String.prototype,"search",function(t){var r=e(this),n=String(r);if(b(t,st))var o=t;else o=new RegExp(t);return o[st](n)},!pt),o(String.prototype,"split",function(t,r){var n=e(this);return b(t,ft)?t[ft](n,r):nt.call(n,t,r)},!pt),o(String.prototype,"startsWith",function(t){var r=arguments[1],n=e(this),o=String(n),i=String(t),a=c(r),u=o.length,s=J(K(a,0),u),f=i.length;return!(f+s>u)&&o.substring(s,s+f)===i}),o(String.prototype,at,function(){return N(this,"value")});var ht=Object.create(it);D.prototype=ht,o(ht,"next",function(){var t=f(this),e=String(t["[[IteratedString]]"]),r=t["[[StringIteratorNextIndex]]"],n=e.length;if(r>=n)return i(t,"[[StringIteratorNextIndex]]",1/0),j(k,!0);var o=e.codePointAt(r);return i(t,"[[StringIteratorNextIndex]]",r+(o>65535?2:1)),j(String.fromCodePoint(o),!1)}),o(ht,lt,"String Iterator"),"flags"in RegExp.prototype||Object.defineProperty(RegExp.prototype,"flags",{get:function(){var t=String(this);return t.substring(t.lastIndexOf("/")+1)}}),o(RegExp.prototype,ut,function(t){var r=e(this);return tt.call(t,r)}),o(RegExp.prototype,ct,function(t,r){var n=e(this);return et.call(t,n,r)}),o(RegExp.prototype,st,function(t){var r=e(this);return rt.call(t,r)}),o(RegExp.prototype,ft,function(t,r){var n=e(this);return nt.call(t,n,r)}),o(Array,"from",function(t){var r=arguments[1],n=arguments[2],o=e(this);if(r===k)var i=!1;else{if(!p(r))throw TypeError();var a=n;i=!0}var u=m(t,at);if(u!==k){if(h(o))var c=new o;else c=new Array(0);for(var s=E(t,u),y=0;;){var v=T(s);if(v===!1)return c.length=y,c;var d=S(v);if(i)var b=r.call(a,d);else b=d;c[y]=b,y+=1}}var g=f(t),w=g.length,j=l(w);for(c=h(o)?new o(j):new Array(j),y=0;y0;){var m=String(s),g=String(a),E=b(n,m);if(E){var w=n[m];n[g]=w}else delete n[g];s+=v,a+=v,d-=1}return n});var yt="entries"in Array.prototype&&"next"in[].entries();o(Array.prototype,"entries",function(){return x(this,"key+value")},!yt),o(Array.prototype,"fill",function(t){var e=arguments[1],r=arguments[2],n=f(this),o=n.length,i=l(o);i=K(i,0);var a,u=c(e);a=u<0?K(i+u,0):J(u,i);var s;s=r===k?i:c(r);var p;for(p=s<0?K(i+s,0):J(s,i);a1?arguments[1]:k,i=0;i1?arguments[1]:k,i=0;i=l)return i(t,"[[ArrayIteratorNextIndex]]",1/0),j(k,!0);if(r=a,i(t,"[[ArrayIteratorNextIndex]]",a+1),c.indexOf("value")!==-1&&(n=o[r]),c.indexOf("key+value")!==-1)return j([r,n],!1);if(c.indexOf("key")!==-1)return j(r,!1);if("value"===c)return j(n,!1);throw Error("Internal error")}),o(vt,lt,"Array Iterator"),["Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Uint16Array","Int32Array","Uint32Array","Float32Array","Float64Array"].forEach(function(r){if(r in t){var n=t[r];o(n,"from",function(t){var r=arguments[1],n=arguments[2],o=e(this);if(!h(o))throw TypeError();if(r===k)var i=!1;else{if(p(r))throw TypeError();var a=n;i=!0}var u=m(t,at);if(u!==k){for(var c=E(t,u),s=[],y=!0;y!==!1;)if(y=T(c),y!==!1){var v=S(y);s.push(v)}for(var d=s.length,b=new o(d),g=0;gr?1:0}var e=arguments[0];return Array.prototype.sort.call(this,t)}),o(n.prototype,"values",Array.prototype.values),o(n.prototype,at,n.prototype.values),o(n.prototype,lt,r)}}),function(){function r(){var t=e(this),r=arguments[0];if("object"!==u(t))throw TypeError();if("[[MapData]]"in t)throw TypeError();if(r!==k){var n=t.set;if(!p(n))throw TypeError();var o=E(f(r))}if(i(t,"[[MapData]]",{keys:[],values:[]}),o===k)return t;for(;;){var a=T(o);if(a===!1)return t;var c=S(a);if("object"!==u(c))throw TypeError();var s=c[0],l=c[1];n.call(t,s,l)}return t}function n(t,e){var r;if(e===e)return t.keys.indexOf(e);for(r=0;r=0?o.values[i]:k}),o(r.prototype,"has",function(t){var r=e(this);if("object"!==u(r))throw TypeError();if(!("[[MapData]]"in r))throw TypeError();if(r["[[MapData]]"]===k)throw TypeError();var o=r["[[MapData]]"];return n(o,t)>=0}),o(r.prototype,"keys",function(){var t=e(this);if("object"!==u(t))throw TypeError();return c(t,"key")}),o(r.prototype,"set",function(t,r){var o=e(this);if("object"!==u(o))throw TypeError();if(!("[[MapData]]"in o))throw TypeError();if(o["[[MapData]]"]===k)throw TypeError();var i=o["[[MapData]]"],a=n(i,t);return a<0&&(a=i.keys.length),y(t,-0)&&(t=0),i.keys[a]=t,i.values[a]=r,o}),Object.defineProperty(r.prototype,"size",{get:function(){var t=e(this);if("object"!==u(t))throw TypeError();if(!("[[MapData]]"in t))throw TypeError();if(t["[[MapData]]"]===k)throw TypeError();for(var r=t["[[MapData]]"],n=0,o=0;o=0)var s=c;else s=u+c,s<0&&(s=0);for(;s>16&255)),o.push(String.fromCharCode(i>>8&255)),o.push(String.fromCharCode(255&i)),a=0,i=0),r+=1;return 12===a?(i>>=4,o.push(String.fromCharCode(255&i))):18===a&&(i>>=2,o.push(String.fromCharCode(i>>8&255)),o.push(String.fromCharCode(255&i))),o.join("")}function r(t){t=String(t);var e,r,o,i,a,u,c,s=0,f=[];if(/[^\x00-\xFF]/.test(t))throw Error("InvalidCharacterError");for(;s>2,a=(3&e)<<4|r>>4,u=(15&r)<<2|o>>6,c=63&o,s===t.length+2?(u=64,c=64):s===t.length+1&&(c=64),f.push(n.charAt(i),n.charAt(a),n.charAt(u),n.charAt(c));return f.join("")}if(!("atob"in t&&"btoa"in t)){var n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";t.atob=e,t.btoa=r}}(),function(){function e(t){return t.offsetWidth>0&&t.offsetHeight>0}function r(){var t=a;a=Object.create(null),c=-1,Object.keys(t).forEach(function(r){var n=t[r];n.element&&!e(n.element)||n.callback(Date.now())})}function n(e,n){var o=++u;return a[o]={callback:e,element:n},c===-1&&(c=t.setTimeout(r,1e3/i)),o}function o(e){delete a[e],0===Object.keys(a).length&&(t.clearTimeout(c),c=-1)}if(!("requestAnimationFrame"in t)){var i=60,a=Object.create(null),u=0,c=-1;t.requestAnimationFrame=n,t.cancelAnimationFrame=o}}())}(self),function(t){"use strict";function e(t,e){t&&Object.keys(e).forEach(function(r){if(!(r in t||r in t.prototype))try{Object.defineProperty(t.prototype,r,Object.getOwnPropertyDescriptor(e,r))}catch(n){t[r]=e[r]}})}function r(t){var e=null;return t=t.map(function(t){return t instanceof Node?t:document.createTextNode(t)}),1===t.length?e=t[0]:(e=document.createDocumentFragment(),t.forEach(function(t){e.appendChild(t)})),e}if("window"in t&&"document"in t){document.querySelectorAll||(document.querySelectorAll=function(t){var e,r=document.createElement("style"),n=[];for(document.documentElement.firstChild.appendChild(r),document._qsa=[],r.styleSheet.cssText=t+"{x-qsa:expression(document._qsa && document._qsa.push(this))}",window.scrollBy(0,0),r.parentNode.removeChild(r);document._qsa.length;)e=document._qsa.shift(),e.style.removeAttribute("x-qsa"),n.push(e);return document._qsa=null,n}),document.querySelector||(document.querySelector=function(t){var e=document.querySelectorAll(t);return e.length?e[0]:null}),document.getElementsByClassName||(document.getElementsByClassName=function(t){return t=String(t).replace(/^|\s+/g,"."),document.querySelectorAll(t)}),t.Node=t.Node||function(){throw TypeError("Illegal constructor")},[["ELEMENT_NODE",1],["ATTRIBUTE_NODE",2],["TEXT_NODE",3],["CDATA_SECTION_NODE",4],["ENTITY_REFERENCE_NODE",5],["ENTITY_NODE",6],["PROCESSING_INSTRUCTION_NODE",7],["COMMENT_NODE",8],["DOCUMENT_NODE",9],["DOCUMENT_TYPE_NODE",10],["DOCUMENT_FRAGMENT_NODE",11],["NOTATION_NODE",12]].forEach(function(e){e[0]in t.Node||(t.Node[e[0]]=e[1])}),t.DOMException=t.DOMException||function(){throw TypeError("Illegal constructor")},[["INDEX_SIZE_ERR",1],["DOMSTRING_SIZE_ERR",2],["HIERARCHY_REQUEST_ERR",3],["WRONG_DOCUMENT_ERR",4],["INVALID_CHARACTER_ERR",5],["NO_DATA_ALLOWED_ERR",6],["NO_MODIFICATION_ALLOWED_ERR",7],["NOT_FOUND_ERR",8],["NOT_SUPPORTED_ERR",9],["INUSE_ATTRIBUTE_ERR",10],["INVALID_STATE_ERR",11],["SYNTAX_ERR",12],["INVALID_MODIFICATION_ERR",13],["NAMESPACE_ERR",14],["INVALID_ACCESS_ERR",15]].forEach(function(e){e[0]in t.DOMException||(t.DOMException[e[0]]=e[1])}),function(){function e(t,e,r){if("function"==typeof e){"DOMContentLoaded"===t&&(t="load");var n=this,o=function(t){t._timeStamp=Date.now(),t._currentTarget=n,e.call(this,t),t._currentTarget=null};this["_"+t+e]=o,this.attachEvent("on"+t,o)}}function r(t,e,r){if("function"==typeof e){"DOMContentLoaded"===t&&(t="load");var n=this["_"+t+e];n&&(this.detachEvent("on"+t,n),this["_"+t+e]=null)}}"Element"in t&&!Element.prototype.addEventListener&&Object.defineProperty&&(Event.CAPTURING_PHASE=1,Event.AT_TARGET=2,Event.BUBBLING_PHASE=3,Object.defineProperties(Event.prototype,{CAPTURING_PHASE:{get:function(){return 1}},AT_TARGET:{get:function(){return 2}},BUBBLING_PHASE:{get:function(){return 3}},target:{get:function(){return this.srcElement}},currentTarget:{get:function(){return this._currentTarget}},eventPhase:{get:function(){return this.srcElement===this.currentTarget?Event.AT_TARGET:Event.BUBBLING_PHASE}},bubbles:{get:function(){switch(this.type){case"click":case"dblclick":case"mousedown":case"mouseup":case"mouseover":case"mousemove":case"mouseout":case"mousewheel":case"keydown":case"keypress":case"keyup":case"resize":case"scroll":case"select":case"change":case"submit":case"reset":return!0}return!1}},cancelable:{get:function(){switch(this.type){case"click":case"dblclick":case"mousedown":case"mouseup":case"mouseover":case"mouseout":case"mousewheel":case"keydown":case"keypress":case"keyup":case"submit":return!0}return!1}},timeStamp:{get:function(){return this._timeStamp}},stopPropagation:{value:function(){this.cancelBubble=!0}},preventDefault:{value:function(){this.returnValue=!1}},defaultPrevented:{get:function(){return this.returnValue===!1}}}),[Window,HTMLDocument,Element].forEach(function(t){t.prototype.addEventListener=e,t.prototype.removeEventListener=r}))}(),function(){function e(t,e){e=e||{bubbles:!1,cancelable:!1,detail:void 0};var r=document.createEvent("CustomEvent");return r.initCustomEvent(t,e.bubbles,e.cancelable,e.detail),r}"CustomEvent"in t&&"function"==typeof t.CustomEvent||(e.prototype=t.Event.prototype,t.CustomEvent=e)}(),window.addEvent=function(t,e,r){t.addEventListener?t.addEventListener(e,r,!1):t.attachEvent&&(t["e"+e+r]=r,t[e+r]=function(){var n=window.event;n.currentTarget=t,n.preventDefault=function(){n.returnValue=!1},n.stopPropagation=function(){n.cancelBubble=!0},n.target=n.srcElement,n.timeStamp=Date.now(),t["e"+e+r].call(this,n)},t.attachEvent("on"+e,t[e+r]))},window.removeEvent=function(t,e,r){t.removeEventListener?t.removeEventListener(e,r,!1):t.detachEvent&&(t.detachEvent("on"+e,t[e+r]),t[e+r]=null,t["e"+e+r]=null)},function(){function e(t,e){function r(t){return t.length?t.split(/\s+/g):[]}function n(t,e){var n=r(e),o=n.indexOf(t);return o!==-1&&n.splice(o,1),n.join(" ")}if(Object.defineProperties(this,{length:{get:function(){return r(t[e]).length}},item:{value:function(n){var o=r(t[e]);return 0<=n&&n=0&&i.item(e)!==this;);return e>-1})),window.Element&&!Element.prototype.closest&&(Element.prototype.closest=function(t){var e,r=(this.document||this.ownerDocument).querySelectorAll(t),n=this;do for(e=r.length;--e>=0&&r.item(e)!==n;);while(e<0&&(n=n.parentElement));return n});var n={prepend:function(){var t=[].slice.call(arguments);t=r(t),this.insertBefore(t,this.firstChild)},append:function(){var t=[].slice.call(arguments);t=r(t),this.appendChild(t)}};e(t.Document||t.HTMLDocument,n),e(t.DocumentFragment,n),e(t.Element,n);var o={before:function(){var t=[].slice.call(arguments),e=this.parentNode;if(e){for(var n=this.previousSibling;t.indexOf(n)!==-1;)n=n.previousSibling;var o=r(t);e.insertBefore(o,n?n.nextSibling:e.firstChild)}},after:function(){var t=[].slice.call(arguments),e=this.parentNode;if(e){for(var n=this.nextSibling;t.indexOf(n)!==-1;)n=n.nextSibling;var o=r(t);e.insertBefore(o,n)}},replaceWith:function(){var t=[].slice.call(arguments),e=this.parentNode;if(e){for(var n=this.nextSibling;t.indexOf(n)!==-1;)n=n.nextSibling;var o=r(t);this.parentNode===e?e.replaceChild(o,this):e.insertBefore(o,n)}},remove:function(){this.parentNode&&this.parentNode.removeChild(this)}};e(t.DocumentType,o),e(t.Element,o),e(t.CharacterData,o)}}(self),function(t){"use strict";"window"in t&&"document"in t&&(t.XMLHttpRequest=t.XMLHttpRequest||function(){try{return new ActiveXObject("Msxml2.XMLHTTP.6.0")}catch(t){}try{return new ActiveXObject("Msxml2.XMLHTTP.3.0")}catch(t){}try{return new ActiveXObject("Msxml2.XMLHTTP")}catch(t){}throw Error("This browser does not support XMLHttpRequest.")},[["UNSENT",0],["OPENED",1],["HEADERS_RECEIVED",2],["LOADING",3],["DONE",4]].forEach(function(e){e[0]in t.XMLHttpRequest||(t.XMLHttpRequest[e[0]]=e[1])}),function(){function e(t){if(this._data=[],t)for(var e=0;e=t.length)return{done:!0,value:void 0};var n=t[r++];return{done:!1,value:"key"===e?n.name:"value"===e?n.value:[n.name,n.value]}}}function c(e,r){function n(){var t=c.href.replace(/#$|\?$|\?(?=#)/g,"");c.href!==t&&(c.href=t)}function u(){h._setList(c.search?o(c.search.substring(1)):[]),h._update_steps()}if(!(this instanceof t.URL))throw new TypeError("Failed to construct 'URL': Please use the 'new' operator.");r&&(e=function(){if(s)return new f(e,r).href;var t;if(document.implementation&&document.implementation.createHTMLDocument?t=document.implementation.createHTMLDocument(""):document.implementation&&document.implementation.createDocument?(t=document.implementation.createDocument("http://www.w3.org/1999/xhtml","html",null),t.documentElement.appendChild(t.createElement("head")),t.documentElement.appendChild(t.createElement("body"))):window.ActiveXObject&&(t=new window.ActiveXObject("htmlfile"),t.write(""),t.close()),!t)throw Error("base not supported");var n=t.createElement("base");n.href=r,t.getElementsByTagName("head")[0].appendChild(n);var o=t.createElement("a");return o.href=e,o.href}());var c=i(e||""),l=function(){if(!("defineProperties"in Object))return!1;try{var t={};return Object.defineProperties(t,{prop:{get:function(){return!0}}}),t.prop}catch(e){return!1}}(),p=l?this:document.createElement("a"),h=new a(c.search?c.search.substring(1):null);return h._url_object=p,Object.defineProperties(p,{href:{get:function(){return c.href},set:function(t){c.href=t,n(),u()},enumerable:!0,configurable:!0},origin:{get:function(){return"origin"in c?c.origin:this.protocol+"//"+this.host},enumerable:!0,configurable:!0},protocol:{get:function(){return c.protocol},set:function(t){c.protocol=t},enumerable:!0,configurable:!0},username:{get:function(){return c.username},set:function(t){c.username=t},enumerable:!0,configurable:!0},password:{get:function(){return c.password},set:function(t){c.password=t},enumerable:!0,configurable:!0},host:{get:function(){var t={"http:":/:80$/,"https:":/:443$/,"ftp:":/:21$/}[c.protocol];return t?c.host.replace(t,""):c.host},set:function(t){c.host=t},enumerable:!0,configurable:!0},hostname:{get:function(){return c.hostname},set:function(t){c.hostname=t},enumerable:!0,configurable:!0},port:{get:function(){return c.port},set:function(t){c.port=t},enumerable:!0,configurable:!0},pathname:{get:function(){return"/"!==c.pathname.charAt(0)?"/"+c.pathname:c.pathname},set:function(t){c.pathname=t},enumerable:!0,configurable:!0},search:{get:function(){return c.search},set:function(t){c.search!==t&&(c.search=t,n(),u())},enumerable:!0,configurable:!0},searchParams:{get:function(){return h},enumerable:!0,configurable:!0},hash:{get:function(){return c.hash},set:function(t){c.hash=t,n()},enumerable:!0,configurable:!0},toString:{value:function(){return c.toString()},enumerable:!1,configurable:!0},valueOf:{value:function(){return c.valueOf()},enumerable:!1,configurable:!0}}),p}var s,f=t.URL;try{if(f){if(s=new t.URL("http://example.com"),"searchParams"in s)return;"href"in s||(s=void 0)}}catch(l){}if(Object.defineProperties(a.prototype,{append:{value:function(t,e){this._list.push({name:t,value:e}),this._update_steps()},writable:!0,enumerable:!0,configurable:!0},"delete":{value:function(t){for(var e=0;e1?arguments[1]:void 0;this._list.forEach(function(r,n){t.call(e,r.value,r.name)})},writable:!0,enumerable:!0,configurable:!0},toString:{value:function(){return n(this._list)},writable:!0,enumerable:!1,configurable:!0}}),"Symbol"in t&&"iterator"in t.Symbol&&(Object.defineProperty(a.prototype,t.Symbol.iterator,{value:a.prototype.entries,writable:!0,enumerable:!0,configurable:!0}),Object.defineProperty(u.prototype,t.Symbol.iterator,{value:function(){return this},writable:!0,enumerable:!0,configurable:!0})),f)for(var p in f)f.hasOwnProperty(p)&&"function"==typeof f[p]&&(c[p]=f[p]);t.URL=c,t.URLSearchParams=a}(),function(){if("1"!==new t.URLSearchParams([["a",1]]).get("a")||"1"!==new t.URLSearchParams({a:1}).get("a")){var n=t.URLSearchParams;t.URLSearchParams=function(t){if(t&&"object"==typeof t&&e(t)){var o=new n;return r(t).forEach(function(t){if(!e(t))throw TypeError();var n=r(t);if(2!==n.length)throw TypeError();o.append(n[0],n[1])}),o}return t&&"object"==typeof t?(o=new n,Object.keys(t).forEach(function(e){o.set(e,t[e])}),o):new n(t)}}}()}(self),function(t){"use strict";function e(t){if(t=String(t),t.match(/[^\x00-\xFF]/))throw TypeError("Not a valid ByteString");return t}function r(t){return t=String(t),t.replace(/([\u0000-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDFFF])/g,function(t){return/^[\uD800-\uDFFF]$/.test(t)?"�":t})}function n(t){return 65535&t}function o(t){return String(t).replace(/[a-z]/g,function(t){return t.toUpperCase()})}function i(t){return t=o(t),"CONNECT"===t||"TRACE"===t||"TRACK"===t}function a(t){var e=o(t);return"DELETE"===e||"GET"===e||"HEAD"===e||"OPTIONS"===e||"POST"===e||"PUT"===e?e:t}function u(t){return/^[!#$%&'*+\-.09A-Z^_`a-z|~]+$/.test(t)}function c(t){return!0}function s(t){t=String(t).toLowerCase();var e={"accept-charset":!0,"accept-encoding":!0,"access-control-request-headers":!0,"access-control-request-method":!0,connection:!0,"content-length":!0,cookie:!0,cookie2:!0,date:!0,dnt:!0,expect:!0,host:!0,"keep-alive":!0,origin:!0,referer:!0,te:!0,trailer:!0,"transfer-encoding":!0,upgrade:!0,"user-agent":!0,via:!0};return e[t]||"proxy-"===t.substring(0,6)||"sec-"===t.substring(0,4)}function f(t){t=String(t).toLowerCase();var e={"set-cookie":!0,"set-cookie2":!0};return e[t]}function l(t,e){return t=String(t).toLowerCase(),"accept"===t||"accept-language"===t||"content-language"===t||"content-type"===t&&["application/x-www-form-encoded","multipart/form-data","text/plain"].indexOf(e)!==-1}function p(t){this._guard="none",this._headerList=[],t&&h(this,t)}function h(t,e){e instanceof p?e._headerList.forEach(function(e){t.append(e[0],e[1])}):Array.isArray(e)?e.forEach(function(e){if(!Array.isArray(e)||2!==e.length)throw TypeError();t.append(e[0],e[1])}):(e=Object(e),Object.keys(e).forEach(function(r){t.append(r,e[r])}))}function y(t){this._headers=t, 3 | this._index=0}function v(t){this._stream=t,this.bodyUsed=!1}function d(t,n){if(arguments.length<1)throw TypeError("Not enough arguments");if(v.call(this,null),this.method="GET",this.url="",this.headers=new p,this.headers._guard="request",this.referrer=null,this.mode=null,this.credentials="omit",t instanceof d){if(t.bodyUsed)throw TypeError();t.bodyUsed=!0,this.method=t.method,this.url=t.url,this.headers=new p(t.headers),this.headers._guard=t.headers._guard,this.credentials=t.credentials,this._stream=t._stream}else t=r(t),this.url=String(new URL(t,self.location));if(n=Object(n),"method"in n){var o=e(n.method);if(i(o))throw TypeError();this.method=a(o)}"headers"in n&&(this.headers=new p,h(this.headers,n.headers)),"body"in n&&(this._stream=n.body),"credentials"in n&&["omit","same-origin","include"].indexOf(n.credentials)!==-1&&(this.credentials=n.credentials)}function m(t,e){if(arguments.length<1&&(t=""),this.headers=new p,this.headers._guard="response",t instanceof XMLHttpRequest&&"_url"in t){var o=t;return this.type="basic",this.url=r(o._url),this.status=o.status,this.ok=200<=this.status&&this.status<=299,this.statusText=o.statusText,o.getAllResponseHeaders().split(/\r?\n/).filter(function(t){return t.length}).forEach(function(t){var e=t.indexOf(":");this.headers.append(t.substring(0,e),t.substring(e+2))},this),void v.call(this,o.responseText)}v.call(this,t),e=Object(e)||{},this.url="";var i="status"in e?n(e.status):200;if(i<200||i>599)throw RangeError();this.status=i,this.ok=200<=this.status&&this.status<=299;var a="statusText"in e?String(e.statusText):"OK";if(/[^\x00-\xFF]/.test(a))throw TypeError();this.statusText=a,"headers"in e&&h(this.headers,e),this.type="basic"}function b(t,e){return new Promise(function(r,n){var o=new d(t,e),i=new XMLHttpRequest,a=!0;i._url=o.url;try{i.open(o.method,o.url,a)}catch(u){throw TypeError(u.message)}for(var c=o.headers[Symbol.iterator](),s=c.next();!s.done;s=c.next())i.setRequestHeader(s.value[0],s.value[1]);"include"===o.credentials&&(i.withCredentials=!0),i.onreadystatechange=function(){i.readyState===XMLHttpRequest.DONE&&(0===i.status?n(new TypeError("Network error")):r(new m(i)))},i.send(o._stream)})}p.prototype={append:function(t,r){if(t=e(t),!u(t)||!c(r))throw TypeError();if("immutable"===this._guard)throw TypeError();"request"===this._guard&&s(t)||("request-no-CORS"!==this._guard||l(t,r))&&("response"===this._guard&&f(t)||(t=t.toLowerCase(),this._headerList.push([t,r])))},"delete":function(t){if(t=e(t),!u(t))throw TypeError();if("immutable"===this._guard)throw TypeError();if(("request"!==this._guard||!s(t))&&("request-no-CORS"!==this._guard||l(t,"invalid"))&&("response"!==this._guard||!f(t))){t=t.toLowerCase();for(var r=0;r=this._headers._headerList.length?{value:void 0,done:!0}:{value:this._headers._headerList[this._index++],done:!1}},y.prototype[Symbol.iterator]=function(){return this},v.prototype={arrayBuffer:function(){if(this.bodyUsed)return Promise.reject(TypeError());if(this.bodyUsed=!0,this._stream instanceof ArrayBuffer)return Promise.resolve(this._stream);var t=this._stream;return new Promise(function(e,r){var n=unescape(encodeURIComponent(t)).split("").map(function(t){return t.charCodeAt(0)});e(new Uint8Array(n).buffer)})},blob:function(){return this.bodyUsed?Promise.reject(TypeError()):(this.bodyUsed=!0,this._stream instanceof Blob?Promise.resolve(this._stream):Promise.resolve(new Blob([this._stream])))},formData:function(){return this.bodyUsed?Promise.reject(TypeError()):(this.bodyUsed=!0,this._stream instanceof FormData?Promise.resolve(this._stream):Promise.reject(Error("Not yet implemented")))},json:function(){if(this.bodyUsed)return Promise.reject(TypeError());this.bodyUsed=!0;var t=this;return new Promise(function(e,r){e(JSON.parse(t._stream))})},text:function(){return this.bodyUsed?Promise.reject(TypeError()):(this.bodyUsed=!0,Promise.resolve(String(this._stream)))}},d.prototype=v.prototype,m.prototype=v.prototype,m.redirect=function(){throw Error("Not supported")},"fetch"in t||(t.Headers=p,t.Request=d,t.Response=m,t.fetch=b)}(self); 4 | -------------------------------------------------------------------------------- /packages/web/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miguelcast/cra-init-dashboard/52ef1e8832cfe0da798cec78e50406234f55eeac/packages/web/screenshot.png -------------------------------------------------------------------------------- /packages/web/src/components/Auth/Login.js: -------------------------------------------------------------------------------- 1 | import { Button, Form, Icon, Input } from 'antd'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | import { useTranslation } from 'react-i18next'; 5 | import { Link } from 'react-router-dom'; 6 | import { Title } from '../Shared'; 7 | 8 | function Login(props) { 9 | const { loading } = props; 10 | const { getFieldDecorator } = props.form; 11 | const { t } = useTranslation(); 12 | const handleSubmit = e => { 13 | e.preventDefault(); 14 | props.form.validateFields((err, values) => { 15 | if (!err) { 16 | props.authentication(values.userName, values.password); 17 | } 18 | }); 19 | }; 20 | 21 | return ( 22 |
23 | 24 | <Form.Item> 25 | {getFieldDecorator('userName', { 26 | rules: [ 27 | { required: true, message: t('common.usernameRequired') }, 28 | { type: 'email', message: t('common.invalidEmail') }, 29 | ], 30 | })( 31 | <Input 32 | prefix={<Icon type="user" className="custom-prefix-icon" />} 33 | placeholder={t('common.email')} 34 | size="large" 35 | />, 36 | )} 37 | </Form.Item> 38 | <Form.Item> 39 | {getFieldDecorator('password', { 40 | rules: [{ required: true, message: t('common.passwordRequired') }], 41 | })( 42 | <Input 43 | prefix={<Icon type="lock" className="custom-prefix-icon" />} 44 | type="password" 45 | size="large" 46 | placeholder={t('common.password')} 47 | />, 48 | )} 49 | </Form.Item> 50 | <Form.Item> 51 | <Link to="/forgotPassword" className="custom-forgot-link"> 52 | {t('login.forgotPassword')} 53 | </Link> 54 | <Button 55 | type="primary" 56 | size="large" 57 | htmlType="submit" 58 | loading={loading} 59 | className="custom-button"> 60 | {t('login.login')} 61 | </Button> 62 | {t('common.or')} <Link to="/register">{t('login.registerNow')}</Link> 63 | </Form.Item> 64 | </Form> 65 | ); 66 | } 67 | 68 | const formShape = { 69 | validateFields: PropTypes.func, 70 | getFieldDecorator: PropTypes.func, 71 | }; 72 | 73 | Login.propTypes = { 74 | form: PropTypes.shape(formShape).isRequired, 75 | loading: PropTypes.bool, 76 | authentication: PropTypes.func, 77 | }; 78 | 79 | export default Form.create({ name: 'normal_login' })(Login); 80 | -------------------------------------------------------------------------------- /packages/web/src/components/Auth/Register.js: -------------------------------------------------------------------------------- 1 | import { Button, Form, Icon, Input } from 'antd'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | import { useTranslation } from 'react-i18next'; 5 | import { Title } from '../Shared'; 6 | 7 | const formShape = { 8 | validateFields: PropTypes.func, 9 | getFieldDecorator: PropTypes.func, 10 | }; 11 | 12 | function Register(props) { 13 | const { getFieldDecorator } = props.form; 14 | const { t } = useTranslation(); 15 | 16 | const handleSubmit = e => { 17 | e.preventDefault(); 18 | props.form.validateFields((err, values) => { 19 | if (!err) { 20 | console.log('Received values of form: ', values); 21 | } 22 | }); 23 | }; 24 | 25 | return ( 26 | <Form onSubmit={handleSubmit} className="custom-form-register"> 27 | <Title text={t('register.title')} /> 28 | <Form.Item> 29 | {getFieldDecorator('userName', { 30 | rules: [ 31 | { required: true, message: t('common.usernameRequired') }, 32 | { type: 'email', message: t('common.invalidEmail') }, 33 | ], 34 | })( 35 | <Input 36 | prefix={<Icon type="mail" className="custom-prefix-icon" />} 37 | placeholder={t('common.email')} 38 | size="large" 39 | />, 40 | )} 41 | </Form.Item> 42 | <Form.Item> 43 | {getFieldDecorator('name', { 44 | rules: [{ required: true, message: t('register.nameRequired') }], 45 | })( 46 | <Input 47 | prefix={<Icon type="user" className="custom-prefix-icon" />} 48 | placeholder={t('register.name')} 49 | size="large" 50 | />, 51 | )} 52 | </Form.Item> 53 | <Form.Item> 54 | {getFieldDecorator('password', { 55 | rules: [{ required: true, message: t('common.passwordRequired') }], 56 | })( 57 | <Input 58 | prefix={<Icon type="lock" className="custom-prefix-icon" />} 59 | type="password" 60 | size="large" 61 | placeholder={t('common.password')} 62 | />, 63 | )} 64 | </Form.Item> 65 | <Form.Item> 66 | <Button 67 | className="custom-button" 68 | type="primary" 69 | size="large" 70 | htmlType="submit"> 71 | {t('register.signIn')} 72 | </Button> 73 | </Form.Item> 74 | </Form> 75 | ); 76 | } 77 | 78 | Register.propTypes = { 79 | form: PropTypes.shape(formShape).isRequired, 80 | }; 81 | 82 | export default Form.create({ name: 'register' })(Register); 83 | -------------------------------------------------------------------------------- /packages/web/src/components/Auth/hooks.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import store from '../../config/store'; 3 | 4 | export function useAuthenticated() { 5 | const [auth, setAuth] = useState(store.getState().auth); 6 | 7 | useEffect(() => { 8 | const unsubscribe = store.subscribe(() => { 9 | const newAuth = store.getState().auth; 10 | if (auth !== newAuth) { 11 | setAuth(newAuth); 12 | } 13 | }); 14 | return unsubscribe; 15 | }, [auth]); 16 | return { ...(auth || {}) }; 17 | } 18 | -------------------------------------------------------------------------------- /packages/web/src/components/Auth/index.js: -------------------------------------------------------------------------------- 1 | export { default as Login } from './Login'; 2 | export { default as Register } from './Register'; 3 | export { useAuthenticated } from './hooks'; 4 | -------------------------------------------------------------------------------- /packages/web/src/components/Layout/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Icon, Layout, Row, Col } from 'antd'; 3 | import PropTypes from 'prop-types'; 4 | import MenuHeader from './MenuHeader'; 5 | 6 | const Header = ({ pathname, isCollapsed, showDrawer, drawerVisible }) => ( 7 | <Layout.Header className="custom-header"> 8 | <Row type="flex" align="middle" justify="space-between"> 9 | <Col> 10 | {isCollapsed && ( 11 | <Icon 12 | type={drawerVisible ? 'menu-fold' : 'menu-unfold'} 13 | onClick={showDrawer} 14 | className="custom-header-toggle-icon" 15 | /> 16 | )} 17 | </Col> 18 | <Col span={18} order={2} className="custom-align-right"> 19 | <Row type="flex" align="middle" justify="end"> 20 | <MenuHeader isCollapse={isCollapsed} pathname={pathname} /> 21 | </Row> 22 | </Col> 23 | </Row> 24 | </Layout.Header> 25 | ); 26 | 27 | Header.propTypes = { 28 | pathname: PropTypes.string, 29 | isCollapsed: PropTypes.bool, 30 | drawerVisible: PropTypes.bool, 31 | showDrawer: PropTypes.func, 32 | }; 33 | 34 | Header.defaultProps = { 35 | isCollapsed: false, 36 | drawerVisible: false, 37 | }; 38 | 39 | export default Header; 40 | -------------------------------------------------------------------------------- /packages/web/src/components/Layout/HeaderUser.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Avatar, Dropdown, Menu, Icon } from 'antd'; 4 | import { connect } from 'react-redux'; 5 | import { useTranslation } from 'react-i18next'; 6 | 7 | const MenuDrop = ({ logout }) => { 8 | const { t } = useTranslation(); 9 | return ( 10 | <Menu> 11 | <Menu.Item onClick={logout}> 12 | <Icon type="logout" /> 13 | {t('common.logout')} 14 | </Menu.Item> 15 | </Menu> 16 | ); 17 | }; 18 | MenuDrop.propTypes = { 19 | logout: PropTypes.func, 20 | }; 21 | 22 | const HeaderUser = ({ logout, user }) => { 23 | return ( 24 | <Dropdown 25 | overlay={() => <MenuDrop logout={logout} />} 26 | placement="bottomRight"> 27 | <div className="custom-header-user"> 28 | <Avatar icon="user" /> 29 | <span style={{ paddingLeft: 8 }}>{user.name || user.username}</span> 30 | </div> 31 | </Dropdown> 32 | ); 33 | }; 34 | 35 | HeaderUser.propTypes = { 36 | logout: PropTypes.func, 37 | user: PropTypes.object, 38 | }; 39 | 40 | HeaderUser.displayName = 'HeaderUser'; 41 | 42 | export default connect( 43 | state => ({ user: state.auth.user }), 44 | dispatch => ({ 45 | logout: () => dispatch.auth.logout(), 46 | }), 47 | )(HeaderUser); 48 | -------------------------------------------------------------------------------- /packages/web/src/components/Layout/Logo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import logo from '../../img/logo.png'; 3 | 4 | const Logo = () => ( 5 | <div className="custom-logo"> 6 | <img src={logo} alt="cra-rr4-redux-antd" /> 7 | </div> 8 | ); 9 | 10 | export default Logo; 11 | -------------------------------------------------------------------------------- /packages/web/src/components/Layout/MenuHeader.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { Menu, Icon } from 'antd'; 3 | import PropTypes from 'prop-types'; 4 | import { Link } from 'react-router-dom'; 5 | import { useTranslation } from 'react-i18next'; 6 | import { useMenu } from './hooks'; 7 | 8 | const MenuHeader = ({ pathname, isCollapse, ...rest }) => { 9 | const menus = useMenu('header'); 10 | const { t } = useTranslation(); 11 | return ( 12 | <Fragment> 13 | <Menu 14 | mode="horizontal" 15 | selectedKeys={[pathname || '/']} 16 | style={{ height: 64, marginTop: isCollapse ? '-3px' : 0 }} 17 | {...rest}> 18 | {menus.map( 19 | ({ path, icon, title }) => 20 | path && ( 21 | <Menu.Item key={path} className="custom-menu-item-header"> 22 | <Link to={path}> 23 | {icon && ( 24 | <Icon 25 | type={icon} 26 | className="custom-menu-header-item-icon" 27 | /> 28 | )} 29 | <span className="nav-text custom-align-middle"> 30 | {t(title)} 31 | </span> 32 | </Link> 33 | </Menu.Item> 34 | ), 35 | )} 36 | </Menu> 37 | {menus.map( 38 | ({ component, index }) => 39 | component && React.createElement(component(), { key: index }, null), 40 | )} 41 | </Fragment> 42 | ); 43 | }; 44 | 45 | MenuHeader.propTypes = { 46 | pathname: PropTypes.string, 47 | isCollapse: PropTypes.bool, 48 | }; 49 | 50 | export default MenuHeader; 51 | -------------------------------------------------------------------------------- /packages/web/src/components/Layout/MenuPrimary.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { Menu, Icon } from 'antd'; 3 | import PropTypes from 'prop-types'; 4 | import { useTranslation } from 'react-i18next'; 5 | import { Link } from 'react-router-dom'; 6 | import { useMenu } from './hooks'; 7 | 8 | const MenuPrimary = ({ pathname, ...rest }) => { 9 | const menus = useMenu('primary'); 10 | const { t } = useTranslation(); 11 | return ( 12 | <Fragment> 13 | <Menu mode="inline" selectedKeys={[pathname || '/']} {...rest}> 14 | {menus.map( 15 | item => 16 | item.path && ( 17 | <Menu.Item key={item.path}> 18 | <Link to={item.path}> 19 | {item.icon && ( 20 | <Icon type={item.icon} style={{ fontSize: '1.2rem' }} /> 21 | )} 22 | <span className="nav-text">{t(item.title)}</span> 23 | </Link> 24 | </Menu.Item> 25 | ), 26 | )} 27 | </Menu> 28 | {menus.map( 29 | ({ component, index }) => 30 | component && React.createElement(component(), { key: index }, null), 31 | )} 32 | </Fragment> 33 | ); 34 | }; 35 | 36 | MenuPrimary.propTypes = { 37 | pathname: PropTypes.string, 38 | }; 39 | 40 | export default MenuPrimary; 41 | -------------------------------------------------------------------------------- /packages/web/src/components/Layout/hooks.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { useAuthenticated } from '../Auth'; 3 | import { GUEST, LOGGED } from '../../config/constants'; 4 | import myMenus from '../../config/menus'; 5 | 6 | export function useMenu(position) { 7 | const { isAuthenticated } = useAuthenticated(); 8 | const [menus, setMenus] = useState(myMenus[position]); 9 | 10 | useEffect(() => { 11 | setMenus( 12 | myMenus[position].filter(menu => { 13 | return ( 14 | menu.when === undefined || 15 | (isAuthenticated === false && menu.when === GUEST) || 16 | (isAuthenticated === true && menu.when === LOGGED) 17 | ); 18 | }), 19 | ); 20 | }, [isAuthenticated, position]); 21 | 22 | return menus; 23 | } 24 | -------------------------------------------------------------------------------- /packages/web/src/components/Layout/index.js: -------------------------------------------------------------------------------- 1 | export { default as MenuPrimary } from './MenuPrimary'; 2 | export { default as Logo } from './Logo'; 3 | export { default as Header } from './Header'; 4 | export { default as HeaderUser } from './HeaderUser'; 5 | -------------------------------------------------------------------------------- /packages/web/src/components/Shared/ChangeLanguage.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { Button } from 'antd'; 3 | import '../../config/localization/i18n'; 4 | import { useTranslation } from 'react-i18next'; 5 | import { LANGUAGES } from '../../config/constants'; 6 | 7 | const ChangeLanguage = () => { 8 | const { t, i18n } = useTranslation(); 9 | return ( 10 | <Fragment> 11 | <Button 12 | htmlType="button" 13 | type="primary" 14 | style={{ marginRight: 5 }} 15 | onClick={() => i18n.changeLanguage(LANGUAGES.es)}> 16 | {t('common.spanish')} 17 | </Button> 18 | <Button 19 | htmlType="button" 20 | type="primary" 21 | onClick={() => i18n.changeLanguage(LANGUAGES.en)}> 22 | {t('common.english')} 23 | </Button>{' '} 24 | </Fragment> 25 | ); 26 | }; 27 | 28 | export default ChangeLanguage; 29 | -------------------------------------------------------------------------------- /packages/web/src/components/Shared/Title.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Title = ({ text }) => <h1 className="custom-title">{text}</h1>; 5 | 6 | Title.propTypes = { 7 | text: PropTypes.string.isRequired, 8 | }; 9 | 10 | export default Title; 11 | -------------------------------------------------------------------------------- /packages/web/src/components/Shared/index.js: -------------------------------------------------------------------------------- 1 | export { default as Title } from './Title'; 2 | export { default as ChangeLanguage } from './ChangeLanguage'; 3 | -------------------------------------------------------------------------------- /packages/web/src/config/constants.js: -------------------------------------------------------------------------------- 1 | export const GUEST = 'guest'; 2 | export const LOGGED = 'logged'; 3 | 4 | export const URL_LOCALIZATION = '/locales/{{lng}}/{{ns}}.json'; 5 | export const VERSION_LOCALIZATION = 5; 6 | export const TIME_CACHE_LOCALIZATION = 7 * 24 * 60 * 60 * 1000; 7 | export const LANGUAGES = { 8 | es: 'es', 9 | en: 'en', 10 | default: 'en', 11 | }; 12 | -------------------------------------------------------------------------------- /packages/web/src/config/cruds/user.js: -------------------------------------------------------------------------------- 1 | const user = { 2 | keyName: 'key', 3 | getList: { url: '/users' }, 4 | getByKey: { url: '/user/{keyName}' }, 5 | upsert: { url: '/postUser.json' }, 6 | delete: { url: '/deleteUser.json' }, 7 | fields: [ 8 | { 9 | title: 'Name', 10 | key: 'name', 11 | sorter: true, 12 | filter: true, 13 | type: 'string', 14 | initialValue: 'My Name', 15 | rules: [ 16 | { required: true, message: 'Is required!' }, 17 | { type: 'string', message: 'Should be string!' }, 18 | { max: 50, message: 'Max 50 characters!' }, 19 | ], 20 | }, 21 | { 22 | title: 'Age', 23 | key: 'age', 24 | sorter: true, 25 | filter: true, 26 | type: 'number', 27 | columnStyle: { 28 | align: 'right', 29 | width: 80, 30 | }, 31 | rules: [{ type: 'integer', message: 'Should be integer!' }], 32 | }, 33 | { 34 | title: 'Address', 35 | key: 'address', 36 | sorter: true, 37 | filter: true, 38 | hidden: ['column', 'form'], 39 | type: 'string', 40 | rules: [ 41 | { required: true, message: 'Is required!' }, 42 | { max: 150, message: 'Max 150 characters!' }, 43 | ], 44 | }, 45 | { 46 | title: 'Color', 47 | key: 'color', 48 | sorter: true, 49 | filter: true, 50 | type: 'select', 51 | options: { 52 | red: 'Red', 53 | green: 'Green', 54 | yellow: 'Yellow', 55 | black: 'Black', 56 | }, 57 | rules: [{ required: true, message: 'Is required!' }], 58 | }, 59 | { 60 | title: 'Country async load', 61 | key: 'country', 62 | columnKey: 'countryName', 63 | sorter: true, 64 | filter: true, 65 | type: 'select', 66 | options: {}, 67 | configOptions: { 68 | url: '/countries', 69 | // { key: text }, mapper with loaded data 70 | map: item => ({ [item.key]: item.name }), 71 | // default get, you can use get or post; 72 | method: 'get', 73 | }, 74 | dependencies: { 75 | fields: ['color'], 76 | onChange: () => ({ 77 | disabled: false, 78 | }), 79 | }, 80 | disabled: true, 81 | rules: [{ required: true, message: 'Is required!' }], 82 | }, 83 | { 84 | title: 'Gender', 85 | key: 'gender', 86 | type: 'radio', 87 | sorter: true, 88 | filter: true, 89 | options: { 90 | male: 'Male', 91 | female: 'Female', 92 | }, 93 | rules: [{ required: true, message: 'Is required!' }], 94 | }, 95 | { 96 | title: 'Birthday', 97 | key: 'birthday', 98 | type: 'date', 99 | sorter: true, 100 | filter: true, 101 | }, 102 | { 103 | title: 'Status', 104 | key: 'status', 105 | sorter: true, 106 | filter: true, 107 | type: 'bool', 108 | initialValue: true, 109 | options: { 110 | true: 'Active', 111 | false: 'Inactive', 112 | }, 113 | }, 114 | ], 115 | }; 116 | 117 | export default user; 118 | -------------------------------------------------------------------------------- /packages/web/src/config/localization/antdLocale.js: -------------------------------------------------------------------------------- 1 | /** 2 | * List of Ant design locales 3 | * https://ant.design/docs/react/i18n 4 | */ 5 | import { LANGUAGES } from '../constants'; 6 | 7 | export default (language = LANGUAGES.default) => { 8 | switch (language) { 9 | case LANGUAGES.es: { 10 | return require('antd/lib/locale-provider/es_ES').default; 11 | } 12 | default: 13 | case LANGUAGES.en: { 14 | return require('antd/lib/locale-provider/en_US').default; 15 | } 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /packages/web/src/config/localization/i18n.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import i18next from 'i18next'; 3 | import Backend from 'i18next-chained-backend'; 4 | import LocalStorageBackend from 'i18next-localstorage-backend'; 5 | import XHR from 'i18next-xhr-backend'; 6 | import LngDetector from 'i18next-browser-languagedetector'; 7 | import { initReactI18next } from 'react-i18next'; 8 | import { 9 | LANGUAGES, 10 | URL_LOCALIZATION, 11 | TIME_CACHE_LOCALIZATION, 12 | VERSION_LOCALIZATION, 13 | } from '../constants'; 14 | 15 | const URL_API_LOCALIZATION = 16 | process.env.REACT_APP_API_LOCALIZATION || 'http://localhost:3005'; 17 | 18 | function loadLocales(url, options, callback) { 19 | axios 20 | .get(`${url}?v=${options.queryStringParams.v}`, { 21 | headers: { 22 | Accept: 'application/json', 23 | 'Content-Type': 'application/json', 24 | }, 25 | }) 26 | .then(res => { 27 | callback(res.data, { status: '200' }); 28 | }) 29 | .catch(err => { 30 | callback(null, { status: '404', message: err.message }); 31 | }); 32 | } 33 | 34 | export default i18next 35 | .use(LngDetector) 36 | .use(Backend) 37 | .use(initReactI18next) 38 | .init( 39 | { 40 | whitelist: Object.values(LANGUAGES), 41 | fallbackLng: LANGUAGES.default, 42 | // have a common namespace used around the full app 43 | ns: 'translation', 44 | defaultNS: 'translation', 45 | debug: process.env.NODE_ENV === 'development', 46 | backend: { 47 | backends: [LocalStorageBackend, XHR], 48 | backendOptions: [ 49 | { 50 | expirationTime: TIME_CACHE_LOCALIZATION, 51 | prefix: 'i18next_res_', 52 | }, 53 | { 54 | queryStringParams: { v: VERSION_LOCALIZATION }, 55 | parse: data => data, 56 | loadPath: `${URL_API_LOCALIZATION}${URL_LOCALIZATION}`, 57 | ajax: loadLocales, 58 | }, 59 | ], 60 | }, 61 | }, 62 | err => { 63 | if (err) return console.log('something went wrong loading', err); 64 | }, 65 | ); 66 | -------------------------------------------------------------------------------- /packages/web/src/config/menus.js: -------------------------------------------------------------------------------- 1 | import { ChangeLanguage } from '../components/Shared'; 2 | import { createMenu, createComponent } from '../utils/general'; 3 | import { GUEST, LOGGED } from './constants'; 4 | import { HeaderUser } from '../components/Layout'; 5 | 6 | const menus = { 7 | primary: [ 8 | createMenu('/', 'menu.home', 'home'), 9 | createMenu('/about', 'menu.about', 'rocket'), 10 | createMenu('/list', 'menu.users', 'team', LOGGED), 11 | ], 12 | header: [ 13 | createMenu('/register', 'menu.register', 'user', GUEST), 14 | createMenu('/login', 'menu.login', 'login', GUEST), 15 | createComponent(() => ChangeLanguage), 16 | createComponent(() => HeaderUser, LOGGED), 17 | ], 18 | }; 19 | 20 | export default menus; 21 | -------------------------------------------------------------------------------- /packages/web/src/config/routes.js: -------------------------------------------------------------------------------- 1 | import { lazy } from 'react'; 2 | import { createRoute } from '../utils/general'; 3 | import Home from '../pages/Home'; 4 | import { LOGGED, GUEST } from './constants'; 5 | 6 | const AsyncAbout = lazy(() => import('../pages/About.js')); 7 | const AsyncRegister = lazy(() => import('../pages/Register.js')); 8 | const AsyncLogin = lazy(() => import('../pages/Login.js')); 9 | const AsyncForgotPass = lazy(() => import('../pages/ForgotPassword.js')); 10 | const AsyncList = lazy(() => import('../pages/List.js')); 11 | const AsyncForm = lazy(() => import('../pages/Form.js')); 12 | 13 | export default [ 14 | createRoute('/', Home, null, true), 15 | createRoute('/about', AsyncAbout), 16 | createRoute('/register', AsyncRegister, GUEST), 17 | createRoute('/login', AsyncLogin, GUEST), 18 | createRoute('/forgotPassword', AsyncForgotPass, GUEST), 19 | createRoute('/list', AsyncList, LOGGED), 20 | createRoute('/form/:id?', AsyncForm, LOGGED), 21 | ]; 22 | -------------------------------------------------------------------------------- /packages/web/src/config/services.js: -------------------------------------------------------------------------------- 1 | export const auth = { 2 | login: '/auth/login', 3 | logout: '/auth/logout', 4 | }; 5 | -------------------------------------------------------------------------------- /packages/web/src/config/store.js: -------------------------------------------------------------------------------- 1 | import { init } from '@rematch/core'; 2 | import createRematchPersist from '@rematch/persist'; 3 | import createLoadingPlugin from '@rematch/loading'; 4 | import * as models from '../models'; 5 | 6 | const persistPlugin = createRematchPersist({ 7 | whitelist: ['auth'], 8 | keyPrefix: '--persist-key-', 9 | throttle: 500, 10 | version: 1, 11 | }); 12 | 13 | const loadingPlugin = createLoadingPlugin({}); 14 | 15 | const store = init({ 16 | models, 17 | plugins: [persistPlugin, loadingPlugin], 18 | }); 19 | 20 | export default store; 21 | -------------------------------------------------------------------------------- /packages/web/src/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/miguelcast/cra-init-dashboard/52ef1e8832cfe0da798cec78e50406234f55eeac/packages/web/src/img/logo.png -------------------------------------------------------------------------------- /packages/web/src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { BrowserRouter as Router } from 'react-router-dom'; 5 | import { ConfigProvider, Spin } from 'antd'; 6 | import { getPersistor } from '@rematch/persist'; 7 | import { PersistGate } from 'redux-persist/integration/react'; 8 | import { useTranslation } from 'react-i18next'; 9 | import { ProviderEasyCrud } from "react-easy-crud"; 10 | import Layout from './pages/Layout'; 11 | import store from './config/store'; 12 | import client from './services/instance'; 13 | import getLocalesAntd from './config/localization/antdLocale'; 14 | import * as registerServiceWorker from './registerServiceWorker'; 15 | import './styles/index.less'; 16 | import './config/localization/i18n'; 17 | 18 | const persistor = getPersistor(); 19 | 20 | const AppSuspense = () => ( 21 | <Suspense fallback={<Spin size="large" className="custom-layout-spin" />}> 22 | <App /> 23 | </Suspense> 24 | ); 25 | 26 | const App = () => { 27 | const { i18n } = useTranslation(); 28 | return ( 29 | <ConfigProvider locale={getLocalesAntd(i18n.language)}> 30 | <ProviderEasyCrud client={client} type='rest'> 31 | <Provider store={store}> 32 | <PersistGate persistor={persistor}> 33 | <Router> 34 | <Layout /> 35 | </Router> 36 | </PersistGate> 37 | </Provider> 38 | </ProviderEasyCrud> 39 | </ConfigProvider> 40 | ); 41 | }; 42 | 43 | ReactDOM.render(<AppSuspense />, document.getElementById('root')); 44 | registerServiceWorker.register(); 45 | -------------------------------------------------------------------------------- /packages/web/src/models/auth.js: -------------------------------------------------------------------------------- 1 | import instance from '../services/instance'; 2 | import { authenticationService } from '../services/auth'; 3 | 4 | const auth = { 5 | state: { 6 | token: null, 7 | user: {}, 8 | isAuthenticated: false, 9 | }, 10 | reducers: { 11 | setAuthenticated: (state, payload) => ({ 12 | ...state, 13 | ...payload, 14 | isAuthenticated: !!payload.token, 15 | }), 16 | setLogout: () => ({ 17 | token: null, 18 | user: {}, 19 | isAuthenticated: false, 20 | }), 21 | }, 22 | effects: { 23 | async authentication(credentials) { 24 | try { 25 | const promise = await authenticationService( 26 | credentials.username, 27 | credentials.password, 28 | ); 29 | const { user, token } = await promise.data; 30 | this.setAuthenticated({ user, token }); 31 | instance.setToken(token); 32 | } catch (e) { 33 | this.setAuthenticated({ user: {}, token: null }); 34 | } 35 | }, 36 | logout() { 37 | instance.removeToken(); 38 | this.setLogout(); 39 | }, 40 | }, 41 | }; 42 | 43 | export default auth; 44 | -------------------------------------------------------------------------------- /packages/web/src/models/home.js: -------------------------------------------------------------------------------- 1 | const home = { 2 | state: 'Hello from store.', 3 | }; 4 | 5 | export default home; 6 | -------------------------------------------------------------------------------- /packages/web/src/models/index.js: -------------------------------------------------------------------------------- 1 | export { default as home } from './home'; 2 | export { default as auth } from './auth'; 3 | -------------------------------------------------------------------------------- /packages/web/src/pages/404.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Row, Col } from 'antd'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | const NotFound404 = () => { 6 | const { t } = useTranslation(); 7 | 8 | return ( 9 | <Row 10 | type="flex" 11 | align="middle" 12 | justify="center" 13 | style={{ minHeight: 'calc(100vh - 200px)' }}> 14 | <Col> 15 | <strong style={{ fontSize: '1.5rem' }}> 16 | {t('common.pageNotFound')} | 404 17 | </strong> 18 | </Col> 19 | </Row> 20 | ); 21 | }; 22 | 23 | export default NotFound404; 24 | -------------------------------------------------------------------------------- /packages/web/src/pages/About.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from 'antd'; 3 | import { Link } from 'react-router-dom'; 4 | import { useTranslation } from 'react-i18next'; 5 | import { Title } from '../components/Shared'; 6 | 7 | const About = () => { 8 | const { t } = useTranslation(); 9 | return ( 10 | <div> 11 | <Title text={t('about.title')} /> 12 | <Link to="/"> 13 | <Button htmlType="button" icon="home" type="primary" size="large"> 14 | {t('about.goToHome')} 15 | </Button> 16 | </Link> 17 | </div> 18 | ); 19 | }; 20 | 21 | export default About; 22 | -------------------------------------------------------------------------------- /packages/web/src/pages/ForgotPassword.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Button, Form, Icon, Input } from 'antd'; 4 | import { withRouter } from 'react-router-dom'; 5 | import { useTranslation } from 'react-i18next'; 6 | import { Title } from '../components/Shared'; 7 | 8 | const FormItem = Form.Item; 9 | 10 | const ForgotPassword = ({ form, history }) => { 11 | const { getFieldDecorator } = form; 12 | const { t } = useTranslation(); 13 | 14 | function onSubmit(e) { 15 | e.preventDefault(); 16 | form.validateFields((err, values) => { 17 | if (!err) { 18 | console.log('Received values of form: ', values); 19 | } 20 | }); 21 | } 22 | 23 | return ( 24 | <Form onSubmit={onSubmit} className="custom-form-forgot"> 25 | <Title text={t('forgotPassword.title')} /> 26 | <span>{t('forgotPassword.description')}</span> 27 | <FormItem label={t('common.email')}> 28 | {getFieldDecorator('email', { 29 | rules: [ 30 | { required: true, message: t('common.emailRequired') }, 31 | { type: 'email', message: t('common.invalidEmail') }, 32 | ], 33 | })( 34 | <Input 35 | prefix={<Icon type="mail" className="custom-prefix-icon" />} 36 | placeholder={t('common.email')} 37 | size="large" 38 | />, 39 | )} 40 | </FormItem> 41 | <FormItem> 42 | <Button 43 | className="custom-button" 44 | type="primary" 45 | size="large" 46 | htmlType="submit"> 47 | {t('forgotPassword.send')} 48 | </Button> 49 | </FormItem> 50 | <FormItem> 51 | <Button 52 | htmlType="button" 53 | onClick={history.goBack} 54 | className="custom-button" 55 | size="large"> 56 | {t('common.return')} 57 | </Button> 58 | </FormItem> 59 | </Form> 60 | ); 61 | }; 62 | 63 | ForgotPassword.propTypes = { 64 | form: PropTypes.object, 65 | history: PropTypes.object, 66 | }; 67 | 68 | export default withRouter(Form.create()(ForgotPassword)); 69 | -------------------------------------------------------------------------------- /packages/web/src/pages/Form.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Form as FormCrud, useCrudForm } from 'react-easy-crud'; 4 | import userConfig from '../config/cruds/user'; 5 | 6 | const Form = ({ match }) => { 7 | console.log(match.params.id); 8 | const propsForm = useCrudForm(userConfig, match.params.id || null); 9 | if (match.params.id > 0 && !propsForm.fields[1].hasOwnProperty('value')) { 10 | return 'Loading...'; 11 | } 12 | return ( 13 | <FormCrud title="Add new user" rowKey={userConfig.keyName} {...propsForm} /> 14 | ); 15 | }; 16 | 17 | Form.propTypes = { 18 | match: PropTypes.object, 19 | }; 20 | 21 | export default Form; 22 | -------------------------------------------------------------------------------- /packages/web/src/pages/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { useTranslation } from 'react-i18next'; 4 | import { Button } from 'antd'; 5 | import { Title } from '../components/Shared'; 6 | 7 | const Home = () => { 8 | const { t } = useTranslation(); 9 | return ( 10 | <div> 11 | <Title text={t('home.title')} /> 12 | <Link to="about"> 13 | <Button htmlType="button" type="primary" size="large"> 14 | {t('home.goToAbout')} 15 | </Button> 16 | </Link> 17 | </div> 18 | ); 19 | }; 20 | 21 | export default Home; 22 | -------------------------------------------------------------------------------- /packages/web/src/pages/Layout.js: -------------------------------------------------------------------------------- 1 | import React, { useState, Suspense } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Route, Switch, withRouter, Redirect } from 'react-router-dom'; 4 | import { Layout, Drawer, Spin } from 'antd'; 5 | import { GUEST, LOGGED } from '../config/constants'; 6 | import routes from '../config/routes'; 7 | import { MenuPrimary, Logo, Header } from '../components/Layout'; 8 | import { useAuthenticated } from '../components/Auth'; 9 | import PageNotFound404 from './404'; 10 | 11 | const { Footer, Content, Sider } = Layout; 12 | 13 | const Routes = () => { 14 | const { isAuthenticated } = useAuthenticated(); 15 | return ( 16 | <Suspense fallback={<Spin size="large" className="custom-layout-spin" />}> 17 | <Switch> 18 | {routes.map(route => ( 19 | <Route 20 | key={route.index} 21 | exact={route.exact ? route.exact : false} 22 | path={route.path} 23 | render={props => 24 | route.when === undefined || 25 | route.when === null || 26 | (isAuthenticated === false && route.when === GUEST) || 27 | (isAuthenticated === true && route.when === LOGGED) ? ( 28 | React.createElement(route.component, props, null) 29 | ) : ( 30 | <Redirect to="/" /> 31 | ) 32 | } 33 | /> 34 | ))} 35 | <Route component={PageNotFound404} /> 36 | </Switch> 37 | </Suspense> 38 | ); 39 | }; 40 | 41 | const Document = props => { 42 | const { 43 | location: { pathname }, 44 | } = props; 45 | 46 | const [visible, setVisible] = useState(false); 47 | const [isCollapsed, setIsCollapsed] = useState(false); 48 | 49 | const showDrawer = () => setVisible(true); 50 | const onClose = () => setVisible(false); 51 | const toggleCollapse = () => setIsCollapsed(!isCollapsed); 52 | 53 | return ( 54 | <Layout className="custom-layout"> 55 | <Sider 56 | width={250} 57 | breakpoint="md" 58 | collapsedWidth="0" 59 | collapsed={isCollapsed} 60 | onCollapse={toggleCollapse} 61 | className="custom-layout-sider"> 62 | <Logo /> 63 | <strong className="custom-menu-title">Dashboard</strong> 64 | <MenuPrimary pathname={pathname} /> 65 | </Sider> 66 | <Drawer 67 | title={<Logo />} 68 | placement="left" 69 | onClose={onClose} 70 | visible={visible} 71 | bodyStyle={{ padding: 0, margin: 0 }}> 72 | <MenuPrimary pathname={pathname} onClick={onClose} /> 73 | </Drawer> 74 | <Layout style={{ marginLeft: isCollapsed ? 0 : 250 }}> 75 | <Header 76 | pathname={pathname} 77 | isCollapsed={isCollapsed} 78 | showDrawer={showDrawer} 79 | drawerVisible={visible} 80 | /> 81 | <Content className="custom-layout-content"> 82 | <Routes /> 83 | </Content> 84 | <Footer>Footer</Footer> 85 | </Layout> 86 | </Layout> 87 | ); 88 | }; 89 | 90 | Document.propTypes = { 91 | location: PropTypes.object, 92 | }; 93 | 94 | export default withRouter(Document); 95 | -------------------------------------------------------------------------------- /packages/web/src/pages/List.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { List as ListCrud, useCrudList } from 'react-easy-crud'; 4 | import userConfig from '../config/cruds/user'; 5 | 6 | const List = props => { 7 | const { history } = props; 8 | const { columns, dataSource, onDelete, loading } = useCrudList(userConfig); 9 | return ( 10 | <ListCrud 11 | title="Users" 12 | columns={columns} 13 | dataSource={dataSource} 14 | addButtons={[ 15 | { 16 | text: 'Add User', 17 | icon: 'user-add', 18 | onClick: () => history.push('/form'), 19 | }, 20 | ]} 21 | addActions={[ 22 | { 23 | text: 'Edit', 24 | icon: 'edit', 25 | type: 'primary', 26 | onClick: record => 27 | history.push(`/form/${record[userConfig.keyName]}`), 28 | }, 29 | { 30 | text: 'Delete', 31 | icon: 'delete', 32 | type: 'danger', 33 | confirm: 'Are you sure?', 34 | onClick: record => onDelete(record[userConfig.keyName]), 35 | }, 36 | ]} 37 | loading={loading} 38 | pagination={{ 39 | pageSize: 20, 40 | showQuickJumper: true, 41 | showSizeChanger: true, 42 | }} 43 | /> 44 | ); 45 | }; 46 | 47 | List.propTypes = { 48 | history: PropTypes.object, 49 | }; 50 | 51 | export default List; 52 | -------------------------------------------------------------------------------- /packages/web/src/pages/Login.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Login as LoginForm } from '../components/Auth'; 4 | 5 | const Login = props => { 6 | return <LoginForm {...props} />; 7 | }; 8 | 9 | const mapState = state => ({ 10 | loading: state.loading.effects.auth.authentication, 11 | }); 12 | 13 | const mapDispatch = dispatch => ({ 14 | authentication: (username, password) => 15 | dispatch.auth.authentication({ username, password }), 16 | }); 17 | 18 | export default connect( 19 | mapState, 20 | mapDispatch, 21 | )(Login); 22 | -------------------------------------------------------------------------------- /packages/web/src/pages/Register.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Register as RegisterForm } from '../components/Auth'; 3 | 4 | const Register = () => <RegisterForm />; 5 | 6 | export default Register; 7 | -------------------------------------------------------------------------------- /packages/web/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read http://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/, 20 | ), 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit http://bit.ly/CRA-PWA', 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.', 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.', 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /packages/web/src/services/auth.js: -------------------------------------------------------------------------------- 1 | import instance from './instance'; 2 | import { auth } from '../config/services'; 3 | 4 | export const authenticationService = (userName, password) => 5 | instance.post(auth.login, { userName, password }); 6 | -------------------------------------------------------------------------------- /packages/web/src/services/instance.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | class AxiosCreate { 4 | static _instance = null; 5 | 6 | constructor() { 7 | const headers = {}; 8 | 9 | this._instance = axios.create({ 10 | baseURL: process.env.REACT_APP_API || 'http://localhost:4000', 11 | headers, 12 | }); 13 | } 14 | 15 | get instance() { 16 | return this._instance; 17 | } 18 | } 19 | 20 | const InstAx = new AxiosCreate(); 21 | 22 | InstAx.instance.setToken = function(token) { 23 | InstAx.instance.defaults.headers.Authorizations = `Bearer ${token}`; 24 | }; 25 | 26 | InstAx.instance.removeToken = function() { 27 | InstAx.instance.defaults.headers.Authorizations = null; 28 | }; 29 | 30 | export default InstAx.instance; 31 | -------------------------------------------------------------------------------- /packages/web/src/styles/auth.less: -------------------------------------------------------------------------------- 1 | @import (once) "../../../../node_modules/antd/dist/antd.less"; 2 | 3 | .custom-auth-form { 4 | max-width: 400px; 5 | margin: 0 auto; 6 | 7 | .custom-prefix-icon { 8 | color: rgba(0,0,0,.25); 9 | } 10 | 11 | .custom-button { 12 | width: 100%; 13 | } 14 | } 15 | 16 | .custom-form-login { 17 | .custom-auth-form(); 18 | 19 | .custom-forgot-link { 20 | float: right; 21 | } 22 | } 23 | 24 | .custom-form-register { 25 | .custom-auth-form(); 26 | } 27 | 28 | .custom-form-forgot { 29 | .custom-auth-form(); 30 | } 31 | -------------------------------------------------------------------------------- /packages/web/src/styles/index.less: -------------------------------------------------------------------------------- 1 | @import (once) "../../../../node_modules/antd/dist/antd.less"; 2 | 3 | @primary-color: #2779ff; 4 | @layout-header-background: #ffffff; 5 | @layout-sider-background: #ffffff; 6 | @menu-dark-color: #ffffff; 7 | @menu-dark-item-active-bg: lighten(@primary-color, 5%); 8 | @table-border-radius-base: 1rem; 9 | @form-item-margin-bottom: 20px; 10 | 11 | @import "layout.less"; 12 | @import "table.less"; 13 | @import "title.less"; 14 | @import "auth.less"; 15 | 16 | .custom-full-width { 17 | width: 100%; 18 | } 19 | 20 | .custom-align-right { 21 | text-align: right; 22 | } 23 | 24 | .custom-align-middle { 25 | vertical-align: middle; 26 | } 27 | -------------------------------------------------------------------------------- /packages/web/src/styles/layout.less: -------------------------------------------------------------------------------- 1 | @import (once) "../../../../node_modules/antd/dist/antd.less"; 2 | 3 | .custom-layout-spin { 4 | width: 100%; 5 | height: 300px; 6 | } 7 | 8 | .custom-layout { 9 | .custom-layout-sider { 10 | overflow: auto; 11 | height: 100vh; 12 | position: fixed; 13 | left: 0; 14 | box-shadow: 0 64px 5px rgba(0, 0, 0, 0.15); 15 | border-right: 1px solid #f0f0f0; 16 | } 17 | 18 | .custom-layout-content { 19 | padding: 1rem; 20 | min-height: calc(~"100vh - 64px - 69px"); 21 | } 22 | 23 | .custom-menu-title { 24 | padding: 0.8rem 0 1rem 1.5rem; 25 | display: inline-block; 26 | } 27 | 28 | .custom-header { 29 | padding-left: 1rem; 30 | padding-right: 1rem; 31 | box-shadow: 0 1px 5px rgba(0, 0, 0, 0.15); 32 | 33 | .custom-header-toggle-icon { 34 | font-size: 1.5rem; 35 | margin-top: 1.22rem; 36 | color: grey; 37 | } 38 | } 39 | 40 | .custom-logo { 41 | width: 100%; 42 | padding: 1rem; 43 | text-align: center; 44 | } 45 | 46 | .custom-menu-item-header { 47 | height: 64px; 48 | padding-top: 8px; 49 | 50 | .custom-menu-header-item-icon { 51 | font-size: 1.2rem; 52 | vertical-align: middle; 53 | } 54 | } 55 | } 56 | 57 | .custom-header-user { 58 | display: inline-flex; 59 | justify-content: flex-end; 60 | align-items: center; 61 | margin-left: 5px; 62 | height: 100%; 63 | text-align: right; 64 | cursor: pointer; 65 | } 66 | -------------------------------------------------------------------------------- /packages/web/src/styles/table.less: -------------------------------------------------------------------------------- 1 | @import (once) "../../../../node_modules/antd/dist/antd.less"; 2 | 3 | .ant-table td { white-space: nowrap; } 4 | 5 | .custom-table { 6 | .custom-search-filter { 7 | padding: 8px; 8 | width: 220px; 9 | } 10 | 11 | .ant-table-filter-dropdown-btns { 12 | & > a { 13 | padding: 0 5px; 14 | } 15 | } 16 | 17 | .ant-table-thead > tr > th.ant-table-column-sort { 18 | background-color: transparent; 19 | } 20 | .ant-table-tbody > tr > td.ant-table-column-sort { 21 | background-color: lighten(@primary-color, 40%); 22 | } 23 | 24 | .ant-table-body{ 25 | overflow: auto !important; 26 | } 27 | 28 | table { 29 | border-collapse: separate; 30 | border-spacing: 0 5px; 31 | 32 | tr { 33 | & > td { 34 | padding: 10px; 35 | background-color: #ffffff; 36 | } 37 | & > th { background-color: transparent; } 38 | & > td, & > th { border: none !important; } 39 | & > td:first-child, & > th:first-child { border-top-left-radius: 8px; } 40 | & > td:last-child, & > th:last-child { border-top-right-radius: 8px; } 41 | & > td:first-child, & > th:first-child { border-bottom-left-radius: 8px; } 42 | & > td:last-child, & > th:last-child { border-bottom-right-radius: 8px; } 43 | } 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/web/src/styles/title.less: -------------------------------------------------------------------------------- 1 | @import (once) "../../../../node_modules/antd/dist/antd.less"; 2 | 3 | .custom-line-bottom { 4 | content:''; 5 | position: absolute; 6 | bottom: 0; 7 | display: inline-block; 8 | height: 2px; 9 | background-color: @primary-color; 10 | } 11 | 12 | .custom-title { 13 | position: relative; 14 | color: @primary-color; 15 | font-size: 2.5rem; 16 | 17 | @media screen and (max-width: @screen-sm-max) { 18 | font-size: 2rem; 19 | } 20 | 21 | &::before { 22 | .custom-line-bottom(); 23 | left: 0; 24 | width: 120px; 25 | @media screen and (max-width: @screen-sm-max) { 26 | width: 80px; 27 | } 28 | } 29 | 30 | &::after { 31 | .custom-line-bottom(); 32 | left: 124px; 33 | width: 4px; 34 | @media screen and (max-width: @screen-sm-max) { 35 | left: 84px; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/web/src/utils/general.js: -------------------------------------------------------------------------------- 1 | export const nextNumber = (next = 1) => () => next++; 2 | 3 | const nextRouteIndex = nextNumber(); 4 | export const createRoute = (url, component, when = null, exact = false) => ({ 5 | index: nextRouteIndex(), 6 | path: url, 7 | component, 8 | when, 9 | exact, 10 | }); 11 | 12 | const nextMenuIndex = nextNumber(); 13 | export const createMenu = (url, title, icon, when) => ({ 14 | index: nextMenuIndex(), 15 | title, 16 | path: url, 17 | icon, 18 | when, 19 | }); 20 | 21 | export const createComponent = (component, when) => ({ 22 | index: nextMenuIndex(), 23 | component, 24 | when, 25 | }); 26 | 27 | export const sortString = key => (a, b) => 28 | a[key] ? a[key].localeCompare(b[key]) : true; 29 | export const sortNumber = key => (a, b) => a[key] - b[key]; 30 | export const sortBool = key => (a, b) => b[key] - a[key]; 31 | --------------------------------------------------------------------------------