├── .editorconfig ├── .env.example ├── .eslintrc.js ├── .gitattributes ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .yarnclean ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── Dockerfile.dev ├── LICENSE.md ├── PULL_REQUEST_TEMPLATE.md ├── Procfile ├── README.md ├── app ├── .htaccess ├── .nginx.conf ├── app.js ├── assets │ ├── header │ │ ├── avator.jpeg │ │ ├── en_US.svg │ │ ├── language.svg │ │ ├── notice.svg │ │ └── zh_CN.svg │ ├── i18n │ │ ├── en.svg │ │ └── ne.svg │ ├── images │ │ ├── helps-bg.jpg │ │ ├── icons │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── icon-512x512.png │ │ │ └── site.webmanifest │ │ ├── logo │ │ │ ├── long-logo.svg │ │ │ └── short-logo.svg │ │ ├── navbar.png │ │ └── reporting.gif │ ├── menu │ │ ├── account.svg │ │ ├── dashboard.svg │ │ ├── documentation.svg │ │ ├── guide.svg │ │ └── permission.svg │ └── sass │ │ ├── _variable.scss │ │ ├── components │ │ ├── _buttons.scss │ │ ├── _card.scss │ │ ├── _font.scss │ │ ├── _form.scss │ │ ├── _helpers.scss │ │ ├── _pagination.scss │ │ ├── _spacing.scss │ │ ├── _table.scss │ │ └── _typography.scss │ │ ├── layout │ │ ├── _footer.scss │ │ └── _navbar.scss │ │ ├── main.scss │ │ ├── mixins │ │ ├── _box.scss │ │ ├── _button.scss │ │ ├── _display.scss │ │ └── _media.scss │ │ └── pages │ │ ├── _home.scss │ │ └── _user-account.scss ├── common │ ├── helpers │ │ └── index.js │ ├── hooks │ │ ├── passwordStrengthHook.js │ │ └── usePrevious.js │ ├── language │ │ └── index.js │ ├── layout │ │ └── Navbar.js │ ├── messages │ │ └── index.js │ ├── rules │ │ └── index.js │ ├── saga │ │ └── index.js │ └── validator │ │ └── index.js ├── components │ ├── DraftEditor │ │ ├── ToolWrapper.js │ │ ├── helper.js │ │ └── index.js │ ├── ErrorFallback │ │ ├── index.js │ │ └── messages.js │ ├── Footer │ │ ├── index.js │ │ └── messages.js │ ├── FormButtonWrapper │ │ └── index.js │ ├── FormCheckboxWrapper │ │ └── index.js │ ├── FormInputWrapper │ │ └── index.js │ ├── FormWrapper │ │ └── index.js │ ├── Header │ │ ├── index.js │ │ └── messages.js │ ├── Img │ │ └── index.js │ ├── Layout │ │ ├── Menu │ │ │ ├── index.js │ │ │ └── list.js │ │ ├── index.js │ │ └── index.less │ ├── LoadingIndicator │ │ ├── Circle.js │ │ ├── Wrapper.js │ │ └── index.js │ ├── OtpModal │ │ ├── index.js │ │ ├── messages.js │ │ └── otpInput.js │ ├── PageHeaderWrapper │ │ └── index.js │ ├── SearchInput │ │ ├── index.js │ │ └── messages.js │ ├── SelectInputWrapper │ │ ├── index.js │ │ └── messages.js │ ├── Toggle │ │ └── index.js │ ├── ToggleOption │ │ └── index.js │ └── ToolTipButtonWrapper │ │ └── index.js ├── configure-store.js ├── containers │ ├── AlertMessage │ │ ├── actions.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── reducer.js │ │ ├── saga.js │ │ └── selectors.js │ ├── App │ │ ├── actions.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── index.less │ │ ├── reducer.js │ │ ├── saga.js │ │ └── selectors.js │ ├── Dashboard │ │ ├── Loadable.js │ │ ├── actions.js │ │ ├── charts │ │ │ └── deviceChart.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── messages.js │ │ ├── reducer.js │ │ ├── saga.js │ │ └── selectors.js │ ├── EmailTemplate │ │ ├── Loadable.js │ │ ├── actions.js │ │ ├── constants.js │ │ ├── createEmailTemplateModal.js │ │ ├── editEmailTemplateModal.js │ │ ├── emailTemplateTable.js │ │ ├── hooks │ │ │ └── useGetEmailTemplateForm.js │ │ ├── index.js │ │ ├── messages.js │ │ ├── reducer.js │ │ ├── saga.js │ │ └── selectors.js │ ├── ForgotPassword │ │ ├── Loadable.js │ │ ├── actions.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── messages.js │ │ ├── reducer.js │ │ ├── saga.js │ │ └── selectors.js │ ├── HomePage │ │ ├── Loadable.js │ │ ├── actions.js │ │ ├── banner.js │ │ ├── constants.js │ │ ├── contributors.js │ │ ├── index.js │ │ ├── messages.js │ │ ├── reducer.js │ │ ├── saga.js │ │ ├── selector.js │ │ └── truthyHelp.js │ ├── LanguageProvider │ │ ├── actions.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── reducer.js │ │ └── selectors.js │ ├── LocaleToggle │ │ ├── Wrapper.js │ │ ├── index.js │ │ └── messages.js │ ├── LoginPage │ │ ├── Loadable.js │ │ ├── actions.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── loginForm.js │ │ ├── messages.js │ │ ├── reducer.js │ │ ├── saga.js │ │ └── selectors.js │ ├── NotFoundPage │ │ ├── Loadable.js │ │ ├── index.js │ │ └── messages.js │ ├── Permission │ │ ├── Loadable.js │ │ ├── actions.js │ │ ├── constants.js │ │ ├── createPermissionModal.js │ │ ├── editPermissionModal.js │ │ ├── hooks │ │ │ └── useGetPermissionForm.js │ │ ├── index.js │ │ ├── messages.js │ │ ├── permissionTable.js │ │ ├── reducer.js │ │ ├── saga.js │ │ └── selectors.js │ ├── PermissionDeniedPage │ │ ├── Loadable.js │ │ ├── index.js │ │ └── messages.js │ ├── PrivateRoute │ │ └── index.js │ ├── Profile │ │ ├── Loadable.js │ │ ├── index.js │ │ └── messages.js │ ├── PublicRoute │ │ └── index.js │ ├── RegisterPage │ │ ├── Loadable.js │ │ ├── actions.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── index.less │ │ ├── messages.js │ │ ├── reducer.js │ │ ├── registerForm.js │ │ ├── saga.js │ │ └── selectors.js │ ├── ResetPassword │ │ ├── Loadable.js │ │ ├── actions.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── messages.js │ │ ├── reducer.js │ │ ├── saga.js │ │ └── selectors.js │ ├── Role │ │ ├── Loadable.js │ │ ├── actions.js │ │ ├── constants.js │ │ ├── createRoleModal.js │ │ ├── editRoleModal.js │ │ ├── hooks │ │ │ └── useGetRoleForm.js │ │ ├── index.js │ │ ├── messages.js │ │ ├── modifyPermissionModal.js │ │ ├── reducer.js │ │ ├── roleTable.js │ │ ├── saga.js │ │ └── selectors.js │ ├── SnackMessage │ │ ├── actions.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── reducer.js │ │ ├── saga.js │ │ └── selectors.js │ ├── UserAccount │ │ ├── Loadable.js │ │ ├── actions.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── loginActivity.js │ │ ├── messages.js │ │ ├── profileForm.js │ │ ├── reducer.js │ │ ├── saga.js │ │ ├── securityTab.js │ │ └── selectors.js │ ├── Users │ │ ├── Loadable.js │ │ ├── actions.js │ │ ├── constants.js │ │ ├── createUserModal.js │ │ ├── editUserModal.js │ │ ├── hooks │ │ │ └── useGetUserForm.js │ │ ├── index.js │ │ ├── messages.js │ │ ├── reducer.js │ │ ├── saga.js │ │ ├── selectors.js │ │ └── userTable.js │ └── VerifyAccount │ │ ├── Loadable.js │ │ ├── actions.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── messages.js │ │ ├── reducer.js │ │ ├── saga.js │ │ └── selectors.js ├── global-styles.js ├── helpers │ ├── Validation.js │ ├── ValidatonModel.js │ └── messages.js ├── hooks │ ├── localstorage.js │ └── useCookie.js ├── i18n.js ├── i18n │ ├── en.json │ └── ne.json ├── index.html ├── logo.svg ├── reducers │ └── index.js ├── reportWebVitals.js ├── routes │ ├── index.js │ └── messages.js ├── services │ ├── cookie.service.js │ └── persist.service.js ├── setupTests.js ├── store │ └── index.js └── utils │ ├── api.js │ ├── checkStore.js │ ├── colors.js │ ├── common.js │ ├── constants.js │ ├── history.js │ ├── injectReducer.js │ ├── injectSaga.js │ ├── loadable.js │ ├── pagination.js │ ├── permission.js │ ├── reducerInjectors.js │ ├── request.bak.js │ ├── request.js │ ├── rwd.js │ └── sagaInjectors.js ├── babel.config.js ├── bin └── cli.js ├── internals ├── scripts │ ├── analyze.js │ ├── clean.js │ ├── extract-intl.js │ ├── helpers │ │ ├── checkmark.js │ │ ├── get-npm-config.js │ │ ├── progress.js │ │ └── xmark.js │ ├── npmcheckversion.js │ └── setup.js └── webpack │ ├── webpack.base.babel.js │ ├── webpack.dev.babel.js │ └── webpack.prod.babel.js ├── jsconfig.json ├── nginx.conf ├── package.json ├── server ├── argv.js ├── index.js ├── logger.js ├── middlewares │ ├── addDevMiddlewares.js │ ├── addProdMiddlewares.js │ └── frontendMiddleware.js └── port.js ├── vercel.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 2 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | REACT_APP_URI=http://localhost:3000 3 | REACT_APP_API_BASE_URI=http://localhost:7777 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # From https://github.com/Danimoth/gitattributes/blob/master/Web.gitattributes 2 | 3 | # Handle line endings automatically for files detected as text 4 | # and leave all files detected as binary untouched. 5 | * text=auto 6 | 7 | # 8 | # The above will handle all files NOT found below 9 | # 10 | 11 | # 12 | ## These files are text and should be normalized (Convert crlf => lf) 13 | # 14 | 15 | # source code 16 | *.php text 17 | *.css text 18 | *.sass text 19 | *.scss text 20 | *.less text 21 | *.styl text 22 | *.js text eol=lf 23 | *.coffee text 24 | *.json text 25 | *.htm text 26 | *.html text 27 | *.xml text 28 | *.svg text 29 | *.txt text 30 | *.ini text 31 | *.inc text 32 | *.pl text 33 | *.rb text 34 | *.py text 35 | *.scm text 36 | *.sql text 37 | *.sh text 38 | *.bat text 39 | 40 | # templates 41 | *.ejs text 42 | *.hbt text 43 | *.jade text 44 | *.haml text 45 | *.hbs text 46 | *.dot text 47 | *.tmpl text 48 | *.phtml text 49 | 50 | # server config 51 | .htaccess text 52 | .nginx.conf text 53 | 54 | # git config 55 | .gitattributes text 56 | .gitignore text 57 | .gitconfig text 58 | 59 | # code analysis config 60 | .jshintrc text 61 | .jscsrc text 62 | .jshintignore text 63 | .csslintrc text 64 | 65 | # misc config 66 | *.yaml text 67 | *.yml text 68 | .editorconfig text 69 | 70 | # build config 71 | *.npmignore text 72 | *.bowerrc text 73 | 74 | # Heroku 75 | Procfile text 76 | .slugignore text 77 | 78 | # Documentation 79 | *.md text 80 | LICENSE text 81 | AUTHORS text 82 | 83 | 84 | # 85 | ## These files are binary and should be left untouched 86 | # 87 | 88 | # (binary is a macro for -text -diff) 89 | *.png binary 90 | *.jpg binary 91 | *.jpeg binary 92 | *.gif binary 93 | *.ico binary 94 | *.mov binary 95 | *.mp4 binary 96 | *.mp3 binary 97 | *.flv binary 98 | *.fla binary 99 | *.swf binary 100 | *.gz binary 101 | *.zip binary 102 | *.7z binary 103 | *.ttf binary 104 | *.eot binary 105 | *.woff binary 106 | *.pyc binary 107 | *.pdf binary 108 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Call function '...' 16 | 2. Pass value '...' 17 | 3. See error 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Additional context** 26 | Add any other context about the problem here. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | .env 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | .idea 26 | .idea/* 27 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/dubnium 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | internals/generators/ 4 | internals/scripts/ 5 | package-lock.json 6 | yarn.lock 7 | package.json 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "all" 8 | } 9 | -------------------------------------------------------------------------------- /.yarnclean: -------------------------------------------------------------------------------- 1 | # test directories 2 | __tests__ 3 | test 4 | tests 5 | powered-test 6 | 7 | # asset directories 8 | docs 9 | doc 10 | website 11 | images 12 | assets 13 | 14 | # examples 15 | example 16 | examples 17 | 18 | # code coverage directories 19 | coverage 20 | .nyc_output 21 | 22 | # build scripts 23 | Makefile 24 | Gulpfile.js 25 | Gruntfile.js 26 | 27 | # configs 28 | appveyor.yml 29 | circle.yml 30 | codeship-services.yml 31 | codeship-steps.yml 32 | wercker.yml 33 | .tern-project 34 | .gitattributes 35 | .editorconfig 36 | .*ignore 37 | .eslintrc 38 | .jshintrc 39 | .flowconfig 40 | .documentup.json 41 | .yarn-metadata.json 42 | .travis.yml 43 | 44 | # misc 45 | *.md 46 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14 as build-deps 2 | 3 | WORKDIR /app 4 | 5 | COPY . . 6 | 7 | RUN yarn install 8 | RUN yarn build 9 | 10 | 11 | FROM nginx:1.21-alpine 12 | COPY --from=build-deps /app/build /usr/share/nginx/html 13 | COPY nginx.conf /etc/nginx/nginx.conf 14 | 15 | CMD ["nginx", "-g", "daemon off;"] -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | WORKDIR '/app' 4 | 5 | COPY package.json . 6 | RUN npm install 7 | 8 | COPY . . 9 | 10 | CMD ["npm", "run", "start"] -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Truthy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | 5 | Fixes # (issue) 6 | 7 | ## Type of change 8 | 9 | Please delete options that are not relevant. 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] This change requires a documentation update 15 | 16 | # How Has This Been Tested? 17 | 18 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration 19 | 20 | - [ ] Test A 21 | - [ ] Test B 22 | 23 | # Checklist: 24 | 25 | - [ ] My code follows the style guidelines of this project 26 | - [ ] I have performed a self-review of my own code 27 | - [ ] I have commented my code, particularly in hard-to-understand areas 28 | - [ ] I have made corresponding changes to the documentation 29 | - [ ] My changes generate no new warnings 30 | - [ ] I have added tests that prove my fix is effective or that my feature works 31 | - [ ] New and existing unit tests pass locally with my changes 32 | - [ ] Any dependent changes have been merged and published in downstream modules -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm run start 2 | -------------------------------------------------------------------------------- /app/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ####################################################################### 5 | # GENERAL # 6 | ####################################################################### 7 | 8 | # Make apache follow sym links to files 9 | Options +FollowSymLinks 10 | # If somebody opens a folder, hide all files from the resulting folder list 11 | IndexIgnore */* 12 | 13 | 14 | ####################################################################### 15 | # REWRITING # 16 | ####################################################################### 17 | 18 | # Enable rewriting 19 | RewriteEngine On 20 | 21 | # If its not HTTPS 22 | RewriteCond %{HTTPS} off 23 | 24 | # Comment out the RewriteCond above, and uncomment the RewriteCond below if you're using a load balancer (e.g. CloudFlare) for SSL 25 | # RewriteCond %{HTTP:X-Forwarded-Proto} !https 26 | 27 | # Redirect to the same URL with https://, ignoring all further rules if this one is in effect 28 | RewriteRule ^(.*) https://%{HTTP_HOST}/$1 [R,L] 29 | 30 | # If we get to here, it means we are on https:// 31 | 32 | # If the file with the specified name in the browser doesn't exist 33 | RewriteCond %{REQUEST_FILENAME} !-f 34 | 35 | # and the directory with the specified name in the browser doesn't exist 36 | RewriteCond %{REQUEST_FILENAME} !-d 37 | 38 | # and we are not opening the root already (otherwise we get a redirect loop) 39 | RewriteCond %{REQUEST_FILENAME} !\/$ 40 | 41 | # Rewrite all requests to the root 42 | RewriteRule ^(.*) / 43 | 44 | 45 | 46 | 47 | # Do not cache sw.js, required for offline-first updates. 48 | 49 | Header set Cache-Control "private, no-cache, no-store, proxy-revalidate, no-transform" 50 | Header set Pragma "no-cache" 51 | 52 | 53 | -------------------------------------------------------------------------------- /app/app.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | /* eslint-disable import/no-import-module-exports */ 3 | /** 4 | * app.js 5 | * 6 | * This is the entry file for the application, only setup and boilerplate 7 | * code. 8 | */ 9 | 10 | // Needed for redux-saga es6 generator support 11 | import 'react-app-polyfill/ie11'; 12 | import 'react-app-polyfill/stable'; 13 | import React from 'react'; 14 | import ReactDOM from 'react-dom'; 15 | import { Provider } from 'react-redux'; 16 | import 'antd/dist/antd.css'; 17 | import 'assets/sass/main.scss'; 18 | import App from 'containers/App'; 19 | import LanguageProvider from 'containers/LanguageProvider'; 20 | import '!file-loader?name=[name].[ext]!./assets/images/icons/favicon.ico'; 21 | import 'file-loader?name=.htaccess!./.htaccess'; 22 | import { saveState } from 'services/persist.service'; 23 | import { throttle } from 'lodash'; 24 | import reportWebVitals from 'reportWebVitals'; 25 | import { store } from './store'; 26 | import { translationMessages } from './i18n'; 27 | 28 | const MOUNT_NODE = document.getElementById('app'); 29 | 30 | store.subscribe( 31 | throttle(() => { 32 | saveState({ 33 | language: store.getState().language, 34 | // global: store.getState().global, 35 | }); 36 | }, 1000), 37 | ); 38 | 39 | const render = (messages) => { 40 | ReactDOM.render( 41 | 42 | 43 | 44 | 45 | , 46 | MOUNT_NODE, 47 | ); 48 | }; 49 | 50 | if (module.hot) { 51 | // Hot reloadable React components and translation json files 52 | // modules.hot.accept does not accept dynamic dependencies, 53 | // have to be constants at compile-time 54 | module.hot.accept(['./i18n', 'containers/App'], () => { 55 | ReactDOM.unmountComponentAtNode(MOUNT_NODE); 56 | render(translationMessages); 57 | }); 58 | } 59 | 60 | render(translationMessages); 61 | 62 | if (process.env.NODE_ENV === 'development') { 63 | // eslint-disable-next-line no-console 64 | reportWebVitals(console.log); 65 | } 66 | -------------------------------------------------------------------------------- /app/assets/header/avator.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gobeam/truthy-react-frontend/858dfd35e345ae7b10e8e1eb4aecc419df28600b/app/assets/header/avator.jpeg -------------------------------------------------------------------------------- /app/assets/header/notice.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/header/zh_CN.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/i18n/ne.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/assets/images/helps-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gobeam/truthy-react-frontend/858dfd35e345ae7b10e8e1eb4aecc419df28600b/app/assets/images/helps-bg.jpg -------------------------------------------------------------------------------- /app/assets/images/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gobeam/truthy-react-frontend/858dfd35e345ae7b10e8e1eb4aecc419df28600b/app/assets/images/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /app/assets/images/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gobeam/truthy-react-frontend/858dfd35e345ae7b10e8e1eb4aecc419df28600b/app/assets/images/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /app/assets/images/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gobeam/truthy-react-frontend/858dfd35e345ae7b10e8e1eb4aecc419df28600b/app/assets/images/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /app/assets/images/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gobeam/truthy-react-frontend/858dfd35e345ae7b10e8e1eb4aecc419df28600b/app/assets/images/icons/favicon-16x16.png -------------------------------------------------------------------------------- /app/assets/images/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gobeam/truthy-react-frontend/858dfd35e345ae7b10e8e1eb4aecc419df28600b/app/assets/images/icons/favicon-32x32.png -------------------------------------------------------------------------------- /app/assets/images/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gobeam/truthy-react-frontend/858dfd35e345ae7b10e8e1eb4aecc419df28600b/app/assets/images/icons/favicon.ico -------------------------------------------------------------------------------- /app/assets/images/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gobeam/truthy-react-frontend/858dfd35e345ae7b10e8e1eb4aecc419df28600b/app/assets/images/icons/icon-512x512.png -------------------------------------------------------------------------------- /app/assets/images/icons/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /app/assets/images/navbar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gobeam/truthy-react-frontend/858dfd35e345ae7b10e8e1eb4aecc419df28600b/app/assets/images/navbar.png -------------------------------------------------------------------------------- /app/assets/images/reporting.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gobeam/truthy-react-frontend/858dfd35e345ae7b10e8e1eb4aecc419df28600b/app/assets/images/reporting.gif -------------------------------------------------------------------------------- /app/assets/menu/account.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/menu/dashboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/menu/documentation.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/menu/guide.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/menu/permission.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/assets/sass/_variable.scss: -------------------------------------------------------------------------------- 1 | 2 | // ************* COLORS PALLETS **************** // 3 | 4 | 5 | // Blue Pallets Color 6 | 7 | $primary:#39af9f; 8 | $secondary:#3fceba; 9 | $white:#ffffff; 10 | $bg-color:#ececec; 11 | $border-color:#d4d5d6; 12 | 13 | 14 | 15 | // Red paalets 16 | $danger:#611a15; 17 | $danger-bg: #fdecea; 18 | $danger-icon:#f44336; 19 | 20 | 21 | // Green pallets 22 | 23 | $success: #1e4620; 24 | $success-bg: #edf7ed; 25 | $success-icon:#4caf50; 26 | 27 | 28 | $text-color: #000000d9; 29 | 30 | 31 | 32 | // font sizes 33 | $font-md: 1.375rem; 34 | $font-sm: 1.25rem; 35 | $font-regular: 1rem; 36 | $font-xs: 0.875rem; 37 | $font-xxs: 0.625rem; 38 | 39 | 40 | // font-weight 41 | $light:300; 42 | $regular:400; 43 | $semi-bold:600; 44 | $bold: 700; 45 | 46 | 47 | // font-path 48 | $open-sans: 'Open Sans', sans-serif; 49 | 50 | 51 | // media queries 52 | $screen-sm-max: 575px; // Small tablets and large smartphones (landscape view) 53 | $screen-md-max: 767px; // Small tablets (portrait view) 54 | $screen-lg-max: 991px; // Tablets and small desktops 55 | $screen-xl-max: 1200px; // Large tablets and desktops -------------------------------------------------------------------------------- /app/assets/sass/components/_buttons.scss: -------------------------------------------------------------------------------- 1 | .ant-btn-link { 2 | color: $primary; 3 | &:hover, 4 | &:active, 5 | &:focus { 6 | color: $primary; 7 | background: transparent; 8 | border-color: transparent; 9 | } 10 | } 11 | .ant-btn-primary { 12 | @include bg-border-color($primary, $primary); 13 | background-position: center; 14 | transition: background 0.8s; 15 | height: 38px; 16 | text-transform: capitalize; 17 | border-radius: 5px; 18 | &.active, 19 | &:hover, 20 | &:focus { 21 | @include bg-border-color($primary, $primary); 22 | } 23 | &:hover { 24 | background: $primary radial-gradient(circle, transparent 1%, $primary 1%) 25 | center/15000%; 26 | } 27 | &.active { 28 | background-color: rgba($primary, 0.5); 29 | background-size: 100%; 30 | transition: background 0s; 31 | } 32 | } 33 | 34 | .otp-wrapper { 35 | display: flex; 36 | justify-content: space-around; 37 | margin: 20px 0; 38 | .danger { 39 | border: 1px solid $danger; 40 | } 41 | input { 42 | transition: box-shadow 0.5s; 43 | &.shake-input { 44 | animation: shake 0.1s ease-in-out 0s 2; 45 | box-shadow: 2px 3px 6px rgba($danger, 0.15); 46 | } 47 | } 48 | } 49 | 50 | .ant-modal-footer, 51 | .ant-upload { 52 | .ant-btn { 53 | background-position: center; 54 | transition: all ease 0.3s; 55 | height: 38px; 56 | text-transform: capitalize; 57 | border-radius: 5px; 58 | 59 | &:hover, 60 | &:active, 61 | &:focus { 62 | color: $primary; 63 | background: #fff; 64 | border-color: $primary; 65 | } 66 | &.active { 67 | background-size: 100%; 68 | transition: background 0s; 69 | } 70 | } 71 | } 72 | 73 | @keyframes shake { 74 | 0% { 75 | margin-left: 0rem; 76 | } 77 | 25% { 78 | margin-left: 0.5rem; 79 | } 80 | 75% { 81 | margin-left: -0.5rem; 82 | } 83 | 100% { 84 | margin-left: 0rem; 85 | } 86 | } 87 | 88 | .ant-switch-checked { 89 | background-color: $primary; 90 | } 91 | 92 | .mr-2 { 93 | margin-right: 12px; 94 | } 95 | -------------------------------------------------------------------------------- /app/assets/sass/components/_card.scss: -------------------------------------------------------------------------------- 1 | .ant-alert{ 2 | &-error{ 3 | @include bg-border-color($danger-bg, $danger-bg); 4 | margin-bottom: 10px; 5 | border-radius: 5px; 6 | .ant-alert-with-description{ 7 | padding: 8px 15px; 8 | } 9 | .ant-alert-message{ 10 | margin-bottom: 0; 11 | color: $danger !important; 12 | line-height: 19px; 13 | } 14 | .ant-alert-icon{ 15 | font-size: 20px; 16 | margin-right: 10px; 17 | color: $danger-icon; 18 | } 19 | 20 | } 21 | &-success{ 22 | @include bg-border-color($success-bg, $success-bg); 23 | margin-bottom: 10px; 24 | border-radius: 5px; 25 | .ant-alert-with-description{ 26 | padding: 8px 15px; 27 | } 28 | .ant-alert-message{ 29 | margin-bottom: 0; 30 | color: $success !important; 31 | line-height: 19px; 32 | } 33 | .ant-alert-icon{ 34 | font-size: 20px; 35 | margin-right: 10px; 36 | color: $success-icon; 37 | } 38 | 39 | } 40 | &-description{ 41 | margin-top: 5px; 42 | } 43 | 44 | } 45 | 46 | 47 | .ant-modal{ 48 | &-content{ 49 | border-radius: 5px; 50 | } 51 | &-header{ 52 | border-radius: 5px 5px 0 0; 53 | } 54 | &-title{ 55 | font-weight: 500; 56 | } 57 | } -------------------------------------------------------------------------------- /app/assets/sass/components/_font.scss: -------------------------------------------------------------------------------- 1 | // @import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;600;700&display=swap'); 2 | 3 | @import url('https://fonts.googleapis.com/css2?family=Rubik:wght@300;400;500;600;700&display=swap'); -------------------------------------------------------------------------------- /app/assets/sass/components/_form.scss: -------------------------------------------------------------------------------- 1 | .ant-input-affix-wrapper, .ant-input{ 2 | border-radius: 5px; 3 | min-height: 42px; 4 | padding:0px; 5 | overflow: hidden; 6 | &:focus{ 7 | box-shadow: 0 0 0 2px rgba($primary, 0.2); 8 | border-color: $primary; 9 | } 10 | } 11 | 12 | .ant-input-affix-wrapper:not(.ant-input-affix-wrapper-disabled){ 13 | &:hover{ 14 | border-color: $primary; 15 | } 16 | } 17 | 18 | // #create-user{ 19 | // @extend .ant-input-affix-wrapper; 20 | // } 21 | 22 | .ant-input-affix-wrapper > input.ant-input{ 23 | padding-left: 30px; 24 | } 25 | 26 | .ant-form-item-has-success.ant-form-item-has-feedback .ant-form-item-children-icon{ 27 | color: $primary; 28 | } 29 | 30 | .ant-input-suffix{ 31 | position: absolute; 32 | right: 0; 33 | top: 50%; 34 | transform: translate(-50%, -50%); 35 | } 36 | .ant-input-prefix{ 37 | position: absolute; 38 | left: 15px; 39 | top: 50%; 40 | z-index: 2; 41 | transform: translate(-50%, -50%); 42 | } 43 | 44 | .ant-checkbox-checked { 45 | .ant-checkbox-inner{ 46 | @include bg-border-color($primary, $primary) 47 | } 48 | } 49 | 50 | .ant-form-item-label{ 51 | text-align: left; 52 | } 53 | 54 | .form-ant-items{ 55 | .ant-input{ 56 | padding-left: 15px !important; 57 | } 58 | } 59 | .ant-input{ 60 | &:placeholder-shown{ 61 | text-transform: capitalize; 62 | } 63 | } 64 | 65 | .ant-form-item-label{ 66 | text-transform: capitalize; 67 | } 68 | 69 | .ant-progress-steps-item-active{ 70 | background: $primary; 71 | } 72 | 73 | .ant-progress-steps-outer{ 74 | margin-bottom: 15px; 75 | } -------------------------------------------------------------------------------- /app/assets/sass/components/_helpers.scss: -------------------------------------------------------------------------------- 1 | .mh-100{ 2 | min-height: 100vh; 3 | } 4 | 5 | .vh-center{ 6 | @include d-center; 7 | } 8 | 9 | .spin{ 10 | i{ 11 | background-color: $primary; 12 | 13 | } 14 | 15 | } 16 | .d-flex{ 17 | display: flex; 18 | } 19 | 20 | .w-100{ 21 | width: 100%; 22 | } 23 | 24 | .m-auto{ 25 | margin: auto; 26 | } 27 | 28 | 29 | .ml-auto{ 30 | margin-left: auto; 31 | } 32 | 33 | a{ 34 | color: $primary; 35 | &:hover{ 36 | color: $primary; 37 | text-decoration: underline; 38 | } 39 | } -------------------------------------------------------------------------------- /app/assets/sass/components/_pagination.scss: -------------------------------------------------------------------------------- 1 | .ant-pagination-item-active{ 2 | border-color: $primary; 3 | } -------------------------------------------------------------------------------- /app/assets/sass/components/_spacing.scss: -------------------------------------------------------------------------------- 1 | h1,h2,h3,h4,h5,h6, p{ 2 | margin-bottom: 0; 3 | } 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/assets/sass/components/_table.scss: -------------------------------------------------------------------------------- 1 | .truthy-table { 2 | background-color: $white; 3 | padding: 20px; 4 | border-radius: 8px; 5 | .ant-table-title{ 6 | padding-left: 0; 7 | padding-top: 0; 8 | } 9 | .ant-table-thead{ 10 | 11 | } 12 | } 13 | 14 | 15 | .ant-table-container{ 16 | border: 1px solid $border-color !important; 17 | border-bottom: 0 !important; 18 | } 19 | .ant-table-thead > tr > th{ 20 | background: $white; 21 | border-bottom: 0 !important; 22 | border-left: 1px solid $border-color !important; 23 | // color: $secondary-color; 24 | // font-weight: $bold; 25 | } 26 | .ant-table-thead > tr > th::before{ 27 | display: none; 28 | } 29 | .ant-table-thead > tr > th:first-child{ 30 | border-left: 0 !important; 31 | } 32 | .ant-table-tbody > tr > td{ 33 | border-bottom: 1px solid $border-color !important; 34 | border-left: 1px solid $border-color !important; 35 | // color: $secondary-color; 36 | // text-align: right; 37 | } 38 | .ant-table-tbody > tr > td:first-child{ 39 | border-left: 0 !important; 40 | } 41 | .ant-table-tbody > tr.ant-table-row:hover > td{ 42 | background: transparent; 43 | } 44 | 45 | .ant-table-thead > tr > th, .ant-table-tbody > tr > td, .ant-table tfoot > tr > th, .ant-table tfoot > tr > td{ 46 | padding: 10px 15px; 47 | } 48 | 49 | -------------------------------------------------------------------------------- /app/assets/sass/components/_typography.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gobeam/truthy-react-frontend/858dfd35e345ae7b10e8e1eb4aecc419df28600b/app/assets/sass/components/_typography.scss -------------------------------------------------------------------------------- /app/assets/sass/layout/_footer.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gobeam/truthy-react-frontend/858dfd35e345ae7b10e8e1eb4aecc419df28600b/app/assets/sass/layout/_footer.scss -------------------------------------------------------------------------------- /app/assets/sass/layout/_navbar.scss: -------------------------------------------------------------------------------- 1 | .ant-menu-vertical .ant-menu-item::after, 2 | .ant-menu-vertical-left .ant-menu-item::after, 3 | .ant-menu-vertical-right .ant-menu-item::after, 4 | .ant-menu-inline .ant-menu-item::after { 5 | border-right: 3px solid $primary; 6 | } 7 | .ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected { 8 | background-color: #f1fffd; 9 | } 10 | .ant-menu-item-selected { 11 | color: $primary; 12 | } 13 | 14 | .ant-menu-light .ant-menu-item:hover, 15 | .ant-menu-light .ant-menu-item-active, 16 | .ant-menu-light .ant-menu:not(.ant-menu-inline) .ant-menu-submenu-open, 17 | .ant-menu-light .ant-menu-submenu-active, 18 | .ant-menu-light .ant-menu-submenu-title:hover { 19 | color: $primary; 20 | } 21 | 22 | .ant-menu-item:active, 23 | .ant-menu-submenu-title:active { 24 | background-color: #f1fffd; 25 | } 26 | 27 | .home-page { 28 | .navbar{ 29 | width: 100%; 30 | .logos{ 31 | width: 250px; 32 | @include d-y-center; 33 | flex-grow: 1; 34 | font-weight: 900; 35 | color: #637381; 36 | font-size: 24px; 37 | text-shadow: #e0e0e0 1px 1px 0; 38 | } 39 | .header-main{ 40 | @include d-y-center; 41 | list-style: none; 42 | margin: 0; 43 | li{ 44 | margin-left: 50px; 45 | a{ 46 | font-size: 16px; 47 | color: #637381; 48 | &.active{ 49 | color: $primary; 50 | 51 | } 52 | &:hover{ 53 | text-decoration: none; 54 | color: $primary; 55 | } 56 | } 57 | } 58 | 59 | } 60 | } 61 | .ant-layout-header { 62 | background: #ececec; 63 | // background: url('./../../../assets//images/navbar.png'); 64 | } 65 | .ant-menu-horizontal{ 66 | border: 0; 67 | } 68 | .ant-menu-item::after{ 69 | display: none; 70 | } 71 | .ant-menu{ 72 | background: #ececec; 73 | } 74 | } 75 | 76 | 77 | -------------------------------------------------------------------------------- /app/assets/sass/main.scss: -------------------------------------------------------------------------------- 1 | @import 'variable'; 2 | 3 | @import 'mixins/display'; 4 | @import 'mixins/button'; 5 | @import 'mixins/box'; 6 | @import 'mixins/media'; 7 | 8 | @import 'components/helpers'; 9 | @import 'components/spacing'; 10 | @import 'components/buttons'; 11 | @import 'components/card'; 12 | @import 'components/form'; 13 | @import 'components/table'; 14 | @import 'components/typography'; 15 | @import 'components/pagination'; 16 | 17 | @import 'layout/navbar'; 18 | @import 'layout/footer'; 19 | 20 | @import 'pages/home'; 21 | @import 'pages/user-account'; 22 | -------------------------------------------------------------------------------- /app/assets/sass/mixins/_box.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gobeam/truthy-react-frontend/858dfd35e345ae7b10e8e1eb4aecc419df28600b/app/assets/sass/mixins/_box.scss -------------------------------------------------------------------------------- /app/assets/sass/mixins/_button.scss: -------------------------------------------------------------------------------- 1 | @mixin transition($transition...) { 2 | -moz-transition: $transition; 3 | -o-transition: $transition; 4 | -webkit-transition: $transition; 5 | transition: $transition; 6 | } 7 | 8 | 9 | @mixin button-bg($bg) { 10 | background: $bg; 11 | @include transition(all 0.4s ease); 12 | &:hover { 13 | background:darken($bg,8%); 14 | 15 | border: none; 16 | } 17 | &:active { 18 | background:darken($bg,25%); 19 | } 20 | } 21 | 22 | @mixin bg-border-color($bg, $border){ 23 | background-color: $bg; 24 | border-color: $border; 25 | } 26 | 27 | -------------------------------------------------------------------------------- /app/assets/sass/mixins/_display.scss: -------------------------------------------------------------------------------- 1 | @mixin d-center{ 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | } 6 | 7 | 8 | @mixin d-y-center{ 9 | display: flex; 10 | align-items: center; 11 | } 12 | 13 | @mixin d-x-center{ 14 | display: flex; 15 | justify-content: center; 16 | } -------------------------------------------------------------------------------- /app/assets/sass/mixins/_media.scss: -------------------------------------------------------------------------------- 1 | 2 | // Custom devices 3 | @mixin rwd($screen) { 4 | @media (max-width: $screen+'px') { 5 | @content; 6 | } 7 | } 8 | 9 | // Extra large devices 10 | @mixin xl { 11 | @media (max-width: #{$screen-xl-max}) { 12 | @content; 13 | } 14 | } 15 | 16 | // Large devices 17 | @mixin lg { 18 | @media (max-width: #{$screen-lg-max}) { 19 | @content; 20 | } 21 | } 22 | 23 | // Medium devices 24 | @mixin md { 25 | @media (max-width: #{$screen-md-max}) { 26 | @content; 27 | } 28 | } 29 | 30 | @mixin sm { 31 | @media (max-width: #{$screen-sm-max}) { 32 | @content; 33 | } 34 | } -------------------------------------------------------------------------------- /app/assets/sass/pages/_user-account.scss: -------------------------------------------------------------------------------- 1 | .user-acccount, .change-password-form{ 2 | .ant-input-affix-wrapper > input.ant-input{ 3 | padding-left: 10px; 4 | } 5 | } 6 | 7 | .card{ 8 | background-color: $white; 9 | padding: 20px; 10 | border-radius: 8px; 11 | } 12 | 13 | .profile{ 14 | .user-image{ 15 | border-radius: 8px; 16 | border: 1px solid #f0f0f0; 17 | background-color: #efefef; 18 | height: 100px; 19 | width: 100px; 20 | margin-bottom: 10px; 21 | img{ 22 | height: 100%; 23 | width: 100%; 24 | object-fit: contain; 25 | } 26 | } 27 | .ant-tabs-ink-bar{ 28 | background: $primary; 29 | } 30 | // .ant-tabs-nav-list{ 31 | // padding-right: 20px; 32 | // .ant-tabs-tab-active{ 33 | // background-color: rgba($primary, 0.14); 34 | // border-radius: 5px; 35 | // } 36 | // } 37 | } 38 | 39 | .ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn{ 40 | color: $primary; 41 | } 42 | 43 | .ant-tabs-tab:hover { 44 | color: $primary; 45 | } 46 | 47 | 48 | .profile-details-card{ 49 | .ant-card-body{ 50 | margin: 0; 51 | 52 | } 53 | .ant-page-header{ 54 | padding: 0; 55 | margin-bottom: 15px; 56 | } 57 | } 58 | 59 | -------------------------------------------------------------------------------- /app/common/helpers/index.js: -------------------------------------------------------------------------------- 1 | const buildQueryString = (keywords, pageNumber, limit) => { 2 | const queryObj = { 3 | page: pageNumber > 0 ? pageNumber : 1, 4 | limit: limit > 0 ? limit : 10, 5 | }; 6 | if (keywords && keywords.trim().length > 0) { 7 | queryObj.keywords = keywords; 8 | } 9 | return Object.keys(queryObj) 10 | .map((key) => `${key}=${queryObj[key]}`) 11 | .join('&'); 12 | }; 13 | 14 | export { buildQueryString }; 15 | -------------------------------------------------------------------------------- /app/common/hooks/passwordStrengthHook.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | const usePasswordStrengthCheckHook = (password) => { 4 | const [lowerCheck, setLowerCheck] = useState(0); 5 | const [upperCheck, setUpperCheck] = useState(0); 6 | const [numChecker, setNumChecker] = useState(0); 7 | const [charCheck, setCharChecker] = useState(0); 8 | 9 | useEffect(() => { 10 | if (/^(?=.*[a-z]).+$/.test(password)) { 11 | setLowerCheck(1); 12 | } else { 13 | setLowerCheck(0); 14 | } 15 | 16 | if (/^(?=.*[A-Z]).+$/.test(password)) { 17 | setUpperCheck(1); 18 | } else { 19 | setUpperCheck(0); 20 | } 21 | 22 | if (/\d/.test(password)) { 23 | setNumChecker(1); 24 | } else { 25 | setNumChecker(0); 26 | } 27 | 28 | if (/^(?=.*[-+_!@#$%^&*.,?]).+$/.test(password)) { 29 | setCharChecker(1); 30 | } else { 31 | setCharChecker(0); 32 | } 33 | }, [password]); 34 | 35 | return [lowerCheck, upperCheck, numChecker, charCheck]; 36 | }; 37 | 38 | export default usePasswordStrengthCheckHook; 39 | -------------------------------------------------------------------------------- /app/common/hooks/usePrevious.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | /** 4 | * Tracks previous state of a value. 5 | * 6 | * @param value Props, state or any other calculated value. 7 | * @returns Value from the previous render of the enclosing component. 8 | * 9 | * @example 10 | * function Component() { 11 | * const [count, setCount] = useState(0); 12 | * const prevCount = usePrevious(count); 13 | * // ... 14 | * return `Now: ${count}, before: ${prevCount}`; 15 | * } 16 | */ 17 | export default function usePrevious(value) { 18 | const ref = useRef(); 19 | useEffect(() => { 20 | ref.current = value; 21 | }); 22 | return ref.current; 23 | } 24 | -------------------------------------------------------------------------------- /app/common/language/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import EnSvg from 'assets/i18n/en.svg'; 3 | import NeSvg from 'assets/i18n/ne.svg'; 4 | 5 | export const appLocales = [ 6 | { label: 'en', flag: en }, 7 | { label: 'ne', flag: ne }, 8 | ]; 9 | -------------------------------------------------------------------------------- /app/common/layout/Navbar.js: -------------------------------------------------------------------------------- 1 | import { Header } from 'antd/lib/layout/layout'; 2 | import React from 'react'; 3 | import { FormattedMessage } from 'react-intl'; 4 | import { NavLink } from 'react-router-dom'; 5 | import messages from 'containers/HomePage/messages'; 6 | import LocaleToggle from 'containers/LocaleToggle'; 7 | 8 | function Navbar() { 9 | return ( 10 |
11 |
12 |
13 |
Truthy
14 |
    15 |
  • 16 | 17 | 18 | 19 |
  • 20 |
  • 21 | 22 | 23 | 24 |
  • 25 |
  • 26 | 27 |
  • 28 |
29 |
30 |
31 |
32 | ); 33 | } 34 | 35 | export default Navbar; 36 | -------------------------------------------------------------------------------- /app/common/rules/index.js: -------------------------------------------------------------------------------- 1 | import { FormattedMessage } from 'react-intl'; 2 | import commonMessage from 'common/messages'; 3 | import React from 'react'; 4 | import { checkIfStrongPassword } from 'common/validator'; 5 | 6 | export const rules = { 7 | email: [ 8 | { 9 | type: 'email', 10 | message: , 11 | }, 12 | { 13 | required: true, 14 | whitespace: true, 15 | message: , 16 | }, 17 | ], 18 | password: [ 19 | { 20 | required: true, 21 | whitespace: true, 22 | message: , 23 | }, 24 | { 25 | validator: checkIfStrongPassword, 26 | }, 27 | ], 28 | username: [ 29 | { 30 | required: true, 31 | whitespace: true, 32 | message: , 33 | }, 34 | ], 35 | name: [ 36 | { 37 | required: true, 38 | whitespace: true, 39 | message: , 40 | }, 41 | ], 42 | }; 43 | -------------------------------------------------------------------------------- /app/common/saga/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { put } from 'redux-saga/effects'; 3 | import { FormattedMessage } from 'react-intl'; 4 | import { enqueueAlertAction } from 'containers/AlertMessage/actions'; 5 | import { enqueueSnackMessageAction } from 'containers/SnackMessage/actions'; 6 | 7 | /** 8 | * show formatted error alert 9 | * @param type 10 | * @param message 11 | * @returns {IterableIterator<*>} 12 | */ 13 | export function* showFormattedAlert(type, message) { 14 | return yield put( 15 | enqueueAlertAction({ 16 | message: , 17 | type, 18 | }), 19 | ); 20 | } 21 | 22 | /** 23 | * show formatted snack message 24 | * @param payload: {type: string, message: string, translate: boolean, id: string} 25 | * @returns {IterableIterator<*>} 26 | */ 27 | export function* showMessage(payload) { 28 | return yield put(enqueueSnackMessageAction(payload)); 29 | } 30 | 31 | /** 32 | * show error alert 33 | * @param type 34 | * @param message 35 | * @returns {IterableIterator<*>} 36 | */ 37 | export function* showAlert(type, message) { 38 | return yield put( 39 | enqueueAlertAction({ 40 | message, 41 | type, 42 | }), 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /app/common/validator/index.js: -------------------------------------------------------------------------------- 1 | import messages from 'common/messages'; 2 | import { FormattedMessage } from 'react-intl'; 3 | import React from 'react'; 4 | 5 | export const checkIfStrongPassword = (rule, value, callback) => { 6 | const re = 7 | /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{6,20}$/; 8 | if (re.test(value)) { 9 | callback(); 10 | } else { 11 | callback(); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /app/components/DraftEditor/ToolWrapper.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const ToolWrapper = styled.div` 4 | border: 1px solid #f1f1f1; 5 | padding: 5px; 6 | min-width: 25px; 7 | height: 20px; 8 | border-radius: 2px; 9 | margin: 0 4px; 10 | display: flex; 11 | justify-content: center; 12 | align-items: center; 13 | cursor: pointer; 14 | background: white; 15 | `; 16 | 17 | export default ToolWrapper; 18 | -------------------------------------------------------------------------------- /app/components/ErrorFallback/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import messages from 'components/ErrorFallback/messages'; 4 | import { injectIntl } from 'react-intl'; 5 | 6 | const ErrorFallback = (props) => { 7 | const { error, resetErrorBoundary, intl } = props; 8 | return ( 9 |
10 |

{intl.formatMessage(messages.somethingWrong)}

11 |
{error.message}
12 | 15 |
16 | ); 17 | }; 18 | 19 | ErrorFallback.propTypes = { 20 | error: PropTypes.object, 21 | resetErrorBoundary: PropTypes.func, 22 | intl: PropTypes.object.isRequired, 23 | }; 24 | 25 | export default injectIntl(ErrorFallback); 26 | -------------------------------------------------------------------------------- /app/components/ErrorFallback/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ErrorFallback Messages 3 | * 4 | * This contains all the text for the ErrorFallback component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export const scope = 'components.ErrorFallback'; 9 | 10 | export default defineMessages({ 11 | somethingWrong: { 12 | id: `${scope}.somethingWrong`, 13 | defaultMessage: 'Something went wrong:', 14 | }, 15 | tryAgain: { 16 | id: `${scope}.tryAgain`, 17 | defaultMessage: 'Try again', 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /app/components/Footer/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Footer Messages 3 | * 4 | * This contains all the text for the Footer component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export const scope = 'components.Footer'; 9 | 10 | export default defineMessages({ 11 | licenseMessage: { 12 | id: `${scope}.license.message`, 13 | defaultMessage: 'This project is licensed under the MIT license.', 14 | }, 15 | appName: { 16 | id: `${scope}.appName`, 17 | defaultMessage: 'Truthy', 18 | }, 19 | copyright: { 20 | id: `${scope}.copyright`, 21 | defaultMessage: 'Copyright', 22 | }, 23 | settings: { 24 | id: `${scope}.settings`, 25 | defaultMessage: 'Settings', 26 | }, 27 | download: { 28 | id: `${scope}.download`, 29 | defaultMessage: 'Download', 30 | }, 31 | star: { 32 | id: `${scope}.star`, 33 | defaultMessage: 'Star', 34 | }, 35 | language: { 36 | id: `${scope}.language`, 37 | defaultMessage: 'Language', 38 | }, 39 | }); 40 | -------------------------------------------------------------------------------- /app/components/FormButtonWrapper/index.js: -------------------------------------------------------------------------------- 1 | import { injectIntl } from 'react-intl'; 2 | import React from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import { Button, Form } from 'antd'; 5 | 6 | /** 7 | * 8 | * FormButtonWrapper 9 | * 10 | */ 11 | 12 | const FormButtonWrapper = (props) => { 13 | const { 14 | label, 15 | icon, 16 | intl, 17 | variant, 18 | show = true, 19 | className, 20 | disabled, 21 | form, 22 | } = props; 23 | return ( 24 | <> 25 | {show ? ( 26 | 27 | {() => ( 28 | 41 | )} 42 | 43 | ) : ( 44 | '' 45 | )} 46 | 47 | ); 48 | }; 49 | 50 | FormButtonWrapper.propTypes = { 51 | form: PropTypes.object, 52 | icon: PropTypes.object, 53 | intl: PropTypes.object, 54 | disabled: PropTypes.bool, 55 | show: PropTypes.bool, 56 | variant: PropTypes.string.isRequired, 57 | className: PropTypes.string, 58 | label: PropTypes.object.isRequired, 59 | }; 60 | 61 | export default injectIntl(FormButtonWrapper); 62 | -------------------------------------------------------------------------------- /app/components/FormCheckboxWrapper/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { injectIntl } from 'react-intl'; 4 | import { Checkbox, Form } from 'antd'; 5 | 6 | const FormCheckBoxWrapper = (props) => { 7 | const { 8 | layout = {}, 9 | label, 10 | rules = [], 11 | intl, 12 | children = null, 13 | name, 14 | id, 15 | classname = '', 16 | value, 17 | disabled = false, 18 | changeHandler = () => {}, 19 | } = props; 20 | 21 | return ( 22 | 29 | 37 | {children} 38 | 39 | 40 | ); 41 | }; 42 | 43 | FormCheckBoxWrapper.propTypes = { 44 | children: PropTypes.node, 45 | rules: PropTypes.array, 46 | disabled: PropTypes.bool, 47 | layout: PropTypes.object, 48 | label: PropTypes.object, 49 | name: PropTypes.string, 50 | id: PropTypes.string, 51 | classname: PropTypes.string, 52 | changeHandler: PropTypes.func, 53 | value: PropTypes.bool, 54 | placeholder: PropTypes.object, 55 | intl: PropTypes.object, 56 | }; 57 | 58 | export default injectIntl(FormCheckBoxWrapper); 59 | -------------------------------------------------------------------------------- /app/components/FormWrapper/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Form, Row } from 'antd'; 4 | 5 | const FormWrapper = ({ 6 | layout, 7 | formInstance, 8 | children, 9 | device, 10 | name, 11 | classname, 12 | values, 13 | responsive = false, 14 | ...props 15 | }) => ( 16 |
25 | {responsive ? {children} : children} 26 |
27 | ); 28 | 29 | FormWrapper.propTypes = { 30 | formInstance: PropTypes.object, 31 | children: PropTypes.node, 32 | device: PropTypes.string, 33 | classname: PropTypes.string, 34 | name: PropTypes.string.isRequired, 35 | values: PropTypes.object, 36 | layout: PropTypes.object, 37 | responsive: PropTypes.bool, 38 | }; 39 | 40 | export default FormWrapper; 41 | -------------------------------------------------------------------------------- /app/components/Header/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Header Messages 3 | * 4 | * This contains all the text for the Header component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export const scope = 'components.Header'; 9 | 10 | export default defineMessages({ 11 | profile: { 12 | id: `${scope}.profile`, 13 | defaultMessage: 'My Profile', 14 | }, 15 | setting: { 16 | id: `${scope}.setting`, 17 | defaultMessage: 'Settings', 18 | }, 19 | logout: { 20 | id: `${scope}.logout`, 21 | defaultMessage: 'Logout', 22 | }, 23 | 24 | login: { 25 | id: `${scope}.login`, 26 | defaultMessage: 'Login', 27 | }, 28 | }); 29 | -------------------------------------------------------------------------------- /app/components/Img/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Img.js 4 | * 5 | * Renders an image, enforcing the usage of the alt="" tag 6 | */ 7 | 8 | import React from 'react'; 9 | import PropTypes from 'prop-types'; 10 | 11 | function Img(props) { 12 | return {props.alt}; 13 | } 14 | 15 | // We require the use of src and alt, only enforced by react in dev mode 16 | Img.propTypes = { 17 | src: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired, 18 | alt: PropTypes.string.isRequired, 19 | className: PropTypes.string, 20 | }; 21 | 22 | export default Img; 23 | -------------------------------------------------------------------------------- /app/components/Layout/Menu/list.js: -------------------------------------------------------------------------------- 1 | import { FormattedMessage } from 'react-intl'; 2 | import messages from 'routes/messages'; 3 | import React from 'react'; 4 | import { 5 | DashboardOutlined, 6 | UserOutlined, 7 | LockOutlined, 8 | UsergroupAddOutlined, 9 | MailOutlined, 10 | } from '@ant-design/icons'; 11 | 12 | export const menuList = [ 13 | { 14 | name: , 15 | icon: , 16 | key: 'dashboard-page', 17 | path: '/dashboard', 18 | method: 'get', 19 | resource: 'dashboard', 20 | defaultPermission: true, 21 | }, 22 | { 23 | name: , 24 | icon: , 25 | key: 'user-page', 26 | path: '/users', 27 | method: 'get', 28 | resource: 'user', 29 | defaultPermission: false, 30 | }, 31 | { 32 | name: , 33 | icon: , 34 | key: 'role-page', 35 | path: '/roles', 36 | method: 'get', 37 | resource: 'role', 38 | defaultPermission: false, 39 | }, 40 | { 41 | name: , 42 | icon: , 43 | key: 'permission-page', 44 | path: '/permissions', 45 | method: 'get', 46 | resource: 'permission', 47 | defaultPermission: false, 48 | }, 49 | { 50 | name: , 51 | icon: , 52 | key: 'email-template', 53 | path: '/email-templates', 54 | method: 'get', 55 | resource: 'emailTemplates', 56 | defaultPermission: false, 57 | }, 58 | ]; 59 | -------------------------------------------------------------------------------- /app/components/LoadingIndicator/Circle.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import styled, { keyframes } from 'styled-components'; 4 | 5 | const circleFadeDelay = keyframes` 6 | 0%, 7 | 39%, 8 | 100% { 9 | opacity: 0; 10 | } 11 | 40% { 12 | opacity: 1; 13 | } 14 | `; 15 | 16 | const Circle = (props) => { 17 | const CirclePrimitive = styled.div` 18 | width: 100%; 19 | height: 100%; 20 | position: absolute; 21 | left: 0; 22 | top: 0; 23 | ${props.rotate && 24 | ` 25 | -webkit-transform: rotate(${props.rotate}deg); 26 | -ms-transform: rotate(${props.rotate}deg); 27 | transform: rotate(${props.rotate}deg); 28 | `} &:before { 29 | content: ''; 30 | display: block; 31 | margin: 0 auto; 32 | width: 15%; 33 | height: 15%; 34 | background-color: #999; 35 | border-radius: 100%; 36 | animation: ${circleFadeDelay} 1.2s infinite ease-in-out both; 37 | ${props.delay && 38 | ` 39 | -webkit-animation-delay: ${props.delay}s; 40 | animation-delay: ${props.delay}s; 41 | `}; 42 | } 43 | `; 44 | return ; 45 | }; 46 | 47 | Circle.propTypes = { 48 | delay: PropTypes.number, 49 | rotate: PropTypes.number, 50 | }; 51 | 52 | export default Circle; 53 | -------------------------------------------------------------------------------- /app/components/LoadingIndicator/Wrapper.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Wrapper = styled.div` 4 | margin: 2em auto; 5 | width: 40px; 6 | height: 40px; 7 | position: relative; 8 | `; 9 | 10 | export default Wrapper; 11 | -------------------------------------------------------------------------------- /app/components/LoadingIndicator/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Spin } from 'antd'; 4 | import Wrapper from 'components/LoadingIndicator/Wrapper'; 5 | 6 | const LoadingIndicator = () => ( 7 | 8 | 9 | 10 | ); 11 | 12 | export default LoadingIndicator; 13 | -------------------------------------------------------------------------------- /app/components/OtpModal/messages.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gobeam/truthy-react-frontend/858dfd35e345ae7b10e8e1eb4aecc419df28600b/app/components/OtpModal/messages.js -------------------------------------------------------------------------------- /app/components/OtpModal/otpInput.js: -------------------------------------------------------------------------------- 1 | import React, { memo, useLayoutEffect, useRef } from 'react'; 2 | import usePrevious from 'common/hooks/usePrevious'; 3 | import PropTypes from 'prop-types'; 4 | 5 | const OTPInputComponent = (props) => { 6 | const { focus, autoFocus, ...rest } = props; 7 | const inputRef = useRef(null); 8 | const prevFocus = usePrevious(!!focus); 9 | useLayoutEffect(() => { 10 | if (inputRef.current) { 11 | if (focus && autoFocus) { 12 | inputRef.current.focus(); 13 | } 14 | if (focus && autoFocus && focus !== prevFocus) { 15 | inputRef.current.focus(); 16 | inputRef.current.select(); 17 | } 18 | } 19 | }, [autoFocus, focus, prevFocus]); 20 | 21 | return ; 22 | }; 23 | 24 | OTPInputComponent.propTypes = { 25 | focus: PropTypes.bool, 26 | autoFocus: PropTypes.bool, 27 | }; 28 | 29 | const OTPInput = memo(OTPInputComponent); 30 | 31 | export default OTPInput; 32 | -------------------------------------------------------------------------------- /app/components/PageHeaderWrapper/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { useIntl } from 'react-intl'; 4 | import { PageHeader } from 'antd'; 5 | import history from 'utils/history'; 6 | 7 | const PageHeaderWrapper = (props) => { 8 | const intl = useIntl(); 9 | const { ghost = false, children, title, subtitle, extra, avatar } = props; 10 | 11 | return ( 12 | history.back()} 15 | title={intl.formatMessage(title)} 16 | subTitle={subtitle ? intl.formatMessage(subtitle) : null} 17 | extra={extra} 18 | avatar={avatar} 19 | // extra={[]} 20 | > 21 | {children} 22 | 23 | ); 24 | }; 25 | 26 | PageHeaderWrapper.propTypes = { 27 | ghost: PropTypes.bool, 28 | title: PropTypes.object.isRequired, 29 | subtitle: PropTypes.object, 30 | extra: PropTypes.array, 31 | avatar: PropTypes.object, 32 | children: PropTypes.node, 33 | }; 34 | 35 | export default PageHeaderWrapper; 36 | -------------------------------------------------------------------------------- /app/components/SearchInput/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import messages from 'components/SearchInput/messages'; 4 | import { injectIntl } from 'react-intl'; 5 | import { Input } from 'antd'; 6 | const { Search } = Input; 7 | 8 | const SearchInput = (props) => { 9 | const { onSearch, intl, isLoading } = props; 10 | 11 | return ( 12 | 19 | ); 20 | }; 21 | 22 | SearchInput.propTypes = { 23 | isLoading: PropTypes.bool, 24 | onSearch: PropTypes.func.isRequired, 25 | intl: PropTypes.object.isRequired, 26 | }; 27 | 28 | export default injectIntl(SearchInput); 29 | -------------------------------------------------------------------------------- /app/components/SearchInput/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * SearchInput 3 | * 4 | * This contains all the text for the SearchInput component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export const scope = 'components.SearchInput'; 9 | 10 | export default defineMessages({ 11 | searchPlaceHolder: { 12 | id: `${scope}.searchPlaceHolder`, 13 | defaultMessage: 'Search', 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /app/components/SelectInputWrapper/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { useIntl } from 'react-intl'; 4 | import { Form, Select } from 'antd'; 5 | 6 | const SelectInputWrapper = (props) => { 7 | const intl = useIntl(); 8 | const { 9 | children, 10 | rules = [], 11 | label, 12 | name, 13 | id, 14 | classname = '', 15 | value, 16 | disabled = false, 17 | required = true, 18 | changeHandler = () => {}, 19 | error, 20 | } = props; 21 | 22 | return ( 23 | 31 | 40 | 41 | ); 42 | }; 43 | 44 | SelectInputWrapper.propTypes = { 45 | name: PropTypes.string.isRequired, 46 | value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 47 | disabled: PropTypes.bool, 48 | required: PropTypes.bool, 49 | error: PropTypes.string, 50 | children: PropTypes.node, 51 | rules: PropTypes.array, 52 | label: PropTypes.object.isRequired, 53 | id: PropTypes.string, 54 | classname: PropTypes.string, 55 | changeHandler: PropTypes.func, 56 | }; 57 | 58 | export default SelectInputWrapper; 59 | -------------------------------------------------------------------------------- /app/components/SelectInputWrapper/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Select input Messages 3 | * 4 | * This contains all the text for the LoginForm component. 5 | */ 6 | 7 | import { defineMessages } from 'react-intl'; 8 | 9 | export const scope = 'components.selectInput'; 10 | 11 | export default defineMessages({ 12 | select: { 13 | id: `${scope}.select`, 14 | defaultMessage: 'Select', 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /app/components/Toggle/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * LocaleToggle 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import PropTypes from 'prop-types'; 9 | import { Select } from 'antd'; 10 | import { injectIntl } from 'react-intl'; 11 | 12 | const { Option } = Select; 13 | 14 | const Toggle = (props) => { 15 | const { values, value, intl, onToggle, messages } = props; 16 | let content = ; 17 | if (values) { 18 | content = values.map((optVal) => ( 19 | 22 | )); 23 | } 24 | 25 | return ( 26 | 29 | ); 30 | }; 31 | 32 | Toggle.propTypes = { 33 | onToggle: PropTypes.func, 34 | intl: PropTypes.object, 35 | values: PropTypes.array, 36 | value: PropTypes.string, 37 | messages: PropTypes.object, 38 | }; 39 | 40 | export default injectIntl(Toggle); 41 | -------------------------------------------------------------------------------- /app/components/ToggleOption/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * ToggleOption 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import PropTypes from 'prop-types'; 9 | import { injectIntl } from 'react-intl'; 10 | import { Select } from 'antd'; 11 | const { Option } = Select; 12 | 13 | const ToggleOption = ({ value, message, intl }) => ( 14 | 15 | ); 16 | 17 | ToggleOption.propTypes = { 18 | value: PropTypes.string.isRequired, 19 | message: PropTypes.object, 20 | intl: PropTypes.object, 21 | }; 22 | 23 | export default injectIntl(ToggleOption); 24 | -------------------------------------------------------------------------------- /app/components/ToolTipButtonWrapper/index.js: -------------------------------------------------------------------------------- 1 | import { Button, Tooltip } from 'antd'; 2 | import PropTypes from 'prop-types'; 3 | import React from 'react'; 4 | import { useIntl } from 'react-intl'; 5 | 6 | const ToolTipButtonWrapper = ({ 7 | title, 8 | clickEvent, 9 | children, 10 | className = '', 11 | danger = false, 12 | color = '#39af9f', 13 | }) => { 14 | const intl = useIntl(); 15 | return ( 16 | 17 | 25 | 26 | ); 27 | }; 28 | 29 | ToolTipButtonWrapper.propTypes = { 30 | title: PropTypes.object, 31 | danger: PropTypes.bool, 32 | clickEvent: PropTypes.func, 33 | children: PropTypes.node, 34 | className: PropTypes.string, 35 | color: PropTypes.string, 36 | }; 37 | 38 | export default ToolTipButtonWrapper; 39 | -------------------------------------------------------------------------------- /app/containers/AlertMessage/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | ALERT_UNMOUNT, 3 | AUTO_DISMISS_ALERT, 4 | CLEAR_ALERT, 5 | SHOW_ALERT_MESSAGE, 6 | } from 'containers/AlertMessage/constants'; 7 | 8 | /** 9 | * Enqueue alert on the screen 10 | * @param {object} alert payload 11 | * 12 | */ 13 | 14 | export function enqueueAlertAction(alert) { 15 | return { 16 | type: SHOW_ALERT_MESSAGE, 17 | alert, 18 | }; 19 | } 20 | 21 | /** 22 | * Remove alert from the screen 23 | * 24 | * @return {object} An action object with a type of CLEAR_ALERT 25 | */ 26 | export function clearAlertAction() { 27 | return { 28 | type: CLEAR_ALERT, 29 | }; 30 | } 31 | 32 | export function autoDismissAlertAction() { 33 | return { 34 | type: AUTO_DISMISS_ALERT, 35 | }; 36 | } 37 | 38 | export function alertUnmountAction() { 39 | return { 40 | type: ALERT_UNMOUNT, 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /app/containers/AlertMessage/constants.js: -------------------------------------------------------------------------------- 1 | export const CLEAR_ALERT = 'containers/AlertMessage/CLEAR_ALERT'; 2 | export const SHOW_ALERT_MESSAGE = 'containers/AlertMessage/SHOW_ALERT_MESSAGE'; 3 | export const AUTO_DISMISS_ALERT = 'containers/AlertMessage/AUTO_DISMISS_ALERT'; 4 | export const ALERT_UNMOUNT = 'containers/AlertMessage/ALERT_UNMOUNT'; 5 | -------------------------------------------------------------------------------- /app/containers/AlertMessage/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Alert Message 4 | * 5 | */ 6 | 7 | import React, { useEffect } from 'react'; 8 | import { useDispatch, useSelector } from 'react-redux'; 9 | import { useInjectReducer } from 'utils/injectReducer'; 10 | import reducer from 'containers/AlertMessage/reducer'; 11 | import { useInjectSaga } from 'utils/injectSaga'; 12 | import saga from 'containers/AlertMessage/saga'; 13 | import commonMessage from 'common/messages'; 14 | import { FormattedMessage } from 'react-intl'; 15 | import { createStructuredSelector } from 'reselect'; 16 | import { 17 | makeAlertMessageSelector, 18 | makeAlertMessageTypeSelector, 19 | makeAlertShowSelector, 20 | makeIdSelector, 21 | } from 'containers/AlertMessage/selectors'; 22 | import { Alert } from 'antd'; 23 | import { autoDismissAlertAction } from 'containers/AlertMessage/actions'; 24 | 25 | const key = 'alertMessage'; 26 | 27 | const stateSelector = createStructuredSelector({ 28 | message: makeAlertMessageSelector(), 29 | show: makeAlertShowSelector(), 30 | type: makeAlertMessageTypeSelector(), 31 | id: makeIdSelector(), 32 | }); 33 | 34 | export default function AlertMessage() { 35 | const dispatch = useDispatch(); 36 | 37 | const autoDismiss = () => dispatch(autoDismissAlertAction()); 38 | 39 | useInjectReducer({ key, reducer }); 40 | 41 | useInjectSaga({ key, saga }); 42 | 43 | const { message, type, id, show } = useSelector(stateSelector); 44 | 45 | useEffect(() => { 46 | if (message !== '') { 47 | autoDismiss(); 48 | } 49 | }, [message]); 50 | 51 | if (!show) { 52 | return <>; 53 | } 54 | 55 | return ( 56 | : ''} 59 | description={message} 60 | type={type} 61 | showIcon 62 | closable 63 | /> 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /app/containers/AlertMessage/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Alert Message reducer 4 | * 5 | */ 6 | import produce, { setAutoFreeze } from 'immer'; 7 | import uuid from 'react-uuid'; 8 | import { 9 | SHOW_ALERT_MESSAGE, 10 | CLEAR_ALERT, 11 | ALERT_UNMOUNT, 12 | } from 'containers/AlertMessage/constants'; 13 | 14 | export const initialState = { 15 | show: false, 16 | message: '', 17 | type: '', 18 | id: '', 19 | }; 20 | 21 | setAutoFreeze(false); 22 | /* eslint-disable default-case, no-param-reassign */ 23 | const alertReducer = produce((draft, action) => { 24 | switch (action.type) { 25 | case SHOW_ALERT_MESSAGE: 26 | draft.show = true; 27 | draft.message = action.alert.message; 28 | draft.type = action.alert.type; 29 | draft.id = uuid(); 30 | break; 31 | case ALERT_UNMOUNT: 32 | case CLEAR_ALERT: 33 | draft.show = false; 34 | draft.message = ''; 35 | draft.type = ''; 36 | draft.id = ''; 37 | break; 38 | default: 39 | } 40 | }, initialState); 41 | 42 | export default alertReducer; 43 | -------------------------------------------------------------------------------- /app/containers/AlertMessage/saga.js: -------------------------------------------------------------------------------- 1 | import { delay, put, takeLatest } from 'redux-saga/effects'; 2 | import { AUTO_DISMISS_ALERT } from 'containers/AlertMessage/constants'; 3 | import { clearAlertAction } from 'containers/AlertMessage/actions'; 4 | 5 | export function* dismissAlertAction() { 6 | yield delay(5000); 7 | yield put(clearAlertAction()); 8 | } 9 | 10 | export default function* snackBarSaga() { 11 | yield takeLatest(AUTO_DISMISS_ALERT, dismissAlertAction); 12 | } 13 | -------------------------------------------------------------------------------- /app/containers/AlertMessage/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import { initialState } from 'containers/AlertMessage/reducer'; 3 | 4 | const selectAlertMessage = (state) => state.alertMessage || initialState; 5 | 6 | const makeAlertMessageSelector = () => 7 | createSelector(selectAlertMessage, (substate) => substate.message); 8 | 9 | const makeAlertMessageTypeSelector = () => 10 | createSelector(selectAlertMessage, (substate) => substate.type); 11 | 12 | const makeAlertShowSelector = () => 13 | createSelector(selectAlertMessage, (substate) => substate.show); 14 | 15 | const makeIdSelector = () => 16 | createSelector(selectAlertMessage, (substate) => substate.id); 17 | 18 | export { 19 | makeIdSelector, 20 | makeAlertShowSelector, 21 | makeAlertMessageSelector, 22 | makeAlertMessageTypeSelector, 23 | }; 24 | -------------------------------------------------------------------------------- /app/containers/App/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * @type {string} 4 | */ 5 | export const LOGGED_IN = 'containers/App/LOGGED_IN'; 6 | export const GET_PROFILE_REQUEST = 'containers/App/GET_PROFILE_REQUEST'; 7 | export const GET_PROFILE_SUCCESS = 'containers/App/GET_PROFILE_SUCCESS'; 8 | export const GET_PROFILE_ERROR = 'containers/App/GET_PROFILE_ERROR'; 9 | export const IS_LOGGED = 'containers/App/IS_LOGGED'; 10 | export const IS_LOGGED_SUCCESS = 'containers/App/IS_LOGGED_SUCCESS'; 11 | export const IS_LOGGED_ERROR = 'containers/App/IS_LOGGED_ERROR'; 12 | export const CHANGE_FIELD = 'containers/App/CHANGE_FIELD'; 13 | export const QUERY_NOTIFICATIONS = 'containers/App/QUERY_NOTIFICATIONS'; 14 | export const LOGOUT = 'containers/App/LOGOUT'; 15 | export const LOGOUT_SUCCESS = 'containers/App/LOGOUT_SUCCESS'; 16 | export const LOGOUT_ERROR = 'containers/App/LOGOUT_ERROR'; 17 | export const HIDE_HEADER = 'containers/App/HIDE_HEADER'; 18 | export const ASYNC_START = 'containers/App/ASYNC_START'; 19 | export const ASYNC_END = 'containers/App/ASYNC_END'; 20 | export const REFRESH_TOKEN = 'containers/App/REFRESH_TOKEN'; 21 | export const TOGGLE_COLLAPSE = 'containers/App/TOGGLE_COLLAPSE'; 22 | export const CHANGE_DEVICE = 'containers/App/CHANGE_DEVICE'; 23 | export const OTP_VERIFIED = 'containers/App/OTP_VERIFIED'; 24 | export const OTP_UNVERIFIED = 'containers/App/OTP_UNVERIFIED'; 25 | export const CHANGE_OTP_VALUE = 'containers/App/CHANGE_OTP_VALUE'; 26 | export const AUTHENTICATE_OTP = 'containers/App/AUTHENTICATE_OTP'; 27 | export const OTP_ERROR = 'containers/App/OTP_ERROR'; 28 | -------------------------------------------------------------------------------- /app/containers/App/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * App.js 4 | * 5 | * This component is the skeleton around the actual pages, and should only 6 | * contain code that should be seen on all pages. (e.g. navigation bar) 7 | * 8 | */ 9 | 10 | import React, { useEffect } from 'react'; 11 | import { useDispatch } from 'react-redux'; 12 | import { BrowserRouter } from 'react-router-dom'; 13 | import { Helmet } from 'react-helmet'; 14 | import RenderRouter from 'routes'; 15 | import { getProfileAction } from 'containers/App/actions'; 16 | import { useInjectReducer } from 'utils/injectReducer'; 17 | import reducer from 'containers/App/reducer'; 18 | import { useInjectSaga } from 'utils/injectSaga'; 19 | import saga from 'containers/App/saga'; 20 | import { Layout } from 'antd'; 21 | import 'containers/App/index.less'; 22 | import GlobalStyle from 'global-styles'; 23 | import SnackMessage from 'containers/SnackMessage'; 24 | 25 | const key = 'global'; 26 | export default function App() { 27 | const dispatch = useDispatch(); 28 | const getLoggedInUserProfile = () => dispatch(getProfileAction()); 29 | useInjectReducer({ key, reducer }); 30 | useInjectSaga({ key, saga }); 31 | 32 | useEffect(() => { 33 | getLoggedInUserProfile(); 34 | }, []); 35 | return ( 36 | <> 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /app/containers/App/index.less: -------------------------------------------------------------------------------- 1 | .content-page { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | background: #f0f2f5; 6 | &-form { 7 | width: 300px; 8 | padding: 50px 40px 10px; 9 | border-radius: 10px; 10 | h2 { 11 | text-align: center; 12 | } 13 | &_button { 14 | width: 100%; 15 | } 16 | } 17 | } 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/containers/App/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import { initialState } from 'containers/App/reducer'; 3 | 4 | const selectGlobal = (state) => state.global || initialState; 5 | 6 | const selectRouter = (state) => state.router; 7 | 8 | const makeLoggedInUserSelector = () => 9 | createSelector(selectGlobal, (globalState) => globalState.user); 10 | 11 | const makeIsLoggedSelector = () => 12 | createSelector(selectGlobal, (globalState) => globalState.isLogged); 13 | 14 | const makeSelectLocation = () => 15 | createSelector(selectRouter, (routerState) => routerState.location); 16 | 17 | const makeHideHeaderSelector = () => 18 | createSelector(selectGlobal, (globalState) => globalState.hideHeader); 19 | 20 | const makeIsLoadingSelector = () => 21 | createSelector(selectGlobal, (globalState) => globalState.isLoading); 22 | 23 | const makeDeviceSelector = () => 24 | createSelector(selectGlobal, (globalState) => globalState.device); 25 | 26 | const makeCollapsedSelector = () => 27 | createSelector(selectGlobal, (globalState) => globalState.collapsed); 28 | 29 | const makeOtpVerificationSelector = () => 30 | createSelector(selectGlobal, (globalState) => globalState.otpVerified); 31 | 32 | const makeOtpValueSelector = () => 33 | createSelector(selectGlobal, (globalState) => globalState.otp); 34 | 35 | const makeOtpErrorSelector = () => 36 | createSelector(selectGlobal, (globalState) => globalState.otpError); 37 | 38 | export { 39 | makeOtpErrorSelector, 40 | makeOtpValueSelector, 41 | makeOtpVerificationSelector, 42 | makeCollapsedSelector, 43 | makeDeviceSelector, 44 | makeIsLoadingSelector, 45 | makeHideHeaderSelector, 46 | makeLoggedInUserSelector, 47 | makeIsLoggedSelector, 48 | makeSelectLocation, 49 | }; 50 | -------------------------------------------------------------------------------- /app/containers/Dashboard/Loadable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Asynchronously loads the component for Dashboard 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import loadable from 'utils/loadable'; 9 | import LoadingIndicator from 'components/LoadingIndicator'; 10 | 11 | export default loadable(() => import('./index'), { 12 | fallback: , 13 | }); 14 | -------------------------------------------------------------------------------- /app/containers/Dashboard/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Dashboard actions 4 | * 5 | */ 6 | 7 | import { 8 | QUERY_USER_STATS, 9 | SET_USER_STATS, 10 | ASYNC_START, 11 | ASYNC_END, 12 | SET_DEVICE_TYPE, 13 | QUERY_DEVICE_STATS, 14 | SET_DEVICE_CHART, 15 | } from 'containers/Dashboard/constants'; 16 | 17 | export function queryUserStatsAction() { 18 | return { 19 | type: QUERY_USER_STATS, 20 | }; 21 | } 22 | 23 | export function setUserStatsAction(stats) { 24 | return { 25 | type: SET_USER_STATS, 26 | stats, 27 | }; 28 | } 29 | 30 | export function asyncStartAction() { 31 | return { 32 | type: ASYNC_START, 33 | }; 34 | } 35 | 36 | export function asyncEndAction() { 37 | return { 38 | type: ASYNC_END, 39 | }; 40 | } 41 | 42 | export function setDeviceTypeAction(deviceType) { 43 | return { 44 | type: SET_DEVICE_TYPE, 45 | deviceType, 46 | }; 47 | } 48 | 49 | export function setDeviceChartAction(deviceChart) { 50 | return { 51 | type: SET_DEVICE_CHART, 52 | deviceChart, 53 | }; 54 | } 55 | 56 | export function queryDeviceAction() { 57 | return { 58 | type: QUERY_DEVICE_STATS, 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /app/containers/Dashboard/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Dashboard constants 4 | * 5 | */ 6 | export const UMOUNT_DASHBOARD = 'app/Dashboard/UMOUNT_DASHBOARD'; 7 | export const QUERY_USER_STATS = 'app/Dashboard/QUERY_USER_STATS'; 8 | export const SET_USER_STATS = 'app/Dashboard/SET_USER_STATS'; 9 | export const ASYNC_START = 'containers/Dashboard/ASYNC_START'; 10 | export const ASYNC_END = 'containers/Dashboard/ASYNC_END'; 11 | export const BROWSER = 'BROWSER'; 12 | export const OS = 'OS'; 13 | export const GET_DEVICE_STATS = 'containers/Dashboard/GET_DEVICE_STATS'; 14 | export const SET_DEVICE_TYPE = 'containers/Dashboard/SET_DEVICE_TYPE'; 15 | export const QUERY_DEVICE_STATS = 'containers/Dashboard/QUERY_DEVICE_STATS'; 16 | export const SET_DEVICE_CHART = 'containers/Dashboard/SET_DEVICE_CHART'; 17 | -------------------------------------------------------------------------------- /app/containers/Dashboard/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Dashboard Messages 3 | * 4 | * This contains all the text for the LoginPage container. 5 | */ 6 | 7 | import { defineMessages } from 'react-intl'; 8 | 9 | export const scope = 'containers.Dashboard'; 10 | 11 | export default defineMessages({ 12 | totalUser: { 13 | id: `${scope}.totalUser`, 14 | defaultMessage: 15 | 'Total {count, plural, =0 {user} one { user} other { users}}', 16 | }, 17 | activeUser: { 18 | id: `${scope}.activeUser`, 19 | defaultMessage: 20 | 'Active {count, plural, =0 {user} one { user} other { users}}', 21 | }, 22 | inActiveUser: { 23 | id: `${scope}.inActiveUser`, 24 | defaultMessage: 25 | 'In-active {count, plural, =0 {user} one { user} other { users}}', 26 | }, 27 | deviceChart: { 28 | id: `${scope}.deviceChart`, 29 | defaultMessage: 'Devices', 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /app/containers/Dashboard/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LoginPage reducer 4 | * 5 | */ 6 | import produce from 'immer'; 7 | import { 8 | SET_DEVICE_CHART, 9 | UMOUNT_DASHBOARD, 10 | SET_USER_STATS, 11 | BROWSER, 12 | SET_DEVICE_TYPE, 13 | } from 'containers/Dashboard/constants'; 14 | 15 | export const initialState = { 16 | isLoading: false, 17 | userStats: { 18 | total: 0, 19 | active: 0, 20 | inactive: 0, 21 | }, 22 | deviceChart: [], 23 | deviceType: BROWSER, 24 | }; 25 | 26 | /* eslint-disable default-case, no-param-reassign */ 27 | const DashboardReducer = produce((draft, action) => { 28 | switch (action.type) { 29 | case SET_USER_STATS: 30 | draft.userStats = action.stats; 31 | break; 32 | case SET_DEVICE_TYPE: 33 | draft.deviceType = action.deviceType; 34 | break; 35 | case SET_DEVICE_CHART: 36 | draft.deviceChart = action.deviceChart; 37 | break; 38 | case UMOUNT_DASHBOARD: 39 | draft.userStats = { 40 | total: 0, 41 | active: 0, 42 | inactive: 0, 43 | }; 44 | draft.isLoading = false; 45 | break; 46 | } 47 | }, initialState); 48 | 49 | export default DashboardReducer; 50 | -------------------------------------------------------------------------------- /app/containers/Dashboard/saga.js: -------------------------------------------------------------------------------- 1 | import { 2 | asyncEndAction, 3 | asyncStartAction, 4 | setDeviceChartAction, 5 | setUserStatsAction, 6 | } from 'containers/Dashboard/actions'; 7 | import { 8 | BROWSER, 9 | QUERY_DEVICE_STATS, 10 | QUERY_USER_STATS, 11 | } from 'containers/Dashboard/constants'; 12 | import { makeDeviceTypeSelector } from 'containers/Dashboard/selectors'; 13 | import { call, put, select, takeLatest } from 'redux-saga/effects'; 14 | import ApiEndpoint from 'utils/api'; 15 | import { GET } from 'utils/constants'; 16 | import request from 'utils/request'; 17 | 18 | export function* handleQueryUserStats() { 19 | yield put(asyncStartAction()); 20 | const requestUrl = '/dashboard/users'; 21 | const payload = ApiEndpoint.makeApiPayload(requestUrl, GET); 22 | try { 23 | const response = yield call(request, payload); 24 | return yield put(setUserStatsAction(response)); 25 | } catch (error) { 26 | return yield put(asyncEndAction()); 27 | } 28 | } 29 | 30 | export function* handleQueryDeviceStats() { 31 | yield put(asyncStartAction()); 32 | const deviceType = yield select(makeDeviceTypeSelector()); 33 | const requestUrl = `/dashboard/${deviceType === BROWSER ? 'browser' : 'os'}`; 34 | const payload = ApiEndpoint.makeApiPayload(requestUrl, GET); 35 | try { 36 | const response = yield call(request, payload); 37 | return yield put(setDeviceChartAction(response)); 38 | } catch (error) { 39 | return yield put(asyncEndAction()); 40 | } 41 | } 42 | 43 | export default function* DashboardSaga() { 44 | yield takeLatest(QUERY_USER_STATS, handleQueryUserStats); 45 | yield takeLatest(QUERY_DEVICE_STATS, handleQueryDeviceStats); 46 | } 47 | -------------------------------------------------------------------------------- /app/containers/Dashboard/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import { initialState } from 'containers/Dashboard/reducer'; 3 | 4 | const selectDashboardDomain = (state) => state.dashboard || initialState; 5 | 6 | const makeIsLoadingSelector = () => 7 | createSelector(selectDashboardDomain, (substate) => substate.isLoading); 8 | 9 | const makeUserStatsSelector = () => 10 | createSelector(selectDashboardDomain, (substate) => substate.userStats); 11 | 12 | const makeDeviceChartSelector = () => 13 | createSelector(selectDashboardDomain, (substate) => substate.deviceChart); 14 | 15 | const makeDeviceTypeSelector = () => 16 | createSelector(selectDashboardDomain, (substate) => substate.deviceType); 17 | 18 | export { 19 | makeIsLoadingSelector, 20 | makeUserStatsSelector, 21 | makeDeviceChartSelector, 22 | makeDeviceTypeSelector, 23 | }; 24 | -------------------------------------------------------------------------------- /app/containers/EmailTemplate/Loadable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Asynchronously loads the component for Email Template Module 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import loadable from 'utils/loadable'; 9 | import LoadingIndicator from 'components/LoadingIndicator'; 10 | 11 | export default loadable(() => import('./index'), { 12 | fallback: , 13 | }); 14 | -------------------------------------------------------------------------------- /app/containers/EmailTemplate/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * EmailTemplate constants 4 | * 5 | */ 6 | 7 | export const ADD_VALIDATION_ERROR = 8 | 'containers/EmailTemplate/ADD_VALIDATION_ERROR'; 9 | export const ASYNC_START = 'containers/EmailTemplate/ASYNC_START'; 10 | export const ASYNC_END = 'containers/EmailTemplate/ASYNC_END'; 11 | export const SET_PAGE_SIZE = 'containers/EmailTemplate/SET_PAGE_SIZE'; 12 | export const SET_FORM_METHOD = 'containers/EmailTemplate/SET_FORM_METHOD'; 13 | export const SET_INITIAL_VALUES = 'containers/EmailTemplate/SET_INITIAL_VALUES'; 14 | export const SET_ID = 'containers/EmailTemplate/SET_ID'; 15 | export const INITIATE_CLEAN = 'containers/EmailTemplate/INITIATE_CLEAN'; 16 | export const SET_FORM_VALUES = 'containers/EmailTemplate/SET_FORM_VALUES'; 17 | export const SET_KEYWORD = 'containers/EmailTemplate/SET_KEYWORD'; 18 | export const QUERY_TEMPLATE = 'containers/EmailTemplate/QUERY_TEMPLATE'; 19 | export const SET_PAGE_NUMBER = 'containers/EmailTemplate/SET_PAGE_NUMBER'; 20 | export const DELETE_ITEM_BY_ID = 'containers/EmailTemplate/DELETE_ITEM_BY_ID'; 21 | export const SUBMIT_FORM = 'containers/EmailTemplate/SUBMIT_FORM'; 22 | export const GET_TEMPLATE_BY_ID = 'containers/EmailTemplate/GET_TEMPLATE_BY_ID'; 23 | export const CLEAR_FORM = 'containers/EmailTemplate/CLEAR_FORM'; 24 | export const ASSIGN_TEMPLATE = 'containers/EmailTemplate/ASSIGN_TEMPLATE'; 25 | -------------------------------------------------------------------------------- /app/containers/ForgotPassword/Loadable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Asynchronously loads the component for ForgotPassword 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import loadable from 'utils/loadable'; 9 | import LoadingIndicator from 'components/LoadingIndicator'; 10 | 11 | export default loadable(() => import('./index'), { 12 | fallback: , 13 | }); 14 | -------------------------------------------------------------------------------- /app/containers/ForgotPassword/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ForgotPasswordPage actions 4 | * 5 | */ 6 | 7 | import { 8 | ADD_VALIDATION_ERROR, 9 | FORGOT_PASSWORD, 10 | SET_FORM_VALUES, 11 | ASYNC_START, 12 | ASYNC_END, 13 | } from 'containers/ForgotPassword/constants'; 14 | 15 | export function forgotPasswordAction() { 16 | return { 17 | type: FORGOT_PASSWORD, 18 | }; 19 | } 20 | 21 | export function enterValidationErrorAction(errors) { 22 | return { 23 | type: ADD_VALIDATION_ERROR, 24 | errors, 25 | }; 26 | } 27 | 28 | export function setFormValuesAction(formValues) { 29 | return { 30 | type: SET_FORM_VALUES, 31 | formValues, 32 | }; 33 | } 34 | 35 | export function asyncStartAction() { 36 | return { 37 | type: ASYNC_START, 38 | }; 39 | } 40 | 41 | export function asyncEndAction() { 42 | return { 43 | type: ASYNC_END, 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /app/containers/ForgotPassword/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ForgotPasswordPage constants 4 | * 5 | */ 6 | 7 | export const FORGOT_PASSWORD = 'app/ForgotPasswordPage/FORGOT_PASSWORD'; 8 | export const ADD_VALIDATION_ERROR = 9 | 'app/ForgotPasswordPage/ADD_VALIDATION_ERROR'; 10 | export const SET_FORM_VALUES = 'app/ForgotPasswordPage/SET_FORM_VALUES'; 11 | export const ASYNC_END = 'app/ForgotPasswordPage/ASYNC_END'; 12 | export const ASYNC_START = 'app/ForgotPasswordPage/ASYNC_START'; 13 | -------------------------------------------------------------------------------- /app/containers/ForgotPassword/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Forgot Password Messages 3 | * 4 | * This contains all the text for the Forgot Password container. 5 | */ 6 | 7 | import { defineMessages } from 'react-intl'; 8 | 9 | export const scope = 'containers.ForgotPassword'; 10 | 11 | export default defineMessages({ 12 | mailSent: { 13 | id: `${scope}.mailSent`, 14 | defaultMessage: 15 | 'If your email exists in our system you will be sent a link to reset password!', 16 | }, 17 | mailSentError: { 18 | id: `${scope}.mailSentError`, 19 | defaultMessage: 'Error during process!', 20 | }, 21 | helmetForgotPwdTitle: { 22 | id: `${scope}.helmetForgotPwdTitle`, 23 | defaultMessage: 'Forgot Password Page', 24 | }, 25 | forgotPassword: { 26 | id: `${scope}.forgotPassword`, 27 | defaultMessage: 'Forgot Password', 28 | }, 29 | forgotPasswordBtn: { 30 | id: `${scope}.forgotPasswordBtn`, 31 | defaultMessage: 'Send Reset Link', 32 | }, 33 | back: { 34 | id: `${scope}.back`, 35 | defaultMessage: 'Go Back', 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /app/containers/ForgotPassword/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ForgotPasswordPage reducer 4 | * 5 | */ 6 | import produce from 'immer'; 7 | import { 8 | ADD_VALIDATION_ERROR, 9 | SET_FORM_VALUES, 10 | } from 'containers/ForgotPassword/constants'; 11 | 12 | export const initialState = { 13 | initialValues: { email: '' }, 14 | formValues: {}, 15 | isLoading: false, 16 | errors: [], 17 | }; 18 | 19 | /* eslint-disable default-case, no-param-reassign */ 20 | const forgotPasswordReducer = produce((draft, action) => { 21 | switch (action.type) { 22 | case SET_FORM_VALUES: 23 | draft.formValues = action.formValues; 24 | break; 25 | case ADD_VALIDATION_ERROR: 26 | draft.errors = action.errors; 27 | break; 28 | } 29 | }, initialState); 30 | 31 | export default forgotPasswordReducer; 32 | -------------------------------------------------------------------------------- /app/containers/ForgotPassword/saga.js: -------------------------------------------------------------------------------- 1 | import { call, put, select, takeLatest } from 'redux-saga/effects'; 2 | import { FORGOT_PASSWORD } from 'containers/ForgotPassword/constants'; 3 | import { makeFormValuesSelector } from 'containers/ForgotPassword/selectors'; 4 | import ApiEndpoint from 'utils/api'; 5 | import request from 'utils/request'; 6 | import { 7 | asyncEndAction, 8 | asyncStartAction, 9 | } from 'containers/ForgotPassword/actions'; 10 | import messages from 'containers/ForgotPassword/messages'; 11 | import { showAlert, showFormattedAlert } from 'common/saga'; 12 | import { PUT } from 'utils/constants'; 13 | import { enterValidationErrorAction } from 'containers/LoginPage/actions'; 14 | 15 | export function* handleForgotPassword() { 16 | yield put(asyncStartAction()); 17 | const formValues = yield select(makeFormValuesSelector()); 18 | const requestUrl = `/auth/forgot-password`; 19 | const requestPayload = ApiEndpoint.makeApiPayload( 20 | requestUrl, 21 | PUT, 22 | formValues, 23 | ); 24 | try { 25 | yield call(request, requestPayload); 26 | yield put(asyncEndAction()); 27 | return yield showFormattedAlert('success', messages.mailSent); 28 | } catch (error) { 29 | yield put(asyncEndAction()); 30 | if (error.data && error.data.statusCode === 422) { 31 | return yield put(enterValidationErrorAction(error.data.message)); 32 | } 33 | return yield showAlert('error', error.data.message); 34 | } 35 | } 36 | 37 | export default function* forgotPasswordSaga() { 38 | yield takeLatest(FORGOT_PASSWORD, handleForgotPassword); 39 | } 40 | -------------------------------------------------------------------------------- /app/containers/ForgotPassword/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import { initialState } from 'containers/ForgotPassword/reducer'; 3 | const selectForgotPasswordPageDomain = (state) => 4 | state.forgotPassword || initialState; 5 | 6 | const makeFormValuesSelector = () => 7 | createSelector( 8 | selectForgotPasswordPageDomain, 9 | (substate) => substate.formValues, 10 | ); 11 | 12 | const makeInitialValuesSelector = () => 13 | createSelector( 14 | selectForgotPasswordPageDomain, 15 | (substate) => substate.initialValues, 16 | ); 17 | 18 | const makeErrorsSelector = () => 19 | createSelector(selectForgotPasswordPageDomain, (substate) => substate.errors); 20 | 21 | const makeIsLoadingSelector = () => 22 | createSelector( 23 | selectForgotPasswordPageDomain, 24 | (substate) => substate.isLoading, 25 | ); 26 | 27 | export { 28 | makeFormValuesSelector, 29 | makeErrorsSelector, 30 | makeIsLoadingSelector, 31 | makeInitialValuesSelector, 32 | }; 33 | -------------------------------------------------------------------------------- /app/containers/HomePage/Loadable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Asynchronously loads the component for HomePage 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import loadable from 'utils/loadable'; 9 | import LoadingIndicator from 'components/LoadingIndicator'; 10 | 11 | export default loadable(() => import('./index'), { 12 | fallback: , 13 | }); 14 | -------------------------------------------------------------------------------- /app/containers/HomePage/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * HomePage actions 4 | * 5 | */ 6 | 7 | import { 8 | IS_LOGGED, 9 | ASYNC_START, 10 | ASYNC_END, 11 | SET_CONTRIBUTORS, 12 | GET_CONTRIBUTORS, 13 | } from 'containers/HomePage/constants'; 14 | 15 | export function isLoggedAction() { 16 | return { 17 | type: IS_LOGGED, 18 | }; 19 | } 20 | 21 | export function asyncStartAction() { 22 | return { 23 | type: ASYNC_START, 24 | }; 25 | } 26 | 27 | export function asyncEndAction() { 28 | return { 29 | type: ASYNC_END, 30 | }; 31 | } 32 | 33 | export function setContributorAction(contributors) { 34 | return { 35 | type: SET_CONTRIBUTORS, 36 | contributors, 37 | }; 38 | } 39 | 40 | export function getContributorAction() { 41 | return { 42 | type: GET_CONTRIBUTORS, 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /app/containers/HomePage/banner.js: -------------------------------------------------------------------------------- 1 | import { Button, Col, Row } from 'antd'; 2 | import React from 'react'; 3 | import { useIntl } from 'react-intl'; 4 | import Welcome from 'assets/images/reporting.gif'; 5 | import messages from 'containers/HomePage/messages'; 6 | 7 | const Banner = () => { 8 | const intl = useIntl(); 9 | return ( 10 |
11 |
12 | 13 | 14 |
15 |

{intl.formatMessage(messages.header)}

16 |

{intl.formatMessage(messages.description)}

17 | 23 |
24 | 25 | 26 |
27 | banner-img 28 |
29 | 30 |
31 |
32 |
33 | ); 34 | }; 35 | 36 | export default Banner; 37 | -------------------------------------------------------------------------------- /app/containers/HomePage/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * HomePage constants 4 | * 5 | */ 6 | 7 | export const IS_LOGGED = 'app/HomePage/IS_LOGGED'; 8 | export const SET_CONTRIBUTORS = 'app/HomePage/SET_CONTRIBUTORS'; 9 | export const GET_CONTRIBUTORS = 'app/HomePage/GET_CONTRIBUTORS'; 10 | export const ASYNC_START = 'app/HomePage/ASYNC_START'; 11 | export const ASYNC_END = 'app/HomePage/ASYNC_END'; 12 | -------------------------------------------------------------------------------- /app/containers/HomePage/index.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | /** 3 | * 4 | * HomePage 5 | * 6 | */ 7 | import React, { useEffect } from 'react'; 8 | import { useDispatch, useSelector } from 'react-redux'; 9 | import { useInjectSaga } from 'utils/injectSaga'; 10 | import { useInjectReducer } from 'utils/injectReducer'; 11 | import { createStructuredSelector } from 'reselect'; 12 | import Navbar from 'common/layout/Navbar'; 13 | import TruthyHelps from 'containers/HomePage/truthyHelp'; 14 | import Contributors from 'containers/HomePage/contributors'; 15 | // import Footer from 'components/Footer'; 16 | import reducer from 'containers/HomePage/reducer'; 17 | import saga from 'containers/HomePage/saga'; 18 | import Banner from 'containers/HomePage/banner'; 19 | import { 20 | makeContributorsSelector, 21 | makeIsLoadingSelector, 22 | } from 'containers/HomePage/selector'; 23 | import { getContributorAction } from 'containers/HomePage/actions'; 24 | 25 | const key = 'homePage'; 26 | const stateSelector = createStructuredSelector({ 27 | contributors: makeContributorsSelector(), 28 | isLoading: makeIsLoadingSelector(), 29 | }); 30 | 31 | export default function HomePage() { 32 | const dispatch = useDispatch(); 33 | 34 | useInjectReducer({ key, reducer }); 35 | useInjectSaga({ key, saga }); 36 | 37 | const { contributors, isLoading } = useSelector(stateSelector); 38 | 39 | useEffect(() => { 40 | dispatch(getContributorAction()); 41 | }, []); 42 | 43 | return ( 44 |
45 |
46 |
47 | {/* nav-img */} 48 |
49 | 50 | 51 | 52 | 53 | {/*
*/} 54 | {/* 55 | Login 56 | */} 57 |
58 |
59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /app/containers/HomePage/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ForgotPasswordPage reducer 4 | * 5 | */ 6 | import produce from 'immer'; 7 | import { 8 | SET_CONTRIBUTORS, 9 | ASYNC_START, 10 | ASYNC_END, 11 | } from 'containers/HomePage/constants'; 12 | 13 | export const initialState = { 14 | contributors: [], 15 | isLoading: false, 16 | errors: [], 17 | }; 18 | 19 | /* eslint-disable default-case, no-param-reassign */ 20 | const homePageReducer = produce((draft, action) => { 21 | switch (action.type) { 22 | case SET_CONTRIBUTORS: 23 | draft.contributors = action.contributors; 24 | break; 25 | case ASYNC_START: 26 | draft.isLoading = true; 27 | break; 28 | case ASYNC_END: 29 | draft.isLoading = false; 30 | break; 31 | } 32 | }, initialState); 33 | 34 | export default homePageReducer; 35 | -------------------------------------------------------------------------------- /app/containers/HomePage/saga.js: -------------------------------------------------------------------------------- 1 | import { call, put, takeLatest } from 'redux-saga/effects'; 2 | import { GET_CONTRIBUTORS } from 'containers/HomePage/constants'; 3 | import { 4 | asyncEndAction, 5 | asyncStartAction, 6 | setContributorAction, 7 | } from 'containers/HomePage/actions'; 8 | // import messages from 'containers/HomePage/messages'; 9 | import { showAlert } from 'common/saga'; 10 | import axios from 'axios'; 11 | 12 | const contribUri = 13 | 'https://gobeam.github.io/truthy-contributors/contributors.json'; 14 | 15 | async function getContrib() { 16 | try { 17 | const response = await axios.get(contribUri); 18 | return response; 19 | } catch (error) { 20 | return []; 21 | } 22 | } 23 | 24 | export function* handleGetContributors() { 25 | yield put(asyncStartAction()); 26 | try { 27 | const contributors = yield call(getContrib); 28 | if (contributors?.data) { 29 | yield put(setContributorAction(contributors.data)); 30 | } 31 | 32 | return yield put(asyncEndAction()); 33 | } catch (error) { 34 | yield put(asyncEndAction()); 35 | return yield showAlert('error', error.data.message); 36 | } 37 | } 38 | 39 | export default function* homePageSaga() { 40 | yield takeLatest(GET_CONTRIBUTORS, handleGetContributors); 41 | } 42 | -------------------------------------------------------------------------------- /app/containers/HomePage/selector.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import { initialState } from 'containers/HomePage/reducer'; 3 | const selectHomePageDomain = (state) => state.homePage || initialState; 4 | 5 | const makeContributorsSelector = () => 6 | createSelector(selectHomePageDomain, (substate) => substate.contributors); 7 | 8 | const makeIsLoadingSelector = () => 9 | createSelector(selectHomePageDomain, (substate) => substate.isLoading); 10 | 11 | export { makeIsLoadingSelector, makeContributorsSelector }; 12 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider actions 4 | * 5 | */ 6 | 7 | import { CHANGE_LOCALE } from './constants'; 8 | 9 | /** 10 | * Change the language on the client side, 11 | * 12 | * @param {string} languageLocale The language locale 13 | * 14 | * @return {object} An action object with a type of CHANGE_LOCALE 15 | */ 16 | export function changeLocaleAction(languageLocale) { 17 | return { 18 | type: CHANGE_LOCALE, 19 | locale: languageLocale, 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider constants 4 | * 5 | */ 6 | 7 | export const CHANGE_LOCALE = 'app/LanguageToggle/CHANGE_LOCALE'; 8 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider 4 | * 5 | * this component connects the redux state language locale to the 6 | * IntlProvider component and i18n messages (loaded from `app/translations`) 7 | */ 8 | 9 | import React from 'react'; 10 | import PropTypes from 'prop-types'; 11 | import { useSelector } from 'react-redux'; 12 | import { createSelector } from 'reselect'; 13 | import { IntlProvider } from 'react-intl'; 14 | import { makeSelectLocale } from 'containers/LanguageProvider/selectors'; 15 | 16 | const stateSelector = createSelector(makeSelectLocale(), (locale) => ({ 17 | locale, 18 | })); 19 | 20 | export default function LanguageProvider(props) { 21 | const { locale } = useSelector(stateSelector); 22 | 23 | return ( 24 | 29 | {React.Children.only(props.children)} 30 | 31 | ); 32 | } 33 | 34 | LanguageProvider.propTypes = { 35 | messages: PropTypes.object, 36 | children: PropTypes.element.isRequired, 37 | }; 38 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageProvider reducer 4 | * 5 | */ 6 | import produce, { setAutoFreeze } from 'immer'; 7 | import { DEFAULT_LOCALE } from 'i18n'; 8 | import { CHANGE_LOCALE } from 'containers/LanguageProvider/constants'; 9 | 10 | export const initialState = { 11 | locale: DEFAULT_LOCALE, 12 | }; 13 | 14 | setAutoFreeze(false); 15 | /* eslint-disable default-case, no-param-reassign */ 16 | const languageProviderReducer = produce((draft, action) => { 17 | switch (action.type) { 18 | case CHANGE_LOCALE: 19 | draft.locale = action.locale; 20 | break; 21 | default: 22 | } 23 | }, initialState); 24 | 25 | export default languageProviderReducer; 26 | -------------------------------------------------------------------------------- /app/containers/LanguageProvider/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import { initialState } from './reducer'; 3 | 4 | /** 5 | * Direct selector to the languageToggle state domain 6 | */ 7 | const selectLanguage = (state) => state.language || initialState; 8 | 9 | /** 10 | * Select the language locale 11 | */ 12 | 13 | const makeSelectLocale = () => 14 | createSelector(selectLanguage, (languageState) => languageState.locale); 15 | 16 | export { selectLanguage, makeSelectLocale }; 17 | -------------------------------------------------------------------------------- /app/containers/LocaleToggle/Wrapper.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Wrapper = styled.div` 4 | padding: 2px; 5 | `; 6 | 7 | export default Wrapper; 8 | -------------------------------------------------------------------------------- /app/containers/LocaleToggle/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LanguageToggle 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { useDispatch, useSelector } from 'react-redux'; 9 | import { createStructuredSelector } from 'reselect'; 10 | import { Dropdown, Menu } from 'antd'; 11 | import { TranslationOutlined } from '@ant-design/icons'; 12 | import { changeLocaleAction } from 'containers/LanguageProvider/actions'; 13 | import { useCookie } from 'hooks/useCookie'; 14 | import { makeSelectLocale } from 'containers/LanguageProvider/selectors'; 15 | import { appLocales } from 'common/language'; 16 | 17 | const stateSelector = createStructuredSelector({ 18 | locale: makeSelectLocale(), 19 | }); 20 | 21 | export function LocaleToggle() { 22 | const dispatch = useDispatch(); 23 | // eslint-disable-next-line no-unused-vars 24 | const [cookie, updateCookie] = useCookie('lang', 'en'); 25 | const { locale } = useSelector(stateSelector); 26 | 27 | const selectLocale = ({ key }) => { 28 | dispatch(changeLocaleAction(key)); 29 | // @ts-ignore 30 | updateCookie(key, 10); 31 | }; 32 | 33 | return ( 34 | 39 | {appLocales.map((lang) => ( 40 | 45 | {lang.flag} {lang.label} 46 | 47 | ))} 48 | 49 | } 50 | > 51 | 52 | 53 | 54 | 55 | ); 56 | } 57 | 58 | export default LocaleToggle; 59 | -------------------------------------------------------------------------------- /app/containers/LocaleToggle/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * LocaleToggle Messages 3 | * 4 | * This contains all the text for the LanguageToggle component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export const scope = 'containers.LocaleToggle'; 9 | 10 | export default defineMessages({ 11 | en: { 12 | id: `${scope}.en`, 13 | defaultMessage: 'en', 14 | }, 15 | de: { 16 | id: `${scope}.de`, 17 | defaultMessage: 'de', 18 | }, 19 | ne: { 20 | id: `${scope}.ne`, 21 | defaultMessage: 'ne', 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /app/containers/LoginPage/Loadable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Asynchronously loads the component for LoginPage 4 | * 5 | */ 6 | 7 | import loadable from 'utils/loadable'; 8 | import LoadingIndicator from 'components/LoadingIndicator'; 9 | import React from 'react'; 10 | 11 | export default loadable(() => import('./index'), { 12 | fallback: , 13 | }); 14 | -------------------------------------------------------------------------------- /app/containers/LoginPage/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LoginPage actions 4 | * 5 | */ 6 | 7 | import { 8 | ADD_VALIDATION_ERROR, 9 | ASYNC_END, 10 | ASYNC_START, 11 | LOGIN_PROCESS, 12 | SET_FORM_VALUES, 13 | } from 'containers/LoginPage/constants'; 14 | 15 | export function setFormValuesAction(formValues) { 16 | return { 17 | type: SET_FORM_VALUES, 18 | formValues, 19 | }; 20 | } 21 | 22 | export function asyncStartAction() { 23 | return { 24 | type: ASYNC_START, 25 | }; 26 | } 27 | 28 | export function asyncEndAction() { 29 | return { 30 | type: ASYNC_END, 31 | }; 32 | } 33 | 34 | export function enterValidationErrorAction(errors) { 35 | return { 36 | type: ADD_VALIDATION_ERROR, 37 | errors, 38 | }; 39 | } 40 | 41 | export function enterLoginAction() { 42 | return { 43 | type: LOGIN_PROCESS, 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /app/containers/LoginPage/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LoginPage constants 4 | * 5 | */ 6 | export const ADD_VALIDATION_ERROR = 'containers/LoginPage/ADD_VALIDATION_ERROR'; 7 | export const SET_FORM_VALUES = 'containers/LoginPage/SET_FORM_VALUES'; 8 | export const ASYNC_START = 'containers/LoginPage/ASYNC_START'; 9 | export const ASYNC_END = 'containers/LoginPage/ASYNC_END'; 10 | export const LOGIN_PROCESS = 'containers/LoginPage/LOGIN_PROCESS'; 11 | export const CHANGE_LOGIN = 'containers/LoginPage/CHANGE_LOGIN'; 12 | export const ENTER_LOGIN_ERROR = 'containers/LoginPage/ENTER_LOGIN_ERROR'; 13 | export const LOGIN = 'containers/LoginPage/LOGIN'; 14 | export const LOGIN_SUCCESS = 'containers/LoginPage/LOGIN_SUCCESS'; 15 | export const LOGIN_ERROR = 'containers/LoginPage/LOGIN_ERROR'; 16 | export const IS_LOGGED = 'containers/LoginPage/IS_LOGGED'; 17 | export const SUCCESS_REDIRECT = '/dashboard'; 18 | -------------------------------------------------------------------------------- /app/containers/LoginPage/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * LoginPage 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { useInjectSaga } from 'utils/injectSaga'; 9 | import { useInjectReducer } from 'utils/injectReducer'; 10 | import reducer from 'containers/LoginPage/reducer'; 11 | import LoginForm from 'containers/LoginPage/loginForm'; 12 | import saga from 'containers/LoginPage/saga'; 13 | // import 'containers/LoginPage/index.less'; 14 | import Helmet from 'react-helmet'; 15 | import { FormattedMessage } from 'react-intl'; 16 | import messages from 'containers/LoginPage/messages'; 17 | import { Row, Col } from 'antd'; 18 | 19 | const key = 'login'; 20 | 21 | export default function LoginPage() { 22 | useInjectReducer({ key, reducer }); 23 | useInjectSaga({ key, saga }); 24 | 25 | return ( 26 |
27 | 28 | {(title) => ( 29 | 30 | {title} 31 | 32 | )} 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /app/containers/LoginPage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * LoginPage Messages 3 | * 4 | * This contains all the text for the LoginPage container. 5 | */ 6 | 7 | import { defineMessages } from 'react-intl'; 8 | 9 | export const scope = 'containers.LoginPage'; 10 | 11 | export default defineMessages({ 12 | back: { 13 | id: `${scope}.back`, 14 | defaultMessage: 'Go Back', 15 | }, 16 | errorLogin: { 17 | id: `${scope}.errorLogin`, 18 | defaultMessage: 'Invalid credentials', 19 | }, 20 | sessionOut: { 21 | id: `${scope}.sessionOut`, 22 | defaultMessage: 'Your session has expired', 23 | }, 24 | serverError: { 25 | id: `${scope}.serverError`, 26 | defaultMessage: 'Please try again in a moment!', 27 | }, 28 | loginToTheSystem: { 29 | id: `${scope}.loginToTheSystem`, 30 | defaultMessage: 'Log in', 31 | }, 32 | loginSuccess: { 33 | id: `${scope}.loginSuccess`, 34 | defaultMessage: 'Successfully logged in!', 35 | }, 36 | helmetLoginTitle: { 37 | id: `${scope}.HelmetLoginTitle`, 38 | defaultMessage: 'Login', 39 | }, 40 | submit: { 41 | id: `${scope}.submit`, 42 | defaultMessage: 'Sign in', 43 | }, 44 | lostPassword: { 45 | id: `${scope}.lostPassword`, 46 | defaultMessage: 'Lost password?', 47 | }, 48 | inputLogin: { 49 | id: `${scope}.inputLogin`, 50 | defaultMessage: 'Log in', 51 | }, 52 | register: { 53 | id: `${scope}.register`, 54 | defaultMessage: 'register now!', 55 | }, 56 | rememberMe: { 57 | id: `${scope}.rememberMe`, 58 | defaultMessage: 'Remember me', 59 | }, 60 | }); 61 | -------------------------------------------------------------------------------- /app/containers/LoginPage/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * LoginPage reducer 4 | * 5 | */ 6 | import produce from 'immer'; 7 | import { 8 | ADD_VALIDATION_ERROR, 9 | ASYNC_END, 10 | ASYNC_START, 11 | CHANGE_LOGIN, 12 | ENTER_LOGIN_ERROR, 13 | LOGIN, 14 | LOGIN_ERROR, 15 | LOGIN_SUCCESS, 16 | SET_FORM_VALUES, 17 | } from 'containers/LoginPage/constants'; 18 | 19 | const EmptyFields = { 20 | username: '', 21 | password: '', 22 | remember: false, 23 | }; 24 | export const initialState = { 25 | initialValues: EmptyFields, 26 | formValues: {}, 27 | errors: [], 28 | isLoading: false, 29 | }; 30 | 31 | /* eslint-disable default-case, no-param-reassign */ 32 | const loginPageReducer = produce((draft, action) => { 33 | switch (action.type) { 34 | case ADD_VALIDATION_ERROR: 35 | draft.errors = action.errors; 36 | draft.isLoading = false; 37 | break; 38 | case SET_FORM_VALUES: 39 | draft.formValues = action.formValues; 40 | break; 41 | case CHANGE_LOGIN: 42 | draft.login = action.login; 43 | draft.error = ''; 44 | break; 45 | case ASYNC_START: 46 | draft.isLoading = true; 47 | break; 48 | case ASYNC_END: 49 | draft.isLoading = false; 50 | break; 51 | case ENTER_LOGIN_ERROR: 52 | draft.error = action.error; 53 | draft.isLoading = false; 54 | break; 55 | case LOGIN: 56 | draft.login = action.login; 57 | draft.password = action.password; 58 | draft.isLoading = true; 59 | break; 60 | case LOGIN_SUCCESS: 61 | draft.isLoading = false; 62 | break; 63 | case LOGIN_ERROR: 64 | draft.error = action.error; 65 | draft.isLoading = false; 66 | break; 67 | } 68 | }, initialState); 69 | 70 | export default loginPageReducer; 71 | -------------------------------------------------------------------------------- /app/containers/LoginPage/saga.js: -------------------------------------------------------------------------------- 1 | import { call, put, select, takeEvery } from 'redux-saga/effects'; 2 | import ApiEndpoint from 'utils/api'; 3 | import request from 'utils/request'; 4 | // import messages from 'containers/LoginPage/messages'; 5 | import { makeFormValuesSelector } from 'containers/LoginPage/selectors'; 6 | import { LOGIN_PROCESS } from 'containers/LoginPage/constants'; 7 | import { getProfileAction } from 'containers/App/actions'; 8 | import { 9 | asyncEndAction, 10 | asyncStartAction, 11 | enterValidationErrorAction, 12 | } from 'containers/LoginPage/actions'; 13 | import { showAlert } from 'common/saga'; 14 | import { POST } from 'utils/constants'; 15 | import { clearSnackMessageAction } from 'containers/SnackMessage/actions'; 16 | 17 | export function* attemptLogin() { 18 | yield put(asyncStartAction()); 19 | const formValues = yield select(makeFormValuesSelector()); 20 | const requestUrl = ApiEndpoint.getLoginPath(); 21 | const requestPayload = ApiEndpoint.makeApiPayload( 22 | requestUrl, 23 | POST, 24 | formValues, 25 | ); 26 | try { 27 | yield call(request, requestPayload); 28 | yield put(asyncEndAction()); 29 | yield put(clearSnackMessageAction()); 30 | return yield put(getProfileAction()); 31 | // return yield showFormattedAlert('success', messages.loginSuccess); 32 | } catch (error) { 33 | yield put(asyncEndAction()); 34 | if (error.data && error.data.statusCode === 422) { 35 | return yield put(enterValidationErrorAction(error.data.message)); 36 | } 37 | return yield showAlert('error', error.data.message); 38 | } 39 | } 40 | 41 | export default function* loginPageSaga() { 42 | yield takeEvery(LOGIN_PROCESS, attemptLogin); 43 | } 44 | -------------------------------------------------------------------------------- /app/containers/LoginPage/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import { initialState } from 'containers/LoginPage/reducer'; 3 | 4 | /** 5 | * Direct selector to the loginPage state domain 6 | */ 7 | 8 | const selectLoginPageDomain = (state) => state.login || initialState; 9 | 10 | /** 11 | * Other specific selectors 12 | */ 13 | 14 | const makeInitialValuesSelector = () => 15 | createSelector(selectLoginPageDomain, (substate) => substate.initialValues); 16 | 17 | const makeFormValuesSelector = () => 18 | createSelector(selectLoginPageDomain, (substate) => substate.formValues); 19 | 20 | const makeErrorSelector = () => 21 | createSelector(selectLoginPageDomain, (substate) => substate.errors); 22 | 23 | const makeIsLoadingSelector = () => 24 | createSelector(selectLoginPageDomain, (substate) => substate.isLoading); 25 | 26 | /** 27 | * Default selector used by LoginPage 28 | */ 29 | 30 | const makeSelectLoginPage = () => 31 | createSelector(selectLoginPageDomain, (substate) => substate); 32 | 33 | export default makeSelectLoginPage; 34 | 35 | export { 36 | makeInitialValuesSelector, 37 | makeFormValuesSelector, 38 | makeErrorSelector, 39 | makeIsLoadingSelector, 40 | }; 41 | -------------------------------------------------------------------------------- /app/containers/NotFoundPage/Loadable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Asynchronously loads the component for NotFoundPage 3 | */ 4 | 5 | import React from 'react'; 6 | import loadable from 'utils/loadable'; 7 | import LoadingIndicator from 'components/LoadingIndicator'; 8 | 9 | export default loadable(() => import('./index'), { 10 | fallback: , 11 | }); 12 | -------------------------------------------------------------------------------- /app/containers/NotFoundPage/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * NotFoundPage 3 | * 4 | * This is the page we show when the user visits a url that doesn't have a route 5 | */ 6 | import React from 'react'; 7 | import { FormattedMessage } from 'react-intl'; 8 | import messages from 'containers/NotFoundPage/messages'; 9 | import { Button, Result } from 'antd'; 10 | import history from 'utils/history'; 11 | 12 | /** 13 | * @return {boolean} 14 | */ 15 | 16 | const NotFound = () => { 17 | const clickBack = () => history.push('/'); 18 | return ( 19 |
20 | } 23 | subTitle={} 24 | extra={ 25 | 28 | } 29 | /> 30 |
31 | ); 32 | }; 33 | 34 | export default NotFound; 35 | -------------------------------------------------------------------------------- /app/containers/NotFoundPage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * NotFoundPage Messages 3 | * 4 | * This contains all the text for the NotFoundPage component. 5 | */ 6 | import { defineMessages } from 'react-intl'; 7 | 8 | export const scope = 'containers.NotFoundPage'; 9 | 10 | export default defineMessages({ 11 | header: { 12 | id: `${scope}.header`, 13 | defaultMessage: 'Page not found.', 14 | }, 15 | back: { 16 | id: `${scope}.back`, 17 | defaultMessage: 'Go Back Home', 18 | }, 19 | message: { 20 | id: `${scope}.message`, 21 | defaultMessage: 22 | 'Oops! Looks like you followed a bad link. If you think this is a problem with us, please tell us.', 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /app/containers/Permission/Loadable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Asynchronously loads the component for Users 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import loadable from 'utils/loadable'; 9 | import LoadingIndicator from 'components/LoadingIndicator'; 10 | 11 | export default loadable(() => import('./index'), { 12 | fallback: , 13 | }); 14 | -------------------------------------------------------------------------------- /app/containers/Permission/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Permission constants 4 | * 5 | */ 6 | 7 | export const ADD_VALIDATION_ERROR = 8 | 'containers/Permission/ADD_VALIDATION_ERROR'; 9 | export const ASYNC_START = 'containers/Permission/ASYNC_START'; 10 | export const ASYNC_END = 'containers/Permission/ASYNC_END'; 11 | export const QUERY_PERMISSION = 'containers/Permission/QUERY_PERMISSION'; 12 | export const SET_ID = 'containers/Permission/SET_ID'; 13 | export const SET_FORM_VALUES = 'containers/Permission/SET_FORM_VALUES'; 14 | export const SET_KEYWORD = 'containers/Permission/SET_KEYWORD'; 15 | export const SET_FORM_METHOD = 'containers/Permission/SET_FORM_METHOD'; 16 | export const INITIATE_CLEAN = 'containers/Permission/INITIATE_CLEAN'; 17 | export const SET_PAGE_SIZE = 'containers/Permission/SET_PAGE_SIZE'; 18 | export const SET_INITIAL_VALUES = 'containers/Permission/SET_INITIAL_VALUES'; 19 | 20 | export const SET_PAGE_NUMBER = 'containers/Permission/SET_PAGE_NUMBER'; 21 | export const SYNC_PERMISSION = 'containers/Permission/SYNC_PERMISSION'; 22 | export const DELETE_ITEM_BY_ID = 'containers/Permission/DELETE_ITEM_BY_ID'; 23 | export const SUBMIT_FORM = 'containers/Permission/SUBMIT_FORM'; 24 | 25 | export const GET_PERMISSION_BY_ID = 26 | 'containers/Permission/GET_PERMISSION_BY_ID'; 27 | export const CLEAR_FORM = 'containers/Permission/CLEAR_FORM'; 28 | 29 | export const ASSIGN_PERMISSION = 'containers/Permission/ASSIGN_PERMISSION'; 30 | -------------------------------------------------------------------------------- /app/containers/Permission/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import { initialState } from 'containers/Permission/reducer'; 3 | 4 | /** 5 | * Direct selector to the Permission state domain 6 | */ 7 | 8 | const selectPermissionDomain = (state) => state.permission || initialState; 9 | 10 | /** 11 | * Other specific selectors 12 | */ 13 | 14 | const makeInitialValuesSelector = () => 15 | createSelector(selectPermissionDomain, (substate) => substate.initialValues); 16 | 17 | const makeIsLoadingSelector = () => 18 | createSelector(selectPermissionDomain, (substate) => substate.isLoading); 19 | 20 | const makeKeywordsSelector = () => 21 | createSelector(selectPermissionDomain, (substate) => substate.keywords); 22 | 23 | const makeFormMethodSelector = () => 24 | createSelector(selectPermissionDomain, (substate) => substate.formMethod); 25 | 26 | const makeUpdateIdSelector = () => 27 | createSelector(selectPermissionDomain, (substate) => substate.id); 28 | 29 | const makePageNumberSelector = () => 30 | createSelector(selectPermissionDomain, (substate) => substate.pageNumber); 31 | 32 | const makeLimitSelector = () => 33 | createSelector(selectPermissionDomain, (substate) => substate.pageSize); 34 | 35 | const makePermissionsSelector = () => 36 | createSelector(selectPermissionDomain, (substate) => substate.permissions); 37 | 38 | const makeErrorSelector = () => 39 | createSelector(selectPermissionDomain, (substate) => substate.errors); 40 | 41 | const makeInitiateCleanFieldSelector = () => 42 | createSelector(selectPermissionDomain, (substate) => substate.initiateClean); 43 | 44 | const makeFormValuesSelector = () => 45 | createSelector(selectPermissionDomain, (substate) => substate.formValues); 46 | 47 | export { 48 | makeInitialValuesSelector, 49 | makeFormValuesSelector, 50 | makeInitiateCleanFieldSelector, 51 | makeLimitSelector, 52 | makeKeywordsSelector, 53 | makeUpdateIdSelector, 54 | makeFormMethodSelector, 55 | makeIsLoadingSelector, 56 | makeErrorSelector, 57 | makePageNumberSelector, 58 | makePermissionsSelector, 59 | }; 60 | -------------------------------------------------------------------------------- /app/containers/PermissionDeniedPage/Loadable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Asynchronously loads the component for PermissionDeniedPage 3 | */ 4 | 5 | import React from 'react'; 6 | import loadable from 'utils/loadable'; 7 | import LoadingIndicator from 'components/LoadingIndicator'; 8 | 9 | export default loadable(() => import('./index'), { 10 | fallback: , 11 | }); 12 | -------------------------------------------------------------------------------- /app/containers/PermissionDeniedPage/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PermissionDeniedPage 3 | * 4 | * This is the page we show when the user visits a url that doesn't have permission to a route 5 | */ 6 | import React from 'react'; 7 | import { Button, Result } from 'antd'; 8 | import { FormattedMessage } from 'react-intl'; 9 | import messages from 'containers/PermissionDeniedPage/messages'; 10 | import { useNavigate } from 'react-router-dom'; 11 | 12 | /** 13 | * @return {boolean} 14 | */ 15 | 16 | const PermissionDeniedPage = () => { 17 | const navigate = useNavigate(); 18 | const clickBack = () => navigate('/login'); 19 | return ( 20 |
21 | } 24 | subTitle={} 25 | extra={ 26 | 29 | } 30 | /> 31 |
32 | ); 33 | }; 34 | 35 | export default PermissionDeniedPage; 36 | -------------------------------------------------------------------------------- /app/containers/PermissionDeniedPage/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * PermissionDeniedPage Messages 3 | * 4 | */ 5 | import { defineMessages } from 'react-intl'; 6 | 7 | export const scope = 'containers.PermissionDeniedPage'; 8 | 9 | export default defineMessages({ 10 | header: { 11 | id: `${scope}.header`, 12 | defaultMessage: 'Permission Denied', 13 | }, 14 | back: { 15 | id: `${scope}.back`, 16 | defaultMessage: 'Go Back Home', 17 | }, 18 | message: { 19 | id: `${scope}.message`, 20 | defaultMessage: "Oops! You don't have permission to visit this page.", 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /app/containers/PrivateRoute/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Private Route 4 | * 5 | */ 6 | 7 | import LoadingIndicator from 'components/LoadingIndicator'; 8 | import { 9 | makeIsLoggedSelector, 10 | makeLoggedInUserSelector, 11 | makeOtpErrorSelector, 12 | makeOtpVerificationSelector, 13 | } from 'containers/App/selectors'; 14 | import PermissionDeniedPage from 'containers/PermissionDeniedPage'; 15 | import PropTypes from 'prop-types'; 16 | import React, { useEffect, useState } from 'react'; 17 | import { useSelector } from 'react-redux'; 18 | import { Navigate } from 'react-router-dom'; 19 | import { createStructuredSelector } from 'reselect'; 20 | import { checkPermissionForComponent } from 'utils/permission'; 21 | 22 | const stateSelector = createStructuredSelector({ 23 | user: makeLoggedInUserSelector(), 24 | otpError: makeOtpErrorSelector(), 25 | isLogged: makeIsLoggedSelector(), 26 | otpVerified: makeOtpVerificationSelector(), 27 | }); 28 | 29 | function PrivateRoute({ children, path, resource, method, defaultPermission }) { 30 | const { isLogged, user, otpVerified } = useSelector(stateSelector); 31 | const [permitted, setPermitted] = useState(true); 32 | 33 | useEffect(() => { 34 | if (isLogged) { 35 | setPermitted( 36 | checkPermissionForComponent(user.role, { 37 | path, 38 | resource, 39 | method, 40 | defaultPermission, 41 | }), 42 | ); 43 | } 44 | }, [user, path]); 45 | 46 | if (isLogged === null) { 47 | return ; 48 | } 49 | 50 | if (!permitted && otpVerified) { 51 | return ; 52 | } 53 | return isLogged ? children : ; 54 | } 55 | 56 | PrivateRoute.propTypes = { 57 | defaultPermission: PropTypes.bool, 58 | path: PropTypes.string, 59 | resource: PropTypes.string.isRequired, 60 | method: PropTypes.string.isRequired, 61 | children: PropTypes.node, 62 | }; 63 | 64 | export default PrivateRoute; 65 | -------------------------------------------------------------------------------- /app/containers/Profile/Loadable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Asynchronously loads the component for HomePage 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import loadable from 'utils/loadable'; 9 | import LoadingIndicator from 'components/LoadingIndicator'; 10 | 11 | export default loadable(() => import('./index'), { 12 | fallback: , 13 | }); 14 | -------------------------------------------------------------------------------- /app/containers/Profile/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * RegisterPage Messages 3 | * 4 | * This contains all the text for the RegisterPage container. 5 | */ 6 | 7 | import { defineMessages } from 'react-intl'; 8 | 9 | export const scope = 'containers.Profile'; 10 | 11 | export default defineMessages({ 12 | helmetTitle: { 13 | id: `${scope}.helmetTitle`, 14 | defaultMessage: 'Profile - Page', 15 | }, 16 | pageHeader: { 17 | id: `${scope}.pageHeader`, 18 | defaultMessage: 'Profile', 19 | }, 20 | na: { 21 | id: `${scope}.na`, 22 | defaultMessage: 'Not Available', 23 | }, 24 | editProfileLabel: { 25 | id: `${scope}.editProfileLabel`, 26 | defaultMessage: 'Edit Profile', 27 | }, 28 | }); 29 | -------------------------------------------------------------------------------- /app/containers/PublicRoute/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Public Route 4 | * 5 | */ 6 | 7 | import React, { useEffect } from 'react'; 8 | import { useSelector } from 'react-redux'; 9 | import { useNavigate } from 'react-router-dom'; 10 | import { createStructuredSelector } from 'reselect'; 11 | import { makeIsLoggedSelector } from 'containers/App/selectors'; 12 | import LoadingIndicator from 'components/LoadingIndicator'; 13 | import Common from 'utils/common'; 14 | import { SUCCESS_REDIRECT } from 'containers/LoginPage/constants'; 15 | import PropTypes from 'prop-types'; 16 | 17 | const stateSelector = createStructuredSelector({ 18 | isLogged: makeIsLoggedSelector(), 19 | }); 20 | 21 | function PublicRoute({ children }) { 22 | const navigate = useNavigate(); 23 | const { isLogged } = useSelector(stateSelector); 24 | 25 | useEffect(() => { 26 | if (isLogged) { 27 | const redirectUrl = Common.getParameterByName('path') || SUCCESS_REDIRECT; 28 | navigate(redirectUrl); 29 | } 30 | }, [isLogged]); 31 | 32 | if (isLogged === null) { 33 | return ; 34 | } 35 | return children; 36 | } 37 | 38 | PublicRoute.propTypes = { 39 | children: PropTypes.node, 40 | }; 41 | 42 | export default PublicRoute; 43 | -------------------------------------------------------------------------------- /app/containers/RegisterPage/Loadable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Asynchronously loads the component for RegisterPage 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import loadable from 'utils/loadable'; 9 | import LoadingIndicator from 'components/LoadingIndicator'; 10 | 11 | export default loadable(() => import('./index'), { 12 | fallback: , 13 | }); 14 | -------------------------------------------------------------------------------- /app/containers/RegisterPage/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * RegisterPage actions 4 | * 5 | */ 6 | 7 | import { 8 | ADD_VALIDATION_ERROR, 9 | ASYNC_END, 10 | ASYNC_START, 11 | REGISTER_PROCESS, 12 | REGISTER_SUCCESS, 13 | SET_FORM_VALUES, 14 | CLEAR_FORM_VALUES, 15 | } from 'containers/RegisterPage/constants'; 16 | 17 | export function setFormValuesAction(formValues) { 18 | return { 19 | type: SET_FORM_VALUES, 20 | formValues, 21 | }; 22 | } 23 | 24 | export function clearFormAction(clearFormValue) { 25 | return { 26 | type: CLEAR_FORM_VALUES, 27 | clearFormValue, 28 | }; 29 | } 30 | 31 | export function asyncStartAction() { 32 | return { 33 | type: ASYNC_START, 34 | }; 35 | } 36 | 37 | export function asyncEndAction() { 38 | return { 39 | type: ASYNC_END, 40 | }; 41 | } 42 | 43 | export function enterValidationErrorAction(errors) { 44 | return { 45 | type: ADD_VALIDATION_ERROR, 46 | errors, 47 | }; 48 | } 49 | 50 | export function enterRegisterAction() { 51 | return { 52 | type: REGISTER_PROCESS, 53 | }; 54 | } 55 | 56 | export function registerSuccessAction() { 57 | return { 58 | type: REGISTER_SUCCESS, 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /app/containers/RegisterPage/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * RegisterPage constants 4 | * 5 | */ 6 | const DOMAIN = 'containers/RegisterPage/'; 7 | 8 | export const REGISTER_SUCCESS = `${DOMAIN}REGISTER_SUCCESS`; 9 | export const REGISTER_PROCESS = `${DOMAIN}REGISTER_PROCESS`; 10 | export const ADD_VALIDATION_ERROR = `${DOMAIN}ADD_VALIDATION_ERROR`; 11 | export const ASYNC_START = `${DOMAIN}ASYNC_START`; 12 | export const ASYNC_END = `${DOMAIN}ASYNC_END`; 13 | export const SET_FORM_VALUES = `${DOMAIN}SET_FORM_VALUES`; 14 | export const CLEAR_FORM_VALUES = `${DOMAIN}CLEAR_FORM_VALUES`; 15 | -------------------------------------------------------------------------------- /app/containers/RegisterPage/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Register Page 4 | * 5 | */ 6 | import React, { useEffect } from 'react'; 7 | import { useDispatch, useSelector } from 'react-redux'; 8 | import { Helmet } from 'react-helmet'; 9 | import { useInjectSaga } from 'utils/injectSaga'; 10 | import { useInjectReducer } from 'utils/injectReducer'; 11 | import { FormattedMessage } from 'react-intl'; 12 | import reducer from 'containers/RegisterPage/reducer'; 13 | import messages from 'containers/RegisterPage/messages'; 14 | import saga from 'containers/RegisterPage/saga'; 15 | import RegisterForm from 'containers/RegisterPage/registerForm'; 16 | import { createStructuredSelector } from 'reselect'; 17 | import { makeLoggedInUserSelector } from 'containers/App/selectors'; 18 | import { hideHeaderAction } from 'containers/App/actions'; 19 | import 'containers/RegisterPage/index.less'; 20 | import { Row, Col } from 'antd'; 21 | 22 | const key = 'register'; 23 | 24 | const stateSelector = createStructuredSelector({ 25 | user: makeLoggedInUserSelector(), 26 | }); 27 | 28 | export default function RegisterPage() { 29 | const dispatch = useDispatch(); 30 | const { user } = useSelector(stateSelector); 31 | const hideHeader = () => dispatch(hideHeaderAction(true)); 32 | 33 | useInjectReducer({ key, reducer }); 34 | useInjectSaga({ key, saga }); 35 | 36 | useEffect(() => { 37 | hideHeader(); 38 | }, [user]); 39 | 40 | return ( 41 |
42 | 43 | {(title) => ( 44 | 45 | {title} 46 | 47 | )} 48 | 49 | 50 | 51 | 52 | 53 | 54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /app/containers/RegisterPage/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gobeam/truthy-react-frontend/858dfd35e345ae7b10e8e1eb4aecc419df28600b/app/containers/RegisterPage/index.less -------------------------------------------------------------------------------- /app/containers/RegisterPage/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Register Page reducer 4 | * 5 | */ 6 | import produce from 'immer'; 7 | import { 8 | ADD_VALIDATION_ERROR, 9 | REGISTER_SUCCESS, 10 | SET_FORM_VALUES, 11 | ASYNC_END, 12 | ASYNC_START, 13 | CLEAR_FORM_VALUES, 14 | } from 'containers/RegisterPage/constants'; 15 | 16 | const EmptyFields = { 17 | email: '', 18 | name: '', 19 | password: '', 20 | confirmPassword: '', 21 | username: '', 22 | accept: false, 23 | }; 24 | 25 | export const initialState = { 26 | initialValues: EmptyFields, 27 | formValues: {}, 28 | errors: [], 29 | error: '', 30 | isLoading: false, 31 | clearFormValue: false, 32 | }; 33 | 34 | /* eslint-disable default-case, no-param-reassign */ 35 | const loginPageReducer = produce((draft, action) => { 36 | switch (action.type) { 37 | case ADD_VALIDATION_ERROR: 38 | draft.errors = action.errors; 39 | draft.isLoading = false; 40 | break; 41 | case SET_FORM_VALUES: 42 | draft.formValues = action.formValues; 43 | break; 44 | case ASYNC_START: 45 | draft.isLoading = true; 46 | break; 47 | case ASYNC_END: 48 | draft.isLoading = false; 49 | break; 50 | case CLEAR_FORM_VALUES: 51 | draft.clearFormValue = action.clearFormValue; 52 | break; 53 | case REGISTER_SUCCESS: 54 | draft.isLoading = false; 55 | break; 56 | } 57 | }, initialState); 58 | 59 | export default loginPageReducer; 60 | -------------------------------------------------------------------------------- /app/containers/RegisterPage/saga.js: -------------------------------------------------------------------------------- 1 | import { REGISTER_PROCESS } from 'containers/RegisterPage/constants'; 2 | import { call, put, select, takeLatest } from 'redux-saga/effects'; 3 | import { 4 | asyncEndAction, 5 | asyncStartAction, 6 | enterValidationErrorAction, 7 | } from 'containers/RegisterPage/actions'; 8 | import { makeFormValuesSelector } from 'containers/RegisterPage/selectors'; 9 | import ApiEndpoint from 'utils/api'; 10 | import request from 'utils/request'; 11 | import messages from 'containers/RegisterPage/messages'; 12 | import { showAlert, showFormattedAlert } from 'common/saga'; 13 | import { POST } from 'utils/constants'; 14 | 15 | export function* handleRegister() { 16 | yield put(asyncStartAction()); 17 | const formValues = yield select(makeFormValuesSelector()); 18 | const requestUrl = ApiEndpoint.getRegisterPath(); 19 | delete formValues.confirmPassword; 20 | delete formValues.accept; 21 | const requestPayload = ApiEndpoint.makeApiPayload( 22 | requestUrl, 23 | POST, 24 | formValues, 25 | ); 26 | try { 27 | const response = yield call(request, requestPayload); 28 | if (response && response.error) { 29 | return yield put(enterValidationErrorAction(response.error)); 30 | } 31 | yield put(asyncEndAction()); 32 | return yield showFormattedAlert('success', messages.registerSuccess); 33 | } catch (error) { 34 | yield put(asyncEndAction()); 35 | if (error.data && error.data.statusCode === 422) { 36 | return yield put(enterValidationErrorAction(error.data.message)); 37 | } 38 | return yield showAlert('error', error.data.message); 39 | } 40 | } 41 | 42 | export default function* registerPageSaga() { 43 | yield takeLatest(REGISTER_PROCESS, handleRegister); 44 | } 45 | -------------------------------------------------------------------------------- /app/containers/RegisterPage/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import { initialState } from 'containers/RegisterPage/reducer'; 3 | 4 | /** 5 | * Direct selector to the RegisterPage state domain 6 | */ 7 | 8 | const selectRegisterPageDomain = (state) => state.register || initialState; 9 | 10 | /** 11 | * Other specific selectors 12 | */ 13 | 14 | const makeInitialValuesSelector = () => 15 | createSelector( 16 | selectRegisterPageDomain, 17 | (substate) => substate.initialValues, 18 | ); 19 | 20 | const makeFormValuesSelector = () => 21 | createSelector(selectRegisterPageDomain, (substate) => substate.formValues); 22 | 23 | const makeErrorSelector = () => 24 | createSelector(selectRegisterPageDomain, (substate) => substate.errors); 25 | 26 | const makeIsLoadingSelector = () => 27 | createSelector(selectRegisterPageDomain, (substate) => substate.isLoading); 28 | 29 | const makeClearFormValueSelector = () => 30 | createSelector( 31 | selectRegisterPageDomain, 32 | (substate) => substate.clearFormValue, 33 | ); 34 | 35 | export { 36 | makeClearFormValueSelector, 37 | makeFormValuesSelector, 38 | makeErrorSelector, 39 | makeIsLoadingSelector, 40 | makeInitialValuesSelector, 41 | }; 42 | -------------------------------------------------------------------------------- /app/containers/ResetPassword/Loadable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Asynchronously loads the component for ForgotPassword 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import loadable from 'utils/loadable'; 9 | import LoadingIndicator from 'components/LoadingIndicator'; 10 | 11 | export default loadable(() => import('./index'), { 12 | fallback: , 13 | }); 14 | -------------------------------------------------------------------------------- /app/containers/ResetPassword/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ResetPassword actions 4 | * 5 | */ 6 | 7 | import { 8 | ADD_VALIDATION_ERROR, 9 | ASYNC_END, 10 | ASYNC_START, 11 | RESET_PASSWORD, 12 | SET_FORM_VALUES, 13 | SET_RESET_CODE, 14 | CLEAR_FORM_VALUES, 15 | } from 'containers/ResetPassword/constants'; 16 | 17 | export function setFormValuesAction(formValues) { 18 | return { 19 | type: SET_FORM_VALUES, 20 | formValues, 21 | }; 22 | } 23 | 24 | export function asyncStartAction() { 25 | return { 26 | type: ASYNC_START, 27 | }; 28 | } 29 | 30 | export function setResetCodeAction(code) { 31 | return { 32 | type: SET_RESET_CODE, 33 | code, 34 | }; 35 | } 36 | 37 | export function asyncEndAction() { 38 | return { 39 | type: ASYNC_END, 40 | }; 41 | } 42 | 43 | export function resetPasswordAction() { 44 | return { 45 | type: RESET_PASSWORD, 46 | }; 47 | } 48 | 49 | export function enterValidationErrorAction(errors) { 50 | return { 51 | type: ADD_VALIDATION_ERROR, 52 | errors, 53 | }; 54 | } 55 | 56 | export function clearFormAction(clearFormValue) { 57 | return { 58 | type: CLEAR_FORM_VALUES, 59 | clearFormValue, 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /app/containers/ResetPassword/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ResetPassword constants 4 | * 5 | */ 6 | 7 | export const RESET_PASSWORD = 'containers/ResetPassword/RESET_PASSWORD'; 8 | export const ADD_VALIDATION_ERROR = 9 | 'containers/ResetPassword/ADD_VALIDATION_ERROR'; 10 | export const ASYNC_START = 'containers/ResetPassword/ASYNC_START'; 11 | export const ASYNC_END = 'containers/ResetPassword/ASYNC_END'; 12 | export const SET_FORM_VALUES = 'containers/ResetPassword/SET_FORM_VALUES'; 13 | export const SET_RESET_CODE = 'containers/ResetPassword/SET_RESET_CODE'; 14 | export const CLEAR_FORM_VALUES = 'containers/ResetPassword/CLEAR_FORM_VALUES'; 15 | -------------------------------------------------------------------------------- /app/containers/ResetPassword/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Reset Password Messages 3 | * 4 | * This contains all the text for the Reset Password container. 5 | */ 6 | 7 | import { defineMessages } from 'react-intl'; 8 | 9 | export const scope = 'containers.ResetPassword'; 10 | 11 | export default defineMessages({ 12 | helmetResetPasswordTitle: { 13 | id: `${scope}.helmetResetPasswordTitle`, 14 | defaultMessage: 'Reset Password Page', 15 | }, 16 | resetPassword: { 17 | id: `${scope}.resetPassword`, 18 | defaultMessage: 'Reset Password', 19 | }, 20 | resetSuccess: { 21 | id: `${scope}.resetSuccess`, 22 | defaultMessage: 'Password changed successful!', 23 | }, 24 | resetPasswordBtn: { 25 | id: `${scope}.resetPasswordBtn`, 26 | defaultMessage: 'Reset', 27 | }, 28 | back: { 29 | id: `${scope}.back`, 30 | defaultMessage: 'Go Back', 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /app/containers/ResetPassword/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ResetPassword reducer 4 | * 5 | */ 6 | import produce from 'immer'; 7 | import { 8 | ADD_VALIDATION_ERROR, 9 | ASYNC_END, 10 | ASYNC_START, 11 | SET_FORM_VALUES, 12 | SET_RESET_CODE, 13 | CLEAR_FORM_VALUES, 14 | } from 'containers/ResetPassword/constants'; 15 | 16 | export const initialState = { 17 | code: '', 18 | initialValues: { 19 | password: '', 20 | confirmPassword: '', 21 | }, 22 | formValues: {}, 23 | isLoading: false, 24 | errors: {}, 25 | clearFormValue: false, 26 | }; 27 | 28 | /* eslint-disable default-case, no-param-reassign */ 29 | const resetPasswordReducer = produce((draft, action) => { 30 | switch (action.type) { 31 | case SET_FORM_VALUES: 32 | draft.formValues = action.formValues; 33 | break; 34 | case SET_RESET_CODE: 35 | draft.code = action.code; 36 | break; 37 | case ADD_VALIDATION_ERROR: 38 | draft.errors = action.errors; 39 | break; 40 | case ASYNC_START: 41 | draft.isLoading = true; 42 | break; 43 | case ASYNC_END: 44 | draft.isLoading = false; 45 | break; 46 | case CLEAR_FORM_VALUES: 47 | draft.clearFormValue = action.clearFormValue; 48 | break; 49 | } 50 | }, initialState); 51 | 52 | export default resetPasswordReducer; 53 | -------------------------------------------------------------------------------- /app/containers/ResetPassword/saga.js: -------------------------------------------------------------------------------- 1 | import { call, put, select, takeLatest } from 'redux-saga/effects'; 2 | import { RESET_PASSWORD } from 'containers/ResetPassword/constants'; 3 | import { 4 | makeCodeSelector, 5 | makeFormValuesSelector, 6 | } from 'containers/ResetPassword/selectors'; 7 | import ApiEndpoint from 'utils/api'; 8 | import request from 'utils/request'; 9 | import { 10 | asyncEndAction, 11 | asyncStartAction, 12 | enterValidationErrorAction, 13 | } from 'containers/ResetPassword/actions'; 14 | import messages from 'containers/ResetPassword/messages'; 15 | import { showAlert, showFormattedAlert } from 'common/saga'; 16 | import { PUT } from 'utils/constants'; 17 | 18 | export function* handleResetPassword() { 19 | yield put(asyncStartAction()); 20 | const formValues = yield select(makeFormValuesSelector()); 21 | const code = yield select(makeCodeSelector()); 22 | const requestUrl = `/auth/reset-password`; 23 | const requestPayload = ApiEndpoint.makeApiPayload(requestUrl, PUT, { 24 | ...formValues, 25 | token: code, 26 | }); 27 | try { 28 | const response = yield call(request, requestPayload); 29 | if (response && response.error) { 30 | yield put(asyncEndAction()); 31 | if (typeof response.error === 'object') { 32 | return yield put(enterValidationErrorAction(response.error)); 33 | } 34 | } 35 | yield showFormattedAlert('success', messages.resetSuccess); 36 | return yield put(asyncEndAction()); 37 | } catch (error) { 38 | yield put(asyncEndAction()); 39 | if (error.data && error.data.statusCode === 422) { 40 | return yield put(enterValidationErrorAction(error.data.message)); 41 | } 42 | return yield showAlert('error', error.data.message); 43 | } 44 | } 45 | 46 | export default function* homePageSaga() { 47 | yield takeLatest(RESET_PASSWORD, handleResetPassword); 48 | } 49 | -------------------------------------------------------------------------------- /app/containers/ResetPassword/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import { initialState } from 'containers/ResetPassword/reducer'; 3 | 4 | const selectResetPasswordDomain = (state) => 5 | state.resetPassword || initialState; 6 | 7 | const makePasswordSelector = () => 8 | createSelector(selectResetPasswordDomain, (substate) => substate.password); 9 | const makeConfirmPasswordSelector = () => 10 | createSelector( 11 | selectResetPasswordDomain, 12 | (substate) => substate.confirmPassword, 13 | ); 14 | 15 | const makeIsLoadingSelector = () => 16 | createSelector(selectResetPasswordDomain, (substate) => substate.isLoading); 17 | 18 | const makeErrorsSelector = () => 19 | createSelector(selectResetPasswordDomain, (substate) => substate.errors); 20 | 21 | const makeCodeSelector = () => 22 | createSelector(selectResetPasswordDomain, (substate) => substate.code); 23 | 24 | const makeFormValuesSelector = () => 25 | createSelector(selectResetPasswordDomain, (substate) => substate.formValues); 26 | 27 | const makeInitialValuesSelector = () => 28 | createSelector( 29 | selectResetPasswordDomain, 30 | (substate) => substate.initialValues, 31 | ); 32 | 33 | const makeClearFormValueSelector = () => 34 | createSelector( 35 | selectResetPasswordDomain, 36 | (substate) => substate.clearFormValue, 37 | ); 38 | 39 | export { 40 | makeClearFormValueSelector, 41 | makeFormValuesSelector, 42 | makeInitialValuesSelector, 43 | makeIsLoadingSelector, 44 | makeCodeSelector, 45 | makePasswordSelector, 46 | makeConfirmPasswordSelector, 47 | makeErrorsSelector, 48 | }; 49 | -------------------------------------------------------------------------------- /app/containers/Role/Loadable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Asynchronously loads the component for Users 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import loadable from 'utils/loadable'; 9 | import LoadingIndicator from 'components/LoadingIndicator'; 10 | 11 | export default loadable(() => import('./index'), { 12 | fallback: , 13 | }); 14 | -------------------------------------------------------------------------------- /app/containers/Role/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Role constants 4 | * 5 | */ 6 | 7 | export const ADD_VALIDATION_ERROR = 'containers/Role/ADD_VALIDATION_ERROR'; 8 | export const ASYNC_START = 'containers/Role/ASYNC_START'; 9 | export const ASYNC_END = 'containers/Role/ASYNC_END'; 10 | export const CHANGE_FORM_FIELD = 'containers/Role/CHANGE_FORM_FIELD'; 11 | export const QUERY_ROLES = 'containers/Role/QUERY_ROLES'; 12 | export const ASSIGN_ROLES = 'containers/Role/ASSIGN_ROLES'; 13 | export const SET_PAGE_NUMBER = 'containers/Role/SET_PAGE_NUMBER'; 14 | export const DELETE_ITEM_BY_ID = 'containers/Role/DELETE_ITEM_BY_ID'; 15 | export const SUBMIT_FORM = 'containers/Role/SUBMIT_FORM'; 16 | export const GET_ROLE_BY_ID = 'containers/Role/GET_ROLE_BY_ID'; 17 | export const CLEAR_FORM = 'containers/Role/CLEAR_FORM'; 18 | export const SET_KEYWORD = 'containers/Role/SET_KEYWORD'; 19 | export const SET_FORM_METHOD = 'containers/Role/SET_FORM_METHOD'; 20 | export const SET_FORM_VALUES = 'containers/Role/SET_FORM_VALUES'; 21 | export const SET_INITIAL_VALUES = 'containers/Role/SET_INITIAL_VALUES'; 22 | export const INITIATE_CLEAN = 'containers/Role/INITIATE_CLEAN'; 23 | export const SET_PAGE_SIZE = 'containers/Role/SET_PAGE_SIZE'; 24 | export const SET_ID = 'containers/Role/SET_ID'; 25 | export const QUERY_PERMISSION_LIST = 'containers/Role/QUERY_PERMISSION_LIST'; 26 | export const ASSIGN_PERMISSION_LIST = 'containers/Role/ASSIGN_PERMISSION_LIST'; 27 | -------------------------------------------------------------------------------- /app/containers/Role/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Role Messages 3 | * 4 | */ 5 | import { defineMessages } from 'react-intl'; 6 | 7 | export const scope = 'containers.Role'; 8 | 9 | export default defineMessages({ 10 | helmetTitle: { 11 | id: `${scope}.helmetTitle`, 12 | defaultMessage: 'Role', 13 | }, 14 | editTitle: { 15 | id: `${scope}.editTitle`, 16 | defaultMessage: 'Edit Role', 17 | }, 18 | addTitle: { 19 | id: `${scope}.addTitle`, 20 | defaultMessage: 'Add Role', 21 | }, 22 | nameLabel: { 23 | id: `${scope}.nameLabel`, 24 | defaultMessage: 'Name', 25 | }, 26 | namePlaceHolder: { 27 | id: `${scope}.namePlaceHolder`, 28 | defaultMessage: 'Input role name', 29 | }, 30 | descriptionPlaceHolder: { 31 | id: `${scope}.descriptionPlaceHolder`, 32 | defaultMessage: 'Input description', 33 | }, 34 | descriptionRequired: { 35 | id: `${scope}.descriptionRequired`, 36 | defaultMessage: 'The description should not be empty!', 37 | }, 38 | nameRequired: { 39 | id: `${scope}.nameRequired`, 40 | defaultMessage: 'Role name should not be empty!', 41 | }, 42 | dateLabel: { 43 | id: `${scope}.dateLabel`, 44 | defaultMessage: 'Created At', 45 | }, 46 | addLabel: { 47 | id: `${scope}.addLabel`, 48 | defaultMessage: 'Add New', 49 | }, 50 | descriptionLabel: { 51 | id: `${scope}.descriptionLabel`, 52 | defaultMessage: 'Description', 53 | }, 54 | permissionLabel: { 55 | id: `${scope}.permissionLabel`, 56 | defaultMessage: 'Permission', 57 | }, 58 | actionLabel: { 59 | id: `${scope}.actionLabel`, 60 | defaultMessage: 'Action', 61 | }, 62 | createdAt: { 63 | id: `${scope}.createdAt`, 64 | defaultMessage: '{ts, date, ::yyyyMMdd}', 65 | }, 66 | listTitle: { 67 | id: `${scope}.listTitle`, 68 | defaultMessage: 'Roles', 69 | }, 70 | dashboardTitle: { 71 | id: `${scope}.dashboardTitle`, 72 | defaultMessage: 'Dashboard', 73 | }, 74 | }); 75 | -------------------------------------------------------------------------------- /app/containers/SnackMessage/actions.js: -------------------------------------------------------------------------------- 1 | import { 2 | SNACK_UNMOUNT, 3 | AUTO_DISMISS_SNACK, 4 | CLEAR_SNACK, 5 | SHOW_SNACK_MESSAGE, 6 | } from 'containers/SnackMessage/constants'; 7 | 8 | export function enqueueSnackMessageAction(snack) { 9 | return { 10 | type: SHOW_SNACK_MESSAGE, 11 | snack, 12 | }; 13 | } 14 | 15 | export function clearSnackMessageAction() { 16 | return { 17 | type: CLEAR_SNACK, 18 | }; 19 | } 20 | 21 | export function autoDismissSnackMessageAction() { 22 | return { 23 | type: AUTO_DISMISS_SNACK, 24 | }; 25 | } 26 | 27 | export function snackUnmountAction() { 28 | return { 29 | type: SNACK_UNMOUNT, 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /app/containers/SnackMessage/constants.js: -------------------------------------------------------------------------------- 1 | export const CLEAR_SNACK = 'containers/AlertMessage/CLEAR_SNACK'; 2 | export const SHOW_SNACK_MESSAGE = 'containers/AlertMessage/SHOW_SNACK_MESSAGE'; 3 | export const AUTO_DISMISS_SNACK = 'containers/AlertMessage/AUTO_DISMISS_SNACK'; 4 | export const SNACK_UNMOUNT = 'containers/AlertMessage/SNACK_UNMOUNT'; 5 | -------------------------------------------------------------------------------- /app/containers/SnackMessage/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Snack Message 4 | * 5 | */ 6 | 7 | import React, { useEffect } from 'react'; 8 | import { useSelector } from 'react-redux'; 9 | import { useInjectReducer } from 'utils/injectReducer'; 10 | import reducer from 'containers/SnackMessage/reducer'; 11 | import { useInjectSaga } from 'utils/injectSaga'; 12 | import saga from 'containers/SnackMessage/saga'; 13 | import { createStructuredSelector } from 'reselect'; 14 | import { 15 | makeSnackMessageSelector, 16 | makeDurationSelector, 17 | makeSnackMessageTypeSelector, 18 | makeIdSelector, 19 | makeTranslateSelector, 20 | } from 'containers/SnackMessage/selectors'; 21 | import { message } from 'antd'; 22 | import { useIntl } from 'react-intl'; 23 | 24 | const key = 'snackMessage'; 25 | 26 | const stateSelector = createStructuredSelector({ 27 | content: makeSnackMessageSelector(), 28 | duration: makeDurationSelector(), 29 | type: makeSnackMessageTypeSelector(), 30 | translate: makeTranslateSelector(), 31 | id: makeIdSelector(), 32 | }); 33 | 34 | export default function SnackMessage() { 35 | useInjectReducer({ key, reducer }); 36 | 37 | useInjectSaga({ key, saga }); 38 | const intl = useIntl(); 39 | 40 | const { content, type, duration, id, translate } = useSelector(stateSelector); 41 | 42 | useEffect(() => { 43 | if (content !== '' && id !== '') { 44 | message[type.toLowerCase()]({ 45 | content: translate ? intl.formatMessage(content) : content, 46 | duration, 47 | key: id, 48 | }); 49 | } 50 | }, [id]); 51 | 52 | return <>; 53 | } 54 | -------------------------------------------------------------------------------- /app/containers/SnackMessage/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Snack Message reducer 4 | * 5 | */ 6 | import produce, { setAutoFreeze } from 'immer'; 7 | import uuid from 'react-uuid'; 8 | import { 9 | SHOW_SNACK_MESSAGE, 10 | CLEAR_SNACK, 11 | SNACK_UNMOUNT, 12 | } from 'containers/SnackMessage/constants'; 13 | 14 | export const initialState = { 15 | message: '', 16 | type: '', 17 | duration: 3, 18 | translate: false, 19 | id: '', 20 | }; 21 | 22 | setAutoFreeze(false); 23 | /* eslint-disable default-case, no-param-reassign */ 24 | const snackMessageReducer = produce((draft, action) => { 25 | switch (action.type) { 26 | case SHOW_SNACK_MESSAGE: 27 | draft.message = action.snack.message; 28 | draft.type = action.snack.type; 29 | draft.translate = action.snack.translate; 30 | draft.duration = action.snack.duration || 3; 31 | draft.id = action.snack.id || uuid(); 32 | break; 33 | case SNACK_UNMOUNT: 34 | case CLEAR_SNACK: 35 | draft.message = ''; 36 | draft.type = ''; 37 | draft.id = ''; 38 | draft.translate = false; 39 | break; 40 | default: 41 | } 42 | }, initialState); 43 | 44 | export default snackMessageReducer; 45 | -------------------------------------------------------------------------------- /app/containers/SnackMessage/saga.js: -------------------------------------------------------------------------------- 1 | import { delay, put, takeLatest } from 'redux-saga/effects'; 2 | import { AUTO_DISMISS_SNACK } from 'containers/SnackMessage/constants'; 3 | import { clearSnackMessageAction } from 'containers/SnackMessage/actions'; 4 | 5 | export function* dismissSnackMessageAction() { 6 | yield delay(5000); 7 | yield put(clearSnackMessageAction()); 8 | } 9 | 10 | export default function* snackBarSaga() { 11 | yield takeLatest(AUTO_DISMISS_SNACK, dismissSnackMessageAction); 12 | } 13 | -------------------------------------------------------------------------------- /app/containers/SnackMessage/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import { initialState } from 'containers/SnackMessage/reducer'; 3 | 4 | const selectSnackMessage = (state) => state.snackMessage || initialState; 5 | 6 | const makeSnackMessageSelector = () => 7 | createSelector(selectSnackMessage, (substate) => substate.message); 8 | 9 | const makeSnackMessageTypeSelector = () => 10 | createSelector(selectSnackMessage, (substate) => substate.type); 11 | 12 | const makeIdSelector = () => 13 | createSelector(selectSnackMessage, (substate) => substate.id); 14 | 15 | const makeDurationSelector = () => 16 | createSelector(selectSnackMessage, (substate) => substate.duration); 17 | 18 | const makeTranslateSelector = () => 19 | createSelector(selectSnackMessage, (substate) => substate.translate); 20 | 21 | export { 22 | makeTranslateSelector, 23 | makeDurationSelector, 24 | makeIdSelector, 25 | makeSnackMessageSelector, 26 | makeSnackMessageTypeSelector, 27 | }; 28 | -------------------------------------------------------------------------------- /app/containers/UserAccount/Loadable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Asynchronously loads the component for HomePage 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import loadable from 'utils/loadable'; 9 | import LoadingIndicator from 'components/LoadingIndicator'; 10 | 11 | export default loadable(() => import('./index'), { 12 | fallback: , 13 | }); 14 | -------------------------------------------------------------------------------- /app/containers/UserAccount/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * UserAccount constants 4 | * 5 | */ 6 | 7 | export const ADD_VALIDATION_ERROR = 'app/UserAccount/ADD_VALIDATION_ERROR'; 8 | export const ASYNC_END = 'app/UserAccount/ASYNC_END'; 9 | export const ASYNC_START = 'app/UserAccount/ASYNC_START'; 10 | export const CLEAR_FORM = 'app/UserAccount/CLEAR_FORM'; 11 | export const INITIATE_CLEAN = 'app/UserAccount/INITIATE_CLEAN'; 12 | export const SET_FORM_VALUES = 'app/UserAccount/SET_FORM_VALUES'; 13 | export const SET_INITIAL_VALUES = 'app/UserAccount/SET_INITIAL_VALUES'; 14 | export const SUBMIT_FORM = 'app/UserAccount/SUBMIT_FORM'; 15 | export const DISABLE_TOKEN = 'app/UserAccount/DISABLE_TOKEN'; 16 | export const UPDATE_TWO_FA_STATUS = 'app/UserAccount/UPDATE_TWO_FA_STATUS'; 17 | export const SET_LIMIT = 'app/UserAccount/SET_LIMIT'; 18 | export const SET_PAGE_SIZE = 'app/UserAccount/SET_PAGE_SIZE'; 19 | export const QUERY_REFRESH_TOKEN_LIST = 20 | 'app/UserAccount/QUERY_REFRESH_TOKEN_LIST'; 21 | export const ASSIGN_REFRESH_TOKEN_LIST = 22 | 'app/UserAccount/ASSIGN_REFRESH_TOKEN_LIST'; 23 | export const SUBMIT_CHANGE_PASSWORD_FORM = 24 | 'app/UserAccount/SUBMIT_CHANGE_PASSWORD_FORM'; 25 | -------------------------------------------------------------------------------- /app/containers/UserAccount/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * VerifyAccount 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import { useInjectSaga } from 'utils/injectSaga'; 9 | import saga from 'containers/UserAccount/saga'; 10 | import reducer from 'containers/UserAccount/reducer'; 11 | import { useInjectReducer } from 'utils/injectReducer'; 12 | import { Tabs } from 'antd'; 13 | import ProfileForm from 'containers/UserAccount/profileForm'; 14 | import messages from 'containers/UserAccount/messages'; 15 | import { useIntl } from 'react-intl'; 16 | import SecurityTab from 'containers/UserAccount/securityTab'; 17 | import LoginActivity from 'containers/UserAccount/loginActivity'; 18 | 19 | const { TabPane } = Tabs; 20 | const key = 'userAccount'; 21 | 22 | export default function UserAccount() { 23 | const intl = useIntl(); 24 | useInjectSaga({ key, saga }); 25 | useInjectReducer({ key, reducer }); 26 | 27 | return ( 28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /app/containers/UserAccount/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import { initialState } from 'containers/UserAccount/reducer'; 3 | 4 | const selectUserAccount = (state) => state.userAccount || initialState; 5 | 6 | const makeInitialValuesSelector = () => 7 | createSelector(selectUserAccount, (substate) => substate.initialValues); 8 | 9 | const makeFormValuesSelector = () => 10 | createSelector(selectUserAccount, (substate) => substate.formValues); 11 | 12 | const makeIsLoadingSelector = () => 13 | createSelector(selectUserAccount, (substate) => substate.isLoading); 14 | 15 | const makeErrorSelector = () => 16 | createSelector(selectUserAccount, (substate) => substate.errors); 17 | 18 | const makeInitiateCleanFieldSelector = () => 19 | createSelector(selectUserAccount, (substate) => substate.initiateClean); 20 | 21 | const makeTokenListSelector = () => 22 | createSelector(selectUserAccount, (substate) => substate.tokenList); 23 | 24 | const makePageSizeSelector = () => 25 | createSelector(selectUserAccount, (substate) => substate.pageSize); 26 | 27 | const makeLimitSelector = () => 28 | createSelector(selectUserAccount, (substate) => substate.limit); 29 | 30 | export { 31 | makePageSizeSelector, 32 | makeLimitSelector, 33 | makeTokenListSelector, 34 | makeInitialValuesSelector, 35 | makeFormValuesSelector, 36 | makeIsLoadingSelector, 37 | makeErrorSelector, 38 | makeInitiateCleanFieldSelector, 39 | }; 40 | -------------------------------------------------------------------------------- /app/containers/Users/Loadable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Asynchronously loads the component for Users 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import loadable from 'utils/loadable'; 9 | import { Space, Skeleton, Row, Col } from 'antd'; 10 | 11 | export default loadable(() => import('./index'), { 12 | fallback: ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ), 32 | }); 33 | 34 | // const LoadAble = () => ( 35 | // //
36 | // 37 | // 38 | // 39 | // //
40 | // ); 41 | // 42 | // export default LoadAble; 43 | -------------------------------------------------------------------------------- /app/containers/Users/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Users constants 4 | * 5 | */ 6 | 7 | export const ADD_VALIDATION_ERROR = 'containers/Users/ADD_VALIDATION_ERROR'; 8 | export const ASYNC_START = 'containers/Users/ASYNC_START'; 9 | export const ASYNC_END = 'containers/Users/ASYNC_END'; 10 | export const QUERY_USERS = 'containers/Users/QUERY_USERS'; 11 | export const QUERY_ROLES = 'containers/Users/QUERY_ROLES'; 12 | export const ASSIGN_ROLES = 'containers/Users/ASSIGN_ROLES'; 13 | 14 | export const SET_PAGE_NUMBER = 'containers/Users/SET_PAGE_NUMBER'; 15 | export const SET_PAGE_SIZE = 'containers/Users/SET_PAGE_SIZE'; 16 | export const DELETE_ITEM_BY_ID = 'containers/Users/DELETE_ITEM_BY_ID'; 17 | export const SUBMIT_FORM = 'containers/Users/SUBMIT_FORM'; 18 | 19 | export const GET_USER_BY_ID = 'containers/Users/GET_USER_BY_ID'; 20 | export const CLEAR_FORM = 'containers/Users/CLEAR_FORM'; 21 | export const CLEAR_FORM_FIELD = 'containers/Users/CLEAR_FORM_FIELD'; 22 | 23 | export const ASSIGN_USERS = 'containers/Users/ASSIGN_USERS'; 24 | export const SET_FORM_METHOD = 'containers/Users/SET_FORM_METHOD'; 25 | export const SET_ID = 'containers/Users/SET_ID'; 26 | export const SET_SEARCH_KEYWORD = 'containers/Users/SET_SEARCH_KEYWORD'; 27 | export const SET_FORM_VALUES = 'containers/Users/SET_FORM_VALUES'; 28 | export const SET_INITIAL_VALUES = 'containers/Users/SET_INITIAL_VALUES'; 29 | -------------------------------------------------------------------------------- /app/containers/VerifyAccount/Loadable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Asynchronously loads the component for HomePage 4 | * 5 | */ 6 | 7 | import React from 'react'; 8 | import loadable from 'utils/loadable'; 9 | import LoadingIndicator from 'components/LoadingIndicator'; 10 | 11 | export default loadable(() => import('./index'), { 12 | fallback: , 13 | }); 14 | -------------------------------------------------------------------------------- /app/containers/VerifyAccount/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * VerifyAccount actions 4 | * 5 | */ 6 | 7 | import { 8 | SET_VERIFY_CODE, 9 | VERIFY_VERIFY_CODE, 10 | } from 'containers/VerifyAccount/constants'; 11 | 12 | /** 13 | * Check user is logged, this action starts the request saga 14 | * 15 | * @return {object} An action object with a type of IS_LOGGED 16 | */ 17 | 18 | export function setVerifyCodeAction(code) { 19 | return { 20 | type: SET_VERIFY_CODE, 21 | code, 22 | }; 23 | } 24 | 25 | export function verifyVerifyCodeAction() { 26 | return { 27 | type: VERIFY_VERIFY_CODE, 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /app/containers/VerifyAccount/constants.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * VerifyAccount constants 4 | * 5 | */ 6 | 7 | export const SET_VERIFY_CODE = 'app/VerifyAccount/SET_VERIFY_CODE'; 8 | export const VERIFY_VERIFY_CODE = 'app/VerifyAccount/VERIFY_VERIFY_CODE'; 9 | -------------------------------------------------------------------------------- /app/containers/VerifyAccount/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * VerifyAccount 4 | * 5 | */ 6 | 7 | import React, { useEffect } from 'react'; 8 | import { useParams } from 'react-router-dom'; 9 | import { useDispatch } from 'react-redux'; 10 | import { useInjectSaga } from 'utils/injectSaga'; 11 | // Import Actions 12 | import { 13 | setVerifyCodeAction, 14 | verifyVerifyCodeAction, 15 | } from 'containers/VerifyAccount/actions'; 16 | 17 | import saga from 'containers/VerifyAccount/saga'; 18 | import reducer from 'containers/VerifyAccount/reducer'; 19 | import LoadingIndicator from 'components/LoadingIndicator'; 20 | import { useInjectReducer } from 'utils/injectReducer'; 21 | 22 | const key = 'verifyPage'; 23 | export default function VerifyAccount() { 24 | const dispatch = useDispatch(); 25 | useInjectSaga({ key, saga }); 26 | useInjectReducer({ key, reducer }); 27 | const { code } = useParams(); 28 | 29 | useEffect(() => { 30 | if (code) { 31 | dispatch(setVerifyCodeAction(code)); 32 | dispatch(verifyVerifyCodeAction()); 33 | } 34 | }, [code]); 35 | 36 | return ; 37 | } 38 | -------------------------------------------------------------------------------- /app/containers/VerifyAccount/messages.js: -------------------------------------------------------------------------------- 1 | /* 2 | * VerifyAccount Messages 3 | * 4 | * This contains all the text for the VerifyAccount container. 5 | */ 6 | 7 | import { defineMessages } from 'react-intl'; 8 | 9 | export const scope = 'containers.VerifyAccount'; 10 | 11 | export default defineMessages({ 12 | activated: { 13 | id: `${scope}.activated`, 14 | defaultMessage: 'Account activated successfully, please login to continue!', 15 | }, 16 | invalidVerification: { 17 | id: `${scope}.invalidVerification`, 18 | defaultMessage: 'Invalid verification link!', 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /app/containers/VerifyAccount/reducer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * VerifyAccount reducer 4 | * 5 | */ 6 | import produce from 'immer'; 7 | import { SET_VERIFY_CODE } from 'containers/VerifyAccount/constants'; 8 | 9 | export const initialState = { 10 | verifyCode: '', 11 | }; 12 | 13 | /* eslint-disable default-case, no-param-reassign */ 14 | const slackPageReducer = produce((draft, action) => { 15 | switch (action.type) { 16 | case SET_VERIFY_CODE: 17 | draft.verifyCode = action.code; 18 | break; 19 | } 20 | }, initialState); 21 | 22 | export default slackPageReducer; 23 | -------------------------------------------------------------------------------- /app/containers/VerifyAccount/saga.js: -------------------------------------------------------------------------------- 1 | import { call, select, takeLatest } from 'redux-saga/effects'; 2 | import { VERIFY_VERIFY_CODE } from 'containers/VerifyAccount/constants'; 3 | import { makeVerifyCodeSelector } from 'containers/VerifyAccount/selectors'; 4 | import ApiEndpoint from 'utils/api'; 5 | import request from 'utils/request'; 6 | import messages from 'containers/VerifyAccount/messages'; 7 | import { showFormattedAlert } from 'common/saga'; 8 | import { GET } from 'utils/constants'; 9 | 10 | export function* handleVerifyCode() { 11 | const code = yield select(makeVerifyCodeSelector()); 12 | const requestUrl = `/auth/activate-account?token=${code}`; 13 | const requestPayload = ApiEndpoint.makeApiPayload(requestUrl, GET); 14 | try { 15 | yield call(request, requestPayload); 16 | return yield showFormattedAlert('success', messages.activated); 17 | // return yield put(push(LOGIN_REDIRECT)); 18 | } catch (e) { 19 | return yield showFormattedAlert('error', messages.invalidVerification); 20 | // return yield put(push(LOGIN_REDIRECT)); 21 | } 22 | } 23 | 24 | export default function* homePageSaga() { 25 | yield takeLatest(VERIFY_VERIFY_CODE, handleVerifyCode); 26 | } 27 | -------------------------------------------------------------------------------- /app/containers/VerifyAccount/selectors.js: -------------------------------------------------------------------------------- 1 | import { createSelector } from 'reselect'; 2 | import { initialState } from 'containers/VerifyAccount/reducer'; 3 | 4 | const selectVerifyPageDomain = (state) => state.verifyPage || initialState; 5 | 6 | const makeVerifyCodeSelector = () => 7 | createSelector(selectVerifyPageDomain, (substate) => substate.verifyCode); 8 | 9 | export { makeVerifyCodeSelector }; 10 | -------------------------------------------------------------------------------- /app/global-styles.js: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | 3 | const GlobalStyle = createGlobalStyle` 4 | body { 5 | margin: 0; 6 | // font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 7 | // 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 8 | // sans-serif; 9 | font-family: 'Rubik', sans-serif; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | } 13 | 14 | code { 15 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 16 | monospace; 17 | } 18 | 19 | .otpInput { 20 | width: 3rem !important; 21 | height: 3rem; 22 | margin: 0 1rem; 23 | font-size: 2rem; 24 | text-align: center; 25 | border-radius: 4px; 26 | border: 1px solid rgba(0, 0, 0, 0.3); 27 | } 28 | 29 | `; 30 | 31 | export default GlobalStyle; 32 | -------------------------------------------------------------------------------- /app/helpers/Validation.js: -------------------------------------------------------------------------------- 1 | import { Validator } from 'helpers/ValidatonModel'; 2 | 3 | /** 4 | * Check if error exists 5 | * @param model 6 | */ 7 | export const checkError = (model) => { 8 | const err = {}; 9 | Object.keys(model).forEach((key) => { 10 | const validator = new Validator(); 11 | const msg = validator.validate( 12 | model[key].value, 13 | model[key].validator, 14 | model[key].key || key, 15 | ); 16 | 17 | if (msg.trim()) { 18 | err[key] = msg; 19 | } 20 | }); 21 | return err; 22 | }; 23 | 24 | export const getParameterByName = (name, url) => { 25 | /* eslint-disable default-case, no-param-reassign */ 26 | if (!url) url = window.location.href; 27 | // eslint-disable-next-line no-useless-escape 28 | name = name.replace(/[\[\]]/g, '\\$&'); 29 | const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`); 30 | const results = regex.exec(url); 31 | if (!results) return null; 32 | if (!results[2]) return ''; 33 | return decodeURIComponent(results[2].replace(/\+/g, ' ')); 34 | }; 35 | -------------------------------------------------------------------------------- /app/hooks/localstorage.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | export const useLocalStorage = (key, initialValue) => { 4 | const [storedValue, setStoredValue] = useState(() => { 5 | try { 6 | const item = window.localStorage.getItem(key); 7 | return item ? JSON.parse(item) : initialValue; 8 | } catch (error) { 9 | return initialValue; 10 | } 11 | }); 12 | 13 | const setValue = (value) => { 14 | const valueToStore = value instanceof Function ? value(storedValue) : value; 15 | setStoredValue(valueToStore); 16 | window.localStorage.setItem(key, JSON.stringify(valueToStore)); 17 | }; 18 | 19 | return [storedValue, setValue]; 20 | }; 21 | -------------------------------------------------------------------------------- /app/hooks/useCookie.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | const getItem = (key) => 4 | document.cookie.split('; ').reduce((total, currentCookie) => { 5 | const item = currentCookie.split('='); 6 | const storedKey = item[0]; 7 | const storedValue = item[1]; 8 | 9 | return key === storedKey ? decodeURIComponent(storedValue) : total; 10 | }, ''); 11 | 12 | const setItem = (key, value, numberOfDays) => { 13 | const now = new Date(); 14 | 15 | // set the time to be now + numberOfDays 16 | now.setTime(now.getTime() + numberOfDays * 60 * 60 * 24 * 1000); 17 | 18 | document.cookie = `${key}=${value}; expires=${now.toUTCString()}; path=/`; 19 | }; 20 | 21 | /** 22 | * 23 | * @param {String} key The key to store our data to 24 | * @param {String} defaultValue The default value to return in case the cookie doesn't exist 25 | */ 26 | const useCookie = (key, defaultValue) => { 27 | const getCookie = () => getItem(key) || defaultValue; 28 | const [cookie, setCookie] = useState(getCookie()); 29 | 30 | const updateCookie = (value, numberOfDays) => { 31 | setCookie(value); 32 | setItem(key, value, numberOfDays); 33 | }; 34 | 35 | return [cookie, updateCookie]; 36 | }; 37 | 38 | export { useCookie, getItem, setItem }; 39 | -------------------------------------------------------------------------------- /app/i18n.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | /** 3 | * i18n.js 4 | * 5 | * This will setup the i18n language files and locale data for your app. 6 | * IMPORTANT: This file is used by the internal build 7 | * script `extract-intl`, and must use CommonJS module syntax 8 | * You CANNOT use import/export in this file. 9 | */ 10 | 11 | const enTranslationMessages = require('./i18n/en.json'); 12 | const neTranslationMessages = require('./i18n/ne.json'); 13 | 14 | const DEFAULT_LOCALE = 'en'; 15 | 16 | const formatTranslationMessages = (locale, messages) => { 17 | const defaultFormattedMessages = 18 | locale !== DEFAULT_LOCALE 19 | ? formatTranslationMessages(DEFAULT_LOCALE, enTranslationMessages) 20 | : {}; 21 | const flattenFormattedMessages = (formattedMessages, key) => { 22 | const formattedMessage = 23 | !messages[key] && locale !== DEFAULT_LOCALE 24 | ? defaultFormattedMessages[key] 25 | : messages[key]; 26 | return Object.assign(formattedMessages, { [key]: formattedMessage }); 27 | }; 28 | return Object.keys(messages).reduce(flattenFormattedMessages, {}); 29 | }; 30 | 31 | const translationMessages = { 32 | en: formatTranslationMessages('en', enTranslationMessages), 33 | ne: formatTranslationMessages('ne', neTranslationMessages), 34 | }; 35 | 36 | module.exports = { 37 | formatTranslationMessages, 38 | translationMessages, 39 | DEFAULT_LOCALE, 40 | }; 41 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Truthy CMS 16 | 17 | 18 | 19 | 20 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/reducers/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Combine all reducers in this file and export the combined reducers. 3 | */ 4 | 5 | import { combineReducers } from 'redux'; 6 | import globalReducer from 'containers/App/reducer'; 7 | import loginPageReducer from 'containers/LoginPage/reducer'; 8 | import registerPageReducer from 'containers/RegisterPage/reducer'; 9 | import alertMessageReducer from 'containers/AlertMessage/reducer'; 10 | import snackMessageReducer from 'containers/SnackMessage/reducer'; 11 | import verifyPageReducer from 'containers/VerifyAccount/reducer'; 12 | import forgotPasswordReducer from 'containers/ForgotPassword/reducer'; 13 | import resetPasswordReducer from 'containers/ResetPassword/reducer'; 14 | import userAccountReducer from 'containers/UserAccount/reducer'; 15 | import languageProviderReducer from 'containers/LanguageProvider/reducer'; 16 | import roleReducer from 'containers/Role/reducer'; 17 | import permissionReducer from 'containers/Permission/reducer'; 18 | import usersReducer from 'containers/Users/reducer'; 19 | import DashboardReducer from 'containers/Dashboard/reducer'; 20 | import emailTemplateReducer from 'containers/EmailTemplate/reducer'; 21 | import homePageReducer from 'containers/HomePage/reducer'; 22 | 23 | /** 24 | * Merges the main reducer with the router state and dynamically injected reducers 25 | */ 26 | export default function createReducer(injectedReducers = {}) { 27 | return combineReducers({ 28 | global: globalReducer, 29 | homePage: homePageReducer, 30 | alertMessage: alertMessageReducer, 31 | snackMessage: snackMessageReducer, 32 | language: languageProviderReducer, 33 | login: loginPageReducer, 34 | register: registerPageReducer, 35 | forgotPassword: forgotPasswordReducer, 36 | resetPassword: resetPasswordReducer, 37 | verifyPage: verifyPageReducer, 38 | userAccount: userAccountReducer, 39 | dashboard: DashboardReducer, 40 | role: roleReducer, 41 | permission: permissionReducer, 42 | users: usersReducer, 43 | emailTemplate: emailTemplateReducer, 44 | ...injectedReducers, 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /app/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = (onPerfEntry) => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /app/routes/messages.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Common Messages 3 | */ 4 | 5 | import { defineMessages } from 'react-intl'; 6 | 7 | export const scope = 'routes.messages'; 8 | 9 | export default defineMessages({ 10 | dashboard: { 11 | id: `${scope}.dashboard`, 12 | defaultMessage: `Dashboard`, 13 | }, 14 | rolePage: { 15 | id: `${scope}.rolePage`, 16 | defaultMessage: `Role`, 17 | }, 18 | emailTemplatePage: { 19 | id: `${scope}.emailTemplatePage`, 20 | defaultMessage: `Email Template`, 21 | }, 22 | userPage: { 23 | id: `${scope}.userPage`, 24 | defaultMessage: `User Management`, 25 | }, 26 | permissionPage: { 27 | id: `${scope}.permissionPage`, 28 | defaultMessage: `Permission`, 29 | }, 30 | 31 | settingPage: { 32 | id: `${scope}.settingPage`, 33 | defaultMessage: `Settings`, 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /app/services/cookie.service.js: -------------------------------------------------------------------------------- 1 | export default class CookieService { 2 | static checkCookieExists = (key) => { 3 | const token = this.readCookie(key); 4 | return !!token; 5 | }; 6 | 7 | static readCookie = (name) => { 8 | const nameEQ = `${name}=`; 9 | const ca = document.cookie.split(';'); 10 | for (let i = 0; i < ca.length; i += 1) { 11 | let c = ca[i]; 12 | while (c.charAt(0) === ' ') c = c.substring(1, c.length); 13 | if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length); 14 | } 15 | return null; 16 | }; 17 | 18 | static setCookie = (cname, cvalue, exdays) => { 19 | const d = new Date(); 20 | d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000); 21 | const expires = `expires=${d.toUTCString()}`; 22 | document.cookie = `${cname}=${cvalue};${expires};path=/`; 23 | }; 24 | 25 | static deleteCookie = (name) => { 26 | document.cookie = `${name}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;`; 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /app/services/persist.service.js: -------------------------------------------------------------------------------- 1 | export const loadState = () => { 2 | try { 3 | const serializedState = localStorage.getItem('state'); 4 | if (serializedState === null) { 5 | return true; 6 | } 7 | return JSON.parse(serializedState); 8 | } catch (e) { 9 | return false; 10 | } 11 | }; 12 | 13 | export const saveState = (state) => { 14 | try { 15 | const serializedState = JSON.stringify(state); 16 | localStorage.setItem('state', serializedState); 17 | return true; 18 | } catch (e) { 19 | return false; 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /app/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /app/store/index.js: -------------------------------------------------------------------------------- 1 | import { loadState } from 'services/persist.service'; 2 | import configureStore from 'configure-store'; 3 | 4 | const initialState = loadState(); 5 | export const store = configureStore(initialState); 6 | -------------------------------------------------------------------------------- /app/utils/checkStore.js: -------------------------------------------------------------------------------- 1 | import { conformsTo, isFunction, isObject } from 'lodash'; 2 | import invariant from 'invariant'; 3 | 4 | /** 5 | * Validate the shape of redux store 6 | */ 7 | export default function checkStore(store) { 8 | const shape = { 9 | dispatch: isFunction, 10 | subscribe: isFunction, 11 | getState: isFunction, 12 | replaceReducer: isFunction, 13 | runSaga: isFunction, 14 | injectedReducers: isObject, 15 | injectedSagas: isObject, 16 | }; 17 | invariant( 18 | conformsTo(store, shape), 19 | '(app/utils...) injectors: Expected a valid redux store', 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /app/utils/colors.js: -------------------------------------------------------------------------------- 1 | export const PRIMARY_BLUE_DARK = '#0029ab'; 2 | export const PRIMARY_BLUE_LIGHT = '#15a0dd'; 3 | export const SECONDARY_BLUE_LIGHT = '#0098db'; 4 | export const BORDER_GREY_LIGHT = 'rgba(0,0,0,0.12)'; 5 | export const PRIMARY_BORDER_GREY = '#b1b1b1'; 6 | export const PRIMARY_LIGHT = '#FFFFFF'; 7 | export const PRIMARY_DARK = '#000000'; 8 | export const PRIMARY_RED = '#e50000'; 9 | export const PRIMARY_GREY = '#f2f4f7'; 10 | export const SECONDARY_GREY = '#b7b7b7'; 11 | export const PRIMARY_BACKGROUND_GREY = '#f7f7f7'; 12 | export const SECONDARY_BACKGROUND_GREY = '#f3f3f3'; 13 | export const SECONDARY_HOVER_GREY = 'rgba(0, 0, 0, 0.08)'; 14 | export const TRANSPARENT = 'rgba(0, 0, 0, 0)'; 15 | -------------------------------------------------------------------------------- /app/utils/common.js: -------------------------------------------------------------------------------- 1 | export default class Common { 2 | static getParameterByName = (name, url) => { 3 | const defaultUri = '/dashboard'; 4 | // eslint-disable-next-line no-param-reassign 5 | if (!url) url = window.location.href; 6 | // eslint-disable-next-line no-param-reassign,no-useless-escape 7 | name = name.replace(/[\[\]]/g, '\\$&'); 8 | const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`); 9 | const results = regex.exec(url); 10 | if (!results) return null; 11 | if (!results[2]) return ''; 12 | const uri = decodeURIComponent(results[2].replace(/\+/g, ' ')); 13 | const exceptUri = ['/', '/login']; 14 | if (exceptUri.includes(uri)) { 15 | return defaultUri; 16 | } 17 | return uri; 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /app/utils/constants.js: -------------------------------------------------------------------------------- 1 | export const RESTART_ON_REMOUNT = '@@saga-injector/restart-on-remount'; 2 | export const DAEMON = '@@saga-injector/daemon'; 3 | export const ONCE_TILL_UNMOUNT = '@@saga-injector/once-till-unmount'; 4 | 5 | export const POST = 'post'; 6 | export const GET = 'get'; 7 | export const DELETE = 'delete'; 8 | export const PUT = 'put'; 9 | 10 | export const StatusCodesList = { 11 | Success: 1001, 12 | ValidationError: 1002, 13 | InternalServerError: 1003, 14 | NotFound: 1004, 15 | UnauthorizedAccess: 1005, 16 | TokenExpired: 1006, 17 | TooManyTries: 1007, 18 | ServiceUnAvailable: 1008, 19 | ThrottleError: 1009, 20 | Forbidden: 1010, 21 | IncorrectOldPassword: 1011, 22 | UserInactive: 1012, 23 | BadRequest: 1013, 24 | InvalidCredentials: 1014, 25 | InvalidRefreshToken: 1015, 26 | UnsupportedFileType: 1016, 27 | OtpRequired: 1017, 28 | defaultItemDeleteError: 1018, 29 | RefreshTokenExpired: 1019, 30 | }; 31 | -------------------------------------------------------------------------------- /app/utils/history.js: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory } from 'history'; 2 | const history = createBrowserHistory(); 3 | export default history; 4 | -------------------------------------------------------------------------------- /app/utils/injectReducer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import hoistNonReactStatics from 'hoist-non-react-statics'; 3 | import { useStore, ReactReduxContext } from 'react-redux'; 4 | 5 | import getInjectors from './reducerInjectors'; 6 | 7 | /** 8 | * Dynamically injects a reducer 9 | * 10 | * @param {string} key A key of the reducer 11 | * @param {function} reducer A reducer that will be injected 12 | * 13 | */ 14 | export default ({ key, reducer }) => 15 | (WrappedComponent) => { 16 | class ReducerInjector extends React.Component { 17 | static WrappedComponent = WrappedComponent; 18 | 19 | // eslint-disable-next-line react/static-property-placement 20 | static contextType = ReactReduxContext; 21 | 22 | // eslint-disable-next-line react/static-property-placement 23 | static displayName = `withReducer(${ 24 | WrappedComponent.displayName || WrappedComponent.name || 'Component' 25 | })`; 26 | 27 | constructor(props, context) { 28 | super(props, context); 29 | 30 | getInjectors(context.store).injectReducer(key, reducer); 31 | } 32 | 33 | render() { 34 | return ; 35 | } 36 | } 37 | 38 | return hoistNonReactStatics(ReducerInjector, WrappedComponent); 39 | }; 40 | 41 | const useInjectReducer = ({ key, reducer }) => { 42 | const store = useStore(); 43 | 44 | React.useEffect(() => { 45 | getInjectors(store).injectReducer(key, reducer); 46 | }, []); 47 | }; 48 | 49 | export { useInjectReducer }; 50 | -------------------------------------------------------------------------------- /app/utils/loadable.js: -------------------------------------------------------------------------------- 1 | import React, { lazy, Suspense } from 'react'; 2 | import { ErrorBoundary } from 'react-error-boundary'; 3 | import ErrorFallback from 'components/ErrorFallback'; 4 | 5 | const loadable = (importFunc, { fallback = null } = { fallback: null }) => { 6 | const LazyComponent = lazy(importFunc); 7 | 8 | return (props) => ( 9 | window.location.reload()} 12 | > 13 | 14 | 15 | 16 | 17 | ); 18 | }; 19 | 20 | export default loadable; 21 | -------------------------------------------------------------------------------- /app/utils/pagination.js: -------------------------------------------------------------------------------- 1 | export function pagination(current, totalItem, pageSize) { 2 | const last = Math.ceil(totalItem / pageSize); 3 | const start = Math.max(getPageStart(pageSize, current - 1), 0); 4 | const end = Math.min(getPageStart(pageSize, current), totalItem); 5 | const delta = 2; 6 | const left = current - delta; 7 | const right = current + delta + 1; 8 | const range = []; 9 | const rangeWithDots = []; 10 | let l = 0; 11 | 12 | for (let i = 1; i <= last; i += 1) { 13 | if (i === 1 || i === last || (i >= left && i < right)) { 14 | range.push(i); 15 | } 16 | } 17 | 18 | // eslint-disable-next-line no-restricted-syntax 19 | for (const i of range) { 20 | if (l) { 21 | if (i - l === 2) { 22 | rangeWithDots.push(l + 1); 23 | } else if (i - l !== 1) { 24 | rangeWithDots.push('...'); 25 | } 26 | } 27 | rangeWithDots.push(i); 28 | l = i; 29 | } 30 | 31 | return { 32 | totalPages: last, 33 | pages: rangeWithDots, 34 | start: start + 1, 35 | end, 36 | }; 37 | } 38 | 39 | function getPageStart(pageSize, pageNr) { 40 | return pageSize * pageNr; 41 | } 42 | -------------------------------------------------------------------------------- /app/utils/permission.js: -------------------------------------------------------------------------------- 1 | export function checkPermissionForComponent(roles, route) { 2 | if (!roles || !roles.permission) return false; 3 | if (route.defaultPermission) return true; 4 | return roles.permission.some( 5 | (role) => 6 | role.resource === route.resource && 7 | role.path === route.path && 8 | role.method === route.method, 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /app/utils/reducerInjectors.js: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant'; 2 | import { isEmpty, isFunction, isString } from 'lodash'; 3 | 4 | import checkStore from './checkStore'; 5 | import createReducer from '../reducers'; 6 | 7 | export function injectReducerFactory(store, isValid) { 8 | return function injectReducer(key, reducer) { 9 | if (!isValid) checkStore(store); 10 | 11 | invariant( 12 | isString(key) && !isEmpty(key) && isFunction(reducer), 13 | '(app/utils...) injectReducer: Expected `reducer` to be a reducer function', 14 | ); 15 | 16 | // Check `store.injectedReducers[key] === reducer` for hot reloading when a key is the same but a reducer is different 17 | if ( 18 | Reflect.has(store.injectedReducers, key) && 19 | store.injectedReducers[key] === reducer 20 | ) 21 | return; 22 | 23 | store.injectedReducers[key] = reducer; // eslint-disable-line no-param-reassign 24 | store.replaceReducer(createReducer(store.injectedReducers)); 25 | }; 26 | } 27 | 28 | export default function getInjectors(store) { 29 | checkStore(store); 30 | 31 | return { 32 | injectReducer: injectReducerFactory(store, true), 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /app/utils/rwd.js: -------------------------------------------------------------------------------- 1 | export const PHONE_LANDSCAPE_VIEWPORT_WIDTH = '480px'; 2 | export const TABLET_VIEWPORT_WIDTH = '768px'; 3 | export const TABLET_LANDSCAPE_VIEWPORT_WIDTH = '1024px'; 4 | export const DESKTOP_VIEWPORT_WIDTH = '1260px'; 5 | 6 | export const HIDDEN_TOOLBAR_TITLE_VIEWPORT_WIDTH = '696px'; 7 | export const TOGGLE_TOOLBAR_VIEWPORT_WIDTH = '660px'; 8 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | modules: false, 7 | }, 8 | ], 9 | '@babel/preset-react', 10 | ], 11 | plugins: [ 12 | 'styled-components', 13 | '@babel/plugin-proposal-class-properties', 14 | '@babel/plugin-syntax-dynamic-import', 15 | [ 16 | 'import', 17 | { 18 | libraryName: '@ant-design/icons', 19 | libraryDirectory: '', // defaults to 'lib' 20 | camel2DashComponentName: false, // defaults to true 21 | }, 22 | ], 23 | [ 24 | 'formatjs', 25 | { 26 | idInterpolationPattern: '[sha512:contenthash:base64:6]', 27 | ast: true, 28 | }, 29 | ], 30 | ], 31 | env: { 32 | production: { 33 | only: ['app'], 34 | plugins: [ 35 | 'lodash', 36 | 'transform-react-remove-prop-types', 37 | '@babel/plugin-transform-react-inline-elements', 38 | '@babel/plugin-transform-react-constant-elements', 39 | ], 40 | }, 41 | test: { 42 | plugins: [ 43 | '@babel/plugin-transform-modules-commonjs', 44 | 'dynamic-import-node', 45 | ], 46 | }, 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const { execSync } = require('child_process'); 3 | 4 | const RESET = '\x1b[0m'; 5 | const GREEN = '\x1b[32m'; 6 | const YELLOW = '\x1b[33m'; 7 | const WHITE = '\x1b[37m'; 8 | const RED = '\x1b[31m'; 9 | const CYAN = '\x1b[36m'; 10 | const MAGENTA = '\x1b[35m'; 11 | 12 | const getColoredText = (text, color) => { 13 | if (color == null) { 14 | // eslint-disable-next-line no-param-reassign 15 | color = WHITE; 16 | } 17 | return color + text + RESET; 18 | }; 19 | 20 | const runCommand = (command) => { 21 | try { 22 | execSync(`${command}`, { stdio: 'inherit' }); 23 | } catch (e) { 24 | console.error( 25 | getColoredText(`Failed to execute ${command}. Error: ${e}`, RED), 26 | ); 27 | return false; 28 | } 29 | return true; 30 | }; 31 | 32 | const repoName = process.argv[2]; 33 | const gitCheckoutCommand = `git clone --depth 1 https://github.com/gobeam/truthy-react-frontend ${repoName}`; 34 | const installDepsCommand = `cd ${repoName} && yarn install`; 35 | 36 | console.log( 37 | getColoredText(`Cloning the repository with name ${repoName}`, CYAN), 38 | ); 39 | const checkedOut = runCommand(gitCheckoutCommand); 40 | if (!checkedOut) process.exit(-1); 41 | 42 | console.log(getColoredText(`Installing dependencies for ${repoName}`, YELLOW)); 43 | const installedDeps = runCommand(installDepsCommand); 44 | if (!installedDeps) process.exit(-1); 45 | 46 | console.log( 47 | getColoredText( 48 | 'Congratulations! You are ready. Follow the following commands to start', 49 | MAGENTA, 50 | ), 51 | ); 52 | console.log(getColoredText(`cd ${repoName} && yarn start`, GREEN)); 53 | -------------------------------------------------------------------------------- /internals/scripts/analyze.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const shelljs = require('shelljs'); 4 | const chalk = require('chalk'); 5 | const animateProgress = require('./helpers/progress'); 6 | const addCheckMark = require('./helpers/checkmark'); 7 | 8 | const progress = animateProgress('Generating stats'); 9 | 10 | // Generate stats.json file with webpack 11 | shelljs.exec( 12 | 'webpack --config internals/webpack/webpack.prod.babel.js --profile --json > stats.json', 13 | addCheckMark.bind(null, callback), // Output a checkmark on completion 14 | ); 15 | 16 | // Called after webpack has finished generating the stats.json file 17 | function callback() { 18 | clearInterval(progress); 19 | process.stdout.write( 20 | '\n\nOpen ' + 21 | chalk.magenta('http://webpack.github.io/analyse/') + 22 | ' in your browser and upload the stats.json file!' + 23 | chalk.blue( 24 | '\n(Tip: ' + chalk.italic('CMD + double-click') + ' the link!)\n\n', 25 | ), 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /internals/scripts/clean.js: -------------------------------------------------------------------------------- 1 | const shell = require('shelljs'); 2 | const addCheckMark = require('./helpers/checkmark.js'); 3 | 4 | if (!shell.which('git')) { 5 | shell.echo('Sorry, this script requires git'); 6 | shell.exit(1); 7 | } 8 | 9 | if (!shell.test('-e', 'internals/templates')) { 10 | shell.echo('The example is deleted already.'); 11 | shell.exit(1); 12 | } 13 | 14 | process.stdout.write('Cleanup started...'); 15 | 16 | // Reuse existing LanguageProvider and i18n tests 17 | shell.mv( 18 | 'app/containers/LanguageProvider/tests', 19 | 'internals/templates/containers/LanguageProvider', 20 | ); 21 | shell.cp('app/tests/i18n.test.js', 'internals/templates/tests/i18n.test.js'); 22 | 23 | // Cleanup components/ 24 | shell.rm('-rf', 'app/components/*'); 25 | 26 | // Handle containers/ 27 | shell.rm('-rf', 'app/containers'); 28 | shell.mv('internals/templates/containers', 'app'); 29 | 30 | // Handle tests/ 31 | shell.mv('internals/templates/tests', 'app'); 32 | 33 | // Handle translations/ 34 | shell.rm('-rf', 'app/translations'); 35 | shell.mv('internals/templates/translations', 'app'); 36 | 37 | // Handle utils/ 38 | shell.rm('-rf', 'app/utils'); 39 | shell.mv('internals/templates/utils', 'app'); 40 | 41 | // Replace the files in the root app/ folder 42 | shell.cp('internals/templates/app.js', 'app/app.js'); 43 | shell.cp('internals/templates/global-styles.js', 'app/global-styles.js'); 44 | shell.cp('internals/templates/i18n.js', 'app/i18n.js'); 45 | shell.cp('internals/templates/index.html', 'app/index.html'); 46 | shell.cp('internals/templates/reducers.js', 'app/reducers.js'); 47 | shell.cp('internals/templates/configureStore.js', 'app/configureStore.js'); 48 | 49 | // Remove the templates folder 50 | shell.rm('-rf', 'internals/templates'); 51 | 52 | addCheckMark(); 53 | 54 | // Commit the changes 55 | if ( 56 | shell.exec('git add . --all && git commit -qm "Remove default example"') 57 | .code !== 0 58 | ) { 59 | shell.echo('\nError: Git commit failed'); 60 | shell.exit(1); 61 | } 62 | 63 | shell.echo('\nCleanup done. Happy Coding!!!'); 64 | -------------------------------------------------------------------------------- /internals/scripts/helpers/checkmark.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | 3 | /** 4 | * Adds mark check symbol 5 | */ 6 | function addCheckMark(callback) { 7 | process.stdout.write(chalk.green(' ✓')); 8 | if (callback) callback(); 9 | } 10 | 11 | module.exports = addCheckMark; 12 | -------------------------------------------------------------------------------- /internals/scripts/helpers/get-npm-config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | module.exports = JSON.parse(fs.readFileSync('package.json', 'utf8')); 4 | -------------------------------------------------------------------------------- /internals/scripts/helpers/progress.js: -------------------------------------------------------------------------------- 1 | const readline = require('readline'); 2 | 3 | /** 4 | * Adds an animated progress indicator 5 | * 6 | * @param {string} message The message to write next to the indicator 7 | * @param {number} [amountOfDots=3] The amount of dots you want to animate 8 | */ 9 | function animateProgress(message, amountOfDots = 3) { 10 | let i = 0; 11 | return setInterval(() => { 12 | readline.cursorTo(process.stdout, 0); 13 | i = (i + 1) % (amountOfDots + 1); 14 | const dots = new Array(i + 1).join('.'); 15 | process.stdout.write(message + dots); 16 | }, 500); 17 | } 18 | 19 | module.exports = animateProgress; 20 | -------------------------------------------------------------------------------- /internals/scripts/helpers/xmark.js: -------------------------------------------------------------------------------- 1 | const chalk = require('chalk'); 2 | 3 | /** 4 | * Adds mark cross symbol 5 | */ 6 | function addXMark(callback) { 7 | process.stdout.write(chalk.red(' ✘')); 8 | if (callback) callback(); 9 | } 10 | 11 | module.exports = addXMark; 12 | -------------------------------------------------------------------------------- /internals/scripts/npmcheckversion.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('child_process'); 2 | exec('npm -v', (err, stdout) => { 3 | if (err) throw err; 4 | if (parseFloat(stdout) < 5) { 5 | // NOTE: This can happen if you have a dependency which lists an old version of npm in its own dependencies. 6 | throw new Error(`[ERROR] You need npm version @>=5 but you have ${stdout}`); 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /internals/webpack/webpack.dev.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * DEVELOPMENT WEBPACK CONFIGURATION 3 | */ 4 | 5 | const path = require('path'); 6 | const webpack = require('webpack'); 7 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 8 | const CircularDependencyPlugin = require('circular-dependency-plugin'); 9 | 10 | module.exports = require('./webpack.base.babel')({ 11 | mode: 'development', 12 | 13 | // Add hot reloading in development 14 | entry: [ 15 | require.resolve('react-app-polyfill/ie11'), 16 | 'webpack-hot-middleware/client?reload=true', 17 | path.join(process.cwd(), 'app/app.js'), // Start with js/app.js 18 | ], 19 | 20 | // Don't use hashes in dev mode for better performance 21 | output: { 22 | filename: '[name].js', 23 | chunkFilename: '[name].chunk.js', 24 | }, 25 | 26 | optimization: { 27 | splitChunks: { 28 | chunks: 'all', 29 | }, 30 | }, 31 | 32 | // Add development plugins 33 | plugins: [ 34 | new webpack.HotModuleReplacementPlugin(), // Tell webpack we want hot reloading 35 | new HtmlWebpackPlugin({ 36 | inject: true, // Inject all files that are generated by webpack, e.g. bundle.js 37 | template: 'app/index.html', 38 | }), 39 | new CircularDependencyPlugin({ 40 | exclude: /a\.js|node_modules/, // exclude node_modules 41 | failOnError: false, // show a warning when there is a circular dependency 42 | }), 43 | ], 44 | 45 | // Emit a source map for easier debugging 46 | // See https://webpack.js.org/configuration/devtool/#devtool 47 | devtool: 'eval-source-map', 48 | 49 | performance: { 50 | hints: false, 51 | }, 52 | }); 53 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2016", 5 | "jsx": "preserve", 6 | "checkJs": true, 7 | "baseUrl": "app" 8 | }, 9 | "exclude": ["node_modules", ".cache", "dist"] 10 | } 11 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes auto; 3 | error_log /var/log/nginx/error.log warn; 4 | pid /var/run/nginx.pid; 5 | events { 6 | worker_connections 1024; 7 | } 8 | http { 9 | include /etc/nginx/mime.types; 10 | default_type application/octet-stream; 11 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 12 | '$status $body_bytes_sent "$http_referer" ' 13 | '"$http_user_agent" "$http_x_forwarded_for"'; 14 | access_log /var/log/nginx/access.log main; 15 | sendfile on; 16 | #tcp_nopush on; 17 | 18 | keepalive_timeout 65; 19 | #gzip on; 20 | #include /etc/nginx/conf.d/*.conf; 21 | server { 22 | listen 80; 23 | location / { 24 | root /usr/share/nginx/html; 25 | index index.html index.htm; 26 | try_files $uri $uri/ /index.html; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /server/argv.js: -------------------------------------------------------------------------------- 1 | module.exports = require('minimist')(process.argv.slice(2)); 2 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | /* eslint consistent-return:0 import/order:0 */ 2 | 3 | const express = require('express'); 4 | const logger = require('./logger'); 5 | 6 | const argv = require('./argv'); 7 | const port = require('./port'); 8 | const setup = require('./middlewares/frontendMiddleware'); 9 | const isDev = process.env.NODE_ENV !== 'production'; 10 | const ngrok = 11 | (isDev && process.env.ENABLE_TUNNEL) || argv.tunnel 12 | ? require('ngrok') 13 | : false; 14 | const { resolve } = require('path'); 15 | const app = express(); 16 | 17 | // If you need a backend, e.g. an API, add your custom backend-specific middleware here 18 | // app.use('/api', myApi); 19 | 20 | // In production we need to pass these values in instead of relying on webpack 21 | setup(app, { 22 | outputPath: resolve(process.cwd(), 'build'), 23 | publicPath: '/', 24 | }); 25 | 26 | // get the intended host and port number, use localhost and port 3000 if not provided 27 | const customHost = argv.host || process.env.HOST; 28 | const host = customHost || '0.0.0.0'; // Let http.Server use its default IPv6/4 host 29 | const prettyHost = customHost || 'localhost'; 30 | 31 | // use the gzipped bundle 32 | app.get('*.js', (req, res, next) => { 33 | req.url = req.url + '.gz'; // eslint-disable-line 34 | res.set('Content-Encoding', 'gzip'); 35 | next(); 36 | }); 37 | 38 | // Start your app. 39 | app.listen(port, host, async (err) => { 40 | if (err) { 41 | return logger.error(err.message); 42 | } 43 | 44 | // Connect to ngrok in dev mode 45 | if (ngrok) { 46 | let url; 47 | try { 48 | url = await ngrok.connect(port); 49 | } catch (e) { 50 | return logger.error(e); 51 | } 52 | logger.appStarted(port, prettyHost, url); 53 | } else { 54 | logger.appStarted(port, prettyHost); 55 | } 56 | }); 57 | -------------------------------------------------------------------------------- /server/logger.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | const chalk = require('chalk'); 4 | const ip = require('ip'); 5 | 6 | const divider = chalk.gray('\n-----------------------------------'); 7 | 8 | /** 9 | * Logger middleware, you can customize it to make messages more personal 10 | */ 11 | const logger = { 12 | // Called whenever there's an error on the server we want to print 13 | error: (err) => { 14 | console.error(chalk.red(err)); 15 | }, 16 | 17 | // Called when express.js app starts on given port w/o errors 18 | appStarted: (port, host, tunnelStarted) => { 19 | console.log(`Server started ! ${chalk.green('✓')}`); 20 | 21 | // If the tunnel started, log that and the URL it's available at 22 | if (tunnelStarted) { 23 | console.log(`Tunnel initialised ${chalk.green('✓')}`); 24 | } 25 | 26 | console.log(`${chalk.bold('Access URLs:')}`); 27 | console.log(`${divider}`); 28 | console.log( 29 | `LAN: ${ 30 | chalk.magenta(`http://${ip.address()}:${port}`) + 31 | (tunnelStarted 32 | ? ` 33 | Proxy: ${chalk.magenta(tunnelStarted)}` 34 | : '') 35 | }`, 36 | ); 37 | console.log(`${divider}`); 38 | console.log(`Localhost: ${chalk.magenta(`http://${host}:${port}`)}`); 39 | console.log(`${divider}`); 40 | console.log(`${chalk.blue(`Press ${chalk.italic('CTRL-C')} to stop`)}`); 41 | }, 42 | }; 43 | 44 | module.exports = logger; 45 | -------------------------------------------------------------------------------- /server/middlewares/addDevMiddlewares.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const webpackDevMiddleware = require('webpack-dev-middleware'); 4 | const webpackHotMiddleware = require('webpack-hot-middleware'); 5 | 6 | function createWebpackMiddleware(compiler, publicPath) { 7 | return webpackDevMiddleware(compiler, { 8 | // logLevel: 'warn', 9 | publicPath, 10 | // silent: true, 11 | stats: { 12 | errorDetails: true, 13 | children: true, 14 | }, 15 | }); 16 | } 17 | 18 | module.exports = function addDevMiddlewares(app, webpackConfig) { 19 | const compiler = webpack(webpackConfig); 20 | const middleware = createWebpackMiddleware( 21 | compiler, 22 | webpackConfig.output.publicPath, 23 | ); 24 | 25 | app.use(middleware); 26 | app.use(webpackHotMiddleware(compiler)); 27 | 28 | // Since webpackDevMiddleware uses memory-fs internally to store build 29 | // artifacts, we use it instead 30 | const fs = compiler.outputFileSystem; 31 | 32 | app.get('*', (req, res) => { 33 | fs.readFile(path.join(compiler.outputPath, 'index.html'), (err, file) => { 34 | if (err) { 35 | res.sendStatus(404); 36 | } else { 37 | res.send(file.toString()); 38 | } 39 | }); 40 | }); 41 | }; 42 | -------------------------------------------------------------------------------- /server/middlewares/addProdMiddlewares.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | const compression = require('compression'); 4 | 5 | module.exports = function addProdMiddlewares(app, options) { 6 | const publicPath = options.publicPath || '/'; 7 | const outputPath = options.outputPath || path.resolve(process.cwd(), 'build'); 8 | 9 | // compression middleware compresses your server responses which makes them 10 | // smaller (applies also to assets). You can read more about that technique 11 | // and other good practices on official Express.js docs http://mxs.is/googmy 12 | app.use(compression()); 13 | app.use(publicPath, express.static(outputPath)); 14 | 15 | app.get('*', (req, res) => 16 | res.sendFile(path.resolve(outputPath, 'index.html')), 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /server/middlewares/frontendMiddleware.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | 3 | /** 4 | * Front-end middleware 5 | */ 6 | module.exports = (app, options) => { 7 | const isProd = process.env.NODE_ENV === 'production'; 8 | 9 | if (isProd) { 10 | const addProdMiddlewares = require('./addProdMiddlewares'); 11 | addProdMiddlewares(app, options); 12 | } else { 13 | const webpackConfig = require('../../internals/webpack/webpack.dev.babel'); 14 | const addDevMiddlewares = require('./addDevMiddlewares'); 15 | addDevMiddlewares(app, webpackConfig); 16 | } 17 | 18 | return app; 19 | }; 20 | -------------------------------------------------------------------------------- /server/port.js: -------------------------------------------------------------------------------- 1 | const argv = require('./argv'); 2 | 3 | module.exports = parseInt(process.env.PORT || argv.port || '3000', 10); 4 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": [ 3 | { 4 | "src": "/(.*)", 5 | "dest": "/" 6 | } 7 | ] 8 | } 9 | --------------------------------------------------------------------------------